Merge branch '5.2-candidate' into soon
This commit is contained in:
commit
4aaa2bd71c
53
CHANGELOG.md
53
CHANGELOG.md
|
@ -1,3 +1,56 @@
|
|||
# 5.2.0
|
||||
|
||||
## Goals
|
||||
|
||||
This release is focused on addressing long-standing user feedback with new features. The most requested are improvements to Forms—multiple submissions and the ability to delete responses—as well as recurring events in Calendar.
|
||||
|
||||
|
||||
## Update notes
|
||||
|
||||
To update from `5.1.0` to `5.2.0`:
|
||||
|
||||
1. Update your reverse proxy configuration to match the settings in our current `./docs/example.nginx.conf` and reload its configuration
|
||||
2. Stop your API server
|
||||
3. Fetch the latest code with git
|
||||
4. Install the latest dependencies with `bower update` and `npm i`
|
||||
5. Run `npm run build` to generate the new static pages
|
||||
5. Restart your server
|
||||
6. Review your instance's checkup page to ensure that you are passing all tests
|
||||
|
||||
<!-- XXX Notes about changes to Nginx conf -->
|
||||
|
||||
## Features
|
||||
|
||||
- Forms
|
||||
- New setting to allow participants (including Guests) to submit a form multiple times and/or delete their responses
|
||||
- Notifications for form owners when new responses are submitted
|
||||
- New option for form authors to delete all responses
|
||||
- New option for form authors/auditors to export responses as JSON (in addition to existing CSV and CryptPad Sheet)
|
||||
- Settings have been refactored in a modal with a summary in the main editor view
|
||||
- Display fixes for long questions/options in some question types
|
||||
|
||||
- Calendar
|
||||
- New event settings to repeat periodically
|
||||
- quick default patterns (e.g. weekly on Mondays, yearly on December 14th, etc), and custom intervals
|
||||
- modify one, future, or all events
|
||||
- easily stop repetition from event preview
|
||||
|
||||
- Drive
|
||||
- filter by doc type
|
||||
|
||||
- Teams
|
||||
- Improved onboarding with the ability to use the same invitation link for a set number of people. Previously each link was limited to one use
|
||||
- Initial role can now be set for invitation links, the recipient is assigned the role directly when joining, previously all new members joined as "Viewers"
|
||||
|
||||
- Code
|
||||
- Asciidoc syntax support AND asciidoc rendering
|
||||
- handle jade language
|
||||
- don't suggest c-language highlighting twice
|
||||
|
||||
- /checkup/
|
||||
- [new test to confirm that public instances are open for registration](https://github.com/xwiki-labs/cryptpad/commit/174d97c442d5400d512dfccc478fd9fbd6fa075c)
|
||||
|
||||
|
||||
# 5.1.0
|
||||
|
||||
## Goals
|
||||
|
|
|
@ -95,7 +95,7 @@ define([
|
|||
return h('a', attrs, [icon, text]);
|
||||
};
|
||||
|
||||
Pages.versionString = "5.1.0";
|
||||
Pages.versionString = "5.2.0";
|
||||
|
||||
var customURLs = Pages.customURLs = {};
|
||||
(function () {
|
||||
|
|
|
@ -183,6 +183,11 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.cp-dropdown-content {
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cp-alertify-type-container {
|
||||
overflow: visible !important;
|
||||
|
|
|
@ -11,10 +11,12 @@
|
|||
}
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: @cp_scrollbar-fg;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
|
|
@ -120,6 +120,7 @@
|
|||
border-width: 0 @checkmark-width @checkmark-width 0;
|
||||
border-width: 0 var(--checkmark-width) var(--checkmark-width) 0;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: 0px 0px 5px @cp_checkmark-back1;
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
|
||||
button {
|
||||
.fa-caret-down {
|
||||
margin-right: 1em !important;
|
||||
margin-right: 0.5em !important;
|
||||
}
|
||||
* {
|
||||
.tools_unselectable();
|
||||
|
|
|
@ -91,7 +91,10 @@
|
|||
height: 100%;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
background-color: @cp_buttons-primary;
|
||||
background-color: @cp_buttons-default-color;
|
||||
&.btn-primary, &.btn-secondary {
|
||||
background-color: @cp_buttons-primary;
|
||||
}
|
||||
&.danger, &.btn-danger, &.danger-alt, &.btn-danger-alt {
|
||||
background-color: @cp_buttons-red;
|
||||
}
|
||||
|
@ -327,6 +330,9 @@
|
|||
fill: @cryptpad_text_col;
|
||||
}
|
||||
}
|
||||
.flatpickr-monthDropdown-month {
|
||||
background: @cp_flatpickr-bg;
|
||||
}
|
||||
}
|
||||
.flatpickr-current-month {
|
||||
span.cur-month:hover {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
& {
|
||||
|
||||
each(@colortheme_apps, {
|
||||
button .cp-icon-color-@{key},
|
||||
.cp-icon-color-@{key} { color: @value; }
|
||||
});
|
||||
|
||||
|
|
|
@ -123,6 +123,11 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
&.cp-teams-invite-role {
|
||||
.cp-radio:not(:first-child) {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cp-teams-invite-message {
|
||||
resize: none;
|
||||
|
|
|
@ -27,14 +27,16 @@
|
|||
color: @cryptpad_color_red;
|
||||
}
|
||||
}
|
||||
.cp-avatar {
|
||||
.avatar_main(30px);
|
||||
padding: 0 5px;
|
||||
.cp-reminder, .cp-avatar {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: @cp_dropdown-bg-hover;
|
||||
}
|
||||
}
|
||||
.cp-avatar {
|
||||
.avatar_main(30px);
|
||||
padding: 0 5px;
|
||||
}
|
||||
.cp-notification-content {
|
||||
flex: 1;
|
||||
align-items: stretch;
|
||||
|
|
|
@ -967,6 +967,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.cp-toolbar-dropdown-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.cp-toolbar-bottom {
|
||||
color: @cp_toolbar-bottom-fg;
|
||||
display: inline-flex;
|
||||
|
@ -998,11 +1001,15 @@
|
|||
.fa, .cptools {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.cp-dropdown-button-title .cp-icon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: fade(@cp_toolbar-bottom-bg, 70%);
|
||||
}
|
||||
}
|
||||
.cp-toolbar-bottom-left > button,
|
||||
.cp-toolbar-bottom-left > span > button,
|
||||
.cp-toolbar-bottom-mid > button,
|
||||
.cp-toolbar-bottom-right > button,
|
||||
.cp-toolbar-bottom-right > span > button {
|
||||
|
@ -1070,7 +1077,7 @@
|
|||
.cp-toolbar-name, .cp-button-name {
|
||||
display: none;
|
||||
}
|
||||
i {
|
||||
i, span {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,10 @@
|
|||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
|
||||
# Let's Encrypt webroot
|
||||
include letsencrypt-webroot;
|
||||
|
||||
# CryptPad serves static assets over these two domains.
|
||||
# `main_domain` is what users will enter in their address bar.
|
||||
|
@ -46,26 +50,35 @@ server {
|
|||
# IMPORTANT: this config is intended to serve assets for at least two domains
|
||||
# (your main domain and your sandbox domain). As such, you'll need to generate a single SSL certificate
|
||||
# that includes both domains in order for things to work as expected.
|
||||
ssl_certificate /home/cryptpad/.acme.sh/your-main-domain.com/fullchain.cer;
|
||||
ssl_certificate_key /home/cryptpad/.acme.sh/your-main-domain.com/your-main-domain.com.key;
|
||||
ssl_trusted_certificate /home/cryptpad/.acme.sh/your-main-domain.com/ca.cer;
|
||||
ssl_certificate /etc/ssl/lets-encrypt/your-main-domain.com/cert;
|
||||
ssl_certificate_key /etc/ssl/lets-encrypt/your-main-domain.com/key;
|
||||
|
||||
# diffie-hellman parameters are used to negotiate keys for your session
|
||||
# generate strong parameters using the following command
|
||||
ssl_dhparam /etc/nginx/dhparam.pem; # openssl dhparam -out /etc/nginx/dhparam.pem 4096
|
||||
|
||||
# Speeds things up a little bit when resuming a session
|
||||
ssl_session_timeout 5m;
|
||||
ssl_session_cache shared:SSL:5m;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:MozSSL:10m;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# You'll need nginx 1.13.0 or better to support TLSv1.3
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
|
||||
# https://cipherli.st/
|
||||
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
|
||||
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
|
||||
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
|
||||
|
||||
# OCSP stapling
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
# verify chain of trust of OCSP response using Root CA and Intermediate certs
|
||||
ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
|
||||
|
||||
# replace with the IP address of your resolver
|
||||
resolver 8.8.8.8 8.8.4.4 1.1.1.1 1.0.0.1 9.9.9.9 149.112.112.112 208.67.222.222 208.67.220.220;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
add_header X-Content-Type-Options nosniff;
|
||||
add_header Access-Control-Allow-Origin "${allowed_origins}";
|
||||
|
|
|
@ -6,6 +6,7 @@ const nThen = require("nthen");
|
|||
const Core = require("./core");
|
||||
const Metadata = require("./metadata");
|
||||
const HK = require("../hk-util");
|
||||
const Nacl = require("tweetnacl/nacl-fast");
|
||||
|
||||
Channel.disconnectChannelMembers = function (Env, Server, channelId, code, cb) {
|
||||
var done = Util.once(Util.mkAsync(cb));
|
||||
|
@ -207,6 +208,40 @@ Channel.trimHistory = function (Env, safeKey, data, cb) {
|
|||
});
|
||||
};
|
||||
|
||||
// Delete a signed mailbox message. This is used when users want
|
||||
// to delete their form reponses.
|
||||
Channel.deleteMailboxMessage = function (Env, data, cb) {
|
||||
const channelId = data.channel;
|
||||
const hash = data.hash;
|
||||
const proof = data.proof;
|
||||
let nonce, proofBytes;
|
||||
try {
|
||||
nonce = Nacl.util.decodeBase64(proof.split('|')[0]);
|
||||
proofBytes = Nacl.util.decodeBase64(proof.split('|')[1]);
|
||||
} catch (e) {
|
||||
return void cb('EINVAL');
|
||||
}
|
||||
Env.msgStore.deleteChannelLine(channelId, hash, function (msg) {
|
||||
// Check if you're allowed to delete this hash
|
||||
try {
|
||||
const mySecret = new Uint8Array(32);
|
||||
const msgBytes = Nacl.util.decodeBase64(msg).subarray(64); // Remove signature
|
||||
const theirPublic = msgBytes.subarray(24,56); // 0-24 = nonce; 24-56=publickey (32 bytes)
|
||||
const hashBytes = Nacl.box.open(proofBytes, nonce, theirPublic, mySecret);
|
||||
return Nacl.util.encodeUTF8(hashBytes) === hash;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}, function (err) {
|
||||
if (err) { return void cb(err); }
|
||||
// clear historyKeeper's cache for this channel
|
||||
Env.historyKeeper.channelClose(channelId);
|
||||
cb();
|
||||
delete Env.channel_cache[channelId];
|
||||
delete Env.metadata_cache[channelId];
|
||||
});
|
||||
};
|
||||
|
||||
var ARRAY_LINE = /^\[/;
|
||||
|
||||
/* Files can contain metadata but not content
|
||||
|
@ -320,10 +355,11 @@ Channel.writePrivateMessage = function (Env, args, _cb, Server, netfluxId) {
|
|||
Server.getChannelUserList(channelId).forEach(function (userId) {
|
||||
Server.send(userId, fullMessage);
|
||||
});
|
||||
|
||||
cb();
|
||||
});
|
||||
|
||||
|
||||
cb();
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -31,7 +31,8 @@ the owners field is guaranteed to exist.
|
|||
* mailbox <STRING|MAP>
|
||||
* ADD_MAILBOX
|
||||
* RM_MAILBOX
|
||||
|
||||
* deleteLines <BOOLEAN>
|
||||
* ALLOW_LINE_DELETION
|
||||
*/
|
||||
|
||||
var commands = {};
|
||||
|
@ -71,6 +72,24 @@ commands.RESTRICT_ACCESS = function (meta, args) {
|
|||
return true;
|
||||
};
|
||||
|
||||
// ["ALLOW_LINE_DELETION", [true], 1561623438989]
|
||||
// ["ALLOW_LINE_DELETION", [false], 1561623438989]
|
||||
commands.ALLOW_LINE_DELETION = function (meta, args) {
|
||||
if (!Array.isArray(args) || typeof(args[0]) !== 'boolean') {
|
||||
throw new Error('INVALID_STATE');
|
||||
}
|
||||
|
||||
var bool = args[0];
|
||||
|
||||
// reject the proposed command if there is no change in state
|
||||
if (meta.deleteLines === bool) { return false; }
|
||||
|
||||
// apply the new state
|
||||
meta.deleteLines = args[0];
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// ["ADD_ALLOWED", ["7eEqelGso3EBr5jHlei6av4r9w2B9XZiGGwA1EgZ-5I=", ...], 1561623438989]
|
||||
commands.ADD_ALLOWED = function (meta, args) {
|
||||
if (!Array.isArray(args)) {
|
||||
|
|
|
@ -20,6 +20,7 @@ const UNAUTHENTICATED_CALLS = {
|
|||
IS_CHANNEL_PINNED: Pinning.isChannelPinned, // FIXME drop this RPC
|
||||
IS_NEW_CHANNEL: Channel.isNewChannel,
|
||||
WRITE_PRIVATE_MESSAGE: Channel.writePrivateMessage,
|
||||
DELETE_MAILBOX_MESSAGE: Channel.deleteMailboxMessage,
|
||||
GET_METADATA: Metadata.getMetadata,
|
||||
};
|
||||
|
||||
|
|
|
@ -928,7 +928,7 @@ var getMessages = function (env, chanName, handler, cb) {
|
|||
});
|
||||
};
|
||||
|
||||
var trimChannel = function (env, channelName, hash, _cb) {
|
||||
var filterMessages = function (env, channelName, check, filterHandler, _cb) {
|
||||
var cb = Util.once(Util.mkAsync(_cb));
|
||||
// this function is queued as a blocking action for the relevant channel
|
||||
|
||||
|
@ -985,6 +985,13 @@ var trimChannel = function (env, channelName, hash, _cb) {
|
|||
}
|
||||
// if there were no errors just fall through to the next block
|
||||
}));
|
||||
}).nThen(function (w) {
|
||||
// If we want to delete a signle line of the file, make sure this pad allows it
|
||||
if (typeof(check) !== "function") { return; }
|
||||
if (!metadataReference.meta || !check(metadataReference.meta)) {
|
||||
w.abort();
|
||||
return void cb("EFORBIDDEN");
|
||||
}
|
||||
}).nThen(function (w) {
|
||||
// create temp buffer writeStream
|
||||
tempStream = Fs.createWriteStream(tempChannelPath, {
|
||||
|
@ -1023,14 +1030,15 @@ var trimChannel = function (env, channelName, hash, _cb) {
|
|||
if (!msg) { return void readMore(); }
|
||||
var msgHash = Extras.getHash(msg[4]);
|
||||
|
||||
if (msgHash === hash) {
|
||||
// everything from this point on should be retained
|
||||
retain = true;
|
||||
return void tempStream.write(s_msg + '\n', function () {
|
||||
var remove = function () { readMore(); };
|
||||
var preserve = function () {
|
||||
tempStream.write(s_msg + '\n', function () {
|
||||
readMore();
|
||||
});
|
||||
}
|
||||
readMore();
|
||||
};
|
||||
var preserveRemaining = function () { retain = true; };
|
||||
|
||||
filterHandler(msg, msgHash, abort, remove, preserve, preserveRemaining);
|
||||
};
|
||||
|
||||
readMessagesBin(env, channelName, 0, handler, w(function (err) {
|
||||
|
@ -1102,6 +1110,35 @@ var trimChannel = function (env, channelName, hash, _cb) {
|
|||
});
|
||||
});
|
||||
};
|
||||
var deleteChannelLine = function (env, channelName, hash, checkRights, _cb) {
|
||||
var check = function (meta) { return Boolean(meta.deleteLines); };
|
||||
var handler = function (msg, msgHash, abort, remove, preserve, preserveRemaining) {
|
||||
if (msgHash === hash) {
|
||||
if (typeof(checkRights) === "function" && !checkRights(msg[4])) {
|
||||
// Not allowed: abort
|
||||
return void abort();
|
||||
}
|
||||
// Line found: remove it and preserve all remaining lines
|
||||
preserveRemaining();
|
||||
return void remove();
|
||||
}
|
||||
// Continue until we find the correct hash
|
||||
preserve();
|
||||
};
|
||||
filterMessages(env, channelName, check, handler, _cb);
|
||||
};
|
||||
var trimChannel = function (env, channelName, hash, _cb) {
|
||||
var handler = function (msg, msgHash, abort, remove, preserve, preserveRemaining) {
|
||||
if (msgHash === hash) {
|
||||
// Everything from this point on should be retained
|
||||
preserveRemaining();
|
||||
return void preserve();
|
||||
}
|
||||
// Remove until we find our hash
|
||||
remove();
|
||||
};
|
||||
filterMessages(env, channelName, null, handler, _cb);
|
||||
};
|
||||
|
||||
module.exports.create = function (conf, _cb) {
|
||||
var cb = Util.once(Util.mkAsync(_cb));
|
||||
|
@ -1236,6 +1273,12 @@ module.exports.create = function (conf, _cb) {
|
|||
trimChannel(env, channelName, hash, Util.both(cb, next));
|
||||
});
|
||||
},
|
||||
deleteChannelLine: function (channelName, hash, checkRights, cb) {
|
||||
if (!isValidChannelId(channelName)) { return void cb(new Error('EINVAL')); }
|
||||
schedule.blocking(channelName, function (next) {
|
||||
deleteChannelLine(env, channelName, hash, checkRights, Util.both(cb, next));
|
||||
});
|
||||
},
|
||||
|
||||
// check if a channel exists in the database
|
||||
isChannelAvailable: function (channelName, cb) {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "cryptpad",
|
||||
"version": "5.1.0",
|
||||
"version": "5.2.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cryptpad",
|
||||
"version": "5.1.0",
|
||||
"version": "5.2.0",
|
||||
"license": "AGPL-3.0+",
|
||||
"dependencies": {
|
||||
"@mcrowe/minibloom": "^0.2.0",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "cryptpad",
|
||||
"description": "realtime collaborative visual editor with zero knowlege server",
|
||||
"version": "5.1.0",
|
||||
"version": "5.2.0",
|
||||
"license": "AGPL-3.0+",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
.cp-small { display: none; }
|
||||
}
|
||||
|
||||
.flatpickr-calendar {
|
||||
z-index: 100001 !important; // Alertify is 100000
|
||||
}
|
||||
#cp-sidebarlayout-container #cp-sidebarlayout-rightside {
|
||||
padding: 0;
|
||||
& > div {
|
||||
|
@ -101,6 +104,21 @@
|
|||
color: @cryptpad_text_col !important;
|
||||
}
|
||||
}
|
||||
|
||||
.tui-full-calendar-floating-layer.cp-calendar-popup-flex {
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
justify-content: center !important;
|
||||
align-items: center !important;
|
||||
.tui-full-calendar-popup {
|
||||
width: 540px !important;
|
||||
}
|
||||
}
|
||||
#tui-full-calendar-popup-arrow {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
.tui-full-calendar-timegrid-timezone {
|
||||
background-color: @cp_sidebar-right-bg !important;
|
||||
|
@ -116,13 +134,30 @@
|
|||
border-color: @cp_calendar-border !important;
|
||||
|
||||
}
|
||||
.tui-full-calendar-popup {
|
||||
border-radius: @variables_radius_L;
|
||||
}
|
||||
.tui-full-calendar-popup-container {
|
||||
background: @cp_flatpickr-bg;
|
||||
color: @cryptpad_text_col;
|
||||
border-radius: @variables_radius;
|
||||
border-radius: @variables_radius_L;
|
||||
font-weight: normal;
|
||||
.tui-full-calendar-icon:not(.tui-full-calendar-calendar-dot):not(.tui-full-calendar-dropdown-arrow):not(.tui-full-calendar-ic-checkbox) {
|
||||
display: none;
|
||||
}
|
||||
.tui-full-calendar-popup-detail-item {
|
||||
a {
|
||||
color: @cryptpad_color_link;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
.tui-full-calendar-section-button-save {
|
||||
height: 40px;
|
||||
.btn-primary { // Update button
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
li.tui-full-calendar-popup-section-item {
|
||||
padding: 0 6px;
|
||||
|
@ -186,6 +221,9 @@
|
|||
width: 100%;
|
||||
height: 32px;
|
||||
border-radius: @variables_radius;
|
||||
input[type="checkbox"].tui-full-calendar-checkbox-square:checked + span {
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAABbmlDQ1BpY2MAACiRdZHNK0RRGMZ/Zoh8NIqFNItZDFmMEiVLxsJmkgZlsLn3zpe6c93uvZNkq2wspizExtfCf8BW2VJKkZKs/AG+Npqu97hqJM7t3PfXc87zds5zIJQyjZJbPwAly3PSE8nYXGY+1vhEMy20EyaqGa49NjWV4t/xfkOdqtf9qtf/+/4cLdmca0Bdk/CwYTue8KhwasWzFW8KdxpFLSu8L5xw5IDCF0rXA35UXAj4VbEzkx6HkOoZK/xg/QcbRack3CccL5ll4/s86iatOWt2Wmq3zCguaSZIEkOnzBImHv1SLcnsb9/Al2+SZfEY8rdZxRFHgaJ4E6KWpWtOal70nHwmqyr333m6+aHBoHtrEhoefP+lBxq3oFrx/Y8D368eQvgezqyaf1lyGnkTvVLT4nsQWYeT85qmb8PpBnTd2ZqjfUlhmaF8Hp6PoS0DHVfQvBBk9b3O0S3MrMkTXcLOLvTK/sjiJ6CLZ94KREMsAAAACXBIWXMAAAsSAAALEgHS3X78AAABHUlEQVQoFWNkaP//n4EMwESGHrAW6mtkZvz3G2g03BsBagz/nRUQ7sNp49//TJ+Byv6BlMbrMjCsC2Jg3BbKwOAgB9GMolGAg4GBESIOIrmA+A9I03xvFHGwCrhGkDOe5TAw9LvAFbEn6jEwwTT9BtodvJ7h/4FHYH0MLBCKgaHTgYGBE8jLN2FgYAJae+4FA8NcLwZWkAtAmoLWMTBsuYNwECMsHrVEGBj2RzEwiIEciATANgE1bb6DJAgMNLhTr71hYHBcxsDw+htCAQ5NIAU/4RpBPKjmf2++MfwHaQpZD7cJyEMB3+BORRL+rybE8FOEk4Hj2FO4KMgdrFAMitun2DTCVSMxYAkBFFYg9j94qCIpwsZEil5wyDIDAAXIUsnSKmq7AAAAAElFTkSuQmCC);
|
||||
}
|
||||
.tui-full-calendar-ic-checkbox {
|
||||
margin-left: 5px;
|
||||
border-radius: 2px;
|
||||
|
@ -196,6 +234,7 @@
|
|||
.tui-full-calendar-popup-detail {
|
||||
font: @colortheme_app-font;
|
||||
color: @cryptpad_text_col;
|
||||
box-shadow: @cryptpad_ui_shadow;
|
||||
.tui-full-calendar-popup-container {
|
||||
padding-bottom: 17px;
|
||||
}
|
||||
|
@ -203,28 +242,44 @@
|
|||
font-size: 14px;
|
||||
}
|
||||
.tui-full-calendar-section-button {
|
||||
margin-top: 10px;
|
||||
border: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: start;
|
||||
button {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.tui-full-calendar-popup-top-line {
|
||||
border-radius: 10px 10px 0px 0px;
|
||||
height: 10px;
|
||||
}
|
||||
.tui-full-calendar-popup-vertical-line {
|
||||
visibility: hidden;
|
||||
width: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-recurrence-label, .cp-notif-label {
|
||||
color: @cryptpad_text_col;
|
||||
margin-right: 1rem;
|
||||
i {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-calendar-recurrence-container {
|
||||
margin-top: 1rem;
|
||||
.cp-calendar-rec-translated-str {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-calendar-add-notif {
|
||||
flex-flow: column;
|
||||
align-items: baseline !important;
|
||||
margin: 10px 0;
|
||||
.cp-notif-label {
|
||||
color: @cp_sidebar-hint;
|
||||
margin-right: 20px;
|
||||
}
|
||||
margin: 1rem 0;
|
||||
* {
|
||||
font-size: @colortheme_app-font-size;
|
||||
font-weight: normal;
|
||||
|
@ -234,34 +289,58 @@
|
|||
}
|
||||
.cp-calendar-notif-list-container {
|
||||
margin-bottom: 10px;
|
||||
.cp-notif-label {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
.cp-calendar-notif-list {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
.cp-notif-entry {
|
||||
margin-bottom: 2px;
|
||||
border-radius: @variables_radius;
|
||||
background-color: fade(@cryptpad_text_col, 10%);
|
||||
padding: 0.25rem;
|
||||
.cp-notif-value {
|
||||
width: 170px;
|
||||
display: inline-flex;
|
||||
line-height: 30px;
|
||||
vertical-align: middle;
|
||||
.cp-before {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
span:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
margin: 0px 5px;
|
||||
}
|
||||
.btn-danger-outline {
|
||||
margin-right: 0px !important;
|
||||
background-color: transparent;
|
||||
color: @cryptpad_text_col;
|
||||
border-color: @cryptpad_text_col;
|
||||
&:hover {
|
||||
color: @cp_buttons-red-color;
|
||||
background-color: @cp_buttons-red;
|
||||
border-color: @cp_buttons-red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.cp-notif-empty {
|
||||
display: none;
|
||||
margin-bottom: 2px;
|
||||
border-radius: @variables_radius;
|
||||
background-color: fade(@cryptpad_text_col, 10%);
|
||||
padding: 0.25rem 0.5rem;
|
||||
line-height: 30px;
|
||||
}
|
||||
.cp-calendar-notif-list:empty ~ .cp-notif-empty {
|
||||
display: block;
|
||||
}
|
||||
.cp-calendar-notif-form {
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
// margin-bottom: 20px;
|
||||
input {
|
||||
width: 80px;
|
||||
margin-right: 5px;
|
||||
|
@ -270,15 +349,120 @@
|
|||
}
|
||||
|
||||
.cp-calendar-close {
|
||||
top: 17px;
|
||||
right: 17px;
|
||||
height: auto;
|
||||
margin-right: 0px;
|
||||
line-height: initial;
|
||||
border: 1px solid;
|
||||
&:not(:hover) {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.cp-calendar-rec-inline, .cp-calendar-rec-block {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
.cp-calendar-rec-inline {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
& > *:not(:first-child) { margin-left: 5px; }
|
||||
.cp-dropdown-container {
|
||||
position: unset;
|
||||
}
|
||||
input[type="number"] {
|
||||
width: 80px !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.cp-checkmark {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
.cp-calendar-rec-block {
|
||||
.cp-calendar-rec-block-title {
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
.cp-radio {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
input[type="radio"]:not(:checked) ~ .cp-checkmark-label {
|
||||
input {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
}
|
||||
.cp-checkmark-label {
|
||||
& > *:not(:first-child) { margin-left: 5px; }
|
||||
width: 100%;
|
||||
//height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& > input {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
input {
|
||||
display: inline;
|
||||
height: 24px !important;
|
||||
padding: 0 5px !important;
|
||||
}
|
||||
input[type="text"] {
|
||||
width: 200px !important;
|
||||
}
|
||||
input[type="number"] {
|
||||
width: 80px !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
#cp-calendar-rec-monthly-pick ~ .cp-checkmark-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& > span {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
button.cp-calendar-pick-el {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
div.cp-calendar-weekly-pick {
|
||||
button {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
div.cp-calendar-monthly-pick {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
& > div {
|
||||
display: flex;
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
button {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
&.lastday {
|
||||
width: 115px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tui-full-calendar-ic-repeat-b {
|
||||
display: none;
|
||||
& ~ * {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#cp-toolbar .cp-calendar-browse {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -395,6 +579,7 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: @variables_radius;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
&.cp-active {
|
||||
background-color: @cp_sidebar-left-item-bg;
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
// Calendars will be exported using this format instead of plain text.
|
||||
define([
|
||||
'/customize/pages.js',
|
||||
], function (Pages) {
|
||||
'/common/common-util.js',
|
||||
'/calendar/recurrence.js'
|
||||
], function (Pages, Util, Rec) {
|
||||
var module = {};
|
||||
|
||||
var getICSDate = function (str) {
|
||||
|
@ -57,60 +59,197 @@ define([
|
|||
var data = content[uid];
|
||||
// DTSTAMP: now...
|
||||
// UID: uid
|
||||
var start, end;
|
||||
if (data.isAllDay && data.startDay && data.endDay) {
|
||||
start = "DTSTART;VALUE=DATE:" + getDate(data.startDay);
|
||||
end = "DTEND;VALUE=DATE:" + getDate(data.endDay, true);
|
||||
} else {
|
||||
start = "DTSTART:"+getICSDate(data.start);
|
||||
end = "DTEND:"+getICSDate(data.end);
|
||||
}
|
||||
var getDT = function (data) {
|
||||
var start, end;
|
||||
if (data.isAllDay) {
|
||||
var startDate = new Date(data.start);
|
||||
var endDate = new Date(data.end);
|
||||
data.startDay = data.startDay || (startDate.getFullYear() + '-' + (startDate.getMonth()+1) + '-' + startDate.getDate());
|
||||
data.endDay = data.endDay || (endDate.getFullYear() + '-' + (endDate.getMonth()+1) + '-' + endDate.getDate());
|
||||
start = "DTSTART;VALUE=DATE:" + getDate(data.startDay);
|
||||
end = "DTEND;VALUE=DATE:" + getDate(data.endDay, true);
|
||||
} else {
|
||||
start = "DTSTART:"+getICSDate(data.start);
|
||||
end = "DTEND:"+getICSDate(data.end);
|
||||
}
|
||||
return {
|
||||
start: start,
|
||||
end: end
|
||||
};
|
||||
};
|
||||
|
||||
Array.prototype.push.apply(ICS, [
|
||||
'BEGIN:VEVENT',
|
||||
'DTSTAMP:'+getICSDate(+new Date()),
|
||||
'UID:'+uid,
|
||||
start,
|
||||
end,
|
||||
'SUMMARY:'+ data.title,
|
||||
'LOCATION:'+ data.location,
|
||||
]);
|
||||
|
||||
if (Array.isArray(data.reminders)) {
|
||||
data.reminders.forEach(function (valueMin) {
|
||||
var time = valueMin * 60;
|
||||
var days = Math.floor(time / DAY);
|
||||
time -= days * DAY;
|
||||
var hours = Math.floor(time / HOUR);
|
||||
time -= hours * HOUR;
|
||||
var minutes = Math.floor(time / MINUTE);
|
||||
time -= minutes * MINUTE;
|
||||
var seconds = time;
|
||||
|
||||
var str = "-P" + days + "D";
|
||||
if (hours || minutes || seconds) {
|
||||
str += "T" + hours + "H" + minutes + "M" + seconds + "S";
|
||||
var getRRule = function (data) {
|
||||
if (!data.recurrenceRule || !data.recurrenceRule.freq) { return; }
|
||||
var r = data.recurrenceRule;
|
||||
var rrule = "RRULE:";
|
||||
rrule += "FREQ="+r.freq.toUpperCase();
|
||||
Object.keys(r).forEach(function (k) {
|
||||
if (k === "freq") { return; }
|
||||
if (k === "by") {
|
||||
Object.keys(r.by).forEach(function (_k) {
|
||||
rrule += ";BY"+_k.toUpperCase()+"="+r.by[_k];
|
||||
});
|
||||
return;
|
||||
}
|
||||
rrule += ";"+k.toUpperCase()+"="+r[k];
|
||||
});
|
||||
return rrule;
|
||||
};
|
||||
|
||||
|
||||
|
||||
var addEvent = function (arr, data, recId) {
|
||||
var uid = data.id;
|
||||
var dt = getDT(data);
|
||||
var start = dt.start;
|
||||
var end = dt.end;
|
||||
var rrule = getRRule(data);
|
||||
|
||||
Array.prototype.push.apply(arr, [
|
||||
'BEGIN:VEVENT',
|
||||
'DTSTAMP:'+getICSDate(+new Date()),
|
||||
'UID:'+uid,
|
||||
start,
|
||||
end,
|
||||
recId,
|
||||
rrule,
|
||||
'SUMMARY:'+ data.title,
|
||||
'LOCATION:'+ data.location,
|
||||
].filter(Boolean));
|
||||
|
||||
if (Array.isArray(data.reminders)) {
|
||||
data.reminders.forEach(function (valueMin) {
|
||||
var time = valueMin * 60;
|
||||
var days = Math.floor(time / DAY);
|
||||
time -= days * DAY;
|
||||
var hours = Math.floor(time / HOUR);
|
||||
time -= hours * HOUR;
|
||||
var minutes = Math.floor(time / MINUTE);
|
||||
time -= minutes * MINUTE;
|
||||
var seconds = time;
|
||||
|
||||
var str = "-P" + days + "D";
|
||||
if (hours || minutes || seconds) {
|
||||
str += "T" + hours + "H" + minutes + "M" + seconds + "S";
|
||||
}
|
||||
Array.prototype.push.apply(arr, [
|
||||
'BEGIN:VALARM',
|
||||
'ACTION:DISPLAY',
|
||||
'DESCRIPTION:This is an event reminder',
|
||||
'TRIGGER:'+str,
|
||||
'END:VALARM'
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(data.cp_hidden)) {
|
||||
Array.prototype.push.apply(arr, data.cp_hidden);
|
||||
}
|
||||
|
||||
arr.push('END:VEVENT');
|
||||
};
|
||||
|
||||
|
||||
var applyChanges = function (base, changes) {
|
||||
var applyDiff = function (obj, k) {
|
||||
var diff = obj[k]; // Diff is always compared to origin start/end
|
||||
var d = new Date(base[k]);
|
||||
d.setDate(d.getDate() + diff.d);
|
||||
d.setHours(d.getHours() + diff.h);
|
||||
d.setMinutes(d.getMinutes() + diff.m);
|
||||
base[k] = +d;
|
||||
};
|
||||
Object.keys(changes || {}).forEach(function (k) {
|
||||
if (k === "start" || k === "end") {
|
||||
return applyDiff(changes, k);
|
||||
}
|
||||
base[k] = changes[k];
|
||||
});
|
||||
};
|
||||
|
||||
var prev = data;
|
||||
|
||||
// Check if we have "one-time" or "from date" updates.
|
||||
// "One-time" updates will be added accordingly to the ICS specs
|
||||
// "From date" updates will be added as new events and will add
|
||||
// an "until" value to the initial event's RRULE
|
||||
var toAdd = [];
|
||||
if (data.recurrenceRule && data.recurrenceRule.freq && data.recUpdate) {
|
||||
var ru = data.recUpdate;
|
||||
var _all = {};
|
||||
var duration = data.end - data.start;
|
||||
|
||||
var all = Rec.getAllOccurrences(data); // "false" if infinite
|
||||
|
||||
Object.keys(ru.from || {}).forEach(function (d) {
|
||||
if (!Object.keys(ru.from[d] || {}).length) { return; }
|
||||
_all[d] = _all[d] || {};
|
||||
_all[d].from = ru.from[d];
|
||||
});
|
||||
Object.keys(ru.one || {}).forEach(function (d) {
|
||||
if (!Object.keys(ru.one[d] || {}).length) { return; }
|
||||
_all[d] = _all[d] || {};
|
||||
_all[d].one = ru.one[d];
|
||||
});
|
||||
Object.keys(_all).sort(function (a, b) {
|
||||
return Number(a) - Number(b);
|
||||
}).forEach(function (d) {
|
||||
d = Number(d);
|
||||
var r = prev.recurrenceRule;
|
||||
|
||||
// This rule won't apply if we've reached "until" or "count"
|
||||
var idx = all && all.indexOf(d);
|
||||
if (all && idx === -1) {
|
||||
// Make sure we don't have both count and until
|
||||
if (all.length === r.count) { delete r.until; }
|
||||
else { delete r.count; }
|
||||
return;
|
||||
}
|
||||
|
||||
var ud = _all[d];
|
||||
|
||||
if (ud.from) { // "From" updates are not supported by ICS: make a new event
|
||||
var _new = Util.clone(prev);
|
||||
r.until = getICSDate(d - 1); // Stop previous recursion
|
||||
delete r.count;
|
||||
addEvent(ICS, prev, null); // Add previous event
|
||||
Array.prototype.push.apply(ICS, toAdd); // Add individual updates
|
||||
toAdd = [];
|
||||
prev = _new;
|
||||
if (all) { all = all.slice(idx); }
|
||||
|
||||
// if we updated the recurrence rule, count is reset, nothing to do
|
||||
// if we didn't update the recurrence, we need to fix the count
|
||||
var _r = _new.recurrenceRule;
|
||||
if (all && !ud.from.recurrenceRule && _r && _r.count) {
|
||||
_r.count -= idx;
|
||||
}
|
||||
|
||||
prev.start = d;
|
||||
prev.end = d + duration;
|
||||
prev.id = Util.uid();
|
||||
applyChanges(prev, ud.from);
|
||||
duration = prev.end - prev.start;
|
||||
}
|
||||
if (ud.one) { // Add update
|
||||
var _one = Util.clone(prev);
|
||||
_one.start = d;
|
||||
_one.end = d + duration;
|
||||
applyChanges(_one, ud.one);
|
||||
var recId = "RECURRENCE-ID:"+getICSDate(+d);
|
||||
delete _one.recurrenceRule;
|
||||
addEvent(toAdd, _one, recId); // Add updated event
|
||||
}
|
||||
Array.prototype.push.apply(ICS, [
|
||||
'BEGIN:VALARM',
|
||||
'ACTION:DISPLAY',
|
||||
'DESCRIPTION:This is an event reminder',
|
||||
'TRIGGER:'+str,
|
||||
'END:VALARM'
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(data.cp_hidden)) {
|
||||
Array.prototype.push.apply(ICS, data.cp_hidden);
|
||||
}
|
||||
|
||||
ICS.push('END:VEVENT');
|
||||
addEvent(ICS, prev);
|
||||
Array.prototype.push.apply(ICS, toAdd); // Add individual updates
|
||||
});
|
||||
|
||||
ICS.push('END:VCALENDAR');
|
||||
|
||||
return new Blob([ ICS.join('\n') ], { type: 'text/calendar;charset=utf-8' });
|
||||
return new Blob([ ICS.join('\r\n') ], { type: 'text/calendar;charset=utf-8' });
|
||||
};
|
||||
|
||||
module.import = function (content, id, cb) {
|
||||
|
@ -171,7 +310,7 @@ define([
|
|||
}
|
||||
|
||||
// Store other properties
|
||||
var used = ['dtstart', 'dtend', 'uid', 'summary', 'location', 'dtstamp'];
|
||||
var used = ['dtstart', 'dtend', 'uid', 'summary', 'location', 'dtstamp', 'rrule', 'recurrence-id'];
|
||||
var hidden = [];
|
||||
ev.getAllProperties().forEach(function (p) {
|
||||
if (used.indexOf(p.name) !== -1) { return; }
|
||||
|
@ -192,8 +331,25 @@ define([
|
|||
if (reminders.indexOf(minutes) === -1) { reminders.push(minutes); }
|
||||
});
|
||||
|
||||
// Get recurrence rule
|
||||
var rrule = ev.getFirstPropertyValue('rrule');
|
||||
var rec;
|
||||
if (rrule && rrule.freq) {
|
||||
rec = {};
|
||||
rec.freq = rrule.freq.toLowerCase();
|
||||
if (rrule.interval) { rec.interval = rrule.interval; }
|
||||
if (rrule.count) { rec.count = rrule.count; }
|
||||
if (Object.keys(rrule).includes('wkst')) { rec.wkst = (rrule.wkst + 6) % 7; }
|
||||
if (rrule.until) { rec.until = +new Date(rrule.until); }
|
||||
Object.keys(rrule.parts || {}).forEach(function (k) {
|
||||
rec.by = rec.by || {};
|
||||
var _k = k.toLowerCase().slice(2); // "BYDAY" ==> "day"
|
||||
rec.by[_k] = rrule.parts[k];
|
||||
});
|
||||
}
|
||||
|
||||
// Create event
|
||||
res[uid] = {
|
||||
var obj = {
|
||||
calendarId: id,
|
||||
id: uid,
|
||||
category: 'time',
|
||||
|
@ -203,15 +359,48 @@ define([
|
|||
start: start,
|
||||
end: end,
|
||||
reminders: reminders,
|
||||
cp_hidden: hidden
|
||||
cp_hidden: hidden,
|
||||
};
|
||||
if (rec) { obj.recurrenceRule = rec; }
|
||||
|
||||
if (!hidden.length) { delete res[uid].cp_hidden; }
|
||||
if (!reminders.length) { delete res[uid].reminders; }
|
||||
if (!hidden.length) { delete obj.cp_hidden; }
|
||||
if (!reminders.length) { delete obj.reminders; }
|
||||
|
||||
var recId = ev.getFirstPropertyValue('recurrence-id');
|
||||
if (recId) {
|
||||
setTimeout(function () {
|
||||
if (!res[uid]) { return; }
|
||||
var old = res[uid];
|
||||
var time = +new Date(recId);
|
||||
var diff = {};
|
||||
var from = {};
|
||||
Object.keys(obj).forEach(function (k) {
|
||||
if (JSON.stringify(old[k]) === JSON.stringify(obj[k])) { return; }
|
||||
if (['start','end'].includes(k)) {
|
||||
diff[k] = Rec.diffDate(old[k], obj[k]);
|
||||
return;
|
||||
}
|
||||
if (k === "recurrenceRule") {
|
||||
from[k] = obj[k];
|
||||
return;
|
||||
}
|
||||
diff[k] = obj[k];
|
||||
});
|
||||
old.recUpdate = old.recUpdate || {one:{},from:{}};
|
||||
if (Object.keys(from).length) { old.recUpdate.from[time] = from; }
|
||||
if (Object.keys(diff).length) { old.recUpdate.one[time] = diff; }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res[uid] = obj;
|
||||
});
|
||||
|
||||
cb(null, res);
|
||||
// setTimeout to make sure we call back after the "recurrence-id" setTimeout
|
||||
// are called
|
||||
setTimeout(function () {
|
||||
cb(null, res);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -6,17 +6,27 @@ define([
|
|||
'/common/sframe-common-outer.js',
|
||||
], function (nThen, ApiConfig, DomReady, SFCommonO) {
|
||||
|
||||
// Loaded in load #2
|
||||
var hash, href;
|
||||
nThen(function (waitFor) {
|
||||
DomReady.onReady(waitFor());
|
||||
}).nThen(function (waitFor) {
|
||||
SFCommonO.initIframe(waitFor);
|
||||
var obj = SFCommonO.initIframe(waitFor, true);
|
||||
href = obj.href;
|
||||
hash = obj.hash;
|
||||
}).nThen(function (/*waitFor*/) {
|
||||
var addData = function (meta) {
|
||||
meta.calendarHash = Boolean(window.location.hash);
|
||||
var addData = function (meta, Cryptpad, user, Utils) {
|
||||
if (hash) {
|
||||
var parsed = Utils.Hash.parsePadUrl(href);
|
||||
if (parsed.hashData && parsed.hashData.newPadOpts) {
|
||||
meta.calendarOpts = Utils.Hash.decodeDataOptions(parsed.hashData.newPadOpts);
|
||||
}
|
||||
}
|
||||
meta.calendarHash = hash;
|
||||
};
|
||||
SFCommonO.start({
|
||||
addData: addData,
|
||||
hash: hash,
|
||||
href: href,
|
||||
noRealtime: true,
|
||||
cache: true,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,869 @@
|
|||
define([
|
||||
'/common/common-util.js',
|
||||
], function (Util) {
|
||||
var Rec = {};
|
||||
|
||||
var debug = function () {};
|
||||
|
||||
// Get week number with any "WKST" (firts day of the week)
|
||||
// Week 1 is the first week of the year containing at least 4 days in this year
|
||||
// It depends on which day is considered the first day of the week (default Monday)
|
||||
// In our case, wkst is a number matching the JS rule: 0 == Sunday
|
||||
var getWeekNo = Rec.getWeekNo = function (date, wkst) {
|
||||
if (typeof(wkst) !== "number") { wkst = 1; } // Default monday
|
||||
|
||||
var newYear = new Date(date.getFullYear(),0,1);
|
||||
var day = newYear.getDay() - wkst; //the day of week the year begins on
|
||||
day = (day >= 0 ? day : day + 7);
|
||||
var daynum = Math.floor((date.getTime() - newYear.getTime())/86400000) + 1;
|
||||
var weeknum;
|
||||
// Week 1 / week 53
|
||||
if (day < 4) {
|
||||
weeknum = Math.floor((daynum+day-1)/7) + 1;
|
||||
if (weeknum > 52) {
|
||||
var nYear = new Date(date.getFullYear() + 1,0,1);
|
||||
var nday = nYear.getDay() - wkst;
|
||||
nday = nday >= 0 ? nday : nday + 7;
|
||||
weeknum = nday < 4 ? 1 : 53;
|
||||
}
|
||||
}
|
||||
else {
|
||||
weeknum = Math.floor((daynum+day-1)/7);
|
||||
}
|
||||
return weeknum;
|
||||
};
|
||||
|
||||
var getYearDay = function (date) {
|
||||
var start = new Date(date.getFullYear(), 0, 0);
|
||||
var diff = (date - start) +
|
||||
((start.getTimezoneOffset() - date.getTimezoneOffset()) * 60 * 1000);
|
||||
var oneDay = 1000 * 60 * 60 * 24;
|
||||
return Math.floor(diff / oneDay);
|
||||
};
|
||||
var setYearDay = function (date, day) {
|
||||
if (typeof(day) !== "number" || Math.abs(day) < 1 || Math.abs(day) > 366) { return; }
|
||||
if (day < 0) {
|
||||
var max = getYearDay(new Date(date.getFullYear(), 11, 31));
|
||||
day = max + day + 1;
|
||||
}
|
||||
date.setMonth(0);
|
||||
date.setDate(day);
|
||||
return true;
|
||||
};
|
||||
|
||||
var getEndData = function (s, e) {
|
||||
if (s > e) { return void console.error("Wrong data"); }
|
||||
var days;
|
||||
if (e.getFullYear() === s.getFullYear()) {
|
||||
days = getYearDay(e) - getYearDay(s);
|
||||
} else { // eYear < sYear
|
||||
var tmp = new Date(s.getFullYear(), 11, 31);
|
||||
var d1 = getYearDay(tmp) - getYearDay(s); // Number of days before December 31st
|
||||
var de = getYearDay(e);
|
||||
days = d1 + de;
|
||||
while ((tmp.getFullYear()+1) < e.getFullYear()) {
|
||||
tmp.setFullYear(tmp.getFullYear()+1);
|
||||
days += getYearDay(tmp);
|
||||
}
|
||||
}
|
||||
return {
|
||||
h: e.getHours(),
|
||||
m: e.getMinutes(),
|
||||
days: days
|
||||
};
|
||||
};
|
||||
var setEndData = function (s, e, data) {
|
||||
e.setTime(+s);
|
||||
if (!data) { return; }
|
||||
e.setHours(data.h);
|
||||
e.setMinutes(data.m);
|
||||
e.setSeconds(0);
|
||||
e.setDate(s.getDate() + data.days);
|
||||
};
|
||||
|
||||
var DAYORDER = Rec.DAYORDER = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
|
||||
var getDayData = function (str) {
|
||||
var pos = Number(str.slice(0,-2));
|
||||
var day = DAYORDER.indexOf(str.slice(-2));
|
||||
return pos ? [pos, day] : day;
|
||||
};
|
||||
|
||||
var goToFirstWeekDay = function (date, wkst) {
|
||||
var d = date.getDay();
|
||||
wkst = typeof(wkst) === "number" ? wkst : 1;
|
||||
if (d >= wkst) {
|
||||
date.setDate(date.getDate() - (d-wkst));
|
||||
} else {
|
||||
date.setDate(date.getDate() - (7+d-wkst));
|
||||
}
|
||||
};
|
||||
|
||||
var getDateStr = function (date) {
|
||||
return date.getFullYear() + '-' + (date.getMonth()+1) + '-' + date.getDate();
|
||||
};
|
||||
var FREQ = {};
|
||||
FREQ['daily'] = function (s, i) {
|
||||
s.setDate(s.getDate()+i);
|
||||
};
|
||||
FREQ['weekly'] = function (s,i) {
|
||||
s.setDate(s.getDate()+(i*7));
|
||||
};
|
||||
FREQ['monthly'] = function (s,i) {
|
||||
s.setMonth(s.getMonth()+i);
|
||||
};
|
||||
FREQ['yearly'] = function (s,i) {
|
||||
s.setFullYear(s.getFullYear()+i);
|
||||
};
|
||||
|
||||
// EXPAND is used to create iterations added from a BYxxx rule
|
||||
// dateA is the start date and b is the number or id of the BYxxx rule item
|
||||
var EXPAND = {};
|
||||
EXPAND['month'] = function (dateS, origin, b) {
|
||||
var oS = new Date(origin.start);
|
||||
var a = dateS.getMonth() + 1;
|
||||
var toAdd = (b-a+12)%12;
|
||||
var m = dateS.getMonth() + toAdd;
|
||||
dateS.setMonth(m);
|
||||
dateS.setDate(oS.getDate());
|
||||
if (dateS.getMonth() !== m) { return; } // Day 31 may move us to the next month
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
EXPAND['weekno'] = function (dateS, origin, week, rule) {
|
||||
var wkst = rule && rule.wkst;
|
||||
if (typeof(wkst) !== "number") { wkst = 1; } // Default monday
|
||||
var oS = new Date(origin.start);
|
||||
|
||||
var lastD = new Date(dateS.getFullYear(), 11, 31); // December 31st
|
||||
var lastW = getWeekNo(lastD, wkst); // Last week of the year is either 52 or 53
|
||||
|
||||
var doubleOne = lastW === 1;
|
||||
if (lastW === 1) { lastW = 52; }
|
||||
|
||||
var a = getWeekNo(dateS, wkst);
|
||||
if (!week || week > lastW) { return false; } // Week 53 may not exist this year
|
||||
|
||||
if (week < 0) { week = lastW + week + 1; } // Turn negative week number into positive
|
||||
|
||||
var toAdd = week - a;
|
||||
var weekS = new Date(+dateS);
|
||||
// Go to the selected week
|
||||
weekS.setDate(weekS.getDate() + (toAdd * 7));
|
||||
goToFirstWeekDay(weekS, wkst);
|
||||
|
||||
// Then make sure we are in the correct start day
|
||||
var all = 'aaaaaaa'.split('').map(function (o, i) {
|
||||
var date = new Date(+weekS);
|
||||
date.setDate(date.getDate() + i);
|
||||
if (date.getFullYear() !== dateS.getFullYear()) { return; }
|
||||
return date.toLocaleDateString() !== oS.toLocaleDateString() && date;
|
||||
}).filter(Boolean);
|
||||
|
||||
// If we're looking for week 1 and the last week is a week 1, add the days
|
||||
if (week === 1 && doubleOne) {
|
||||
goToFirstWeekDay(lastD, wkst);
|
||||
'aaaaaaa'.split('').some(function (o, i) {
|
||||
var date = new Date(+lastD);
|
||||
date.setDate(date.getDate() + i);
|
||||
if (date.toLocaleDateString() === oS.toLocaleDateString()) { return; }
|
||||
if (date.getFullYear() > dateS.getFullYear()) { return true; }
|
||||
all.push(date);
|
||||
});
|
||||
}
|
||||
|
||||
return all.length ? all : undefined;
|
||||
};
|
||||
EXPAND['yearday'] = function (dateS, origin, b) {
|
||||
var y = dateS.getFullYear();
|
||||
var state = setYearDay(dateS, b);
|
||||
if (!state) { return; } // Invalid day "b"
|
||||
if (dateS.getFullYear() !== y) { return; } // Day 366 make move us to the next year
|
||||
return true;
|
||||
};
|
||||
EXPAND['monthday'] = function (dateS, origin, b, rule) {
|
||||
if (typeof(b) !== "number" || Math.abs(b) < 1 || Math.abs(b) > 31) { return false; }
|
||||
|
||||
var setMonthDay = function (date, day) {
|
||||
var m = date.getMonth();
|
||||
if (day < 0) {
|
||||
var tmp = new Date(date.getFullYear(), date.getMonth()+1, 0); // Last day
|
||||
day = tmp.getDate() + day + 1;
|
||||
}
|
||||
date.setDate(day);
|
||||
return date.getMonth() === m; // Don't push if day 31 moved us to the next month
|
||||
|
||||
};
|
||||
|
||||
// Monthly events
|
||||
if (rule.freq === 'monthly') {
|
||||
return setMonthDay(dateS, b);
|
||||
}
|
||||
|
||||
var all = 'aaaaaaaaaaaa'.split('').map(function (o, i) {
|
||||
var date = new Date(dateS.getFullYear(), i, 1);
|
||||
var ok = setMonthDay(date, b);
|
||||
return ok ? date : undefined;
|
||||
}).filter(Boolean);
|
||||
return all.length ? all : undefined;
|
||||
};
|
||||
EXPAND['day'] = function (dateS, origin, b, rule) {
|
||||
// Here "b" can be a single day ("TU") or a position and a day ("1MO")
|
||||
var day = getDayData(b);
|
||||
var pos;
|
||||
if (Array.isArray(day)) {
|
||||
pos = day[0];
|
||||
day = day[1];
|
||||
}
|
||||
|
||||
var all = [];
|
||||
if (![0,1,2,3,4,5,6].includes(day)) { return false; }
|
||||
|
||||
var filterPos = function (m) {
|
||||
if (!pos) { return; }
|
||||
|
||||
var _all = [];
|
||||
'aaaaaaaaaaaa'.split('').some(function (a, i) {
|
||||
if (typeof(m) !== "undefined" && i !== m) { return; }
|
||||
|
||||
var _pos;
|
||||
var tmp = all.filter(function (d) {
|
||||
return d.getMonth() === i;
|
||||
});
|
||||
if (pos < 0) {
|
||||
_pos = tmp.length + pos;
|
||||
} else {
|
||||
_pos = pos - 1; // An array starts at 0 but the recurrence rule starts at 1
|
||||
}
|
||||
_all.push(tmp[_pos]);
|
||||
|
||||
return typeof(m) !== "undefined" && i === m;
|
||||
});
|
||||
all = _all.filter(Boolean); // The "5th" {day} won't always exist
|
||||
};
|
||||
|
||||
var tmp;
|
||||
if (rule.freq === 'yearly') {
|
||||
tmp = new Date(+dateS);
|
||||
var y = dateS.getFullYear();
|
||||
while (tmp.getDay() !== day) { tmp.setDate(tmp.getDate()+1); }
|
||||
while (tmp.getFullYear() === y) {
|
||||
all.push(new Date(+tmp));
|
||||
tmp.setDate(tmp.getDate()+7);
|
||||
}
|
||||
filterPos();
|
||||
return all;
|
||||
}
|
||||
|
||||
if (rule.freq === 'monthly') {
|
||||
tmp = new Date(+dateS);
|
||||
var m = dateS.getMonth();
|
||||
while (tmp.getDay() !== day) { tmp.setDate(tmp.getDate()+1); }
|
||||
while (tmp.getMonth() === m) {
|
||||
all.push(new Date(+tmp));
|
||||
tmp.setDate(tmp.getDate()+7);
|
||||
}
|
||||
filterPos(m);
|
||||
return all;
|
||||
}
|
||||
|
||||
if (rule.freq === 'weekly') {
|
||||
while (dateS.getDay() !== day) { dateS.setDate(dateS.getDate()+1); }
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
var LIMIT = {};
|
||||
LIMIT['month'] = function (events, rule) {
|
||||
return events.filter(function (s) {
|
||||
return rule.includes(s.getMonth()+1);
|
||||
});
|
||||
};
|
||||
LIMIT['weekno'] = function (events, weeks, rules) {
|
||||
return events.filter(function (s) {
|
||||
var wkst = rules && rules.wkst;
|
||||
if (typeof(wkst) !== "number") { wkst = 1; } // Default monday
|
||||
|
||||
var lastD = new Date(s.getFullYear(), 11, 31); // December 31st
|
||||
var lastW = getWeekNo(lastD, wkst); // Last week of the year is either 52 or 53
|
||||
if (lastW === 1) { lastW = 52; }
|
||||
|
||||
var w = getWeekNo(s, wkst);
|
||||
|
||||
return weeks.some(function (week) {
|
||||
if (week > 0) { return week === w; }
|
||||
return w === (lastW + week + 1);
|
||||
});
|
||||
});
|
||||
};
|
||||
LIMIT['yearday'] = function (events, days) {
|
||||
return events.filter(function (s) {
|
||||
var d = getYearDay(s);
|
||||
var max = getYearDay(new Date(s.getFullYear(), 11, 31));
|
||||
|
||||
return days.some(function (day) {
|
||||
if (day > 0) { return day === d; }
|
||||
return d === (max + day + 1);
|
||||
});
|
||||
});
|
||||
};
|
||||
LIMIT['monthday'] = function (events, rule) {
|
||||
return events.filter(function (s) {
|
||||
var r = Util.clone(rule);
|
||||
// Transform the negative monthdays into positive for this specific month
|
||||
r = r.map(function (b) {
|
||||
if (b < 0) {
|
||||
var tmp = new Date(s.getFullYear(), s.getMonth()+1, 0); // Last day
|
||||
b = tmp.getDate() + b + 1;
|
||||
}
|
||||
return b;
|
||||
});
|
||||
return r.includes(s.getDate());
|
||||
});
|
||||
};
|
||||
LIMIT['day'] = function (events, days, rules) {
|
||||
return events.filter(function (s) {
|
||||
var dayStr = s.toLocaleDateString();
|
||||
|
||||
// Check how to handle position in BYDAY rules (last day of the month or the year?)
|
||||
var type = 'yearly';
|
||||
if (rules.freq === 'monthly' ||
|
||||
(rules.freq === 'yearly' && rules.by && rules.by.month)) {
|
||||
type = 'monthly';
|
||||
}
|
||||
|
||||
// Check if this event matches one of the allowed days
|
||||
return days.some(function (r) {
|
||||
// rule elements are strings with pos and day
|
||||
var day = getDayData(r);
|
||||
var pos;
|
||||
if (Array.isArray(day)) {
|
||||
pos = day[0];
|
||||
day = day[1];
|
||||
}
|
||||
if (!pos) {
|
||||
return s.getDay() === day;
|
||||
}
|
||||
|
||||
// If we have a position, we can use EXPAND.day to get the nth {day} of the
|
||||
// year/month and compare if it matches with
|
||||
var d = new Date(s.getFullYear(), s.getMonth(), 1);
|
||||
if (type === 'yearly') { d.setMonth(0); }
|
||||
var res = EXPAND["day"](d, {}, r, {freq: type});
|
||||
return res.some(function (date) {
|
||||
return date.toLocaleDateString() === dayStr;
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
LIMIT['setpos'] = function (events, rule) {
|
||||
var init = events.slice();
|
||||
var rules = Util.deduplicateString(rule.slice().map(function (n) {
|
||||
if (n > 0) { return (n-1); }
|
||||
if (n === 0) { return; }
|
||||
return init.length + n;
|
||||
}));
|
||||
return events.filter(function (ev) {
|
||||
var idx = init.indexOf(ev);
|
||||
return rules.includes(idx);
|
||||
});
|
||||
};
|
||||
|
||||
var BYORDER = ['month','weekno','yearday','monthday','day'];
|
||||
var BYDAYORDER = ['month','monthday','day'];
|
||||
|
||||
Rec.getMonthId = function (d) {
|
||||
return d.getFullYear() + '-' + d.getMonth();
|
||||
};
|
||||
var cache = window.CP_calendar_cache = {};
|
||||
var recurringAcross = {};
|
||||
Rec.resetCache = function () {
|
||||
cache = window.CP_calendar_cache = {};
|
||||
recurringAcross = {};
|
||||
};
|
||||
|
||||
var iterate = function (rule, _origin, s) {
|
||||
// "origin" is the original event to detect the start of BYxxx
|
||||
var origin = Util.clone(_origin);
|
||||
var oS = new Date(origin.start);
|
||||
|
||||
var id = origin.id.split('|')[0]; // Use same cache when updating recurrence rule
|
||||
|
||||
// "uid" is used for the cache
|
||||
var uid = s.toLocaleDateString();
|
||||
cache[id] = cache[id] || {};
|
||||
|
||||
var inter = rule.interval || 1;
|
||||
var freq = rule.freq;
|
||||
|
||||
var all = [];
|
||||
var limit = function (byrule, n) {
|
||||
all = LIMIT[byrule](all, n, rule);
|
||||
};
|
||||
var expand = function (byrule) {
|
||||
return function (n) {
|
||||
// Set the start date at the beginning of the current FREQ
|
||||
var _s = new Date(+s);
|
||||
if (rule.freq === 'yearly') {
|
||||
// January 1st
|
||||
_s.setMonth(0);
|
||||
_s.setDate(1);
|
||||
} else if (rule.freq === 'monthly') {
|
||||
_s.setDate(1);
|
||||
} else if (rule.freq === 'weekly') {
|
||||
goToFirstWeekDay(_s, rule.wkst);
|
||||
} else if (rule.freq === 'daily') {
|
||||
// We don't have < byday rules so we can't expand daily rules
|
||||
}
|
||||
|
||||
var add = EXPAND[byrule](_s, origin, n, rule);
|
||||
|
||||
if (!add) { return; }
|
||||
|
||||
if (Array.isArray(add)) {
|
||||
add = add.filter(function (dateS) {
|
||||
return dateS.toLocaleDateString() !== oS.toLocaleDateString();
|
||||
});
|
||||
Array.prototype.push.apply(all, add);
|
||||
} else {
|
||||
if (_s.toLocaleDateString() === oS.toLocaleDateString()) { return; }
|
||||
all.push(_s);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Manage interval for the next iteration
|
||||
var it = Util.once(function () {
|
||||
FREQ[freq](s, inter);
|
||||
});
|
||||
var addDefault = function () {
|
||||
if (freq === "monthly") {
|
||||
s.setDate(15);
|
||||
} else if (freq === "yearly" && oS.getMonth() === 1 && oS.getDate() === 29) {
|
||||
s.setDate(28);
|
||||
}
|
||||
|
||||
it();
|
||||
|
||||
var _s = new Date(+s);
|
||||
if (freq === "monthly" || freq === "yearly") {
|
||||
_s.setDate(oS.getDate());
|
||||
if (_s.getDate() !== oS.getDate()) { return; } // If 31st or Feb 29th doesn't exist
|
||||
if (freq === "yearly" && _s.getMonth() !== oS.getMonth()) { return; }
|
||||
|
||||
// FIXME if there is a recUpdate that moves the 31st to the 30th, the event
|
||||
// will still only be displayed on months with 31 days
|
||||
}
|
||||
all.push(_s);
|
||||
};
|
||||
|
||||
if (Array.isArray(cache[id][uid])) {
|
||||
debug('Get cache', id, uid);
|
||||
if (freq === "monthly") {
|
||||
s.setDate(15);
|
||||
} else if (freq === "yearly" && oS.getMonth() === 1 && oS.getDate() === 29) {
|
||||
s.setDate(28);
|
||||
}
|
||||
it();
|
||||
return cache[id][uid];
|
||||
}
|
||||
|
||||
if (rule.by && freq === 'yearly') {
|
||||
var order = BYORDER.slice();
|
||||
var monthLimit = false;
|
||||
if (rule.by.weekno || rule.by.yearday || rule.by.monthday || rule.by.day) {
|
||||
order.shift();
|
||||
monthLimit = true;
|
||||
}
|
||||
var first = true;
|
||||
order.forEach(function (_order) {
|
||||
var r = rule.by[_order];
|
||||
if (!r) { return; }
|
||||
if (first) {
|
||||
r.forEach(expand(_order));
|
||||
first = false;
|
||||
} else if (_order === "day") {
|
||||
if (rule.by.yearday || rule.by.monthday || rule.by.weekno) {
|
||||
limit('day', rule.by.day);
|
||||
} else {
|
||||
rule.by.day.forEach(expand('day'));
|
||||
}
|
||||
} else {
|
||||
limit(_order, r);
|
||||
}
|
||||
});
|
||||
if (rule.by.month && monthLimit) {
|
||||
limit('month', rule.by.month);
|
||||
}
|
||||
}
|
||||
if (rule.by && freq === 'monthly') {
|
||||
// We're going to compute all the entries for the coming month
|
||||
if (!rule.by.monthday && !rule.by.day) {
|
||||
addDefault();
|
||||
} else if (rule.by.monthday) {
|
||||
rule.by.monthday.forEach(expand('monthday'));
|
||||
} else if (rule.by.day) {
|
||||
rule.by.day.forEach(expand('day'));
|
||||
}
|
||||
if (rule.by.month) {
|
||||
limit('month', rule.by.month);
|
||||
}
|
||||
if (rule.by.day && rule.by.monthday) {
|
||||
limit('day', rule.by.day);
|
||||
}
|
||||
}
|
||||
if (rule.by && freq === 'weekly') {
|
||||
// We're going to compute all the entries for the coming week
|
||||
if (!rule.by.day) {
|
||||
addDefault();
|
||||
} else {
|
||||
rule.by.day.forEach(expand('day'));
|
||||
}
|
||||
if (rule.by.month) {
|
||||
limit('month', rule.by.month);
|
||||
}
|
||||
}
|
||||
if (rule.by && freq === 'daily') {
|
||||
addDefault();
|
||||
BYDAYORDER.forEach(function (_order) {
|
||||
var r = rule.by[_order];
|
||||
if (!r) { return; }
|
||||
limit(_order, r);
|
||||
});
|
||||
}
|
||||
|
||||
all.sort(function (a, b) {
|
||||
return a-b;
|
||||
});
|
||||
|
||||
if (rule.by && rule.by.setpos) {
|
||||
limit('setpos', rule.by.setpos);
|
||||
}
|
||||
|
||||
if (!rule.by || !Object.keys(rule.by).length) {
|
||||
addDefault();
|
||||
} else {
|
||||
it();
|
||||
}
|
||||
|
||||
|
||||
var done = [];
|
||||
all = all.filter(function (newS) {
|
||||
var start = new Date(+newS).toLocaleDateString();
|
||||
if (done.includes(start)) { return false; }
|
||||
done.push(start);
|
||||
return true;
|
||||
});
|
||||
|
||||
debug('Set cache', id, uid);
|
||||
cache[id][uid] = all;
|
||||
|
||||
return all;
|
||||
};
|
||||
|
||||
var getNextRules = function (obj) {
|
||||
if (!obj.recUpdate) { return []; }
|
||||
var _allRules = {};
|
||||
var _obj = obj.recUpdate.from;
|
||||
Object.keys(_obj || {}).forEach(function (d) {
|
||||
var u = _obj[d];
|
||||
if (u.recurrenceRule) { _allRules[d] = u.recurrenceRule; }
|
||||
});
|
||||
return Object.keys(_allRules).sort(function (a, b) { return Number(a)-Number(b); })
|
||||
.map(function (k) {
|
||||
var r = Util.clone(_allRules[k]);
|
||||
if (!FREQ[r.freq]) { return; }
|
||||
if (r.interval && r.interval < 1) { return; }
|
||||
r._start = Number(k);
|
||||
return r;
|
||||
}).filter(Boolean);
|
||||
};
|
||||
Rec.getRecurring = function (months, events) {
|
||||
if (window.CP_DEV_MODE) { debug = console.warn; }
|
||||
|
||||
var toAdd = [];
|
||||
months.forEach(function (monthId) {
|
||||
// from 1st day of the month at 00:00 to last day at 23:59:59:999
|
||||
var ms = monthId.split('-');
|
||||
var _startMonth = new Date(ms[0], ms[1]);
|
||||
var _endMonth = new Date(+_startMonth);
|
||||
_endMonth.setMonth(_endMonth.getMonth() + 1);
|
||||
_endMonth.setMilliseconds(-1);
|
||||
|
||||
debug('Compute month', _startMonth.toLocaleDateString());
|
||||
|
||||
var rec = events || [];
|
||||
rec.forEach(function (obj) {
|
||||
var _start = new Date(obj.start);
|
||||
var _end = new Date(obj.end);
|
||||
var _origin = obj;
|
||||
var rule = obj.recurrenceRule;
|
||||
if (!rule) { return; }
|
||||
|
||||
var nextRules = getNextRules(obj);
|
||||
var nextRule = nextRules.shift();
|
||||
|
||||
if (_start >= _endMonth) { return; }
|
||||
|
||||
// Check the "until" date of the latest rule we can use and stop now
|
||||
// if the recurrence ends before the current month
|
||||
var until = rule.until;
|
||||
var _nextRules = nextRules.slice();
|
||||
var _nextRule = nextRule;
|
||||
while (_nextRule && _nextRule._start && _nextRule._start < _startMonth) {
|
||||
until = nextRule.until;
|
||||
_nextRule = _nextRules.shift();
|
||||
}
|
||||
if (until < _startMonth) { return; }
|
||||
|
||||
var endData = getEndData(_start, _end);
|
||||
|
||||
if (rule.interval && rule.interval < 1) { return; }
|
||||
if (!FREQ[rule.freq]) { return; }
|
||||
|
||||
/*
|
||||
// Rule examples
|
||||
rule.by = {
|
||||
//month: [1, 4, 5, 8, 12],
|
||||
//weekno: [1, 2, 4, 5, 32, 34, 35, 50],
|
||||
//yearday: [1, 2, 29, 30, -2, -1, 250],
|
||||
//monthday: [1, 2, 3, -3, -2, -1],
|
||||
//day: ["MO", "WE", "FR"],
|
||||
//setpos: [1, 2, -1, -2]
|
||||
};
|
||||
rule.wkst = 0;
|
||||
rule.interval = 2;
|
||||
rule.freq = 'yearly';
|
||||
rule.count = 10;
|
||||
*/
|
||||
debug('Iterate over', obj.title, obj);
|
||||
debug('Use rule', rule);
|
||||
|
||||
var count = rule.count;
|
||||
var c = 1;
|
||||
|
||||
var next = function (start) {
|
||||
var evS = new Date(+start);
|
||||
|
||||
if (count && c >= count) { return; }
|
||||
|
||||
debug('Start iteration', evS.toLocaleDateString());
|
||||
|
||||
var _toAdd = iterate(rule, obj, evS);
|
||||
|
||||
debug('Iteration results', JSON.stringify(_toAdd.map(function (o) { return new Date(o).toLocaleDateString();})));
|
||||
|
||||
// Make sure to continue if the current year doesn't provide any result
|
||||
if (!_toAdd.length) {
|
||||
if (evS.getFullYear() < _startMonth.getFullYear() ||
|
||||
evS < _endMonth) {
|
||||
return void next(evS);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var stop = false;
|
||||
var newrule = false;
|
||||
_toAdd.some(function (_newS) {
|
||||
// Make event with correct start and end time
|
||||
var _ev = Util.clone(obj);
|
||||
_ev.id = _origin.id + '|' + (+_newS);
|
||||
var _evS = new Date(+_newS);
|
||||
var _evE = new Date(+_newS);
|
||||
setEndData(_evS, _evE, endData);
|
||||
_ev.start = +_evS;
|
||||
_ev.end = +_evE;
|
||||
_ev._count = c;
|
||||
if (_ev.isAllDay && _ev.startDay) { _ev.startDay = getDateStr(_evS); }
|
||||
if (_ev.isAllDay && _ev.endDay) { _ev.endDay = getDateStr(_evE); }
|
||||
|
||||
if (nextRule && _ev.start === nextRule._start) {
|
||||
newrule = true;
|
||||
}
|
||||
|
||||
var useNewRule = function () {
|
||||
if (!newrule) { return; }
|
||||
debug('Use new rule', nextRule);
|
||||
_ev._count = c;
|
||||
count = nextRule.count;
|
||||
c = 1;
|
||||
evS = +_evS;
|
||||
obj = _ev;
|
||||
rule = nextRule;
|
||||
nextRule = nextRules.shift();
|
||||
};
|
||||
|
||||
|
||||
if (c >= count) { // Limit reached
|
||||
debug(_evS.toLocaleDateString(), 'count');
|
||||
stop = true;
|
||||
return true;
|
||||
}
|
||||
if (_evS >= _endMonth) { // Won't affect us anymore
|
||||
debug(_evS.toLocaleDateString(), 'endMonth');
|
||||
stop = true;
|
||||
return true;
|
||||
}
|
||||
if (rule.until && _evS > rule.until) {
|
||||
debug(_evS.toLocaleDateString(), 'until');
|
||||
stop = true;
|
||||
return true;
|
||||
}
|
||||
if (_evS < _start) { // "Expand" rules may create events before the _start
|
||||
debug(_evS.toLocaleDateString(), 'start');
|
||||
return;
|
||||
}
|
||||
c++;
|
||||
if (_evE < _startMonth) { // Ended before the current month
|
||||
// Nothing to display but continue the recurrence
|
||||
debug(_evS.toLocaleDateString(), 'startMonth');
|
||||
if (newrule) { useNewRule(); }
|
||||
return;
|
||||
}
|
||||
// If a recurring event start and end in different months, make sure
|
||||
// it is only added once
|
||||
if ((_evS < _endMonth && _evE >= _endMonth) ||
|
||||
(_evS < _startMonth && _evE >= _startMonth)) {
|
||||
if (recurringAcross[_ev.id] && recurringAcross[_ev.id].includes(_ev.start)) {
|
||||
return;
|
||||
} else {
|
||||
recurringAcross[_ev.id] = recurringAcross[_ev.id] || [];
|
||||
recurringAcross[_ev.id].push(_ev.start);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Add this event
|
||||
toAdd.push(_ev);
|
||||
if (newrule) {
|
||||
useNewRule();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (!stop) { next(evS); }
|
||||
};
|
||||
next(_start);
|
||||
debug('Added this month (all events)', toAdd.map(function (ev) {
|
||||
return new Date(ev.start).toLocaleDateString();
|
||||
}));
|
||||
});
|
||||
});
|
||||
return toAdd;
|
||||
};
|
||||
Rec.getAllOccurrences = function (ev) {
|
||||
if (!ev.recurrenceRule) { return [ev.start]; }
|
||||
var r = ev.recurrenceRule;
|
||||
// In case of infinite recursion, we can't get all
|
||||
if (!r.until && !r.count) { return false; }
|
||||
var all = [ev.start];
|
||||
var d = new Date(ev.start);
|
||||
d.setDate(15); // Make sure we won't skip a month if the event starts on day > 28
|
||||
var toAdd = [];
|
||||
|
||||
var i = 0;
|
||||
var check = function () {
|
||||
return r.count ? (all.length < r.count) : (+d <= r.until);
|
||||
};
|
||||
while ((toAdd = Rec.getRecurring([Rec.getMonthId(d)], [ev])) && check() && i < (r.count*12)) {
|
||||
Array.prototype.push.apply(all, toAdd.map(function (_ev) { return _ev.start; }));
|
||||
d.setMonth(d.getMonth() + 1);
|
||||
i++;
|
||||
}
|
||||
|
||||
return all;
|
||||
};
|
||||
|
||||
Rec.diffDate = function (oldTime, newTime) {
|
||||
var n = new Date(newTime);
|
||||
var o = new Date(oldTime);
|
||||
|
||||
// Diff Days
|
||||
var d = 0;
|
||||
var mult = n < o ? -1 : 1;
|
||||
while (n.toLocaleDateString() !== o.toLocaleDateString() || mult >= 10000) {
|
||||
n.setDate(n.getDate() - mult);
|
||||
d++;
|
||||
}
|
||||
d = mult * d;
|
||||
|
||||
// Diff hours
|
||||
n = new Date(newTime);
|
||||
var h = n.getHours() - o.getHours();
|
||||
|
||||
// Diff minutes
|
||||
var m = n.getMinutes() - o.getMinutes();
|
||||
|
||||
return {
|
||||
d: d,
|
||||
h: h,
|
||||
m: m
|
||||
};
|
||||
};
|
||||
|
||||
var sortUpdate = function (obj) {
|
||||
return Object.keys(obj).sort(function (d1, d2) {
|
||||
return Number(d1) - Number(d2);
|
||||
});
|
||||
};
|
||||
Rec.applyUpdates = function (events) {
|
||||
events.forEach(function (ev) {
|
||||
ev.raw = {
|
||||
start: ev.start,
|
||||
end: ev.end,
|
||||
};
|
||||
|
||||
if (!ev.recUpdate) { return; }
|
||||
|
||||
var from = ev.recUpdate.from || {};
|
||||
var one = ev.recUpdate.one || {};
|
||||
var s = ev.start;
|
||||
|
||||
// Add "until" date to our recurrenceRule if it has been modified in future occurences
|
||||
var nextRules = getNextRules(ev).filter(function (r) {
|
||||
return r._start > s;
|
||||
});
|
||||
var nextRule = nextRules.shift();
|
||||
|
||||
var applyDiff = function (obj, k) {
|
||||
var diff = obj[k]; // Diff is always compared to origin start/end
|
||||
var d = new Date(ev.raw[k]);
|
||||
d.setDate(d.getDate() + diff.d);
|
||||
d.setHours(d.getHours() + diff.h);
|
||||
d.setMinutes(d.getMinutes() + diff.m);
|
||||
ev[k] = +d;
|
||||
};
|
||||
|
||||
sortUpdate(from).forEach(function (d) {
|
||||
if (s < Number(d)) { return; }
|
||||
Object.keys(from[d]).forEach(function (k) {
|
||||
if (k === 'start' || k === 'end') { return void applyDiff(from[d], k); }
|
||||
if (k === "recurrenceRule" && !from[d][k]) { return; }
|
||||
ev[k] = from[d][k];
|
||||
});
|
||||
});
|
||||
Object.keys(one[s] || {}).forEach(function (k) {
|
||||
if (k === 'start' || k === 'end') { return void applyDiff(one[s], k); }
|
||||
if (k === "recurrenceRule" && !one[s][k]) { return; }
|
||||
ev[k] = one[s][k];
|
||||
});
|
||||
if (ev.deleted) {
|
||||
Object.keys(ev).forEach(function (k) {
|
||||
delete ev[k];
|
||||
});
|
||||
}
|
||||
|
||||
if (nextRule && ev.recurrenceRule) {
|
||||
ev.recurrenceRule._next = nextRule._start - 1;
|
||||
}
|
||||
|
||||
if (ev.reminders) {
|
||||
ev.raw.reminders = ev.reminders;
|
||||
}
|
||||
});
|
||||
return events;
|
||||
};
|
||||
|
||||
|
||||
return Rec;
|
||||
});
|
|
@ -68,6 +68,7 @@ define([
|
|||
'markdown',
|
||||
'gfm',
|
||||
'html',
|
||||
'asciidoc',
|
||||
'htmlembedded',
|
||||
'htmlmixed',
|
||||
'index.html',
|
||||
|
@ -143,6 +144,31 @@ define([
|
|||
previews['htmlmixed'] = function (val, $div, common) {
|
||||
DiffMd.apply(val, $div, common);
|
||||
};
|
||||
previews['asciidoc'] = function (val, $div, common) {
|
||||
require([
|
||||
'asciidoctor',
|
||||
'/lib/highlight/highlight.pack.js',
|
||||
'css!/lib/highlight/styles/' + (window.CryptPad_theme === 'dark' ? 'dark.css' : 'github.css')
|
||||
], function (asciidoctor) {
|
||||
var reg = asciidoctor.Extensions.create();
|
||||
var Highlight = window.hljs;
|
||||
|
||||
reg.inlineMacro('media-tag', function () {
|
||||
var t = this;
|
||||
t.process(function (parent, target) {
|
||||
var d = target.split('|');
|
||||
return t.createInline(parent, 'quoted', `<media-tag src="${d[0]}" data-crypto-key="${d[1]}"></media-tag>`).convert();
|
||||
});
|
||||
});
|
||||
|
||||
var html = asciidoctor.convert(val, { attributes: 'showtitle', extension_registry: reg });
|
||||
|
||||
DiffMd.apply(html, $div, common);
|
||||
$div.find('pre code').each(function (i, el) {
|
||||
Highlight.highlightBlock(el);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var mkPreviewPane = function (editor, CodeMirror, framework, isPresentMode) {
|
||||
var $previewContainer = $('#cp-app-code-preview');
|
||||
|
@ -370,9 +396,17 @@ define([
|
|||
evModeChange.reg(function (mode) {
|
||||
if (MEDIA_TAG_MODES.indexOf(mode) !== -1) {
|
||||
// Embedding is enabled
|
||||
framework.setMediaTagEmbedder(function (mt) {
|
||||
framework.setMediaTagEmbedder(function (mt, d) {
|
||||
editor.focus();
|
||||
editor.replaceSelection($(mt)[0].outerHTML);
|
||||
var txt = $(mt)[0].outerHTML;
|
||||
if (editor.getMode().name === "asciidoc") {
|
||||
if (d.static) {
|
||||
txt = d.href + `[${d.name}]`;
|
||||
} else {
|
||||
txt = `media-tag:${d.src}|${d.key}[]`;
|
||||
}
|
||||
}
|
||||
editor.replaceSelection(txt);
|
||||
});
|
||||
} else {
|
||||
// Embedding is disabled
|
||||
|
|
|
@ -352,10 +352,21 @@ define([
|
|||
});
|
||||
|
||||
var linkName, linkPassword, linkMessage, linkError, linkSpinText;
|
||||
var linkForm, linkSpin, linkResult;
|
||||
var linkForm, linkSpin, linkResult, linkUses, linkRole;
|
||||
var linkWarning;
|
||||
// Invite from link
|
||||
var dismissButton = h('span.fa.fa-times');
|
||||
|
||||
var roleViewer = UI.createRadio('cp-team-role', 'cp-team-role-viewer',
|
||||
Messages.team_viewers, true, {
|
||||
input: { value: 'VIEWER' },
|
||||
});
|
||||
var roleMember = UI.createRadio('cp-team-role', 'cp-team-role-member',
|
||||
Messages.team_members, false, {
|
||||
input: { value: 'MEMBER' },
|
||||
});
|
||||
|
||||
|
||||
var linkContent = h('div.cp-share-modal', [
|
||||
h('p', Messages.team_inviteLinkTitle ),
|
||||
linkError = h('div.alert.alert-danger.cp-teams-invite-alert', {style : 'display: none;'}),
|
||||
|
@ -372,7 +383,7 @@ define([
|
|||
h('div.cp-teams-invite-block', [
|
||||
h('span', Messages.team_inviteLinkSetPassword),
|
||||
h('a.cp-teams-help.fa.fa-question-circle', {
|
||||
href: origin + Pages.localizeDocsLink('https://docs.cryptpad.org/en/user_guide/security.html#passwords-for-documents-and-folders'),
|
||||
href: Pages.localizeDocsLink('https://docs.cryptpad.org/en/user_guide/security.html#passwords-for-documents-and-folders'),
|
||||
target: "_blank",
|
||||
'data-tippy-placement': "right"
|
||||
})
|
||||
|
@ -387,7 +398,21 @@ define([
|
|||
linkMessage = h('textarea.cp-teams-invite-message', {
|
||||
placeholder: Messages.team_inviteLinkNoteMsg,
|
||||
rows: 3
|
||||
})
|
||||
}),
|
||||
linkRole = h('div.cp-teams-invite-block.cp-teams-invite-role',
|
||||
h('span', Messages.team_inviteRole),
|
||||
roleViewer,
|
||||
roleMember
|
||||
),
|
||||
h('div.cp-teams-invite-block.cp-teams-invite-uses',
|
||||
linkUses = h('input', {
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 999,
|
||||
value: 1
|
||||
}),
|
||||
h('span', Messages.team_inviteUses)
|
||||
),
|
||||
]),
|
||||
linkSpin = h('div.cp-teams-invite-spinner', {
|
||||
style: 'display: none;'
|
||||
|
@ -407,10 +432,11 @@ define([
|
|||
dismissButton
|
||||
])
|
||||
]);
|
||||
$(linkUses).on('change keyup', function(e) {
|
||||
if (e.target.value === '') { e.target.value = 0; }
|
||||
});
|
||||
$(linkMessage).keydown(function (e) {
|
||||
if (e.which === 13) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
if (e.which === 13) { e.stopPropagation(); }
|
||||
});
|
||||
var localStore = window.cryptpadStore;
|
||||
localStore.get('hide-alert-teamInvite', function (val) {
|
||||
|
@ -428,6 +454,12 @@ define([
|
|||
var $nav = $linkContent.closest('.alertify').find('nav');
|
||||
$(linkError).text('').hide();
|
||||
var name = $(linkName).val();
|
||||
|
||||
var uses = Number($(linkUses).val());
|
||||
if (isNaN(uses) || !uses) { uses = -1; }
|
||||
|
||||
var role = $(linkRole).find("input[name='cp-team-role']:checked").val() || 'VIEWER';
|
||||
|
||||
var pw = $(linkPassword).find('input').val();
|
||||
var msg = $(linkMessage).val();
|
||||
var hash = Hash.createRandomHash('invite', pw);
|
||||
|
@ -461,6 +493,8 @@ define([
|
|||
hash: hash,
|
||||
teamId: config.teamId,
|
||||
seeds: seeds,
|
||||
role: role,
|
||||
uses: uses
|
||||
}, waitFor(function (obj) {
|
||||
if (obj && obj.error) {
|
||||
waitFor.abort();
|
||||
|
@ -1417,7 +1451,7 @@ define([
|
|||
return /HTML/.test(Object.prototype.toString.call(o)) &&
|
||||
typeof(o.tagName) === 'string';
|
||||
};
|
||||
var allowedTags = ['a', 'p', 'hr', 'div'];
|
||||
var allowedTags = ['a', 'li', 'p', 'hr', 'div'];
|
||||
var isValidOption = function (o) {
|
||||
if (typeof o !== "object") { return false; }
|
||||
if (isElement(o)) { return true; }
|
||||
|
@ -1541,6 +1575,7 @@ define([
|
|||
$innerblock.find('.cp-dropdown-element-active').removeClass('cp-dropdown-element-active');
|
||||
if (config.isSelect && value) {
|
||||
// We use JSON.stringify here to escape quotes
|
||||
if (typeof(value) === "object") { value = JSON.stringify(value); }
|
||||
var $val = $innerblock.find('[data-value='+JSON.stringify(value)+']');
|
||||
setActive($val);
|
||||
try {
|
||||
|
|
|
@ -125,6 +125,12 @@ define([
|
|||
formSeed = obj;
|
||||
}));
|
||||
}).nThen(function () {
|
||||
if (!formSeed) { // no drive mode
|
||||
formSeed = localStorage.CP_formSeed || Hash.createChannelId();
|
||||
localStorage.CP_formSeed = formSeed;
|
||||
} else {
|
||||
delete localStorage.CP_formSeed;
|
||||
}
|
||||
cb({
|
||||
curvePrivate: curvePrivate,
|
||||
curvePublic: curvePrivate && Hash.getCurvePublicFromPrivate(curvePrivate),
|
||||
|
@ -135,28 +141,162 @@ define([
|
|||
common.getFormAnswer = function (data, cb) {
|
||||
postMessage("GET", {
|
||||
key: ['forms', data.channel],
|
||||
}, cb);
|
||||
};
|
||||
common.storeFormAnswer = function (data) {
|
||||
postMessage("SET", {
|
||||
key: ['forms', data.channel],
|
||||
value: {
|
||||
hash: data.hash,
|
||||
curvePrivate: data.curvePrivate,
|
||||
anonymous: data.anonymous
|
||||
}
|
||||
}, function (obj) {
|
||||
if (obj && obj.error) {
|
||||
if (obj.error === "ENODRIVE") {
|
||||
var answered = JSON.parse(localStorage.CP_formAnswered || "[]");
|
||||
if (answered.indexOf(data.channel) === -1) { answered.push(data.channel); }
|
||||
localStorage.CP_formAnswered = JSON.stringify(answered);
|
||||
return;
|
||||
}
|
||||
console.error(obj.error);
|
||||
if (obj && obj.error === "ENODRIVE") {
|
||||
var all = Util.tryParse(localStorage.CP_formAnswers || "{}");
|
||||
return void cb(all[data.channel]);
|
||||
}
|
||||
});
|
||||
if (obj && obj.error) { return void cb(obj); }
|
||||
|
||||
if (obj) {
|
||||
if (!Array.isArray(obj)) { obj = [obj]; }
|
||||
return void cb(obj);
|
||||
}
|
||||
|
||||
// We have a drive and no answer but maybe we had
|
||||
// previous "nodrive" answers: migrate
|
||||
var old = Util.tryParse(localStorage.CP_formAnswers || "{}");
|
||||
if (Array.isArray(old[data.channel])) {
|
||||
var d = old[data.channel];
|
||||
return void postMessage("SET", {
|
||||
key: ['forms', data.channel],
|
||||
value: d
|
||||
}, function (obj) {
|
||||
// Delete old data if it was correctly stored in the drive
|
||||
if (obj && obj.error) { return void cb(d); }
|
||||
delete old[data.channel];
|
||||
localStorage.CP_formAnswers = JSON.stringify(old);
|
||||
cb(d);
|
||||
});
|
||||
}
|
||||
|
||||
cb();
|
||||
});
|
||||
};
|
||||
common.storeFormAnswer = function (data, cb) {
|
||||
var answer = {
|
||||
uid: data.uid,
|
||||
hash: data.hash,
|
||||
curvePrivate: data.curvePrivate,
|
||||
anonymous: data.anonymous
|
||||
};
|
||||
var answers = [];
|
||||
Nthen(function (waitFor) {
|
||||
common.getFormAnswer(data, waitFor(function (obj) {
|
||||
if (!obj || obj.error) { return; }
|
||||
answers = obj;
|
||||
}));
|
||||
}).nThen(function () {
|
||||
answers.push(answer);
|
||||
postMessage("SET", {
|
||||
key: ['forms', data.channel],
|
||||
value: answers
|
||||
}, function (obj) {
|
||||
if (obj && obj.error) {
|
||||
if (obj.error === "ENODRIVE") {
|
||||
var all = Util.tryParse(localStorage.CP_formAnswers || "{}");
|
||||
all[data.channel] = answers;
|
||||
localStorage.CP_formAnswers = JSON.stringify(all);
|
||||
/*
|
||||
|
||||
var answered = JSON.parse(localStorage.CP_formAnswered || "[]");
|
||||
if (answered.indexOf(data.channel) === -1) { answered.push(data.channel); }
|
||||
localStorage.CP_formAnswered = JSON.stringify(answered);
|
||||
*/
|
||||
return void cb();
|
||||
}
|
||||
console.error(obj.error);
|
||||
}
|
||||
cb();
|
||||
});
|
||||
});
|
||||
};
|
||||
common.deleteFormAnswers = function (data, _cb) {
|
||||
var cb = Util.once(_cb);
|
||||
common.getFormAnswer(data, function (obj) {
|
||||
if (!obj || obj.error) { return void cb(); }
|
||||
if (!obj.length) { return void cb(); }
|
||||
var n = Nthen;
|
||||
var nacl, theirs;
|
||||
n = n(function (waitFor) {
|
||||
require(['/bower_components/tweetnacl/nacl-fast.min.js'], waitFor(function () {
|
||||
nacl = window.nacl;
|
||||
var s = new Uint8Array(32);
|
||||
theirs = nacl.box.keyPair.fromSecretKey(s);
|
||||
}));
|
||||
}).nThen;
|
||||
var toDelete = [];
|
||||
obj.forEach(function (answer) {
|
||||
if (answer.uid !== data.uid) { return; }
|
||||
n = n(function (waitFor) {
|
||||
var hash = answer.hash;
|
||||
var h = nacl.util.decodeUTF8(hash);
|
||||
|
||||
// Make proof
|
||||
var curve = answer.curvePrivate;
|
||||
var mySecret = nacl.util.decodeBase64(curve);
|
||||
var nonce = nacl.randomBytes(24);
|
||||
var proofBytes = nacl.box(h, nonce, theirs.publicKey, mySecret);
|
||||
var proof = nacl.util.encodeBase64(nonce) +'|'+ nacl.util.encodeBase64(proofBytes);
|
||||
var lineData = {
|
||||
channel: data.channel,
|
||||
hash: hash,
|
||||
proof: proof
|
||||
};
|
||||
postMessage("DELETE_MAILBOX_MESSAGE", lineData, waitFor(function (obj) {
|
||||
if (obj && obj.error && obj.error !== 'HASH_NOT_FOUND') {
|
||||
// If HASH_NOT_FOUND, the message is already deleted
|
||||
// so we can delete it locally
|
||||
waitFor.abort();
|
||||
return void cb(obj);
|
||||
}
|
||||
toDelete.push(hash);
|
||||
}));
|
||||
}).nThen;
|
||||
});
|
||||
n(function () {
|
||||
obj = obj.filter(function (answer) { return !toDelete.includes(answer.hash); });
|
||||
if (!obj.length) { obj = undefined; }
|
||||
postMessage("SET", {
|
||||
key: ['forms', data.channel],
|
||||
value: obj
|
||||
}, function (_obj) {
|
||||
if (_obj && _obj.error === "ENODRIVE") {
|
||||
var all = Util.tryParse(localStorage.CP_formAnswers || "{}");
|
||||
if (obj) { all[data.channel] = obj; }
|
||||
else { delete all[data.channel]; }
|
||||
localStorage.CP_formAnswers = JSON.stringify(all);
|
||||
return void cb();
|
||||
}
|
||||
return void cb(_obj);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
common.muteChannel = function (channel, state, cb) {
|
||||
var mutedChannels = [];
|
||||
Nthen(function (waitFor) {
|
||||
postMessage("GET", {
|
||||
key: ['mutedChannels'],
|
||||
}, waitFor(function (obj) {
|
||||
if (obj && obj.error) { waitFor.abort(); return void cb(obj); }
|
||||
mutedChannels = obj || [];
|
||||
}));
|
||||
}).nThen(function () {
|
||||
if (state) {
|
||||
if (!mutedChannels.includes(channel)) {
|
||||
mutedChannels.push(channel);
|
||||
}
|
||||
} else {
|
||||
mutedChannels = mutedChannels.filter(function (chan) {
|
||||
return chan !== channel;
|
||||
});
|
||||
}
|
||||
postMessage("SET", {
|
||||
key: ['mutedChannels'],
|
||||
value: mutedChannels
|
||||
}, cb);
|
||||
});
|
||||
};
|
||||
|
||||
common.makeNetwork = function (cb) {
|
||||
|
@ -1154,8 +1294,8 @@ define([
|
|||
pad.onMetadataEvent = Util.mkEvent();
|
||||
pad.onChannelDeleted = Util.mkEvent();
|
||||
|
||||
pad.requestAccess = function (data, cb) {
|
||||
postMessage("REQUEST_PAD_ACCESS", data, cb);
|
||||
pad.contactOwner = function (data, cb) {
|
||||
postMessage("CONTACT_PAD_OWNER", data, cb);
|
||||
};
|
||||
pad.giveAccess = function (data, cb) {
|
||||
postMessage("GIVE_PAD_ACCESS", data, cb);
|
||||
|
@ -2385,6 +2525,7 @@ define([
|
|||
anonHash: LocalStore.getFSHash(),
|
||||
localToken: tryParsing(localStorage.getItem(Constants.tokenKey)), // TODO move this to LocalStore ?
|
||||
language: common.getLanguage(),
|
||||
form_seed: localStorage.CP_formSeed,
|
||||
cache: rdyCfg.cache,
|
||||
noDrive: rdyCfg.noDrive,
|
||||
disableCache: localStorage['CRYPTPAD_STORE|disableCache'],
|
||||
|
|
|
@ -78,6 +78,7 @@ define([
|
|||
var TAGS_NAME = Messages.fm_tagsName;
|
||||
var SHARED_FOLDER = 'sf';
|
||||
var SHARED_FOLDER_NAME = Messages.fm_sharedFolderName;
|
||||
var FILTER = "filter";
|
||||
|
||||
// Icons
|
||||
var faFolder = 'cptools-folder';
|
||||
|
@ -1149,8 +1150,12 @@ define([
|
|||
common.getMediaTagPreview(mts, idx);
|
||||
};
|
||||
|
||||
var refresh = APP.refresh = function () {
|
||||
APP.displayDirectory(currentPath);
|
||||
var FILTER_BY = "filterBy";
|
||||
|
||||
var refresh = APP.refresh = function (cb) {
|
||||
var type = APP.store[FILTER_BY];
|
||||
var path = type ? [FILTER, type, currentPath] : currentPath;
|
||||
APP.displayDirectory(path, undefined, cb);
|
||||
};
|
||||
|
||||
// `app`: true (force open wiht the app), false (force open in preview),
|
||||
|
@ -2940,72 +2945,147 @@ define([
|
|||
$block.find('a.cp-app-drive-new-link, li.cp-app-drive-new-link').click(showLinkModal);
|
||||
}
|
||||
$block.find('a.cp-app-drive-new-doc, li.cp-app-drive-new-doc')
|
||||
.click(function () {
|
||||
.on('click auxclick', function (e) {
|
||||
e.preventDefault();
|
||||
var type = $(this).attr('data-type') || 'pad';
|
||||
var path = manager.isPathIn(currentPath, [TRASH]) ? '' : currentPath;
|
||||
openIn(type, path, APP.team);
|
||||
});
|
||||
};
|
||||
var getNewPadOptions = function (isInRoot) {
|
||||
var options = [];
|
||||
if (isInRoot) {
|
||||
options.push({
|
||||
class: 'cp-app-drive-new-folder',
|
||||
icon: $folderIcon.clone()[0],
|
||||
name: Messages.fm_folder,
|
||||
});
|
||||
if (!APP.disableSF && !manager.isInSharedFolder(currentPath)) {
|
||||
options.push({
|
||||
class: 'cp-app-drive-new-shared-folder',
|
||||
icon: $sharedFolderIcon.clone()[0],
|
||||
name: Messages.fm_sharedFolder,
|
||||
});
|
||||
}
|
||||
options.push({ separator: true });
|
||||
options.push({
|
||||
class: 'cp-app-drive-new-fileupload',
|
||||
icon: getIcon('fileupload')[0],
|
||||
name: Messages.uploadButton,
|
||||
});
|
||||
if (APP.allowFolderUpload) {
|
||||
options.push({
|
||||
class: 'cp-app-drive-new-folderupload',
|
||||
icon: getIcon('folderupload')[0],
|
||||
name: Messages.uploadFolderButton,
|
||||
});
|
||||
}
|
||||
options.push({ separator: true });
|
||||
options.push({
|
||||
class: 'cp-app-drive-new-link',
|
||||
icon: getIcon('link')[0],
|
||||
name: Messages.fm_link_new,
|
||||
});
|
||||
options.push({ separator: true });
|
||||
}
|
||||
getNewPadTypes().forEach(function (type) {
|
||||
var typeClass = 'cp-app-drive-new-doc';
|
||||
|
||||
var premium = common.checkRestrictedApp(type);
|
||||
if (premium < 0) {
|
||||
typeClass += ' cp-app-hidden cp-app-disabled';
|
||||
} else if (premium === 0) {
|
||||
typeClass += ' cp-app-disabled';
|
||||
}
|
||||
|
||||
options.push({
|
||||
class: typeClass,
|
||||
type: type,
|
||||
icon: getIcon(type)[0],
|
||||
name: Messages.type[type],
|
||||
});
|
||||
});
|
||||
|
||||
if (APP.store[FILTER_BY]) {
|
||||
var typeFilter = APP.store[FILTER_BY];
|
||||
options = options.filter((obj) => {
|
||||
if (obj.separator) { return false; }
|
||||
|
||||
if (typeFilter === 'link') {
|
||||
return obj.class.includes('cp-app-drive-new-link');
|
||||
}
|
||||
if (typeFilter === 'file') {
|
||||
return obj.class.includes('cp-app-drive-new-fileupload');
|
||||
}
|
||||
if (getNewPadTypes().indexOf(typeFilter) !== -1) {
|
||||
return typeFilter === obj.type;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
var createNewButton = function (isInRoot, $container) {
|
||||
if (!APP.editable) { return; }
|
||||
if (!APP.loggedIn) { return; } // Anonymous users can use the + menu in the toolbar
|
||||
|
||||
if (!manager.isPathIn(currentPath, [ROOT, 'hrefArray'])) { return; }
|
||||
|
||||
// Create dropdown
|
||||
var options = getNewPadOptions(isInRoot).map(function (obj) {
|
||||
if (obj.separator) { return { tag: 'hr' }; }
|
||||
var newObj = {
|
||||
tag: 'a',
|
||||
attributes: { 'class': obj.class },
|
||||
content: [ obj.icon, obj.name ]
|
||||
};
|
||||
if (obj.type) {
|
||||
newObj.attributes['data-type'] = obj.type;
|
||||
newObj.attributes['href'] = APP.origin + Hash.hashToHref('', obj.type);
|
||||
}
|
||||
return newObj;
|
||||
});
|
||||
var dropdownConfig = {
|
||||
buttonContent: [
|
||||
h('i.fa.fa-plus'),
|
||||
h('span.cp-button-name', Messages.fm_newButton),
|
||||
],
|
||||
buttonCls: 'cp-toolbar-dropdown-nowrap',
|
||||
options: options,
|
||||
feedback: 'DRIVE_NEWPAD_LOCALFOLDER',
|
||||
common: common
|
||||
};
|
||||
var $block = UIElements.createDropdown(dropdownConfig);
|
||||
|
||||
// Custom style:
|
||||
$block.find('button').addClass('cp-app-drive-toolbar-new');
|
||||
|
||||
addNewPadHandlers($block, isInRoot);
|
||||
|
||||
$container.append($block);
|
||||
};
|
||||
|
||||
var createFilterButton = function (isTemplate, $container) {
|
||||
if (!APP.loggedIn) { return; }
|
||||
|
||||
// Create dropdown
|
||||
var options = [];
|
||||
if (isInRoot) {
|
||||
if (APP.store[FILTER_BY]) {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'cp-app-drive-new-folder pewpew'},
|
||||
attributes: {
|
||||
'class': 'cp-app-drive-rm-filter',
|
||||
},
|
||||
content: [
|
||||
$folderIcon.clone()[0],
|
||||
Messages.fm_folder,
|
||||
],
|
||||
});
|
||||
if (!APP.disableSF && !manager.isInSharedFolder(currentPath)) {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'cp-app-drive-new-shared-folder'},
|
||||
content: [
|
||||
$sharedFolderIcon.clone()[0],
|
||||
Messages.fm_sharedFolder,
|
||||
],
|
||||
});
|
||||
}
|
||||
options.push({tag: 'hr'});
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'cp-app-drive-new-fileupload'},
|
||||
content: [
|
||||
getIcon('fileupload')[0],
|
||||
Messages.uploadButton,
|
||||
],
|
||||
});
|
||||
if (APP.allowFolderUpload) {
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'cp-app-drive-new-folderupload'},
|
||||
content: [
|
||||
getIcon('folderupload')[0],
|
||||
Messages.uploadFolderButton,
|
||||
],
|
||||
});
|
||||
}
|
||||
options.push({tag: 'hr'});
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {'class': 'cp-app-drive-new-link'},
|
||||
content: [
|
||||
getIcon('link')[0],
|
||||
Messages.fm_link_new,
|
||||
h('i.fa.fa-times'),
|
||||
Messages.fm_rmFilter,
|
||||
],
|
||||
});
|
||||
options.push({tag: 'hr'});
|
||||
}
|
||||
getNewPadTypes().forEach(function (type) {
|
||||
var attributes = {
|
||||
'class': 'cp-app-drive-new-doc',
|
||||
'class': 'cp-app-drive-filter-doc',
|
||||
'data-type': type,
|
||||
'href': '#'
|
||||
};
|
||||
|
@ -3026,21 +3106,72 @@ define([
|
|||
],
|
||||
});
|
||||
});
|
||||
if (!isTemplate) {
|
||||
options.push({tag: 'hr'});
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {
|
||||
'class': 'cp-app-drive-filter-doc',
|
||||
'data-type': 'link'
|
||||
},
|
||||
content: [
|
||||
getIcon('link')[0],
|
||||
Messages.fm_link_type,
|
||||
],
|
||||
});
|
||||
options.push({
|
||||
tag: 'a',
|
||||
attributes: {
|
||||
'class': 'cp-app-drive-filter-doc',
|
||||
'data-type': 'file',
|
||||
'href': '#'
|
||||
},
|
||||
content: [
|
||||
getIcon('file')[0],
|
||||
Messages.type['file'],
|
||||
],
|
||||
});
|
||||
}
|
||||
var dropdownConfig = {
|
||||
buttonContent: [
|
||||
h('span.fa.fa-plus'),
|
||||
h('span', Messages.fm_newButton),
|
||||
h('i.fa.fa-filter'),
|
||||
h('span.cp-button-name', Messages.fm_filterBy),
|
||||
],
|
||||
buttonCls: 'cp-toolbar-dropdown-nowrap',
|
||||
options: options,
|
||||
feedback: 'DRIVE_NEWPAD_LOCALFOLDER',
|
||||
feedback: 'DRIVE_FILTERBY',
|
||||
common: common
|
||||
};
|
||||
if (APP.store[FILTER_BY]) {
|
||||
var type = APP.store[FILTER_BY];
|
||||
var message = type === 'link' ? Messages.fm_link_type : Messages.type[type];
|
||||
dropdownConfig.buttonContent.push(
|
||||
h('span.cp-button-name', ':'),
|
||||
getIcon(type)[0],
|
||||
h('span.cp-button-name', message)
|
||||
);
|
||||
}
|
||||
var $block = UIElements.createDropdown(dropdownConfig);
|
||||
|
||||
// Custom style:
|
||||
$block.find('button').addClass('cp-app-drive-toolbar-new');
|
||||
// Add style
|
||||
if (APP.store[FILTER_BY]) {
|
||||
$block.find('button').addClass('cp-toolbar-button-active');
|
||||
}
|
||||
|
||||
addNewPadHandlers($block, isInRoot);
|
||||
// Add handlers
|
||||
if (APP.store[FILTER_BY]) {
|
||||
$block.find('a.cp-app-drive-rm-filter')
|
||||
.click(function () {
|
||||
APP.store[FILTER_BY] = undefined;
|
||||
APP.displayDirectory(currentPath);
|
||||
});
|
||||
}
|
||||
$block.find('a.cp-app-drive-filter-doc')
|
||||
.click(function () {
|
||||
var type = $(this).attr('data-type') || 'invalid-filter';
|
||||
APP.store[FILTER_BY] = type;
|
||||
APP.displayDirectory([FILTER, type, currentPath]);
|
||||
});
|
||||
|
||||
$container.append($block);
|
||||
};
|
||||
|
@ -3302,65 +3433,38 @@ define([
|
|||
return keys;
|
||||
};
|
||||
|
||||
var filterPads = function (files, type, path, useId) {
|
||||
var root = path && manager.find(path);
|
||||
|
||||
return files
|
||||
.filter(function (e) {
|
||||
return useId ? manager.isFile(e) : (path && manager.isFile(root[e]));
|
||||
})
|
||||
.filter(function (e) {
|
||||
var id = useId ? e : root[e];
|
||||
var data = manager.getFileData(id);
|
||||
if (type === 'link') { return data.static; }
|
||||
var href = data.href || data.roHref;
|
||||
return href ? (href.split('/')[1] === type) : true;
|
||||
// if types are unreachable, display files to avoid misleading the user
|
||||
});
|
||||
};
|
||||
|
||||
// Create the ghost icon to add pads/folders
|
||||
var createNewPadIcons = function ($block, isInRoot) {
|
||||
var $container = $('<div>');
|
||||
if (isInRoot) {
|
||||
// Folder
|
||||
var $element1 = $('<li>', {
|
||||
'class': 'cp-app-drive-new-folder cp-app-drive-element-row ' +
|
||||
'cp-app-drive-element-grid'
|
||||
}).prepend($folderIcon.clone()).appendTo($container);
|
||||
$element1.append($('<span>', { 'class': 'cp-app-drive-new-name' })
|
||||
.text(Messages.fm_folder));
|
||||
// Shared Folder
|
||||
if (!APP.disableSF && !manager.isInSharedFolder(currentPath)) {
|
||||
var $element3 = $('<li>', {
|
||||
'class': 'cp-app-drive-new-shared-folder cp-app-drive-element-row ' +
|
||||
'cp-app-drive-element-grid'
|
||||
}).prepend($sharedFolderIcon.clone()).appendTo($container);
|
||||
$element3.append($('<span>', { 'class': 'cp-app-drive-new-name' })
|
||||
.text(Messages.fm_sharedFolder));
|
||||
}
|
||||
// Upload file
|
||||
var $elementFileUpload = $('<li>', {
|
||||
'class': 'cp-app-drive-new-fileupload cp-app-drive-element-row ' +
|
||||
'cp-app-drive-element-grid'
|
||||
}).prepend(getIcon('fileupload')).appendTo($container);
|
||||
$elementFileUpload.append($('<span>', {'class': 'cp-app-drive-new-name'})
|
||||
.text(Messages.uploadButton));
|
||||
// Upload folder
|
||||
if (APP.allowFolderUpload) {
|
||||
var $elementFolderUpload = $('<li>', {
|
||||
'class': 'cp-app-drive-new-folderupload cp-app-drive-element-row ' +
|
||||
'cp-app-drive-element-grid'
|
||||
}).prepend(getIcon('folderupload')).appendTo($container);
|
||||
$elementFolderUpload.append($('<span>', {'class': 'cp-app-drive-new-name'})
|
||||
.text(Messages.uploadFolderButton));
|
||||
}
|
||||
// Link
|
||||
var $elementLink = $('<li>', {
|
||||
'class': 'cp-app-drive-new-link cp-app-drive-element-row ' +
|
||||
'cp-app-drive-element-grid'
|
||||
}).prepend(getIcon('link')).appendTo($container);
|
||||
$elementLink.append($('<span>', {'class': 'cp-app-drive-new-name'})
|
||||
.text(Messages.fm_link_type));
|
||||
}
|
||||
// Pads
|
||||
getNewPadTypes().forEach(function (type) {
|
||||
var $element = $('<li>', {
|
||||
'class': 'cp-app-drive-new-doc cp-app-drive-element-row ' +
|
||||
'cp-app-drive-element-grid'
|
||||
}).prepend(getIcon(type)).appendTo($container);
|
||||
$element.append($('<span>', {'class': 'cp-app-drive-new-name'})
|
||||
.text(Messages.type[type]));
|
||||
$element.attr('data-type', type);
|
||||
getNewPadOptions(isInRoot).forEach(function (obj) {
|
||||
if (obj.separator) { return; }
|
||||
|
||||
var premium = common.checkRestrictedApp(type);
|
||||
if (premium < 0) {
|
||||
$element.addClass('cp-app-hidden cp-app-disabled');
|
||||
} else if (premium === 0) {
|
||||
$element.addClass('cp-app-disabled');
|
||||
var $element = $('<li>', {
|
||||
'class': obj.class + ' cp-app-drive-element-row ' +
|
||||
'cp-app-drive-element-grid'
|
||||
}).prepend(obj.icon).appendTo($container);
|
||||
$element.append($('<span>', { 'class': 'cp-app-drive-new-name' })
|
||||
.text(obj.name));
|
||||
|
||||
if (obj.type) {
|
||||
$element.attr('data-type', obj.type);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -3435,7 +3539,7 @@ define([
|
|||
|
||||
// Unsorted element are represented by "href" in an array: they don't have a filename
|
||||
// and they don't hav a hierarchical structure (folder/subfolders)
|
||||
var displayHrefArray = function ($container, rootName, draggable) {
|
||||
var displayHrefArray = function ($container, rootName, draggable, typeFilter) {
|
||||
var unsorted = files[rootName];
|
||||
if (unsorted.length) {
|
||||
var $fileHeader = getFileListHeader(true);
|
||||
|
@ -3445,6 +3549,7 @@ define([
|
|||
var sortBy = APP.store[SORT_FILE_BY];
|
||||
sortBy = sortBy === "" ? sortBy = 'name' : sortBy;
|
||||
var sortedFiles = sortElements(false, [rootName], keys, sortBy, !getSortFileDesc(), true);
|
||||
sortedFiles = typeFilter ? filterPads(sortedFiles, typeFilter, false, true) : sortedFiles;
|
||||
sortedFiles.forEach(function (id) {
|
||||
var file = manager.getFileData(id);
|
||||
if (!file) {
|
||||
|
@ -3526,7 +3631,7 @@ define([
|
|||
createGhostIcon($container);
|
||||
};
|
||||
|
||||
var displayTrashRoot = function ($list, $folderHeader, $fileHeader) {
|
||||
var displayTrashRoot = function ($list, $folderHeader, $fileHeader, typeFilter) {
|
||||
var filesList = [];
|
||||
var root = files[TRASH];
|
||||
var isEmpty = true;
|
||||
|
@ -3549,14 +3654,25 @@ define([
|
|||
isEmpty = false;
|
||||
});
|
||||
|
||||
var sortedFolders = typeFilter ? [] : sortTrashElements(true, filesList, null, !getSortFolderDesc());
|
||||
var sortedFiles = sortTrashElements(false, filesList, APP.store[SORT_FILE_BY], !getSortFileDesc);
|
||||
|
||||
if (typeFilter) {
|
||||
var ids = sortedFiles.map(function (obj) { return obj.element; });
|
||||
var idsFilter = filterPads(ids, typeFilter, false, true);
|
||||
sortedFiles = sortedFiles.filter(function (obj) {
|
||||
return (idsFilter.indexOf(obj.element) !== -1);
|
||||
});
|
||||
// prevent trash emptying while filter is active
|
||||
isEmpty = true;
|
||||
}
|
||||
|
||||
if (!isEmpty) {
|
||||
var $empty = createEmptyTrashButton();
|
||||
$content.append($empty);
|
||||
}
|
||||
|
||||
var sortedFolders = sortTrashElements(true, filesList, null, !getSortFolderDesc());
|
||||
var sortedFiles = sortTrashElements(false, filesList, APP.store[SORT_FILE_BY], !getSortFileDesc());
|
||||
if (manager.hasSubfolder(root, true)) { $list.append($folderHeader); }
|
||||
if (!typeFilter && manager.hasSubfolder(root, true)) { $list.append($folderHeader); }
|
||||
sortedFolders.forEach(function (f) {
|
||||
var $element = createElement([TRASH], f.spath, root, true);
|
||||
$list.append($element);
|
||||
|
@ -3728,7 +3844,7 @@ define([
|
|||
});
|
||||
};
|
||||
|
||||
var displayRecent = function ($list) {
|
||||
var displayRecent = function ($list, typeFilter) {
|
||||
var filesList = manager.getRecentPads();
|
||||
var limit = 20;
|
||||
|
||||
|
@ -3744,6 +3860,14 @@ define([
|
|||
var i = 0;
|
||||
var channels = [];
|
||||
|
||||
if (typeFilter) {
|
||||
var ids = filesList.map(function (arr) { return arr[0]; });
|
||||
var idsFilter = filterPads(ids, typeFilter, false, true);
|
||||
filesList = filesList.filter(function (arr) {
|
||||
return (idsFilter.indexOf(arr[0]) !== -1);
|
||||
});
|
||||
}
|
||||
|
||||
$list.append(h('li.cp-app-drive-element-separator', h('span', Messages.drive_active1Day)));
|
||||
filesList.some(function (arr) {
|
||||
var id = arr[0];
|
||||
|
@ -3932,6 +4056,17 @@ define([
|
|||
$content.html("");
|
||||
sel.$selectBox = $('<div>', {'class': 'cp-app-drive-content-select-box'})
|
||||
.appendTo($content);
|
||||
|
||||
var typeFilter;
|
||||
var isFilter = path[0] === FILTER;
|
||||
if (isFilter) {
|
||||
if (path.length < 3) { return; }
|
||||
typeFilter = path[1];
|
||||
path = path[2];
|
||||
currentPath = path;
|
||||
} else {
|
||||
APP.store[FILTER_BY] = undefined;
|
||||
}
|
||||
var isInRoot = manager.isPathIn(path, [ROOT]);
|
||||
var inTrash = manager.isPathIn(path, [TRASH]);
|
||||
var isTrashRoot = manager.comparePath(path, [TRASH]);
|
||||
|
@ -3939,6 +4074,8 @@ define([
|
|||
var isAllFiles = manager.comparePath(path, [FILES_DATA]);
|
||||
var isVirtual = virtualCategories.indexOf(path[0]) !== -1;
|
||||
var isSearch = path[0] === SEARCH;
|
||||
var isRecent = path[0] === RECENT;
|
||||
var isOwned = path[0] === OWNED;
|
||||
var isTags = path[0] === TAGS;
|
||||
// ANON_SHARED_FOLDER
|
||||
var isSharedFolder = path[0] === SHARED_FOLDER && APP.newSharedFolder;
|
||||
|
@ -4040,6 +4177,9 @@ define([
|
|||
if (!readOnlyFolder) {
|
||||
createNewButton(isInRoot, APP.toolbar.$bottomL);
|
||||
}
|
||||
if (!isTags && !isSearch) {
|
||||
createFilterButton(isTemplate, APP.toolbar.$bottomL);
|
||||
}
|
||||
|
||||
if (APP.mobile()) {
|
||||
var $context = $('<button>', {
|
||||
|
@ -4075,16 +4215,16 @@ define([
|
|||
var $fileHeader = getFileListHeader(true);
|
||||
|
||||
if (isTemplate) {
|
||||
displayHrefArray($list, path[0], true);
|
||||
displayHrefArray($list, path[0], true, typeFilter);
|
||||
} else if (isAllFiles) {
|
||||
displayAllFiles($list);
|
||||
} else if (isTrashRoot) {
|
||||
displayTrashRoot($list, $folderHeader, $fileHeader);
|
||||
displayTrashRoot($list, $folderHeader, $fileHeader, typeFilter);
|
||||
} else if (isSearch) {
|
||||
displaySearch($list, path[1]);
|
||||
} else if (path[0] === RECENT) {
|
||||
displayRecent($list);
|
||||
} else if (path[0] === OWNED) {
|
||||
} else if (isRecent) {
|
||||
displayRecent($list, typeFilter);
|
||||
} else if (isOwned) {
|
||||
displayOwned($list);
|
||||
} else if (isTags) {
|
||||
displayTags($list);
|
||||
|
@ -4093,11 +4233,12 @@ define([
|
|||
displaySharedFolder($list);
|
||||
} else {
|
||||
if (!inTrash) { $dirContent.contextmenu(openContextMenu('content')); }
|
||||
if (manager.hasSubfolder(root)) { $list.append($folderHeader); }
|
||||
if (!isFilter && manager.hasSubfolder(root)) { $list.append($folderHeader); }
|
||||
// display sub directories
|
||||
var keys = Object.keys(root);
|
||||
var sortedFolders = sortElements(true, path, keys, null, !getSortFolderDesc());
|
||||
var sortedFolders = isFilter ? [] : sortElements(true, path, keys, null, !getSortFolderDesc());
|
||||
var sortedFiles = sortElements(false, path, keys, APP.store[SORT_FILE_BY], !getSortFileDesc());
|
||||
sortedFiles = isFilter ? filterPads(sortedFiles, typeFilter, path) : sortedFiles;
|
||||
sortedFolders.forEach(function (key) {
|
||||
if (manager.isFile(root[key])) { return; }
|
||||
var $element = createElement(path, key, root, true);
|
||||
|
@ -4149,10 +4290,12 @@ define([
|
|||
|
||||
appStatus.ready(true);
|
||||
};
|
||||
var displayDirectory = APP.displayDirectory = function (path, force) {
|
||||
var displayDirectory = APP.displayDirectory = function (path, force, cb) {
|
||||
cb = cb || function () {};
|
||||
if (APP.closed || (APP.$content && !$.contains(document.documentElement, APP.$content[0]))) { return; }
|
||||
if (history.isHistoryMode) {
|
||||
return void _displayDirectory(path, force);
|
||||
_displayDirectory(path, force);
|
||||
return void cb();
|
||||
}
|
||||
if (!manager.comparePath(currentPath, path)) {
|
||||
removeSelected();
|
||||
|
@ -4161,6 +4304,7 @@ define([
|
|||
copyObjectValue(files, proxy.drive);
|
||||
updateSharedFolders(sframeChan, manager, files, folders, function () {
|
||||
_displayDirectory(path, force);
|
||||
cb();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -5206,8 +5350,9 @@ define([
|
|||
};
|
||||
APP.FM = common.createFileManager(fmConfig);
|
||||
|
||||
refresh();
|
||||
UI.removeLoadingScreen();
|
||||
refresh(function () {
|
||||
UI.removeLoadingScreen();
|
||||
});
|
||||
|
||||
/*
|
||||
if (!APP.team) {
|
||||
|
|
|
@ -102,6 +102,7 @@ define([
|
|||
// Send the command
|
||||
sframeChan.query('Q_SET_PAD_METADATA', {
|
||||
channel: channel,
|
||||
channels: otherChan,
|
||||
command: pending ? 'RM_PENDING_OWNERS' : 'RM_OWNERS',
|
||||
value: [ed],
|
||||
teamId: teamOwner
|
||||
|
@ -335,11 +336,6 @@ define([
|
|||
}
|
||||
}).nThen(function (waitFor) {
|
||||
var href = data.href;
|
||||
var hashes = priv.hashes || {};
|
||||
var bestHash = hashes.editHash || hashes.viewHash || hashes.fileHash;
|
||||
if (data.fakeHref) {
|
||||
href = Hash.hashToHref(bestHash, priv.app);
|
||||
}
|
||||
sel.forEach(function (el) {
|
||||
var curve = $(el).attr('data-curve');
|
||||
if (curve === user.curvePublic) { return; }
|
||||
|
@ -928,12 +924,7 @@ define([
|
|||
}
|
||||
|
||||
var href = data.href;
|
||||
var hashes = priv.hashes || {};
|
||||
var bestHash = hashes.editHash || hashes.viewHash || hashes.fileHash;
|
||||
if (data.fakeHref) {
|
||||
href = Hash.hashToHref(bestHash, priv.app);
|
||||
}
|
||||
var isNotStored = Boolean(data.fakeHref);
|
||||
var isNotStored = Boolean(data.isNotStored);
|
||||
sframeChan.query(q, {
|
||||
teamId: typeof(owned) !== "boolean" ? owned : undefined,
|
||||
href: href,
|
||||
|
@ -1055,13 +1046,13 @@ define([
|
|||
var owned = Modal.isOwned(Env, data);
|
||||
|
||||
// Request edit access
|
||||
if (common.isLoggedIn() && ((data.roHref && !data.href) || data.fakeHref) && !owned && !opts.calendar && priv.app !== 'form') {
|
||||
if (common.isLoggedIn() && data.roHref && !owned && !opts.calendar && priv.app !== 'form') {
|
||||
var requestButton = h('button.btn.btn-secondary.no-margin.cp-access-margin-right',
|
||||
Messages.requestEdit_button);
|
||||
var requestBlock = h('p', requestButton);
|
||||
var $requestBlock = $(requestBlock).hide();
|
||||
content.push(requestBlock);
|
||||
sframeChan.query('Q_REQUEST_ACCESS', {
|
||||
sframeChan.query('Q_CONTACT_OWNER', {
|
||||
send: false,
|
||||
metadata: data
|
||||
}, function (err, obj) {
|
||||
|
@ -1072,9 +1063,10 @@ define([
|
|||
$requestBlock.show().find('button').click(function () {
|
||||
if (spinner.getState()) { return; }
|
||||
spinner.spin();
|
||||
sframeChan.query('Q_REQUEST_ACCESS', {
|
||||
sframeChan.query('Q_CONTACT_OWNER', {
|
||||
send: true,
|
||||
metadata: data
|
||||
metadata: data,
|
||||
query: "REQUEST_PAD_ACCESS"
|
||||
}, function (err, obj) {
|
||||
if (obj && obj.state) {
|
||||
UI.log(Messages.requestEdit_sent);
|
||||
|
|
|
@ -42,11 +42,13 @@ define([
|
|||
if (err || !val) {
|
||||
if (opts.access) {
|
||||
data.password = priv.password;
|
||||
// Access modal and the pad is not stored: we're not an owner
|
||||
// so we don't need the correct href, just the type
|
||||
var h = Hash.createRandomHash(priv.app, priv.password);
|
||||
data.fakeHref = true;
|
||||
data.href = base + priv.pathname + '#' + h;
|
||||
// Access modal and the pad is not stored: get the hashes from outer
|
||||
var hashes = priv.hashes || {};
|
||||
data.href = Hash.hashToHref(hashes.editHash || hashes.fileHash, priv.app);
|
||||
if (hashes.viewHash) {
|
||||
data.roHref = Hash.hashToHref(hashes.viewHash, priv.app);
|
||||
}
|
||||
data.isNotStored = true;
|
||||
} else {
|
||||
waitFor.abort();
|
||||
return void cb(err || 'EEMPTY');
|
||||
|
|
|
@ -37,20 +37,18 @@ define([
|
|||
}));
|
||||
}
|
||||
|
||||
if (!data.fakeHref) {
|
||||
if (data.href) {
|
||||
$('<label>', {'for': 'cp-app-prop-link'}).text(Messages.editShare).appendTo($d);
|
||||
$d.append(UI.dialog.selectable(data.href, {
|
||||
id: 'cp-app-prop-link',
|
||||
}));
|
||||
}
|
||||
if (data.href) {
|
||||
$('<label>', {'for': 'cp-app-prop-link'}).text(Messages.editShare).appendTo($d);
|
||||
$d.append(UI.dialog.selectable(data.href, {
|
||||
id: 'cp-app-prop-link',
|
||||
}));
|
||||
}
|
||||
|
||||
if (data.roHref && !opts.noReadOnly) {
|
||||
$('<label>', {'for': 'cp-app-prop-rolink'}).text(Messages.viewShare).appendTo($d);
|
||||
$d.append(UI.dialog.selectable(data.roHref, {
|
||||
id: 'cp-app-prop-rolink',
|
||||
}));
|
||||
}
|
||||
if (data.roHref && !opts.noReadOnly) {
|
||||
$('<label>', {'for': 'cp-app-prop-rolink'}).text(Messages.viewShare).appendTo($d);
|
||||
$d.append(UI.dialog.selectable(data.roHref, {
|
||||
id: 'cp-app-prop-rolink',
|
||||
}));
|
||||
}
|
||||
|
||||
if (data.tags && Array.isArray(data.tags)) {
|
||||
|
|
|
@ -12,7 +12,7 @@ define([
|
|||
"Asterisk asterisk",
|
||||
"Brainfuck brainfuck .b",
|
||||
"C text/x-csrc .c",
|
||||
"C text/x-c++src .cpp",
|
||||
"C++ text/x-c++src .cpp",
|
||||
"C-like clike .c",
|
||||
"Clojure clojure .clj",
|
||||
"CMake cmake _", /* no extension */
|
||||
|
@ -50,7 +50,6 @@ define([
|
|||
"HTML htmlmixed .html",
|
||||
"HTTP http _", /* no extension */
|
||||
"IDL idl .idl",
|
||||
"JADE jade .jade",
|
||||
"Java text/x-java .java",
|
||||
"JavaScript javascript .js",
|
||||
"Jinja2 jinja2 .j2",
|
||||
|
|
|
@ -335,6 +335,28 @@ define([
|
|||
}
|
||||
};
|
||||
|
||||
handlers['FORM_RESPONSE'] = function(common, data) {
|
||||
var content = data.content;
|
||||
var msg = content.msg;
|
||||
|
||||
// Display the notification
|
||||
var title = Util.fixHTML(msg.content.title || Messages.unknownPad);
|
||||
var href = msg.content.href;
|
||||
|
||||
content.getFormatText = function() {
|
||||
return Messages._getKey('form_responseNotification', [title]);
|
||||
};
|
||||
if (href) {
|
||||
content.handler = function() {
|
||||
common.openURL(href);
|
||||
defaultDismiss(common, data)();
|
||||
};
|
||||
}
|
||||
if (!content.archived) {
|
||||
content.dismissHandler = defaultDismiss(common, data);
|
||||
}
|
||||
};
|
||||
|
||||
handlers['COMMENT_REPLY'] = function(common, data) {
|
||||
var content = data.content;
|
||||
var msg = content.msg;
|
||||
|
@ -481,6 +503,18 @@ define([
|
|||
var missed = content.msg.missed;
|
||||
var start = msg.start;
|
||||
var title = Util.fixHTML(msg.title);
|
||||
content.handler = function () {
|
||||
var priv = common.getMetadataMgr().getPrivateData();
|
||||
var time = Util.find(data, ['content', 'msg', 'content', 'start']);
|
||||
if (priv.app === "calendar" && window.APP && window.APP.moveToDate) {
|
||||
return void window.APP.moveToDate(time);
|
||||
}
|
||||
var url = Hash.hashToHref('', 'calendar');
|
||||
var optsUrl = Hash.getNewPadURL(url, {
|
||||
time: time
|
||||
});
|
||||
common.openURL(optsUrl);
|
||||
};
|
||||
content.getFormatText = function () {
|
||||
var now = +new Date();
|
||||
|
||||
|
|
|
@ -659,6 +659,7 @@ define([
|
|||
offline: store.proxy && store.offline,
|
||||
teams: teams,
|
||||
plan: store.ready ? (account.plan || '') : undefined,
|
||||
mutedChannels: proxy.mutedChannels
|
||||
}
|
||||
};
|
||||
cb(JSON.parse(JSON.stringify(metadata)));
|
||||
|
@ -1947,42 +1948,23 @@ define([
|
|||
}).nThen(cb);
|
||||
};
|
||||
|
||||
// requestPadAccess is used to check if we have a way to contact the owner
|
||||
// of the pad AND to send the request if we want
|
||||
// data.send === false ==> check if we can contact them
|
||||
// data.send === true ==> send the request
|
||||
Store.requestPadAccess = function (clientId, data, cb) {
|
||||
// contactPadOwner is used to send "REQUEST_ACCESS" messages
|
||||
// and to notify form owners when sending a response
|
||||
Store.contactPadOwner = function (clientId, data, cb) {
|
||||
var owner = data.owner;
|
||||
|
||||
// If the owner was not is the pad metadata, check if it is a friend.
|
||||
// We'll contact the first owner for whom we know the mailbox
|
||||
/* // TODO decide whether we want to re-enable this feature for our own contacts
|
||||
// communicate the exception to users that 'muting' won't apply to friends
|
||||
check mailbox in our contacts is not compatible with the new "mute pad" feature
|
||||
var owners = data.owners;
|
||||
if (!owner && Array.isArray(owners)) {
|
||||
var friends = store.proxy.friends || {};
|
||||
// If we have friends, check if an owner is one of them (with a mailbox)
|
||||
if (Object.keys(friends).filter(function (curve) { return curve !== 'me'; }).length) {
|
||||
owners.some(function (edPublic) {
|
||||
return Object.keys(friends).some(function (curve) {
|
||||
if (curve === "me") { return; }
|
||||
if (edPublic === friends[curve].edPublic &&
|
||||
friends[curve].notifications) {
|
||||
owner = friends[curve];
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// If send is true, send the request to the owner.
|
||||
if (owner) {
|
||||
if (data.send) {
|
||||
store.mailbox.sendTo('REQUEST_PAD_ACCESS', {
|
||||
channel: data.channel
|
||||
var sendTo = function (query, msg, user, _cb) {
|
||||
if (store.mailbox) {
|
||||
return store.mailbox.sendTo(query, msg, user, _cb);
|
||||
}
|
||||
Mailbox.sendToAnon(store.anon_rpc, query, msg, user, _cb);
|
||||
};
|
||||
sendTo(data.query, {
|
||||
channel: data.channel,
|
||||
data: data.msgData
|
||||
}, {
|
||||
channel: owner.notifications,
|
||||
curvePublic: owner.curvePublic
|
||||
|
@ -2179,6 +2161,13 @@ define([
|
|||
}
|
||||
};
|
||||
|
||||
Store.deleteMailboxMessage = function (clientId, data, cb) {
|
||||
if (!store.anon_rpc) { return void cb({error: 'RPC_NOT_READY'}); }
|
||||
store.anon_rpc.send('DELETE_MAILBOX_MESSAGE', data, function (e) {
|
||||
cb({error:e});
|
||||
});
|
||||
};
|
||||
|
||||
// GET_FULL_HISTORY from sframe-common-outer
|
||||
Store.getFullHistory = function (clientId, data, cb) {
|
||||
var network = store.network;
|
||||
|
@ -2973,6 +2962,9 @@ define([
|
|||
if (!rt.proxy.uid && store.noDriveUid) {
|
||||
rt.proxy.uid = store.noDriveUid;
|
||||
}
|
||||
if (!rt.proxy.form_seed && data.form_seed) {
|
||||
rt.proxy.form_seed = data.form_seed;
|
||||
}
|
||||
/*
|
||||
// deprecating localStorage migration as of 4.2.0
|
||||
var drive = rt.proxy.drive;
|
||||
|
|
|
@ -4,12 +4,13 @@ define([
|
|||
'/common/common-constants.js',
|
||||
'/common/common-realtime.js',
|
||||
'/common/outer/cache-store.js',
|
||||
'/calendar/recurrence.js',
|
||||
'/customize/messages.js',
|
||||
'/bower_components/nthen/index.js',
|
||||
'chainpad-listmap',
|
||||
'/bower_components/chainpad-crypto/crypto.js',
|
||||
'/bower_components/chainpad/chainpad.dist.js',
|
||||
], function (Util, Hash, Constants, Realtime, Cache, Messages, nThen, Listmap, Crypto, ChainPad) {
|
||||
], function (Util, Hash, Constants, Realtime, Cache, Rec, Messages, nThen, Listmap, Crypto, ChainPad) {
|
||||
var Calendar = {};
|
||||
|
||||
var getStore = function (ctx, id) {
|
||||
|
@ -90,7 +91,29 @@ define([
|
|||
});
|
||||
};
|
||||
|
||||
var updateEventReminders = function (ctx, reminders, _ev, useLastVisit) {
|
||||
var getRecurring = function (ev) {
|
||||
var mid = new Date();
|
||||
var start = new Date(mid.getFullYear(), mid.getMonth()-1, 15);
|
||||
var end = new Date(mid.getFullYear(), mid.getMonth()+1, 15);
|
||||
var startId = Rec.getMonthId(start);
|
||||
var midId = Rec.getMonthId(mid);
|
||||
var endId = Rec.getMonthId(end);
|
||||
|
||||
var toAdd = Rec.getRecurring([startId, midId, endId], [ev]);
|
||||
|
||||
var all = [ev];
|
||||
Array.prototype.push.apply(all, toAdd);
|
||||
return Rec.applyUpdates(all);
|
||||
};
|
||||
var clearDismissed = function (ctx, uid) {
|
||||
var h = Util.find(ctx, ['store', 'proxy', 'hideReminders']) || {};
|
||||
Object.keys(h).filter(function (id) {
|
||||
return id.indexOf(uid) === 0;
|
||||
}).forEach(function (id) {
|
||||
delete h[id];
|
||||
});
|
||||
};
|
||||
var _updateEventReminders = function (ctx, reminders, _ev, useLastVisit) {
|
||||
var now = +new Date();
|
||||
var ev = Util.clone(_ev);
|
||||
var uid = ev.id;
|
||||
|
@ -101,6 +124,10 @@ define([
|
|||
}
|
||||
reminders[uid] = [];
|
||||
|
||||
if (_ev.deleted) { return; }
|
||||
|
||||
var d = Util.find(ctx, ['store', 'proxy', 'hideReminders', uid]) || []; // dismissed
|
||||
|
||||
var last = ctx.store.data.lastVisit;
|
||||
|
||||
if (ev.isAllDay) {
|
||||
|
@ -119,10 +146,11 @@ define([
|
|||
if (ev.end <= now && !missed) {
|
||||
// No reminder for past events
|
||||
delete reminders[uid];
|
||||
clearDismissed(ctx, uid);
|
||||
return;
|
||||
}
|
||||
|
||||
var send = function () {
|
||||
var send = function (d) {
|
||||
var hide = Util.find(ctx, ['store', 'proxy', 'settings', 'general', 'calendar', 'hideNotif']);
|
||||
if (hide) { return; }
|
||||
var ctime = ev.start <= now ? ev.start : +new Date(); // Correct order for past events
|
||||
|
@ -133,11 +161,18 @@ define([
|
|||
missed: Boolean(missed),
|
||||
content: ev
|
||||
},
|
||||
hash: 'REMINDER|'+uid
|
||||
hash: 'REMINDER|'+uid+'-'+d
|
||||
}, null, function () {
|
||||
});
|
||||
};
|
||||
var sendNotif = function () { ctx.Store.onReadyEvt.reg(send); };
|
||||
var sent = false;
|
||||
var sendNotif = function (delay) {
|
||||
sent = true;
|
||||
|
||||
ctx.Store.onReadyEvt.reg(function () {
|
||||
send(delay);
|
||||
});
|
||||
};
|
||||
|
||||
var notifs = ev.reminders || [];
|
||||
notifs.sort(function (a, b) {
|
||||
|
@ -148,6 +183,10 @@ define([
|
|||
var delay = delayMinutes * 60000;
|
||||
var time = now + delay;
|
||||
|
||||
if (d.some(function (minutes) {
|
||||
return delayMinutes >= minutes;
|
||||
})) { return; }
|
||||
|
||||
// setTimeout only work with 32bit timeout values. If the event is too far away,
|
||||
// ignore this event for now
|
||||
// FIXME: call this function again in xxx days to reload these missing timeout?
|
||||
|
@ -156,18 +195,35 @@ define([
|
|||
// If we're too late to send a notification, send it instantly and ignore
|
||||
// all notifications that were supposed to be sent even earlier
|
||||
if (ev.start <= time) {
|
||||
sendNotif();
|
||||
sendNotif(delayMinutes);
|
||||
return true;
|
||||
}
|
||||
|
||||
// It starts in more than "delay": prepare the notification
|
||||
reminders[uid].push(setTimeout(function () {
|
||||
sendNotif();
|
||||
sendNotif(delayMinutes);
|
||||
}, (ev.start - time)));
|
||||
});
|
||||
|
||||
if (!sent) {
|
||||
// Remone any existing notification from the UI
|
||||
ctx.Store.onReadyEvt.reg(function () {
|
||||
ctx.store.mailbox.hideMessage('reminders', {
|
||||
hash: 'REMINDER|'+uid
|
||||
}, null, function () {
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
var updateEventReminders = function (ctx, reminders, ev, useLastVisit) {
|
||||
var all = getRecurring(Util.clone(ev));
|
||||
all.forEach(function (_ev) {
|
||||
_updateEventReminders(ctx, reminders, _ev, useLastVisit);
|
||||
});
|
||||
};
|
||||
var addReminders = function (ctx, id, ev) {
|
||||
var calendar = ctx.calendars[id];
|
||||
if (!ev) { return; } // XXX deleted event remote: delete reminders
|
||||
if (!calendar || !calendar.reminders) { return; }
|
||||
if (calendar.stores.length === 1 && calendar.stores[0] === 0) { return; }
|
||||
|
||||
|
@ -352,10 +408,20 @@ define([
|
|||
c.lm = lm;
|
||||
var proxy = c.proxy = lm.proxy;
|
||||
|
||||
var _updateCalled = false;
|
||||
var _update = function () {
|
||||
if (_updateCalled) { return; }
|
||||
_updateCalled = true;
|
||||
setTimeout(function () {
|
||||
_updateCalled = false;
|
||||
update();
|
||||
});
|
||||
};
|
||||
|
||||
lm.proxy.on('cacheready', function () {
|
||||
if (!proxy.metadata) { return; }
|
||||
c.cacheready = true;
|
||||
setTimeout(update);
|
||||
_update();
|
||||
if (cb) { cb(null, lm.proxy); }
|
||||
addInitialReminders(ctx, channel, cfg.lastVisitNotif);
|
||||
}).on('ready', function (info) {
|
||||
|
@ -372,24 +438,39 @@ define([
|
|||
title: data.title
|
||||
};
|
||||
}
|
||||
setTimeout(update);
|
||||
_update();
|
||||
if (cb) { cb(null, lm.proxy); }
|
||||
addInitialReminders(ctx, channel, cfg.lastVisitNotif);
|
||||
}).on('change', [], function () {
|
||||
if (!c.ready) { return; }
|
||||
setTimeout(update);
|
||||
_update();
|
||||
}).on('change', ['content'], function (o, n, p) {
|
||||
if (p.length === 2 && n && !o) { // New event
|
||||
addReminders(ctx, channel, n);
|
||||
return void addReminders(ctx, channel, n);
|
||||
}
|
||||
if (p.length === 2 && !n && o) { // Deleted event
|
||||
addReminders(ctx, channel, {
|
||||
return void addReminders(ctx, channel, {
|
||||
id: p[1],
|
||||
start: 0
|
||||
});
|
||||
}
|
||||
if (p.length === 3 && n && o && p[2] === 'start') { // Update event start
|
||||
setTimeout(function () {
|
||||
if (p.length >= 3 && ['start','reminders','isAllDay'].includes(p[2])) {
|
||||
// Updated event
|
||||
return void setTimeout(function () {
|
||||
addReminders(ctx, channel, proxy.content[p[1]]);
|
||||
});
|
||||
}
|
||||
if (p.length >= 6 && ['start','reminders','isAllDay'].includes(p[5])) {
|
||||
// Updated recurring event
|
||||
return void setTimeout(function () {
|
||||
addReminders(ctx, channel, proxy.content[p[1]]);
|
||||
});
|
||||
}
|
||||
}).on('remove', ['content'], function (x, p) {
|
||||
_update();
|
||||
if ((p.length >= 3 && p[2] === 'reminders') ||
|
||||
(p.length >= 6 && p[5] === 'reminders')) {
|
||||
return void setTimeout(function () {
|
||||
addReminders(ctx, channel, proxy.content[p[1]]);
|
||||
});
|
||||
}
|
||||
|
@ -400,10 +481,10 @@ define([
|
|||
updateLocalCalendars(ctx, c, md);
|
||||
}).on('disconnect', function () {
|
||||
c.offline = true;
|
||||
setTimeout(update);
|
||||
_update();
|
||||
}).on('reconnect', function () {
|
||||
c.offline = false;
|
||||
setTimeout(update);
|
||||
_update();
|
||||
}).on('error', function (info) {
|
||||
if (!info || !info.error) { return; }
|
||||
if (info.error === "EDELETED" ) {
|
||||
|
@ -411,7 +492,7 @@ define([
|
|||
}
|
||||
if (info.error === "ERESTRICTED" ) {
|
||||
c.restricted = true;
|
||||
setTimeout(update);
|
||||
_update();
|
||||
}
|
||||
cb(info);
|
||||
});
|
||||
|
@ -760,8 +841,11 @@ define([
|
|||
var ev = c.proxy.content[data.ev.id];
|
||||
if (!ev) { return void cb({error: "EINVAL"}); }
|
||||
|
||||
data.rawData = data.rawData || {};
|
||||
|
||||
// update the event
|
||||
var changes = data.changes || {};
|
||||
var type = data.type || {};
|
||||
|
||||
var newC;
|
||||
if (changes.calendarId) {
|
||||
|
@ -770,7 +854,122 @@ define([
|
|||
newC.proxy.content = newC.proxy.content || {};
|
||||
}
|
||||
|
||||
var RECUPDATE = {
|
||||
one: {},
|
||||
from: {}
|
||||
};
|
||||
if (['one','from','all'].includes(type.which)) {
|
||||
ev.recUpdate = ev.recUpdate || RECUPDATE;
|
||||
if (!ev.recUpdate.one) { ev.recUpdate.one = {}; }
|
||||
if (!ev.recUpdate.from) { ev.recUpdate.from = {}; }
|
||||
}
|
||||
var update = ev.recUpdate;
|
||||
var alwaysAll = ['calendarId'];
|
||||
var keys = Object.keys(changes).filter(function (s) {
|
||||
// we can only change the calendar or recurrence rule on the origin
|
||||
return !alwaysAll.includes(s);
|
||||
});
|
||||
|
||||
// Delete (future) affected keys
|
||||
var cleanAfter = function (time) {
|
||||
[update.from, update.one].forEach(function (obj) {
|
||||
Object.keys(obj).forEach(function (d) {
|
||||
if (Number(d) < time) { return; }
|
||||
delete obj[d];
|
||||
});
|
||||
});
|
||||
};
|
||||
var cleanKeys = function (obj, when) {
|
||||
Object.keys(obj).forEach(function (d) {
|
||||
if (when && Number(d) < when) { return; }
|
||||
keys.forEach(function (k) {
|
||||
delete obj[d][k];
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Update recurrence rule. We may create a new event here
|
||||
var dontSendUpdate = false;
|
||||
if (typeof(changes.recurrenceRule) !== "undefined") {
|
||||
if (['one','from'].includes(type.which) && !data.rawData.isOrigin) {
|
||||
cleanAfter(type.when);
|
||||
} else {
|
||||
update = ev.recUpdate = RECUPDATE;
|
||||
}
|
||||
}
|
||||
|
||||
if (type.which === "one") {
|
||||
update.one[type.when] = update.one[type.when] || {};
|
||||
// Nothing to delete
|
||||
} else if (type.which === "from") {
|
||||
update.from[type.when] = update.from[type.when] || {};
|
||||
// Delete all "single/from" updates (affected keys only) after this "from" date
|
||||
cleanKeys(update.from, type.when);
|
||||
cleanKeys(update.one, type.when);
|
||||
} else if (type.which === "all") {
|
||||
// Delete all "single/from" updates (affected keys only) after
|
||||
cleanKeys(update.from);
|
||||
cleanKeys(update.one);
|
||||
}
|
||||
|
||||
if (changes.start && update && (!type.which || type.which === "all")) {
|
||||
var diff = changes.start - ev.start;
|
||||
var newOne = {};
|
||||
var newFrom = {};
|
||||
Object.keys(update.one || {}).forEach(function (time) {
|
||||
newOne[Number(time)+diff] = update.one[time];
|
||||
});
|
||||
Object.keys(update.from || {}).forEach(function (time) {
|
||||
newFrom[Number(time)+diff] = update.from[time];
|
||||
});
|
||||
update.one = newOne;
|
||||
update.from = newFrom;
|
||||
}
|
||||
|
||||
|
||||
// Clear the "dismissed" reminders when the user is updating reminders
|
||||
var h = Util.find(ctx, ['store', 'proxy', 'hideReminders']) || {};
|
||||
if (changes.reminders) {
|
||||
if (type.which === 'one') {
|
||||
if (!type.when || type.when === ev.start) { delete h[data.ev.id]; }
|
||||
else { delete h[data.ev.id +'|'+ type.when]; }
|
||||
} else if (type.which === "from") {
|
||||
Object.keys(h).filter(function (id) {
|
||||
return id.indexOf(data.ev.id) === 0;
|
||||
}).forEach(function (id) {
|
||||
var time = Number(id.split('|')[1]);
|
||||
if (!time) { return; }
|
||||
if (time < type.when) { return; }
|
||||
delete h[id];
|
||||
});
|
||||
} else {
|
||||
Object.keys(h).filter(function (id) {
|
||||
return id.indexOf(data.ev.id) === 0;
|
||||
}).forEach(function (id) {
|
||||
delete h[id];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the changes
|
||||
Object.keys(changes).forEach(function (key) {
|
||||
if (!alwaysAll.includes(key) && type.which === "one") {
|
||||
if (key === "recurrenceRule") {
|
||||
if (data.rawData && data.rawData.isOrigin) {
|
||||
return (ev[key] = changes[key]);
|
||||
}
|
||||
// Always "from", never "one" for recurrence rules
|
||||
update.from[type.when] = update.from[type.when] || {};
|
||||
return (update.from[type.when][key] = changes[key]);
|
||||
}
|
||||
update.one[type.when][key] = changes[key];
|
||||
return;
|
||||
}
|
||||
if (!alwaysAll.includes(key) && type.which === "from") {
|
||||
update.from[type.when][key] = changes[key];
|
||||
return;
|
||||
}
|
||||
ev[key] = changes[key];
|
||||
});
|
||||
|
||||
|
@ -790,6 +989,7 @@ define([
|
|||
delete c.proxy.content[data.ev.id];
|
||||
}
|
||||
|
||||
|
||||
nThen(function (waitFor) {
|
||||
Realtime.whenRealtimeSyncs(c.lm.realtime, waitFor());
|
||||
if (newC) { Realtime.whenRealtimeSyncs(newC.lm.realtime, waitFor()); }
|
||||
|
@ -806,8 +1006,8 @@ define([
|
|||
addReminders(ctx, id, ev);
|
||||
}
|
||||
|
||||
sendUpdate(ctx, c);
|
||||
if (newC) { sendUpdate(ctx, newC); }
|
||||
if (!dontSendUpdate || newC) { sendUpdate(ctx, c); }
|
||||
if (newC && !dontSendUpdate) { sendUpdate(ctx, newC); }
|
||||
cb();
|
||||
});
|
||||
};
|
||||
|
@ -816,7 +1016,22 @@ define([
|
|||
var c = ctx.calendars[id];
|
||||
if (!c) { return void cb({error: "ENOENT"}); }
|
||||
c.proxy.content = c.proxy.content || {};
|
||||
delete c.proxy.content[data.id];
|
||||
var evId = data.id.split('|')[0];
|
||||
if (data.id === evId) {
|
||||
delete c.proxy.content[data.id];
|
||||
} else {
|
||||
var ev = c.proxy.content[evId];
|
||||
var s = data.raw && data.raw.start;
|
||||
if (s) {
|
||||
ev.recUpdate = ev.recUpdate || {
|
||||
one: {},
|
||||
from: {}
|
||||
};
|
||||
ev.recUpdate.one[s] = {
|
||||
deleted: true
|
||||
};
|
||||
}
|
||||
}
|
||||
Realtime.whenRealtimeSyncs(c.lm.realtime, function () {
|
||||
addReminders(ctx, id, {
|
||||
id: data.id,
|
||||
|
@ -867,6 +1082,20 @@ define([
|
|||
openChannels(ctx);
|
||||
}));
|
||||
|
||||
ctx.store.proxy.on('change', ['hideReminders'], function (o,n,p) {
|
||||
var uid = p[1].split('|')[0];
|
||||
Object.keys(ctx.calendars).some(function (calId) {
|
||||
var c = ctx.calendars[calId];
|
||||
if (!c || !c.proxy || !c.proxy.content) { return; }
|
||||
if (c.proxy.content[uid]) {
|
||||
setTimeout(function () {
|
||||
addReminders(ctx, calId, c.proxy.content[uid]);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
calendar.closeTeam = function (teamId) {
|
||||
Object.keys(ctx.calendars).forEach(function (id) {
|
||||
var ctxCal = ctx.calendars[id];
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
(function () {
|
||||
var factory = function (Util, Cred, Nacl) {
|
||||
var factory = function (Util, Cred, Nacl, Crypto) {
|
||||
var Invite = {};
|
||||
|
||||
var encode64 = Nacl.util.encodeBase64;
|
||||
|
@ -49,6 +49,24 @@ var factory = function (Util, Cred, Nacl) {
|
|||
roster.invite(toInvite, cb);
|
||||
};
|
||||
|
||||
// Invite links should only be visible to members or above, so
|
||||
// we store them in the roster encrypted with a string only available
|
||||
// to users with edit rights
|
||||
var decodeUTF8 = Nacl.util.decodeUTF8;
|
||||
Invite.encryptHash = function (data, seedStr) {
|
||||
var array = decodeUTF8(seedStr);
|
||||
var bytes = Nacl.hash(array);
|
||||
var cryptKey = bytes.subarray(0, 32);
|
||||
return Crypto.encrypt(data, cryptKey);
|
||||
};
|
||||
Invite.decryptHash = function (encryptedStr, seedStr) {
|
||||
var array = decodeUTF8(seedStr);
|
||||
var bytes = Nacl.hash(array);
|
||||
var cryptKey = bytes.subarray(0, 32);
|
||||
return Crypto.decrypt(encryptedStr, cryptKey);
|
||||
};
|
||||
|
||||
|
||||
/* INPUTS
|
||||
|
||||
* password (for scrypt)
|
||||
|
@ -84,16 +102,17 @@ var factory = function (Util, Cred, Nacl) {
|
|||
module.exports = factory(
|
||||
require("../common-util"),
|
||||
require("../common-credential.js"),
|
||||
require("nthen"),
|
||||
require("tweetnacl/nacl-fast")
|
||||
require("tweetnacl/nacl-fast"),
|
||||
require("chainpad-crypto/crypto")
|
||||
);
|
||||
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
|
||||
define([
|
||||
'/common/common-util.js',
|
||||
'/common/common-credential.js',
|
||||
'/bower_components/chainpad-crypto/crypto.js',
|
||||
'/bower_components/tweetnacl/nacl-fast.min.js',
|
||||
], function (Util, Cred) {
|
||||
return factory(Util, Cred, window.nacl);
|
||||
], function (Util, Cred, Crypto) {
|
||||
return factory(Util, Cred, window.nacl, Crypto);
|
||||
});
|
||||
}
|
||||
}());
|
||||
|
|
|
@ -20,6 +20,10 @@ define([
|
|||
if (!curvePublic) { return false; }
|
||||
return Boolean(muted[curvePublic]);
|
||||
};
|
||||
var isChannelMuted = function (ctx, channel) {
|
||||
var muted = ctx.store.proxy.mutedChannels || [];
|
||||
return muted.includes(channel);
|
||||
};
|
||||
|
||||
// Store the friend request displayed to avoid duplicates
|
||||
var friendRequest = {};
|
||||
|
@ -575,6 +579,61 @@ define([
|
|||
cb();
|
||||
};
|
||||
|
||||
// Hide duplicates when receiving a form notification:
|
||||
// Keep only one notification per channel
|
||||
var formNotifs = {};
|
||||
handlers['FORM_RESPONSE'] = function (ctx, box, data, cb) {
|
||||
var msg = data.msg;
|
||||
var hash = data.hash;
|
||||
var content = msg.content;
|
||||
|
||||
var channel = content.channel;
|
||||
if (!channel) { return void cb(true); }
|
||||
|
||||
if (isChannelMuted(ctx, channel)) { return void cb(true); }
|
||||
|
||||
var title, href;
|
||||
ctx.Store.getAllStores().some(function (s) {
|
||||
var res = s.manager.findChannel(channel);
|
||||
// Check if the pad is in our drive
|
||||
return res.some(function (obj) {
|
||||
if (!obj.data) { return; }
|
||||
if (href && !obj.data.href) { return; } // We already have the VIEW url, we need EDIT
|
||||
href = obj.data.href || obj.data.roHref;
|
||||
title = obj.data.filename || obj.data.title;
|
||||
if (obj.data.href) { return true; } // Abort only if we have the EDIT url
|
||||
});
|
||||
});
|
||||
|
||||
// If we don't have the edit url, ignore this notification
|
||||
if (!href) { return void cb(true); }
|
||||
|
||||
// Add the title
|
||||
content.href = href;
|
||||
content.title = title;
|
||||
|
||||
// Remove duplicates
|
||||
var old = formNotifs[channel];
|
||||
var toRemove = old ? old.data : undefined;
|
||||
|
||||
// Update the data
|
||||
formNotifs[channel] = {
|
||||
data: {
|
||||
type: box.type,
|
||||
hash: hash
|
||||
}
|
||||
};
|
||||
|
||||
cb(false, toRemove);
|
||||
};
|
||||
removeHandlers['FORM_RESPONSE'] = function (ctx, box, data, hash) {
|
||||
var content = data.content;
|
||||
var channel = content.channel;
|
||||
var old = formNotifs[channel];
|
||||
if (old && old.data && old.data.hash === hash) {
|
||||
delete formNotifs[channel];
|
||||
}
|
||||
};
|
||||
// Hide duplicates when receiving a SHARE_PAD notification:
|
||||
// Keep only one notification per channel: the stronger and more recent one
|
||||
var comments = {};
|
||||
|
|
|
@ -164,6 +164,22 @@ proxy.mailboxes = {
|
|||
});
|
||||
});
|
||||
};
|
||||
Mailbox.sendToAnon = function (anonRpc, type, msg, user, cb) {
|
||||
var Nacl = Crypto.Nacl;
|
||||
var curveSeed = Nacl.randomBytes(32);
|
||||
var curvePair = Nacl.box.keyPair.fromSecretKey(new Uint8Array(curveSeed));
|
||||
var curvePrivate = Nacl.util.encodeBase64(curvePair.secretKey);
|
||||
var curvePublic = Nacl.util.encodeBase64(curvePair.publicKey);
|
||||
sendTo({
|
||||
store: {
|
||||
anon_rpc: anonRpc,
|
||||
proxy: {
|
||||
curvePrivate: curvePrivate,
|
||||
curvePublic: curvePublic
|
||||
}
|
||||
}
|
||||
}, type, msg, user, cb);
|
||||
};
|
||||
|
||||
// Mark a message as read
|
||||
var dismiss = function (ctx, data, cId, cb) {
|
||||
|
@ -177,6 +193,15 @@ proxy.mailboxes = {
|
|||
hideMessage(ctx, type, hash, ctx.clients.filter(function (clientId) {
|
||||
return clientId !== cId;
|
||||
}));
|
||||
|
||||
var uid = hash.slice(9).split('-')[0];
|
||||
var d = Util.find(ctx, ['store', 'proxy', 'hideReminders', uid]);
|
||||
if (!d) {
|
||||
var h = ctx.store.proxy.hideReminders = ctx.store.proxy.hideReminders || {};
|
||||
d = h[uid] = h[uid] || [];
|
||||
}
|
||||
var delay = hash.split('-')[1];
|
||||
if (delay && !d.includes(delay)) { d.push(Number(delay)); }
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -590,6 +615,9 @@ proxy.mailboxes = {
|
|||
});
|
||||
};
|
||||
|
||||
mailbox.hideMessage = function (type, msg) {
|
||||
hideMessage(ctx, type, msg.hash, ctx.clients);
|
||||
};
|
||||
mailbox.showMessage = function (type, msg, cId, cb) {
|
||||
if (type === "reminders" && msg) {
|
||||
ctx.boxes.reminders.content[msg.hash] = msg.msg;
|
||||
|
|
|
@ -463,9 +463,22 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto, Feedback)
|
|||
if (typeof(members[curve]) !== 'undefined') { throw new Error("MEMBER_ALREADY_PRESENT"); }
|
||||
|
||||
// copy the new profile from the old one
|
||||
members[curve] = Util.clone(members[author]);
|
||||
// and erase the old one
|
||||
delete members[author];
|
||||
var clone = Util.clone(members[author]);
|
||||
delete clone.remaining;
|
||||
delete clone.totalUses;
|
||||
delete clone.inviteChannel;
|
||||
delete clone.previewChannel;
|
||||
members[curve] = clone;
|
||||
|
||||
// XXX
|
||||
var remaining = members[author].remaining || 1;
|
||||
if (remaining === -1) { return true; } // Infinite uses, keep the link
|
||||
if (remaining > 1) { // Remove 1 use
|
||||
members[author].remaining = remaining - 1;
|
||||
} else { // Disable link
|
||||
delete members[author];
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ define([
|
|||
GET_HISTORY: Store.getHistory,
|
||||
GET_HISTORY_RANGE: Store.getHistoryRange,
|
||||
IS_NEW_CHANNEL: Store.isNewChannel,
|
||||
REQUEST_PAD_ACCESS: Store.requestPadAccess,
|
||||
CONTACT_PAD_OWNER: Store.contactPadOwner,
|
||||
GIVE_PAD_ACCESS: Store.givePadAccess,
|
||||
BURN_PAD: Store.burnPad,
|
||||
GET_PAD_METADATA: Store.getPadMetadata,
|
||||
|
@ -88,6 +88,7 @@ define([
|
|||
GET_LAST_HASH: Store.getLastHash,
|
||||
GET_SNAPSHOT: Store.getSnapshot,
|
||||
CORRUPTED_CACHE: Store.corruptedCache,
|
||||
DELETE_MAILBOX_MESSAGE: Store.deleteMailboxMessage,
|
||||
// Drive
|
||||
DRIVE_USEROBJECT: Store.userObjectCommand,
|
||||
// Settings,
|
||||
|
|
|
@ -1038,6 +1038,17 @@ define([
|
|||
});
|
||||
}
|
||||
|
||||
// Decrypt hash for invite links
|
||||
Object.keys(members).forEach(function (curve) {
|
||||
var member = members[curve];
|
||||
if (!member.inviteChannel) { return; }
|
||||
if (!member.hash) { return; }
|
||||
if (!teamData.hash) { delete member.hash; return; }
|
||||
try {
|
||||
member.hash = Invite.decryptHash(member.hash, teamData.hash);
|
||||
} catch (e) { console.error(e); }
|
||||
});
|
||||
|
||||
cb(members);
|
||||
});
|
||||
};
|
||||
|
@ -1580,10 +1591,12 @@ define([
|
|||
var message = data.message;
|
||||
var name = data.name;
|
||||
|
||||
/*
|
||||
var password = data.password;
|
||||
//var password = data.password;
|
||||
var hash = data.hash;
|
||||
*/
|
||||
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
|
||||
try {
|
||||
var encryptedHash = Invite.encryptHash(hash, teamData.hash);
|
||||
} catch (e) { console.error(e); }
|
||||
|
||||
// derive { channel, cryptKey} for the preview content channel
|
||||
var previewKeys = Invite.derivePreviewKeys(seeds.preview);
|
||||
|
@ -1595,6 +1608,10 @@ define([
|
|||
// and a placeholder in the roster
|
||||
var ephemeralKeys = Invite.generateKeys();
|
||||
|
||||
// Initial role of the invited users
|
||||
var role = data.role || "VIEWER";
|
||||
var uses = data.uses || 1;
|
||||
|
||||
nThen(function (w) {
|
||||
|
||||
|
||||
|
@ -1652,9 +1669,12 @@ define([
|
|||
};
|
||||
putOpts.metadata.validateKey = sign.validateKey;
|
||||
|
||||
|
||||
|
||||
|
||||
// available only with the link and the content
|
||||
var inviteContent = {
|
||||
teamData: getInviteData(ctx, teamId, false),
|
||||
teamData: getInviteData(ctx, teamId, role === "MEMBER"),
|
||||
ephemeral: {
|
||||
edPublic: ephemeralKeys.edPublic,
|
||||
edPrivate: ephemeralKeys.edPrivate,
|
||||
|
@ -1692,6 +1712,10 @@ define([
|
|||
curvePublic: ephemeralKeys.curvePublic,
|
||||
displayName: data.name,
|
||||
pending: true,
|
||||
remaining: uses,
|
||||
totalUses: uses,
|
||||
role: role,
|
||||
hash: encryptedHash,
|
||||
inviteChannel: inviteKeys.channel,
|
||||
previewChannel: previewKeys.channel,
|
||||
}
|
||||
|
@ -1821,20 +1845,22 @@ define([
|
|||
}));
|
||||
}).nThen(function () {
|
||||
var tempRpc = {};
|
||||
initRpc(ctx, tempRpc, inviteContent.ephemeral, function (err) {
|
||||
if (err) { return; }
|
||||
var rpc = tempRpc.rpc;
|
||||
if (rosterState.inviteChannel) {
|
||||
rpc.removeOwnedChannel(rosterState.inviteChannel, function (err) {
|
||||
if (err) { console.error(err); }
|
||||
});
|
||||
}
|
||||
if (rosterState.previewChannel) {
|
||||
rpc.removeOwnedChannel(rosterState.previewChannel, function (err) {
|
||||
if (err) { console.error(err); }
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!rosterState.remaining || rosterState.remaining === 1) {
|
||||
initRpc(ctx, tempRpc, inviteContent.ephemeral, function (err) {
|
||||
if (err) { return; }
|
||||
var rpc = tempRpc.rpc;
|
||||
if (rosterState.inviteChannel) {
|
||||
rpc.removeOwnedChannel(rosterState.inviteChannel, function (err) {
|
||||
if (err) { console.error(err); }
|
||||
});
|
||||
}
|
||||
if (rosterState.previewChannel) {
|
||||
rpc.removeOwnedChannel(rosterState.previewChannel, function (err) {
|
||||
if (err) { console.error(err); }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// Add the team to our list and join...
|
||||
joinTeam(ctx, {
|
||||
team: inviteContent.teamData
|
||||
|
|
|
@ -249,7 +249,6 @@ var factory = function (Util, Rpc) {
|
|||
}, cb);
|
||||
};
|
||||
|
||||
|
||||
cb(e, exp);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -20,7 +20,9 @@ define([
|
|||
'netflux-client': '/bower_components/netflux-websocket/netflux-client',
|
||||
'chainpad-netflux': '/bower_components/chainpad-netflux/chainpad-netflux',
|
||||
'chainpad-listmap': '/bower_components/chainpad-listmap/chainpad-listmap',
|
||||
'cm-extra': '/lib/codemirror-extra-modes'
|
||||
'cm-extra': '/lib/codemirror-extra-modes',
|
||||
// asciidoctor same
|
||||
'asciidoctor': '/lib/asciidoctor/asciidoctor.min'
|
||||
},
|
||||
map: {
|
||||
'*': {
|
||||
|
|
|
@ -80,7 +80,7 @@ define([
|
|||
try {
|
||||
var val = JSON.parse(states[idx].getContent().doc);
|
||||
var md = config.extractMetadata(val);
|
||||
var users = Object.keys(md.users).sort();
|
||||
var users = Object.keys(md.users || {}).sort();
|
||||
return users.join();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
|
@ -13,6 +13,7 @@ define([
|
|||
Mailbox.create = function (Common) {
|
||||
var mailbox = Common.mailbox;
|
||||
var sframeChan = Common.getSframeChannel();
|
||||
var priv = Common.getMetadataMgr().getPrivateData();
|
||||
|
||||
var execCommand = function (cmd, data, cb) {
|
||||
sframeChan.query('Q_MAILBOX_COMMAND', {
|
||||
|
@ -67,6 +68,14 @@ define([
|
|||
}
|
||||
} else if (data.type === 'reminders') {
|
||||
avatar = h('i.fa.fa-calendar.cp-broadcast.preview');
|
||||
if (priv.app !== 'calendar') { avatar.classList.add('cp-reminder'); }
|
||||
$(avatar).click(function (e) {
|
||||
e.stopPropagation();
|
||||
if (data.content && data.content.handler) {
|
||||
return void data.content.handler();
|
||||
}
|
||||
Common.openURL(Hash.hashToHref('', 'calendar'));
|
||||
});
|
||||
} else if (userData && typeof(userData) === "object" && userData.profile) {
|
||||
avatar = h('span.cp-avatar');
|
||||
Common.displayAvatar($(avatar), userData.avatar, userData.displayName || userData.name);
|
||||
|
@ -120,7 +129,8 @@ define([
|
|||
|
||||
onViewedHandlers.push(function (data) {
|
||||
var hash = data.hash.replace(/"/g, '\\\"');
|
||||
var $notif = $('.cp-notification[data-hash="'+hash+'"]:not(.cp-app-notification-archived)');
|
||||
if (/^REMINDER\|/.test(hash)) { hash = hash.split('-')[0]; }
|
||||
var $notif = $('.cp-notification[data-hash^="'+hash+'"]:not(.cp-app-notification-archived)');
|
||||
if ($notif.length) {
|
||||
$notif.remove();
|
||||
}
|
||||
|
|
|
@ -710,6 +710,12 @@ define([
|
|||
additionalPriv.registeredOnly = true;
|
||||
}
|
||||
|
||||
if (metaObj.priv && Array.isArray(metaObj.priv.mutedChannels)
|
||||
&& metaObj.priv.mutedChannels.includes(secret.channel)) {
|
||||
delete metaObj.priv.mutedChannes;
|
||||
additionalPriv.isChannelMuted = true;
|
||||
}
|
||||
|
||||
var priv = metaObj.priv;
|
||||
var _plan = typeof(priv.plan) === "undefined" ? Utils.LocalStore.getPremium() : priv.plan;
|
||||
var p = Utils.Util.checkRestrictedApp(parsed.type, AppConfig,
|
||||
|
@ -942,7 +948,7 @@ define([
|
|||
var metadata = data.metadata;
|
||||
var add = data.add;
|
||||
var _secret = secret;
|
||||
if (metadata && (metadata.href || metadata.roHref) && !metadata.fakeHref) {
|
||||
if (metadata && (metadata.href || metadata.roHref)) {
|
||||
var _parsed = Utils.Hash.parsePadUrl(metadata.href || metadata.roHref);
|
||||
_secret = Utils.Hash.getSecrets(_parsed.type, _parsed.hash, metadata.password);
|
||||
}
|
||||
|
@ -1010,9 +1016,9 @@ define([
|
|||
});
|
||||
});
|
||||
|
||||
// REQUEST_ACCESS is used both to check IF we can contact an owner (send === false)
|
||||
// CONTACT_OWNER is used both to check IF we can contact an owner (send === false)
|
||||
// AND also to send the request if we want (send === true)
|
||||
sframeChan.on('Q_REQUEST_ACCESS', function (data, cb) {
|
||||
sframeChan.on('Q_CONTACT_OWNER', function (data, cb) {
|
||||
if (readOnly && hashes.editHash) {
|
||||
return void cb({error: 'ALREADYKNOWN'});
|
||||
}
|
||||
|
@ -1030,8 +1036,6 @@ define([
|
|||
var crypto = Crypto.createEncryptor(_secret.keys);
|
||||
nThen(function (waitFor) {
|
||||
// Try to get the owner's mailbox from the pad metadata first.
|
||||
// If it's is an older owned pad, check if the owner is a friend
|
||||
// or an acquaintance (from async-store directly in requestAccess)
|
||||
var todo = function (obj) {
|
||||
owners = obj.owners;
|
||||
|
||||
|
@ -1065,11 +1069,12 @@ define([
|
|||
}));
|
||||
}).nThen(function () {
|
||||
// If we are just checking (send === false) and there is a mailbox field, cb state true
|
||||
// If there is no mailbox, we'll have to check if an owner is a friend in the worker
|
||||
if (!send) { return void cb({state: Boolean(owner)}); }
|
||||
|
||||
Cryptpad.padRpc.requestAccess({
|
||||
Cryptpad.padRpc.contactOwner({
|
||||
send: send,
|
||||
query: data.query,
|
||||
msgData: data.msgData,
|
||||
channel: _secret.channel,
|
||||
owner: owner,
|
||||
owners: owners
|
||||
|
|
|
@ -663,49 +663,6 @@ MessengerUI, Messages, Pages) {
|
|||
return $shareBlock;
|
||||
};
|
||||
|
||||
/*
|
||||
var createRequest = function (toolbar, config) {
|
||||
if (!config.metadataMgr) {
|
||||
throw new Error("You must provide a `metadataMgr` to display the request access button");
|
||||
}
|
||||
|
||||
// We can only requets more access if we're in read-only mode
|
||||
if (config.readOnly !== 1) { return; }
|
||||
|
||||
var $requestBlock = $('<button>', {
|
||||
'class': 'fa fa-lock cp-toolbar-share-button',
|
||||
title: Messages.requestEdit_button
|
||||
}).hide();
|
||||
|
||||
// If we have access to the owner's mailbox, display the button and enable it
|
||||
// false => check if we can contact the owner
|
||||
// true ==> send the request
|
||||
Common.getSframeChannel().query('Q_REQUEST_ACCESS', {send:false}, function (err, obj) {
|
||||
if (obj && obj.state) {
|
||||
var locked = false;
|
||||
$requestBlock.show().click(function () {
|
||||
if (locked) { return; }
|
||||
locked = true;
|
||||
Common.getSframeChannel().query('Q_REQUEST_ACCESS', {send:true}, function (err, obj) {
|
||||
if (obj && obj.state) {
|
||||
UI.log(Messages.requestEdit_sent);
|
||||
$requestBlock.hide();
|
||||
} else {
|
||||
locked = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
toolbar.$leftside.append($requestBlock);
|
||||
toolbar.request = $requestBlock;
|
||||
|
||||
return $requestBlock;
|
||||
};
|
||||
*/
|
||||
|
||||
var createTitle = function (toolbar, config) {
|
||||
var $titleContainer = $('<span>', {
|
||||
'class': TITLE_CLS
|
||||
|
|
|
@ -293,7 +293,7 @@ define([
|
|||
};
|
||||
|
||||
Messages.convertPage = "Convert"; // XXX 4.11.0
|
||||
Messages.convert_hint = "Pick the file you want to convert. The list of output format will be visible afterward."; // XXX 4.11.0
|
||||
Messages.convert_hint = "Pick the file you want to convert. The list of output format will be visible afterwards."; // XXX 4.11.0
|
||||
|
||||
var createToolbar = function () {
|
||||
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications'];
|
||||
|
|
|
@ -193,28 +193,20 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
.cp-form-settings-preview {
|
||||
min-width: 260px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.cp-form-color-theme-container {
|
||||
max-width: 300px;
|
||||
}
|
||||
& > div {
|
||||
flex-basis: 33.333333%;
|
||||
flex: 1;
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 600px) and (min-width: 400px) {
|
||||
.cp-form-creator-settings {
|
||||
& > div {
|
||||
flex-basis: 50% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 400px) {
|
||||
.cp-form-creator-settings {
|
||||
& > div {
|
||||
flex-basis: 100% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cp-form-creator-settings {
|
||||
.cp-form-actions {
|
||||
margin-top: 5px;
|
||||
|
@ -230,6 +222,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
div.cp-form-settings-preview {
|
||||
background: @cp_form-bg1;
|
||||
padding: 10px;
|
||||
border-radius: @variables_radius_L;
|
||||
& > *:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.cp-form-setting-title {
|
||||
color: @cryptpad_color_link;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-form-color-container {
|
||||
& > div {
|
||||
display: flex;
|
||||
|
@ -251,12 +255,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cp-form-response-msg-container button {
|
||||
white-space: initial;
|
||||
line-height: 25px;
|
||||
padding: 5.5px 6px;
|
||||
}
|
||||
}
|
||||
div.cp-form-filler-container {
|
||||
width: 300px;
|
||||
|
@ -276,6 +274,7 @@
|
|||
}
|
||||
}
|
||||
div.cp-form-creator-content {
|
||||
position: relative;
|
||||
.cp-form-block-type {
|
||||
margin-top: -35px;
|
||||
&.editable {
|
||||
|
@ -404,7 +403,7 @@
|
|||
display: flex;
|
||||
flex-flow: column;
|
||||
margin: 20px 0px 0px 0px;
|
||||
padding-bottom: 100px;
|
||||
padding-bottom: 75px;
|
||||
&> div:first-child {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
@ -449,6 +448,17 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.cp-form-response-msg-container {
|
||||
padding-bottom: 75px;
|
||||
button {
|
||||
white-space: initial;
|
||||
line-height: 25px;
|
||||
padding: 5.5px 6px;
|
||||
}
|
||||
.cp-form-response-msg-hint {
|
||||
color: @cryptpad_color_link;
|
||||
}
|
||||
}
|
||||
|
||||
.cp-form-send-container {
|
||||
text-align: center;
|
||||
|
@ -666,7 +676,7 @@
|
|||
}
|
||||
}
|
||||
&.editable {
|
||||
cursor: grab;
|
||||
&:not(.nodrag) { cursor: grab; }
|
||||
.cp-form-edit-save {
|
||||
margin-top: 20px;
|
||||
button {
|
||||
|
@ -764,8 +774,13 @@
|
|||
justify-content: center;
|
||||
flex: 1;
|
||||
flex-flow: column;
|
||||
div.cp-form-submit-table {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.cp-form-submit-actions {
|
||||
button:not(:last-child) {
|
||||
span:not(:last-child) {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -833,6 +848,10 @@
|
|||
background: @cp_form-bg2;
|
||||
}
|
||||
|
||||
.cp-form-results-delete {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.cp-form-results-type-multiradio-data {
|
||||
.cp-mr-q {
|
||||
font-weight: bold;
|
||||
|
@ -915,11 +934,27 @@
|
|||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: baseline;
|
||||
.cp-radio {
|
||||
.cp-radio, .cp-checkmark {
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
}
|
||||
.cp-checkmark-label {
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
.cp-form-multiradio-container {
|
||||
overflow: auto;
|
||||
}
|
||||
.cp-form-type-multiradio {
|
||||
.cp-form-multiradio-header {
|
||||
white-space: nowrap;
|
||||
span {
|
||||
text-align: center;
|
||||
max-width: 30ch;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
display: table;
|
||||
& > * {
|
||||
display: table-row;
|
||||
|
@ -928,9 +963,25 @@
|
|||
padding: 5px 20px;
|
||||
vertical-align: middle;
|
||||
&:first-child {
|
||||
min-width: 200px;
|
||||
//overflow: auto;
|
||||
max-width: 300px;
|
||||
min-width: 300px;
|
||||
width: 300px;
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
word-break: break-word;
|
||||
background: @cp_form-bg1;
|
||||
z-index: 2;
|
||||
}
|
||||
.cp-radio-mark {
|
||||
&:nth-child(2) {
|
||||
visibility: hidden;
|
||||
//overflow: auto;
|
||||
max-width: 300px;
|
||||
min-width: 300px;
|
||||
width: 300px;
|
||||
word-break: break-word;
|
||||
}
|
||||
.cp-radio-mark, .cp-checkmark-mark {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
@ -942,8 +993,11 @@
|
|||
.cp-form-type-sort {
|
||||
cursor: grab;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
word-break: break-word;
|
||||
.cp-form-handle {
|
||||
margin-right: 5px;
|
||||
min-width: 12px;
|
||||
}
|
||||
.cp-form-sort-order {
|
||||
border: 1px solid @cryptpad_text_col;
|
||||
|
@ -962,22 +1016,24 @@
|
|||
.cp-form-type-poll-container {
|
||||
overflow: auto;
|
||||
.cp-form-poll-hint {
|
||||
margin-bottom: 10px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
.cp-form-type-poll {
|
||||
margin-top: 32px; // cp-form-poll-hint is "absolute"
|
||||
display: inline-flex;
|
||||
flex-flow: column;
|
||||
width: 100%;
|
||||
& > div {
|
||||
display: flex;
|
||||
}
|
||||
.cp-poll-total {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
.cp-form-poll-body {
|
||||
flex-flow: column;
|
||||
max-height: 225px;
|
||||
overflow: auto;
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
|
@ -985,15 +1041,15 @@
|
|||
}
|
||||
.cp-poll-cell {
|
||||
width: 100px;
|
||||
height: 35px;
|
||||
height: 40px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top:5px;
|
||||
margin-left:5px;
|
||||
&:first-child {
|
||||
width: 200px;
|
||||
}
|
||||
flex-shrink: 0;
|
||||
flex-grow: 1;
|
||||
word-break: break-word;
|
||||
button {
|
||||
width: 100%;
|
||||
border-top: 0px;
|
||||
|
@ -1039,6 +1095,27 @@
|
|||
.cp-poll-time-day {
|
||||
flex-basis: 100px;
|
||||
border-bottom: 1px solid @cryptpad_text_col;
|
||||
&:first-child {
|
||||
border-color: @cp_form-bg1 !important;
|
||||
}
|
||||
}
|
||||
.cp-form-poll-option, .cp-poll-time-day {
|
||||
span {
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
line-height: 1.2;
|
||||
hyphens: auto;
|
||||
text-align: center;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
.cp-poll-switch {
|
||||
button.btn {
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
&:not(.cp-form-poll-switch) {
|
||||
& > div {
|
||||
|
@ -1046,13 +1123,25 @@
|
|||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
.cp-form-poll-body {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
|
||||
.cp-poll-cell {
|
||||
&:first-child {
|
||||
min-width: 200px;
|
||||
background: #424242;
|
||||
position: absolute;
|
||||
margin-left: 0;
|
||||
background: @cp_form-bg1;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
margin-left: 205px;
|
||||
}
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.cp-form-poll-option, .cp-poll-time-day, .cp-form-poll-choice {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cp-form-poll-option, .cp-poll-time-day {
|
||||
flex-flow: column;
|
||||
text-align: center;
|
||||
|
@ -1074,9 +1163,6 @@
|
|||
flex-flow: column;
|
||||
&.cp-form-poll-body {
|
||||
flex-flow: row;
|
||||
max-width: 550px;
|
||||
max-height: unset;
|
||||
scroll-snap-type: x mandatory;
|
||||
& > div {
|
||||
flex-flow: column;
|
||||
}
|
||||
|
@ -1091,12 +1177,18 @@
|
|||
}
|
||||
}
|
||||
.cp-form-poll-option, .cp-poll-switch {
|
||||
span {
|
||||
max-height: 100%;
|
||||
}
|
||||
width: 200px;
|
||||
.cp-form-weekday-separator {
|
||||
margin-right: 5px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
div.cp-poll-time-day-container {
|
||||
width: auto !important;
|
||||
}
|
||||
.cp-poll-time-day {
|
||||
flex-basis: 40px;
|
||||
border-right: none;
|
||||
|
@ -1145,6 +1237,34 @@
|
|||
}
|
||||
}
|
||||
|
||||
#cp-form-settings {
|
||||
.cp-modal {
|
||||
text-align: left;
|
||||
width: 500px;
|
||||
padding: 24px;
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
line-height: 40px;
|
||||
margin-top: -12px;
|
||||
i {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
& > *:not(h2) {
|
||||
color: @cryptpad_text_col;
|
||||
}
|
||||
& > div:not(:last-child) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.cp-form-setting-title {
|
||||
color: @cryptpad_color_link;
|
||||
}
|
||||
}
|
||||
}
|
||||
& > .flatpickr-calendar {
|
||||
z-index: 100001;
|
||||
}
|
||||
|
||||
.charts_main();
|
||||
}
|
||||
|
||||
|
|
|
@ -13,8 +13,74 @@ define([
|
|||
value += '"' + vv + '"';
|
||||
return value;
|
||||
};
|
||||
Export.results = function (content, answers, TYPES, order, isArray) {
|
||||
|
||||
var exportJSON = function (content, answers, TYPES, order) {
|
||||
var form = content.form;
|
||||
var res = {
|
||||
questions: {},
|
||||
responses: []
|
||||
};
|
||||
var q = res.questions;
|
||||
var r = res.responses;
|
||||
|
||||
// Add questions
|
||||
var i = 1;
|
||||
order.forEach(function (key) {
|
||||
var obj = form[key];
|
||||
if (!obj) { return; }
|
||||
var type = obj.type;
|
||||
if (!TYPES[type]) { return; } // Ignore static types
|
||||
var id = `q${i++}`;
|
||||
if (TYPES[type] && TYPES[type].exportCSV) {
|
||||
var _obj = Util.clone(obj);
|
||||
_obj.q = "tmp";
|
||||
q[id] = {
|
||||
question: obj.q,
|
||||
items: TYPES[type].exportCSV(false, _obj).map(function (str) {
|
||||
return str.slice(6); // Remove "tmp | "
|
||||
})
|
||||
};
|
||||
} else {
|
||||
q[id] = obj.q || Messages.form_default;
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(answers || {}).forEach(function (key) {
|
||||
var userObj = answers[key];
|
||||
Object.keys(userObj).forEach(function (k) {
|
||||
var obj = userObj[k];
|
||||
var time = new Date(obj.time).toISOString();
|
||||
var msg = obj.msg || {};
|
||||
var user = msg._userdata || {};
|
||||
var data = {
|
||||
'_time': time,
|
||||
'_name': user.name || Messages.anonymous
|
||||
};
|
||||
|
||||
var i = 1;
|
||||
order.forEach(function (key) {
|
||||
if (!form[key]) { return; }
|
||||
var type = form[key].type;
|
||||
if (!TYPES[type]) { return; } // Ignore static types
|
||||
var id = `q${i++}`;
|
||||
if (TYPES[type].exportCSV) {
|
||||
data[id] = TYPES[type].exportCSV(msg[key], form[key]);
|
||||
return;
|
||||
}
|
||||
data[id] = msg[key];
|
||||
});
|
||||
r.push(data);
|
||||
});
|
||||
});
|
||||
|
||||
return JSON.stringify(res, 0, 2);
|
||||
};
|
||||
Export.results = function (content, answers, TYPES, order, format) {
|
||||
if (!content || !content.form) { return; }
|
||||
|
||||
if (format === "json") { return exportJSON(content, answers, TYPES, order); }
|
||||
|
||||
var isArray = format === "array";
|
||||
var csv = "";
|
||||
var array = [];
|
||||
var form = content.form;
|
||||
|
@ -39,29 +105,32 @@ define([
|
|||
array.push(questions);
|
||||
|
||||
Object.keys(answers || {}).forEach(function (key) {
|
||||
var obj = answers[key];
|
||||
csv += '\n';
|
||||
var time = new Date(obj.time).toISOString();
|
||||
var msg = obj.msg || {};
|
||||
var user = msg._userdata || {};
|
||||
var line = [];
|
||||
line.push(time);
|
||||
line.push(user.name || Messages.anonymous);
|
||||
order.forEach(function (key) {
|
||||
var type = form[key].type;
|
||||
if (!TYPES[type]) { return; } // Ignore static types
|
||||
if (TYPES[type].exportCSV) {
|
||||
var res = TYPES[type].exportCSV(msg[key], form[key]);
|
||||
Array.prototype.push.apply(line, res);
|
||||
return;
|
||||
}
|
||||
line.push(String(msg[key] || ''));
|
||||
var _obj = answers[key];
|
||||
Object.keys(_obj).forEach(function (uid) {
|
||||
var obj = _obj[uid];
|
||||
csv += '\n';
|
||||
var time = new Date(obj.time).toISOString();
|
||||
var msg = obj.msg || {};
|
||||
var user = msg._userdata || {};
|
||||
var line = [];
|
||||
line.push(time);
|
||||
line.push(user.name || Messages.anonymous);
|
||||
order.forEach(function (key) {
|
||||
var type = form[key].type;
|
||||
if (!TYPES[type]) { return; } // Ignore static types
|
||||
if (TYPES[type].exportCSV) {
|
||||
var res = TYPES[type].exportCSV(msg[key], form[key]);
|
||||
Array.prototype.push.apply(line, res);
|
||||
return;
|
||||
}
|
||||
line.push(String(msg[key] || ''));
|
||||
});
|
||||
line.forEach(function (v, i) {
|
||||
if (i) { csv += ','; }
|
||||
csv += escapeCSV(v);
|
||||
});
|
||||
array.push(line);
|
||||
});
|
||||
line.forEach(function (v, i) {
|
||||
if (i) { csv += ','; }
|
||||
csv += escapeCSV(v);
|
||||
});
|
||||
array.push(line);
|
||||
});
|
||||
if (isArray) { return array; }
|
||||
return csv;
|
||||
|
|
1076
www/form/inner.js
1076
www/form/inner.js
File diff suppressed because it is too large
Load Diff
153
www/form/main.js
153
www/form/main.js
|
@ -44,6 +44,7 @@ define([
|
|||
var addRpc = function (sframeChan, Cryptpad, Utils) {
|
||||
sframeChan.on('EV_FORM_PIN', function (data) {
|
||||
channels.answersChannel = data.channel;
|
||||
Cryptpad.changeMetadata();
|
||||
Cryptpad.getPadAttribute('answersChannel', function (err, res) {
|
||||
// If already stored, don't pin it again
|
||||
if (res && res === data.channel) { return; }
|
||||
|
@ -122,6 +123,8 @@ define([
|
|||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
var deleteLines = false; // "false" to support old forms
|
||||
sframeChan.on('Q_FORM_FETCH_ANSWERS', function (data, _cb) {
|
||||
var cb = Utils.Util.once(_cb);
|
||||
var myKeys = {};
|
||||
|
@ -160,6 +163,9 @@ define([
|
|||
Cryptpad.makeNetwork(w(function (err, nw) {
|
||||
network = nw;
|
||||
}));
|
||||
Cryptpad.getPadMetadata({channel: data.channel}, w(function (md) {
|
||||
if (md && md.deleteLines) { deleteLines = true; }
|
||||
}));
|
||||
}).nThen(function () {
|
||||
if (!network) { return void cb({error: "E_CONNECT"}); }
|
||||
|
||||
|
@ -183,6 +189,9 @@ define([
|
|||
validateKey: keys.secondaryValidateKey,
|
||||
owners: [myKeys.edPublic],
|
||||
crypto: crypto,
|
||||
metadata: {
|
||||
deleteLines: true
|
||||
}
|
||||
//Cache: Utils.Cache // TODO enable cache for form responses when the cache stops evicting old answers
|
||||
};
|
||||
var results = {};
|
||||
|
@ -198,7 +207,6 @@ define([
|
|||
nThen(function (waitFor) {
|
||||
accessKeys.forEach(function (obj) {
|
||||
Pinpad.create(network, obj, waitFor(function (e) {
|
||||
console.log('done', obj);
|
||||
if (e) { console.error(e); }
|
||||
}));
|
||||
});
|
||||
|
@ -222,14 +230,26 @@ define([
|
|||
config.onMessage = function (msg, peer, vKey, isCp, hash, senderCurve, cfg) {
|
||||
var parsed = Utils.Util.tryParse(msg);
|
||||
if (!parsed) { return; }
|
||||
var uid = parsed._uid || '000';
|
||||
|
||||
// If we have a "non-anonymous" answer, it may be the edition of a
|
||||
// previous anonymous answer. Check if a previous anonymous answer exists
|
||||
// with the same uid and delete it.
|
||||
if (parsed._proof) {
|
||||
var check = checkAnonProof(parsed._proof, data.channel, curvePrivate);
|
||||
if (check) {
|
||||
delete results[parsed._proof.key];
|
||||
var theirAnonKey = parsed._proof.key;
|
||||
if (check && results[theirAnonKey] && results[theirAnonKey][uid]) {
|
||||
delete results[theirAnonKey][uid];
|
||||
}
|
||||
}
|
||||
if (data.cantEdit && results[senderCurve]) { return; }
|
||||
results[senderCurve] = {
|
||||
|
||||
parsed._time = cfg && cfg.time;
|
||||
if (deleteLines) { parsed._hash = hash; }
|
||||
|
||||
if (data.cantEdit && results[senderCurve]
|
||||
&& results[senderCurve][uid]) { return; }
|
||||
results[senderCurve] = results[senderCurve] || {};
|
||||
results[senderCurve][uid] = {
|
||||
msg: parsed,
|
||||
hash: hash,
|
||||
time: cfg && cfg.time
|
||||
|
@ -239,7 +259,7 @@ define([
|
|||
});
|
||||
});
|
||||
sframeChan.on("Q_FETCH_MY_ANSWERS", function (data, cb) {
|
||||
var answer;
|
||||
var answers = [];
|
||||
var myKeys;
|
||||
nThen(function (w) {
|
||||
Cryptpad.getFormKeys(w(function (keys) {
|
||||
|
@ -259,38 +279,68 @@ define([
|
|||
w.abort();
|
||||
return void cb(obj);
|
||||
}
|
||||
answer = obj;
|
||||
// Get the latest edit per uid
|
||||
var temp = {};
|
||||
obj.forEach(function (ans) {
|
||||
var uid = ans.uid || '000';
|
||||
temp[uid] = ans;
|
||||
});
|
||||
answers = Object.values(temp);
|
||||
}));
|
||||
Cryptpad.getPadMetadata({channel: data.channel}, w(function (md) {
|
||||
if (md && md.deleteLines) { deleteLines = true; }
|
||||
}));
|
||||
}).nThen(function () {
|
||||
if (answer.anonymous) {
|
||||
if (!myKeys.formSeed) { return void cb({ error: "ANONYMOUS_ERROR" }); }
|
||||
myKeys = getAnonymousKeys(myKeys.formSeed, data.channel);
|
||||
}
|
||||
Cryptpad.getHistoryRange({
|
||||
channel: data.channel,
|
||||
lastKnownHash: answer.hash,
|
||||
toHash: answer.hash,
|
||||
}, function (obj) {
|
||||
if (obj && obj.error) { return void cb(obj); }
|
||||
var messages = obj.messages;
|
||||
if (!messages.length) { return void cb(); }
|
||||
if (obj.lastKnownHash !== answer.hash) { return void cb(); }
|
||||
try {
|
||||
var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, {
|
||||
validateKey: data.validateKey,
|
||||
ephemeral_private: Nacl.util.decodeBase64(answer.curvePrivate),
|
||||
my_private: Nacl.util.decodeBase64(myKeys.curvePrivate),
|
||||
their_public: Nacl.util.decodeBase64(data.publicKey)
|
||||
});
|
||||
var parsed = JSON.parse(res.content);
|
||||
parsed._isAnon = answer.anonymous;
|
||||
parsed._time = messages[0].time;
|
||||
cb(parsed);
|
||||
} catch (e) {
|
||||
cb({error: e});
|
||||
}
|
||||
var n = nThen;
|
||||
var err;
|
||||
var all = {};
|
||||
answers.forEach(function (answer) {
|
||||
n = n(function(waitFor) {
|
||||
var finalKeys = myKeys;
|
||||
if (answer.anonymous) {
|
||||
if (!myKeys.formSeed) {
|
||||
err = 'ANONYMOUS_ERROR';
|
||||
console.error('ANONYMOUS_ERROR', answer);
|
||||
return;
|
||||
}
|
||||
finalKeys = getAnonymousKeys(myKeys.formSeed, data.channel);
|
||||
}
|
||||
Cryptpad.getHistoryRange({
|
||||
channel: data.channel,
|
||||
lastKnownHash: answer.hash,
|
||||
toHash: answer.hash,
|
||||
}, waitFor(function (obj) {
|
||||
if (obj && obj.error) { err = obj.error; return; }
|
||||
var messages = obj.messages;
|
||||
if (!messages.length) {
|
||||
// XXX TODO delete from drive.forms
|
||||
return;
|
||||
}
|
||||
if (obj.lastKnownHash !== answer.hash) { return; }
|
||||
try {
|
||||
var res = Utils.Crypto.Mailbox.openOwnSecretLetter(messages[0].msg, {
|
||||
validateKey: data.validateKey,
|
||||
ephemeral_private: Nacl.util.decodeBase64(answer.curvePrivate),
|
||||
my_private: Nacl.util.decodeBase64(finalKeys.curvePrivate),
|
||||
their_public: Nacl.util.decodeBase64(data.publicKey)
|
||||
});
|
||||
var parsed = JSON.parse(res.content);
|
||||
parsed._isAnon = answer.anonymous;
|
||||
parsed._time = messages[0].time;
|
||||
if (deleteLines) { parsed._hash = answer.hash; }
|
||||
var uid = parsed._uid || '000';
|
||||
if (all[uid] && !all[uid]._isAnon) { parsed._isAnon = false; }
|
||||
all[uid] = parsed;
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
}));
|
||||
}).nThen;
|
||||
});
|
||||
n(function () {
|
||||
if (err) { return void cb({error: err}); }
|
||||
cb(all);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -304,11 +354,6 @@ define([
|
|||
// We can create a seed in localStorage.
|
||||
if (!keys.formSeed) {
|
||||
// No drive mode
|
||||
var answered = JSON.parse(localStorage.CP_formAnswered || "[]");
|
||||
if(answered.indexOf(data.channel) !== -1) {
|
||||
// Already answered: abort
|
||||
return void cb({ error: "EANSWERED" });
|
||||
}
|
||||
keys = { formSeed: noDriveSeed };
|
||||
}
|
||||
myKeys = keys;
|
||||
|
@ -334,6 +379,8 @@ define([
|
|||
}
|
||||
|
||||
var crypto = Utils.Crypto.Mailbox.createEncryptor(myKeys);
|
||||
var uid = data.results._uid || Utils.Util.uid();
|
||||
data.results._uid = uid;
|
||||
var text = JSON.stringify(data.results);
|
||||
var ciphertext = crypto.encrypt(text, box.publicKey);
|
||||
|
||||
|
@ -343,15 +390,39 @@ define([
|
|||
ciphertext
|
||||
], function (err, response) {
|
||||
Cryptpad.storeFormAnswer({
|
||||
uid: uid,
|
||||
channel: box.channel,
|
||||
hash: hash,
|
||||
curvePrivate: ephemeral_private,
|
||||
anonymous: Boolean(data.anonymous)
|
||||
}, function () {
|
||||
var res = data.results;
|
||||
res._isAnon = data.anonymous;
|
||||
res._time = +new Date();
|
||||
if (deleteLines) { res._hash = hash; }
|
||||
cb({
|
||||
error: err,
|
||||
response: response,
|
||||
results: res
|
||||
});
|
||||
});
|
||||
cb({error: err, response: response, hash: hash});
|
||||
});
|
||||
});
|
||||
});
|
||||
sframeChan.on("Q_FORM_DELETE_ALL_ANSWERS", function (data, cb) {
|
||||
if (!data || !data.channel) { return void cb({error: 'EINVAL'}); }
|
||||
Cryptpad.clearOwnedChannel(data.channel, cb);
|
||||
});
|
||||
sframeChan.on("Q_FORM_DELETE_ANSWER", function (data, cb) {
|
||||
if (!deleteLines) {
|
||||
return void cb({error: 'EFORBIDDEN'});
|
||||
}
|
||||
Cryptpad.deleteFormAnswers(data, cb);
|
||||
});
|
||||
sframeChan.on("Q_FORM_MUTE", function (data, cb) {
|
||||
if (!Utils.secret) { return void cb({error: 'EINVAL'}); }
|
||||
Cryptpad.muteChannel(Utils.secret.channel, data.muted, cb);
|
||||
});
|
||||
};
|
||||
SFCommonO.start({
|
||||
addData: addData,
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -43,7 +43,14 @@ define([
|
|||
};
|
||||
var getEndDate = function () {
|
||||
setTimeout(function () { $(endPickr.calendarContainer).remove(); });
|
||||
return endPickr.parseDate(e.value);
|
||||
var d = endPickr.parseDate(e.value);
|
||||
|
||||
if (endPickr.config.dateFormat === "Y-m-d") { // All day event
|
||||
// Tui-calendar will remove 1s (1000ms) to the date for an unknown reason...
|
||||
d.setMilliseconds(1000);
|
||||
}
|
||||
|
||||
return d;
|
||||
};
|
||||
|
||||
return {
|
||||
|
|
|
@ -12,4 +12,5 @@ This file is intended to be used as a log of what third-party source we have ven
|
|||
* [Fabricjs 4.6.0](https://github.com/fabricjs/fabric.js) and [Fabric-history](https://github.com/lyzerk/fabric-history) for the whiteboard app
|
||||
* [Requirejs optional module plugin](https://stackoverflow.com/a/27422370)
|
||||
* [asciidoc.js 2.0.0](https://github.com/asciidoctor/codemirror-asciidoc/releases/tag/2.0.0) with slight changes to match the format of other codemirror modes
|
||||
* [Asciidoctor.js 2.2.6](https://github.com/asciidoctor/asciidoctor.js/releases/tag/v2.2.6) for AsciiDoc rendering
|
||||
|
||||
|
|
|
@ -285,6 +285,22 @@
|
|||
}
|
||||
}
|
||||
|
||||
.cp-teams-invite-uses { // XXX
|
||||
input {
|
||||
margin-bottom: 0px !important;
|
||||
margin-right: 10px;
|
||||
width: 75px !important;
|
||||
}
|
||||
|
||||
}
|
||||
.cp-teams-invite-role {
|
||||
margin-bottom: 15px;
|
||||
span:first-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#cp-teams-roster-dialog {
|
||||
table {
|
||||
width: 100%;
|
||||
|
|
|
@ -17,6 +17,7 @@ define([
|
|||
'/customize/application_config.js',
|
||||
'/common/messenger-ui.js',
|
||||
'/common/inner/invitation.js',
|
||||
'/common/clipboard.js',
|
||||
'/common/make-backup.js',
|
||||
'/customize/messages.js',
|
||||
|
||||
|
@ -43,6 +44,7 @@ define([
|
|||
AppConfig,
|
||||
MessengerUI,
|
||||
InviteInner,
|
||||
Clipboard,
|
||||
Backup,
|
||||
Messages)
|
||||
{
|
||||
|
@ -714,6 +716,18 @@ define([
|
|||
title: Messages.team_pendingOwnerTitle
|
||||
}, ' ' + Messages.team_pendingOwner));
|
||||
}
|
||||
if (data.pending && data.inviteChannel && data.remaining === -1) { // Invite link
|
||||
$(name).append(h('em', ' ' + Messages.team_linkUsesInfinite));
|
||||
} else if (data.pending && data.inviteChannel) {
|
||||
$(name).append(h('em', ' ' + Messages._getKey('team_linkUses', [
|
||||
data.remaining || 1,
|
||||
data.totalUses || 1
|
||||
])));
|
||||
}
|
||||
if (data.pending && data.inviteChannel) {
|
||||
var r = data.role === "MEMBER" ? Messages.team_members : Messages.team_viewers;
|
||||
$(name).append(h('em', ' (' + r + ')'));
|
||||
}
|
||||
// Status
|
||||
var status = h('span.cp-team-member-status'+(data.online ? '.online' : ''));
|
||||
// Actions
|
||||
|
@ -817,6 +831,23 @@ define([
|
|||
actions,
|
||||
status,
|
||||
];
|
||||
if (data.inviteChannel) {
|
||||
if (data.hash) {
|
||||
var copy = h('span.fa.fa-copy');
|
||||
$(copy).click(function () {
|
||||
var privateData = common.getMetadataMgr().getPrivateData();
|
||||
var origin = privateData.origin;
|
||||
var href = origin + Hash.hashToHref(data.hash, 'teams');
|
||||
var success = Clipboard.copy(href);
|
||||
if (success) { UI.log(Messages.shareSuccess); }
|
||||
}).prependTo(actions);
|
||||
}
|
||||
content = [
|
||||
avatar,
|
||||
name,
|
||||
actions
|
||||
];
|
||||
}
|
||||
var div = h('div.cp-team-roster-member', content);
|
||||
if (data.profile) {
|
||||
$(div).dblclick(function (e) {
|
||||
|
@ -872,7 +903,7 @@ define([
|
|||
if (!roster[k].pending) { return; }
|
||||
if (!roster[k].inviteChannel) { return; }
|
||||
roster[k].curvePublic = k;
|
||||
return roster[k].role === "VIEWER" || !roster[k].role;
|
||||
return roster[k].role === "MEMBER" || roster[k].role === "VIEWER" || !roster[k].role;
|
||||
}).map(function (k) {
|
||||
return makeMember(common, roster[k], me);
|
||||
});
|
||||
|
@ -1349,7 +1380,7 @@ define([
|
|||
Messages._getKey('team_inviteFromMsg',
|
||||
[Util.fixHTML(getDisplayName(json.author.displayName)),
|
||||
Util.fixHTML(json.teamName)])));
|
||||
if (typeof(json.message) === 'string') {
|
||||
if (typeof(json.message) === 'string' && json.message) {
|
||||
var message = h('div.cp-teams-invite-message');
|
||||
json.message.split('\n').forEach(line => {
|
||||
if (line.trim()) {
|
||||
|
@ -1524,12 +1555,12 @@ define([
|
|||
$div.empty().append(content);
|
||||
});
|
||||
}
|
||||
var $divLink = $('div.cp-team-link').empty();
|
||||
/*var $divLink = $('div.cp-team-link').empty();
|
||||
if ($divLink.length) {
|
||||
refreshLink(common, function (content) {
|
||||
$divLink.append(content);
|
||||
});
|
||||
}
|
||||
}*/
|
||||
var $divCreate = $('div.cp-team-create');
|
||||
if ($divCreate.length) {
|
||||
refreshCreate(common, function (content) {
|
||||
|
|
Loading…
Reference in New Issue