Merge branch 'staging' into merge-owned

pull/1/head
ansuz 5 years ago
commit f63e787fc6

@ -1,3 +1,56 @@
# Thylacine release (3.19.0)
## Goals
The intent of this release was to catch up on our backlog of bug fixes and minor usability improvements.
## Update notes
This release features an update to our clientside dependencies.
To update to 3.19.0 from 3.18.1:
1. Stop your server
2. Get the latest code with git
3. Get the latest clientside dependencies with `bower update`
4. Restart your server
## Features
* The most notable change in this release is that the use of "safe links" (introduced in our 3.11.0 release) has been made the new default for documents. This means that when you open a document that is stored in your drive your browser's address bar will not contain the encryption keys for the document, only an identifier used to look up those encryption keys which are stored in your drive. This makes it less likely that you'll leak access to your documents during video meetings, when sharing screenshots, or when using shared computers that store the history of pages you've viewed.
* To share access to documents with links, you'll need to use the _share menu_ which has recently been made more prominent in the platform's toolbars
* This setting is configurable, so you can still choose to disable the use of safe links via your settings page.
* We've updated the layout of the "user admin menu" which can be found in the top-right corner by clicking your avatar. It features an "About CryptPad" menu which displays the version of the instance you're using as well as some resources which are otherwise only available via the footer of static pages.
* We often receive support tickets in languages that we don't speak, which forces us to use translation services in order to answer questions. To address this issue, we've made it possible for admins to display a notice indicating which languages they speak. An example configuration is provided in `customize.dist/application_config.js`.
* We've integrated two PRs:
1. [Only list premium features when subscriptions are enabled](https://github.com/xwiki-labs/cryptpad/pull/538).
2. [Add privacy policy option](https://github.com/xwiki-labs/cryptpad/pull/537).
* We found it cumbersome to add new cards to the top of our Kanban columns, since we had to create a new card at the bottom and then drag it to the top. In response, we've broken up the rather large "new card" button into two buttons, one which adds a card at the top, and another which adds a new card at the bottom.
* We've made it easier to use tags for files in the drive:
1. You can now select multiple files and apply a set of tags to all of them.
2. Hitting "enter" in an empty tag prompt field will submit the current list of tags.
* We've also made a few tweaks to the kanban layout:
1. The "trash bar" only appears while you are actively dragging a card.
2. The "tag list" now takes up more of the available width, while the button to clear the currently applied tag filter has been moved to the left, replacing the "filter by tag" hint text.
* We've received requests to enable translations for a number of languages over the last few months. The following languages are enabled on [our weblate instance](https://weblate.cryptpad.fr/projects/cryptpad/app/), but have yet to be translated.
* Arabic
* Hindi
* Telugu
* Turkish
* Unregistered users were able to open up the "filepicker modal" in spreadsheets. It was already possible to embed an image which they'd already stored in their drive, but it was not clear why they were not able to upload a new image. We now display a disabled upload button with a tooltip to log in or register in order to upload images.
* Finally, we've updated the styles in our presentation editor to better match our recent toolbar redesign and the mermaidjs integration.
## Bug fixes
* We now preserve formatting in multi-line messages in team invitations.
* The slide editor exhibited some strange behaviour where the page would reload the first time you entered "present mode" after creating the document. We've also fixed some issues with printing.
* We now prevent the local resizing of images in the rich text editor while it is locked due to disconnection or the lack of edit rights.
* We've updated our marked.js dependency to the latest version in order to correct some minor rendering bugs.
* Unregistered users are now redirected to the login page when they visit the support page.
* We've removed the unsupported "rename" entry from the right-click menu in unregistered users drives.
* After a deep investigation we found and fixed the cause of a bug in which user accounts spontaneously removed themselves from teams. A flaw in the serverside cache caused clients to load an incomplete account of the team's membership which caused the team to appear to have been deleted. Unfortunately, the client responded by removing the corrupt team credentials from their account. Our fix will prevent future corruptions, but does not restore unintentionally removed teams.
* Lastly, we've added a "Hind" font to the spreadsheet editor which introduces basic support for Devanagari characters.
# Smilodon's revenge (3.18.1) # Smilodon's revenge (3.18.1)
Our next major release (3.19.0) is still a few weeks away. Our next major release (3.19.0) is still a few weeks away.

@ -62,7 +62,13 @@ define([
var imprintUrl = AppConfig.imprint && (typeof(AppConfig.imprint) === "boolean" ? var imprintUrl = AppConfig.imprint && (typeof(AppConfig.imprint) === "boolean" ?
'/imprint.html' : AppConfig.imprint); '/imprint.html' : AppConfig.imprint);
Pages.versionString = "CryptPad v3.18.1 (Smilodon's revenge)"; Pages.versionString = "CryptPad v3.19.0 (Thylacine)";
// used for the about menu
Pages.imprintLink = AppConfig.imprint ? footLink(imprintUrl, 'imprint') : undefined;
Pages.privacyLink = footLink(AppConfig.privacy, 'privacy');
Pages.githubLink = footLink('https://github.com/xwiki-labs/cryptpad', null, 'GitHub');
Pages.faqLink = footLink('/faq.html', 'faq_link');
Pages.infopageFooter = function () { Pages.infopageFooter = function () {
return h('footer', [ return h('footer', [
@ -74,24 +80,14 @@ define([
languageSelector() languageSelector()
]) ])
], ''), ], ''),
/*footerCol('footer_applications', [
footLink('/drive/', 'main_drive'),
footLink('/pad/', 'main_richText'),
footLink('/code/', 'main_code'),
footLink('/slide/', 'main_slide'),
footLink('/poll/', 'main_poll'),
footLink('/kanban/', 'main_kanban'),
footLink('/whiteboard/', null, Msg.type.whiteboard)
]),*/
footerCol('footer_product', [ footerCol('footer_product', [
footLink('https://cryptpad.fr/what-is-cryptpad.html', 'topbar_whatIsCryptpad'), footLink('/what-is-cryptpad.html', 'topbar_whatIsCryptpad'),
footLink('/faq.html', 'faq_link'), Pages.faqLink,
footLink('https://github.com/xwiki-labs/cryptpad', null, 'GitHub'), Pages.githubLink,
footLink('https://opencollective.com/cryptpad/contribute/', 'footer_donate'), footLink('https://opencollective.com/cryptpad/contribute/', 'footer_donate'),
]), ]),
footerCol('footer_aboutUs', [ footerCol('footer_aboutUs', [
/*footLink('https://blog.cryptpad.fr', 'blog'), /*footLink('https://blog.cryptpad.fr', 'blog'), */
footLink('https://labs.xwiki.com', null, 'XWiki Labs'),*/
footLink('http://www.xwiki.com', null, 'XWiki SAS'), footLink('http://www.xwiki.com', null, 'XWiki SAS'),
footLink('https://www.open-paas.org', null, 'OpenPaaS'), footLink('https://www.open-paas.org', null, 'OpenPaaS'),
footLink('/about.html', 'footer_team'), footLink('/about.html', 'footer_team'),
@ -99,15 +95,9 @@ define([
]), ]),
footerCol('footer_legal', [ footerCol('footer_legal', [
footLink('/terms.html', 'footer_tos'), footLink('/terms.html', 'footer_tos'),
footLink(AppConfig.privacy, 'privacy'), Pages.privacyLink,
AppConfig.imprint ? footLink(imprintUrl, 'imprint') : undefined, Pages.imprintLink,
]), ]),
/*footerCol('footer_contact', [
footLink('https://riot.im/app/#/room/#cryptpad:matrix.org', null, 'Chat'),
footLink('https://twitter.com/cryptpad', null, 'Twitter'),
footLink('https://github.com/xwiki-labs/cryptpad', null, 'GitHub'),
footLink('/contact.html', null, 'Email')
])*/
]) ])
]), ]),
h('div.cp-version-footer', Pages.versionString) h('div.cp-version-footer', Pages.versionString)
@ -150,13 +140,9 @@ define([
h('a.navbar-brand', { href: '/index.html'}), h('a.navbar-brand', { href: '/index.html'}),
button, button,
h('div.collapse.navbar-collapse.justify-content-end#menuCollapse', [ h('div.collapse.navbar-collapse.justify-content-end#menuCollapse', [
//h('a.nav-item.nav-link', { href: '/what-is-cryptpad.html'}, Msg.topbar_whatIsCryptpad), // Moved the FAQ
//h('a.nav-item.nav-link', { href: '/faq.html'}, Msg.faq_link),
h('a.nav-item.nav-link', { href: 'https://blog.cryptpad.fr/'}, Msg.blog), h('a.nav-item.nav-link', { href: 'https://blog.cryptpad.fr/'}, Msg.blog),
h('a.nav-item.nav-link', { href: '/features.html'}, Msg.pricing), h('a.nav-item.nav-link', { href: '/features.html'}, Msg.pricing),
h('a.nav-item.nav-link', { href: '/privacy.html'}, Msg.privacy), h('a.nav-item.nav-link', { href: '/privacy.html'}, Msg.privacy),
//h('a.nav-item.nav-link', { href: '/contact.html'}, Msg.contact),
//h('a.nav-item.nav-link', { href: '/about.html'}, Msg.about),
].concat(rightLinks)) ].concat(rightLinks))
); );
}; };

@ -10,7 +10,6 @@
margin: 15px 0; margin: 15px 0;
cursor: pointer; cursor: pointer;
height: @variables_bar-height; height: @variables_bar-height;
line-height: @variables_bar-height - 10px;
.fa, .cptools { .fa, .cptools {
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;

@ -4,6 +4,9 @@
@msg-bg: #eee; @msg-bg: #eee;
@fromme-bg: #ddd; @fromme-bg: #ddd;
.cp-support-form-container { .cp-support-form-container {
div {
margin-bottom: 10px;
}
[type="text"] { [type="text"] {
width: @sidebar_button-width; width: @sidebar_button-width;
margin-bottom: 10px; margin-bottom: 10px;
@ -15,6 +18,18 @@
height: 300px; height: 300px;
} }
} }
.cp-support-attachments {
display: flex;
.fa {
cursor: pointer;
margin-right: 10px;
}
&> span {
border: 1px solid #ddd;
margin-right: 5px;
padding: 10px;
}
}
.cp-support-container { .cp-support-container {
.cp-support-list-ticket { .cp-support-list-ticket {
display: flex; display: flex;

@ -325,6 +325,9 @@ const storeMessage = function (Env, channel, msg, isCp, optionalMessageHash) {
} }
})); }));
}).nThen((waitFor) => { }).nThen((waitFor) => {
/* TODO we can skip updating the index if there's nobody in the channel.
Populating it might actually be the cause of a memory leak.
*/
getIndex(Env, id, waitFor((err, index) => { getIndex(Env, id, waitFor((err, index) => {
if (err) { if (err) {
Log.warn("HK_STORE_MESSAGE_INDEX", err.stack); Log.warn("HK_STORE_MESSAGE_INDEX", err.stack);
@ -340,7 +343,12 @@ const storeMessage = function (Env, channel, msg, isCp, optionalMessageHash) {
line: ((index.line || 0) + 1) line: ((index.line || 0) + 1)
}); });
} }
if (optionalMessageHash) { /* This 'getIndex' call will construct a new index if one does not already exist.
If that is the case then our message will already be present and updating our offset map
can actually cause it to become incorrect, leading to incorrect behaviour when clients connect
with a lastKnownHash. We avoid this by only assigning new offsets to the map.
*/
if (optionalMessageHash && typeof(index.offsetByHash[optionalMessageHash]) === 'undefined') {
index.offsetByHash[optionalMessageHash] = index.size; index.offsetByHash[optionalMessageHash] = index.size;
index.offsets++; index.offsets++;
} }

2
package-lock.json generated

@ -1,6 +1,6 @@
{ {
"name": "cryptpad", "name": "cryptpad",
"version": "3.18.1", "version": "3.19.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

@ -1,7 +1,7 @@
{ {
"name": "cryptpad", "name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server", "description": "realtime collaborative visual editor with zero knowlege server",
"version": "3.18.1", "version": "3.19.0",
"license": "AGPL-3.0+", "license": "AGPL-3.0+",
"repository": { "repository": {
"type": "git", "type": "git",

@ -0,0 +1,116 @@
/* globals process */
var Client = require("../../lib/client");
var Nacl = require("tweetnacl/nacl-fast");
var nThen = require("nthen");
var CPNetflux = require("../../www/bower_components/chainpad-netflux/chainpad-netflux");
var Hash = require("../../www/common/common-hash");
var Rpc = require("../../www/common/rpc");
var HK = require("../../lib/hk-util");
var identity = function (x) {
return x;
};
var crypto = {
encrypt: identity,
decrypt: identity,
};
var N = 2;
var BREAK;
BREAK = 1;
var client;
nThen(function (w) {
//console.log("Creating client");
Client.create(w(function (err, _client) {
if (err) {
console.error(err);
process.exit(1);
}
client = _client;
}));
}).nThen(function (w) {
//console.log("Creating RPC module");
Rpc.createAnonymous(client.config.network, w(function (err, rpc) {
if (err) {
w.abort();
return void console.error('ANON_RPC_CONNECT_ERR');
}
client.anonRpc = rpc;
}));
}).nThen(function (w) {
var done = w();
//console.log("sending random messages");
client.channel = Hash.createChannelId();
if (BREAK) {
CPNetflux.start({
//lastKnownHash: HK.getHash(client.sent[0]),
network: client.config.network,
channel: client.channel,
crypto: crypto,
noChainPad: true,
onReady: w(),
//onMessage: onMessage,
});
}
// send a few random messages to a channel
client.sent = [];
var i = N;
var send = function () {
//console.log(i);
if (i-- <= 0) { return void done(); }
var ciphertext = Nacl.util.encodeBase64(Nacl.randomBytes(256));
client.anonRpc.send('WRITE_PRIVATE_MESSAGE', [
client.channel,
ciphertext
], function (err) {
if (err) {
console.error(err);
process.exit(1);
}
client.sent.push(ciphertext);
console.log("sent: %s", ciphertext);
//setTimeout(send, 500);
send();
});
};
send();
}).nThen(function () {
//process.exit(1);
// connect to that channel with a lastKnownHash
// check if the first message received has the hash that you asked for
console.log();
var lkh = HK.getHash(client.sent[0]);
var i = 0;
var onMessage = function (msg, user, vKey, isCp, hash /*, author */) {
if (i === 0 && hash !== lkh) {
console.error('incorrect hash: [%s]', hash);
process.exit(1);
}
console.log(msg);
if (++i >= N) {
process.exit(1);
}
};
CPNetflux.start({
lastKnownHash: lkh,
network: client.config.network,
channel: client.channel,
crypto: crypto,
noChainPad: true,
onMessage: onMessage,
});
});

@ -185,6 +185,20 @@ define([
var $container = makeBlock('support-list'); var $container = makeBlock('support-list');
var $div = $(h('div.cp-support-container')).appendTo($container); var $div = $(h('div.cp-support-container')).appendTo($container);
var catContainer = h('div.cp-dropdown-container');
$div.append(catContainer);
var category = 'all';
var $drop = APP.support.makeCategoryDropdown(catContainer, function (key) {
category = key;
if (key === 'all') {
$div.find('.cp-support-list-ticket').show();
return;
}
$div.find('.cp-support-list-ticket').hide();
$div.find('.cp-support-list-ticket[data-cat="'+key+'"]').show();
}, true);
$drop.setValue('all');
var metadataMgr = common.getMetadataMgr(); var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData(); var privateData = metadataMgr.getPrivateData();
var cat = privateData.category || ''; var cat = privateData.category || '';
@ -277,6 +291,9 @@ define([
UI.alert(Messages.error); UI.alert(Messages.error);
}); });
}); });
if (category !== 'all' && $ticket.attr('data-cat') !== category) {
$ticket.hide();
}
} }
$ticket.append(APP.support.makeMessage(content, hash)); $ticket.append(APP.support.makeMessage(content, hash));
reorder(); reorder();

@ -328,11 +328,7 @@ define([
var input = dialog.textInput(); var input = dialog.textInput();
var tagger = dialog.frame([ var tagger = dialog.frame([
dialog.message([ dialog.message([ Messages.tags_add ]),
Messages.tags_add,
h('br'),
Messages.tags_searchHint,
]),
input, input,
h('center', h('small', Messages.tags_notShared)), h('center', h('small', Messages.tags_notShared)),
dialog.nav(), dialog.nav(),

@ -313,26 +313,30 @@ define([
var $friends = $div.find('.cp-usergrid-user.cp-selected'); var $friends = $div.find('.cp-usergrid-user.cp-selected');
$friends.each(function (i, el) { $friends.each(function (i, el) {
var curve = $(el).attr('data-curve'); var curve = $(el).attr('data-curve');
// Check if the selected element is a friend or a team
if (curve) { // Friend
if (!curve || !friends[curve]) { return; }
var friend = friends[curve];
if (!friend.notifications || !friend.curvePublic) { return; }
common.mailbox.sendTo("SHARE_PAD", {
href: href,
password: config.password,
isTemplate: config.isTemplate,
name: myName,
title: title
}, {
channel: friend.notifications,
curvePublic: friend.curvePublic
});
return;
}
// Team
var ed = $(el).attr('data-ed'); var ed = $(el).attr('data-ed');
var friend = curve && friends[curve];
var team = teams[ed]; var team = teams[ed];
// If the selected element is a friend or a team without edit right,
// send a notification
var mailbox = friend || ((team && team.viewer) ? team : undefined);
if (mailbox) { // Friend
if (friends[curve] && !mailbox.notifications) { return; }
if (mailbox.notifications && mailbox.curvePublic) {
common.mailbox.sendTo("SHARE_PAD", {
href: href,
password: config.password,
isTemplate: config.isTemplate,
name: myName,
title: title
}, {
viewed: team && team.id,
channel: mailbox.notifications,
curvePublic: mailbox.curvePublic
});
return;
}
}
// If it's a team with edit right, add the pad directly
if (!team) { return; } if (!team) { return; }
sframeChan.query('Q_STORE_IN_TEAM', { sframeChan.query('Q_STORE_IN_TEAM', {
href: href, href: href,
@ -450,10 +454,11 @@ define([
// config.teamId only exists when we're trying to share a pad from a team drive // config.teamId only exists when we're trying to share a pad from a team drive
// In this case, we don't want to share the pad with the current team // In this case, we don't want to share the pad with the current team
if (config.teamId && config.teamId === id) { return; } if (config.teamId && config.teamId === id) { return; }
if (!teamsData[id].hasSecondaryKey) { return; }
var t = teamsData[id]; var t = teamsData[id];
teams[t.edPublic] = { teams[t.edPublic] = {
notifications: true, viewer: !teamsData[id].hasSecondaryKey,
notifications: t.notifications,
curvePublic: t.curvePublic,
displayName: t.name, displayName: t.name,
edPublic: t.edPublic, edPublic: t.edPublic,
avatar: t.avatar, avatar: t.avatar,
@ -1569,7 +1574,6 @@ define([
button = $('<button>', { button = $('<button>', {
title: Messages.printButtonTitle2, title: Messages.printButtonTitle2,
'class': "fa fa-print cp-toolbar-icon-print", 'class': "fa fa-print cp-toolbar-icon-print",
// XXX people don't realize this does PDF (https://github.com/xwiki-labs/cryptpad/issues/357#issuecomment-640711724)
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.printText)); }).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.printText));
break; break;
case 'history': case 'history':
@ -2204,7 +2208,9 @@ define([
if (config.isSelect && value) { if (config.isSelect && value) {
var $val = $innerblock.find('[data-value="'+value+'"]'); var $val = $innerblock.find('[data-value="'+value+'"]');
setActive($val); setActive($val);
$innerblock.scrollTop($val.position().top + $innerblock.scrollTop()); try {
$innerblock.scrollTop($val.position().top + $innerblock.scrollTop());
} catch (e) {}
} }
if (config.feedback) { Feedback.send(config.feedback); } if (config.feedback) { Feedback.send(config.feedback); }
}; };
@ -2303,29 +2309,38 @@ define([
var priv = metadataMgr.getPrivateData(); var priv = metadataMgr.getPrivateData();
var origin = priv.origin; var origin = priv.origin;
Messages.help_faq = "Review our list of frequently asked questions"; // XXX // TODO link to the most recent changelog/release notes
var faqLine = h('p', // https://github.com/xwiki-labs/cryptpad/releases/latest/ ?
h('a', {
target: '_blank', var template = function (line, link) {
rel: 'noreferrer noopener', if (!line || !link) { return; }
href: origin + '/faq.html', var p = $('<p>').html(line)[0];
}, Messages.help_faq) var sub = link.cloneNode(true);
);
/* This is a hack to make relative URLs point to the main domain
// XXX link to the most recent changelog/release notes instead of the sandbox domain. It will break if the admins have specified
// XXX FAQ some less common URL formats for their customizable links, such as if they've
// XXX GitHub used a protocal-relative absolute URL. The URL API isn't quite safe to use
// XXX privacy policy because of IE (thanks, Bill). */
// XXX legal notice var href = sub.getAttribute('href');
if (/^\//.test(href)) { sub.setAttribute('href', origin + href); }
var content = h('div', [ var a = p.querySelector('a');
// CryptPad version number if (!a) { return; }
sub.innerText = a.innerText;
p.replaceChild(sub, a);
return p;
};
var legalLine = template(Messages.info_imprintFlavour, Pages.imprintLink);
var privacyLine = template(Messages.info_privacyFlavour, Pages.privacyLink);
var faqLine = template(Messages.help.generic.more, Pages.faqLink);
var content = h('div.cp-info-menu-container', [
h('h6', Pages.versionString), h('h6', Pages.versionString),
// First point users to our FAQ h('hr'),
legalLine,
privacyLine,
faqLine, faqLine,
// Link to the support ticket form in case their
// question isn't answered by the FAQ
//supportLine,
]); ]);
var buttons = [ var buttons = [
@ -2373,15 +2388,21 @@ define([
content: $userAdminContent.html() content: $userAdminContent.html()
}); });
} }
options.push({
tag: 'a', if (accountName && !AppConfig.disableProfile) {
attributes: { options.push({
'target': '_blank', tag: 'a',
'href': origin+'/index.html', attributes: {'class': 'cp-toolbar-menu-profile fa fa-user-circle'},
'class': 'fa fa-home' content: h('span', Messages.profileButton),
}, action: function () {
content: h('span', Messages.homePage) if (padType) {
}); window.open(origin+'/profile/');
} else {
window.parent.location = origin+'/profile/';
}
},
});
}
if (padType !== 'drive' || (!accountName && priv.newSharedFolder)) { if (padType !== 'drive' || (!accountName && priv.newSharedFolder)) {
options.push({ options.push({
tag: 'a', tag: 'a',
@ -2415,29 +2436,6 @@ define([
content: h('span', Messages.type.contacts) content: h('span', Messages.type.contacts)
}); });
} }
options.push({ tag: 'hr' });
// Add the change display name button if not in read only mode
if (config.changeNameButtonCls && config.displayChangeName && !AppConfig.disableProfile) {
options.push({
tag: 'a',
attributes: {'class': config.changeNameButtonCls + ' fa fa-user'},
content: h('span', Messages.user_rename)
});
}
if (accountName && !AppConfig.disableProfile) {
options.push({
tag: 'a',
attributes: {'class': 'cp-toolbar-menu-profile fa fa-user-circle'},
content: h('span', Messages.profileButton),
action: function () {
if (padType) {
window.open(origin+'/profile/');
} else {
window.parent.location = origin+'/profile/';
}
},
});
}
if (padType !== 'settings') { if (padType !== 'settings') {
options.push({ options.push({
tag: 'a', tag: 'a',
@ -2452,6 +2450,7 @@ define([
}, },
}); });
} }
options.push({ tag: 'hr' }); options.push({ tag: 'hr' });
// Add administration panel link if the user is an admin // Add administration panel link if the user is an admin
if (priv.edPublic && Array.isArray(Config.adminKeys) && Config.adminKeys.indexOf(priv.edPublic) !== -1) { if (priv.edPublic && Array.isArray(Config.adminKeys) && Config.adminKeys.indexOf(priv.edPublic) !== -1) {
@ -2482,30 +2481,6 @@ define([
}, },
}); });
} }
options.push({ tag: 'hr' });
if (Config.allowSubscriptions) {
options.push({
tag: 'a',
attributes: {
'target': '_blank',
'href': priv.plan ? priv.accounts.upgradeURL : origin+'/features.html',
'class': 'fa fa-star-o'
},
content: h('span', priv.plan ? Messages.settings_cat_subscription : Messages.pricing)
});
}
if (!priv.plan && !Config.removeDonateButton) {
options.push({
tag: 'a',
attributes: {
'target': '_blank',
'rel': 'noopener',
'href': priv.accounts.donateURL,
'class': 'fa fa-gift'
},
content: h('span', Messages.crowdfunding_button2)
});
}
if (AppConfig.surveyURL) { if (AppConfig.surveyURL) {
options.push({ options.push({
tag: 'a', tag: 'a',
@ -2521,7 +2496,6 @@ define([
}, },
}); });
} }
Messages.user_about = 'About CryptPad'; // XXX
options.push({ options.push({
tag: 'a', tag: 'a',
attributes: { attributes: {
@ -2533,6 +2507,49 @@ define([
}, },
}); });
options.push({
tag: 'a',
attributes: {
'target': '_blank',
'href': origin+'/index.html',
'class': 'fa fa-home'
},
content: h('span', Messages.homePage)
});
// Add the change display name button if not in read only mode
/*
if (config.changeNameButtonCls && config.displayChangeName && !AppConfig.disableProfile) {
options.push({
tag: 'a',
attributes: {'class': config.changeNameButtonCls + ' fa fa-user'},
content: h('span', Messages.user_rename)
});
}*/
options.push({ tag: 'hr' });
if (Config.allowSubscriptions) {
options.push({
tag: 'a',
attributes: {
'target': '_blank',
'href': priv.plan ? priv.accounts.upgradeURL : origin+'/features.html',
'class': 'fa fa-star-o'
},
content: h('span', priv.plan ? Messages.settings_cat_subscription : Messages.pricing)
});
}
if (!priv.plan && !Config.removeDonateButton) {
options.push({
tag: 'a',
attributes: {
'target': '_blank',
'rel': 'noopener',
'href': priv.accounts.donateURL,
'class': 'fa fa-gift'
},
content: h('span', Messages.crowdfunding_button2)
});
}
options.push({ tag: 'hr' }); options.push({ tag: 'hr' });
// Add login or logout button depending on the current status // Add login or logout button depending on the current status
if (priv.loggedIn) { if (priv.loggedIn) {

@ -943,6 +943,7 @@ define([
// Ctrl+A select all // Ctrl+A select all
if (e.which === 65 && (e.ctrlKey || (e.metaKey && APP.isMac))) { if (e.which === 65 && (e.ctrlKey || (e.metaKey && APP.isMac))) {
e.preventDefault();
$content.find('.cp-app-drive-element:not(.cp-app-drive-element-selected)') $content.find('.cp-app-drive-element:not(.cp-app-drive-element-selected)')
.each(function (idx, element) { .each(function (idx, element) {
selectElement($(element)); selectElement($(element));

@ -84,6 +84,10 @@ define([
// Share pad // Share pad
Messages.notification_padSharedTeam = "{0} has shared a pad with the team {2}: <b>{1}</b>"; // XXX
Messages.notification_fileSharedTeam = "{0} has shared a file with the team {2}: <b>{1}</b>"; // XXX
Messages.notification_folderSharedTeam = "{0} has shared a pad with the team {2}: <b>{1}</b>"; // XXX
handlers['SHARE_PAD'] = function(common, data) { handlers['SHARE_PAD'] = function(common, data) {
var content = data.content; var content = data.content;
var msg = content.msg; var msg = content.msg;
@ -91,10 +95,22 @@ define([
var key = type === 'drive' ? 'notification_folderShared' : var key = type === 'drive' ? 'notification_folderShared' :
(type === 'file' ? 'notification_fileShared' : (type === 'file' ? 'notification_fileShared' :
'notification_padShared'); 'notification_padShared');
var teamNotification = /^team-/.test(data.type) && Number(data.type.slice(5));
var teamName = '';
if (teamNotification) {
var privateData = common.getMetadataMgr().getPrivateData();
var teamsData = Util.tryParse(JSON.stringify(privateData.teams)) || {};
var team = teamsData[teamNotification];
if (!team || !team.name) { return; }
key += "Team";
teamName = Util.fixHTML(team.name);
}
var name = Util.fixHTML(msg.content.name) || Messages.anonymous; var name = Util.fixHTML(msg.content.name) || Messages.anonymous;
var title = Util.fixHTML(msg.content.title); var title = Util.fixHTML(msg.content.title);
content.getFormatText = function() { content.getFormatText = function() {
return Messages._getKey(key, [name, title]); return Messages._getKey(key, [name, title, teamName]);
}; };
content.handler = function() { content.handler = function() {
var todo = function() { var todo = function() {
@ -105,6 +121,9 @@ define([
if (msg.content.isTemplate) { if (msg.content.isTemplate) {
common.sessionStorage.put(Constants.newPadPathKey, ['template'], waitFor()); common.sessionStorage.put(Constants.newPadPathKey, ['template'], waitFor());
} }
if (teamNotification) {
common.sessionStorage.put(Constants.newPadTeamKey, teamNotification, waitFor());
}
common.sessionStorage.put('newPadPassword', msg.content.password || '', waitFor()); common.sessionStorage.put('newPadPassword', msg.content.password || '', waitFor());
}).nThen(function() { }).nThen(function() {
todo(); todo();
@ -388,8 +407,6 @@ define([
}; };
handlers['SAFE_LINKS_DEFAULT'] = function (common, data) { handlers['SAFE_LINKS_DEFAULT'] = function (common, data) {
Messages.settings_safeLinkDefault = "SAFE LINKS ARE NOW DEFAULT"; // XXX
var content = data.content; var content = data.content;
content.getFormatText = function () { content.getFormatText = function () {
return Messages.settings_safeLinkDefault; return Messages.settings_safeLinkDefault;

@ -122,7 +122,6 @@ define([
if (!state && !readOnly) { if (!state && !readOnly) {
$('#cp-app-oo-editor').append(h('div#cp-app-oo-offline')); $('#cp-app-oo-editor').append(h('div#cp-app-oo-offline'));
} }
debug(state);
}; };
var deleteOffline = function () { var deleteOffline = function () {
@ -462,6 +461,20 @@ define([
}); });
} }
}; };
var deleteLastCp = function () {
var hashes = content.hashes;
if (!hashes || !Object.keys(hashes).length) { return; }
var i = 0;
var idx = Object.keys(hashes).map(Number).sort(function (a, b) {
return a-b;
});
var lastIndex = idx[idx.length - 1 - i];
delete content.hashes[lastIndex];
APP.onLocal();
APP.realtime.onSettle(function () {
UI.log(Messages.saved);
});
};
var restoreLastCp = function () { var restoreLastCp = function () {
content.saveLock = myOOId; content.saveLock = myOOId;
APP.onLocal(); APP.onLocal();
@ -492,6 +505,89 @@ define([
}, to); }, to);
}; };
var loadInitDocument = function (type, useNewDefault) {
var newText;
switch (type) {
case 'sheet' :
newText = EmptyCell(useNewDefault);
break;
case 'oodoc':
newText = EmptyDoc();
break;
case 'ooslide':
newText = EmptySlide();
break;
default:
newText = '';
}
return new Blob([newText], {type: 'text/plain'});
};
var loadLastDocument = function (lastCp, onCpError, cb) {
ooChannel.cpIndex = lastCp.index || 0;
var parsed = Hash.parsePadUrl(lastCp.file);
var secret = Hash.getSecrets('file', parsed.hash);
if (!secret || !secret.channel) { return; }
var hexFileName = secret.channel;
var fileHost = privateData.fileHost || privateData.origin;
var src = fileHost + Hash.getBlobPathFromHex(hexFileName);
var key = secret.keys && secret.keys.cryptKey;
var xhr = new XMLHttpRequest();
xhr.open('GET', src, true);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
if (/^4/.test('' + this.status)) {
onCpError();
return void console.error('XHR error', this.status);
}
var arrayBuffer = xhr.response;
if (arrayBuffer) {
var u8 = new Uint8Array(arrayBuffer);
FileCrypto.decrypt(u8, key, function (err, decrypted) {
if (err) { return void console.error(err); }
var blob = new Blob([decrypted.content], {type: 'plain/text'});
if (cb) {
return cb(blob, getFileType());
}
startOO(blob, getFileType());
});
}
};
xhr.onerror = function () {
onCpError();
};
xhr.send(null);
};
Messages.oo_refresh = "Refresh"; // XXX read-only corner popup when receiving remote updates
Messages.oo_refreshText = "out of date"; // XXX read-only corner popup when receiving remote updates
var refreshReadOnly = function () {
var cancel = h('button.cp-corner-cancel', Messages.cancel);
var reload = h('button.cp-corner-primary', [
h('i.fa.fa-refresh'),
Messages.oo_refresh
]);
var actions = h('div', [cancel, reload]);
var m = UI.cornerPopup(Messages.oo_refreshText, actions, '');
$(reload).click(function () {
ooChannel.ready = false;
var lastCp = getLastCp();
loadLastDocument(lastCp, function () {
var file = getFileType();
var type = common.getMetadataMgr().getPrivateData().ooType;
var blob = loadInitDocument(type, true);
resetData(blob, file);
}, function (blob, file) {
resetData(blob, file);
});
delete APP.refreshPopup;
m.delete();
});
$(cancel).click(function () {
delete APP.refreshPopup;
m.delete();
});
};
var openRtChannel = function (cb) { var openRtChannel = function (cb) {
if (rtChannel.ready) { return void cb(); } if (rtChannel.ready) { return void cb(); }
@ -515,6 +611,18 @@ define([
break; break;
case 'MESSAGE': case 'MESSAGE':
if (ooChannel.ready) { if (ooChannel.ready) {
// In read-only mode, push the message to the queue and prompt
// the user to refresh OO (without reloading the page)
if (readOnly) {
ooChannel.queue.push(obj.data);
if (APP.refreshPopup) { return; }
APP.refreshPopup = true;
// Don't "spam" the user instantly and no more than
// 1 popup every 30s
setTimeout(refreshReadOnly, 30000);
return;
}
ooChannel.send(obj.data.msg); ooChannel.send(obj.data.msg);
ooChannel.lastHash = obj.data.hash; ooChannel.lastHash = obj.data.hash;
ooChannel.cpIndex++; ooChannel.cpIndex++;
@ -972,8 +1080,10 @@ define([
ooChannel.queue.forEach(function (data) { ooChannel.queue.forEach(function (data) {
ooChannel.send(data.msg); ooChannel.send(data.msg);
}); });
var last = ooChannel.queue.pop(); if (!readOnly) {
if (last) { ooChannel.lastHash = last.hash; } var last = ooChannel.queue.pop();
if (last) { ooChannel.lastHash = last.hash; }
}
ooChannel.cpIndex += ooChannel.queue.length; ooChannel.cpIndex += ooChannel.queue.length;
// Apply existing locks // Apply existing locks
deleteOfflineLocks(); deleteOfflineLocks();
@ -1004,7 +1114,7 @@ define([
UI.openCustomModal(UI.dialog.customModal(div, {buttons: []})); UI.openCustomModal(UI.dialog.customModal(div, {buttons: []}));
setTimeout(function () { setTimeout(function () {
makeCheckpoint(true); makeCheckpoint(true);
}, 1000); }, 5000);
} }
} }
} }
@ -1433,41 +1543,6 @@ define([
}, 100); }, 100);
}; };
var loadLastDocument = function (lastCp, onCpError, cb) {
ooChannel.cpIndex = lastCp.index || 0;
var parsed = Hash.parsePadUrl(lastCp.file);
var secret = Hash.getSecrets('file', parsed.hash);
if (!secret || !secret.channel) { return; }
var hexFileName = secret.channel;
var fileHost = privateData.fileHost || privateData.origin;
var src = fileHost + Hash.getBlobPathFromHex(hexFileName);
var key = secret.keys && secret.keys.cryptKey;
var xhr = new XMLHttpRequest();
xhr.open('GET', src, true);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
if (/^4/.test('' + this.status)) {
onCpError();
return void console.error('XHR error', this.status);
}
var arrayBuffer = xhr.response;
if (arrayBuffer) {
var u8 = new Uint8Array(arrayBuffer);
FileCrypto.decrypt(u8, key, function (err, decrypted) {
if (err) { return void console.error(err); }
var blob = new Blob([decrypted.content], {type: 'plain/text'});
if (cb) {
return cb(blob, getFileType());
}
startOO(blob, getFileType());
});
}
};
xhr.onerror = function () {
onCpError();
};
xhr.send(null);
};
var loadDocument = function (noCp, useNewDefault, i) { var loadDocument = function (noCp, useNewDefault, i) {
if (ooLoaded) { return; } if (ooLoaded) { return; }
var type = common.getMetadataMgr().getPrivateData().ooType; var type = common.getMetadataMgr().getPrivateData().ooType;
@ -1497,7 +1572,7 @@ define([
default: default:
newText = ''; newText = '';
} }
var blob = new Blob([newText], {type: 'text/plain'}); var blob = loadInitDocument(type, useNewDefault);
startOO(blob, file); startOO(blob, file);
}; };
@ -1584,6 +1659,14 @@ define([
$save.appendTo(toolbar.$bottomM); $save.appendTo(toolbar.$bottomM);
} }
if (window.CP_DEV_MODE || DISPLAY_RESTORE_BUTTON) { if (window.CP_DEV_MODE || DISPLAY_RESTORE_BUTTON) {
common.createButton('', true, {
name: 'delete',
icon: 'fa-trash',
hiddenReadOnly: true
}).click(function () {
if (initializing) { return void console.error('initializing'); }
deleteLastCp();
}).attr('title', 'Delete last checkpoint').appendTo(toolbar.$bottomM);
common.createButton('', true, { common.createButton('', true, {
name: 'restore', name: 'restore',
icon: 'fa-history', icon: 'fa-history',
@ -1609,6 +1692,7 @@ define([
} }
if (common.isLoggedIn()) { if (common.isLoggedIn()) {
window.CryptPad_deleteLastCp = deleteLastCp;
var $importXLSX = common.createButton('import', true, { var $importXLSX = common.createButton('import', true, {
accept: accept, accept: accept,
binary : ["ods", "xlsx", "odt", "docx", "odp", "pptx"] binary : ["ods", "xlsx", "odt", "docx", "odp", "pptx"]
@ -1765,10 +1849,10 @@ define([
var latest = getLastCp(true); var latest = getLastCp(true);
var newLatest = getLastCp(); var newLatest = getLastCp();
if (newLatest.index > latest.index) { if (newLatest.index > latest.index) {
ooChannel.queue = [];
var hasDrawings = checkDrawings(); var hasDrawings = checkDrawings();
if (hasDrawings) { if (hasDrawings) {
ooChannel.ready = false; ooChannel.ready = false;
ooChannel.queue = [];
} }
// New checkpoint // New checkpoint
sframeChan.query('Q_OO_SAVE', { sframeChan.query('Q_OO_SAVE', {

@ -751,8 +751,7 @@ define([
force: true force: true
}, waitFor()); }, waitFor());
}).nThen(function () { }).nThen(function () {
// XXX users need to login after registration if they register after account deletion (token issue?) // TODO delete block
// XXX delete block
// Log out current worker // Log out current worker
postMessage(clientId, "DELETE_ACCOUNT", token, function () {}); postMessage(clientId, "DELETE_ACCOUNT", token, function () {});
store.network.disconnect(); store.network.disconnect();

@ -109,6 +109,17 @@ proxy.mailboxes = {
}); });
var ciphertext = crypto.encrypt(text, user.curvePublic); var ciphertext = crypto.encrypt(text, user.curvePublic);
// If we've sent this message to one of our teams' mailbox, we may want to "dismiss" it
// automatically
if (user.viewed) {
var team = Util.find(ctx, ['store', 'proxy', 'teams', user.viewed]);
if (team) {
var hash = ciphertext.slice(0,64);
var viewed = Util.find(team, ['keys', 'mailbox', 'viewed']);
if (Array.isArray(viewed)) { viewed.push(hash); }
}
}
anonRpc.send("WRITE_PRIVATE_MESSAGE", [ anonRpc.send("WRITE_PRIVATE_MESSAGE", [
user.channel, user.channel,
ciphertext ciphertext
@ -126,10 +137,9 @@ proxy.mailboxes = {
var dismiss = function (ctx, data, cId, cb) { var dismiss = function (ctx, data, cId, cb) {
var type = data.type; var type = data.type;
var hash = data.hash; var hash = data.hash;
var m = Util.find(ctx, ['store', 'proxy', 'mailboxes', type]);
if (!m) { return void cb({error: 'NOT_FOUND'}); }
var box = ctx.boxes[type]; var box = ctx.boxes[type];
if (!box) { return void cb({error: 'NOT_LOADED'}); } if (!box) { return void cb({error: 'NOT_LOADED'}); }
var m = box.data || {};
// If the hash in in our history, get the index from the history: // If the hash in in our history, get the index from the history:
// - if the index is 0, we can change our lastKnownHash // - if the index is 0, we can change our lastKnownHash
@ -191,7 +201,16 @@ proxy.mailboxes = {
}; };
var openChannel = function (ctx, type, m, onReady) { var leaveChannel = function (ctx, type, cb) {
var box = ctx.boxes[type];
if (!box) { return void cb(); }
if (!box.cpNf || typeof(box.cpNf.stop) !== "function") { return void cb('EINVAL'); }
box.cpNf.stop();
delete ctx.boxes[type];
};
var openChannel = function (ctx, type, m, onReady, opts) {
console.error(type, m, opts);
opts = opts || {};
var box = ctx.boxes[type] = { var box = ctx.boxes[type] = {
channel: m.channel, channel: m.channel,
type: type, type: type,
@ -210,7 +229,8 @@ proxy.mailboxes = {
console.error(e); console.error(e);
} }
box.queue.push(msg); box.queue.push(msg);
} },
data: m
}; };
if (!Crypto.Mailbox) { if (!Crypto.Mailbox) {
return void console.error("chainpad-crypto is outdated and doesn't support mailboxes."); return void console.error("chainpad-crypto is outdated and doesn't support mailboxes.");
@ -224,7 +244,7 @@ proxy.mailboxes = {
channel: m.channel, channel: m.channel,
noChainPad: true, noChainPad: true,
crypto: crypto, crypto: crypto,
owners: [ctx.store.proxy.edPublic], owners: opts.owners || [ctx.store.proxy.edPublic],
lastKnownHash: m.lastKnownHash lastKnownHash: m.lastKnownHash
}; };
cfg.onConnectionChange = function () {}; // Allow reconnections in chainpad-netflux cfg.onConnectionChange = function () {}; // Allow reconnections in chainpad-netflux
@ -346,7 +366,7 @@ proxy.mailboxes = {
// Continue // Continue
onReady(); onReady();
}; };
CpNetflux.start(cfg); box.cpNf = CpNetflux.start(cfg);
}; };
var initializeHistory = function (ctx) { var initializeHistory = function (ctx) {
@ -467,6 +487,19 @@ proxy.mailboxes = {
} }
}); });
Object.keys(store.proxy.teams || {}).forEach(function (teamId) {
var team = store.proxy.teams[teamId];
if (!team) { return; }
var teamMailbox = team.keys.mailbox || {};
if (!teamMailbox.channel) { return; }
var opts = {
owners: [Util.find(team, ['keys', 'drive', 'edPublic'])]
};
openChannel(ctx, 'team-'+teamId, teamMailbox, function () {
//console.log('Mailbox team', teamId);
}, opts);
});
mailbox.post = function (box, type, content) { mailbox.post = function (box, type, content) {
var b = ctx.boxes[box]; var b = ctx.boxes[box];
if (!b) { return; } if (!b) { return; }
@ -477,9 +510,12 @@ proxy.mailboxes = {
}); });
}; };
mailbox.open = function (key, m, cb) { mailbox.open = function (key, m, cb, team, opts) {
if (TYPES.indexOf(key) === -1) { return; } if (TYPES.indexOf(key) === -1 && !team) { return; }
openChannel(ctx, key, m, cb); openChannel(ctx, key, m, cb, opts);
};
mailbox.close = function (key, cb) {
leaveChannel(ctx, key, cb);
}; };
mailbox.dismiss = function (data, cb) { mailbox.dismiss = function (data, cb) {

@ -126,6 +126,9 @@ define([
delete ctx.store.proxy.teams[teamId]; delete ctx.store.proxy.teams[teamId];
ctx.emit('LEAVE_TEAM', teamId, team.clients); ctx.emit('LEAVE_TEAM', teamId, team.clients);
ctx.updateMetadata(); ctx.updateMetadata();
ctx.store.mailbox.close('team-'+teamId, function () {
// Close team mailbox
});
}; };
var getTeamChannelList = function (ctx, id) { var getTeamChannelList = function (ctx, id) {
@ -494,6 +497,8 @@ define([
var roHash = Hash.getViewHashFromKeys(secret); var roHash = Hash.getViewHashFromKeys(secret);
var keyPair = Nacl.sign.keyPair(); // keyPair.secretKey , keyPair.publicKey var keyPair = Nacl.sign.keyPair(); // keyPair.secretKey , keyPair.publicKey
var curvePair = Nacl.box.keyPair();
var rosterSeed = Crypto.Team.createSeed(); var rosterSeed = Crypto.Team.createSeed();
var rosterKeys = Crypto.Team.deriveMemberKeys(rosterSeed, { var rosterKeys = Crypto.Team.deriveMemberKeys(rosterSeed, {
curvePublic: ctx.store.proxy.curvePublic, curvePublic: ctx.store.proxy.curvePublic,
@ -585,6 +590,14 @@ define([
proxy.on('ready', function () { proxy.on('ready', function () {
// Store keys in our drive // Store keys in our drive
var keys = { var keys = {
mailbox: {
channel: Hash.createChannelId(),
viewed: [],
keys: {
curvePrivate: Nacl.util.encodeBase64(curvePair.secretKey),
curvePublic: Nacl.util.encodeBase64(curvePair.publicKey)
}
},
drive: { drive: {
edPrivate: Nacl.util.encodeBase64(keyPair.secretKey), edPrivate: Nacl.util.encodeBase64(keyPair.secretKey),
edPublic: Nacl.util.encodeBase64(keyPair.publicKey) edPublic: Nacl.util.encodeBase64(keyPair.publicKey)
@ -601,7 +614,7 @@ define([
view: rosterKeys.viewKeyStr, view: rosterKeys.viewKeyStr,
} }
}; };
ctx.store.proxy.teams[id] = { var t = ctx.store.proxy.teams[id] = {
owner: true, owner: true,
channel: secret.channel, channel: secret.channel,
hash: hash, hash: hash,
@ -618,6 +631,11 @@ define([
onReady(ctx, id, lm, roster, keys, cId, function () { onReady(ctx, id, lm, roster, keys, cId, function () {
Feedback.send('TEAM_CREATION'); Feedback.send('TEAM_CREATION');
ctx.store.mailbox.open('team-'+id, t.keys.mailbox, function () {
// Team mailbox loaded
}, true, {
owners: t.keys.drive.edPublic
});
ctx.updateMetadata(); ctx.updateMetadata();
cb(); cb();
}); });
@ -720,6 +738,11 @@ define([
team.rpc.removePins(waitFor(function (err) { team.rpc.removePins(waitFor(function (err) {
if (err) { console.error(err); } if (err) { console.error(err); }
})); }));
// Delete the mailbox
var mailboxChan = Util.find(teamData, ['keys', 'mailbox', 'channel']);
team.rpc.removeOwnedChannel(mailboxChan, waitFor(function (err) {
if (err) { console.error(err); }
}));
// Delete the roster // Delete the roster
var rosterChan = Util.find(teamData, ['keys', 'roster', 'channel']); var rosterChan = Util.find(teamData, ['keys', 'roster', 'channel']);
ctx.store.rpc.removeOwnedChannel(rosterChan, waitFor(function (err) { ctx.store.rpc.removeOwnedChannel(rosterChan, waitFor(function (err) {
@ -750,6 +773,12 @@ define([
ctx.onReadyHandlers[id] = []; ctx.onReadyHandlers[id] = [];
openChannel(ctx, team, id, function (obj) { openChannel(ctx, team, id, function (obj) {
if (!(obj && obj.error)) { console.debug('Team joined:' + id); } if (!(obj && obj.error)) { console.debug('Team joined:' + id); }
var t = ctx.store.proxy.teams[id];
ctx.store.mailbox.open('team-'+id, t.keys.mailbox, function () {
// Team mailbox loaded
}, true, {
owners: t.keys.drive.edPublic
});
ctx.updateMetadata(); ctx.updateMetadata();
cb(obj); cb(obj);
}); });
@ -1566,6 +1595,25 @@ define([
}); });
}; };
var deriveMailbox = function (team) {
if (!team) { return; }
if (team.keys && team.keys.mailbox) { return team.keys.mailbox; }
var strSeed = Util.find(team, ['keys', 'roster', 'edit']);
if (!strSeed) { return; }
var hash = Nacl.hash(Nacl.util.decodeUTF8(strSeed));
var seed = hash.slice(0,32);
var mailboxChannel = Util.uint8ArrayToHex(hash.slice(32,48));
var curvePair = Nacl.box.keyPair.fromSecretKey(seed);
return {
channel: mailboxChannel,
viewed: [],
keys: {
curvePrivate: Nacl.util.encodeBase64(curvePair.secretKey),
curvePublic: Nacl.util.encodeBase64(curvePair.publicKey)
}
};
};
Team.init = function (cfg, waitFor, emit) { Team.init = function (cfg, waitFor, emit) {
var team = {}; var team = {};
var store = cfg.store; var store = cfg.store;
@ -1595,6 +1643,9 @@ define([
Object.keys(teams).forEach(function (id) { Object.keys(teams).forEach(function (id) {
ctx.onReadyHandlers[id] = []; ctx.onReadyHandlers[id] = [];
if (!Util.find(teams, [id, 'keys', 'mailbox'])) {
teams[id].keys.mailbox = deriveMailbox(teams[id]);
}
openChannel(ctx, teams[id], id, waitFor(function (err) { openChannel(ctx, teams[id], id, waitFor(function (err) {
if (err) { return void console.error(err); } if (err) { return void console.error(err); }
console.debug('Team '+id+' ready'); console.debug('Team '+id+' ready');
@ -1615,6 +1666,8 @@ define([
edPublic: Util.find(teams[id], ['keys', 'drive', 'edPublic']), edPublic: Util.find(teams[id], ['keys', 'drive', 'edPublic']),
avatar: Util.find(teams[id], ['metadata', 'avatar']), avatar: Util.find(teams[id], ['metadata', 'avatar']),
viewer: !Util.find(teams[id], ['keys', 'drive', 'edPrivate']), viewer: !Util.find(teams[id], ['keys', 'drive', 'edPrivate']),
notifications: Util.find(teams[id], ['keys', 'mailbox', 'channel']),
curvePublic: Util.find(teams[id], ['keys', 'mailbox', 'keys', 'curvePublic']),
}; };
if (safe && ctx.teams[id]) { if (safe && ctx.teams[id]) {

@ -105,11 +105,14 @@ define([
}); });
// Call the onMessage handlers // Call the onMessage handlers
var isNotification = function (type) {
return type === "notifications" || /^team-/.test(type);
};
var pushMessage = function (data, handler) { var pushMessage = function (data, handler) {
var todo = function (f) { var todo = function (f) {
try { try {
var el; var el;
if (data.type === 'notifications') { if (isNotification(data.type)) {
Notifications.add(Common, data); Notifications.add(Common, data);
el = createElement(data); el = createElement(data);
} }
@ -129,7 +132,7 @@ define([
onViewedHandlers.forEach(function (f) { onViewedHandlers.forEach(function (f) {
try { try {
f(data); f(data);
if (data.type === 'notifications') { if (isNotification(data.type)) {
Notifications.remove(Common, data); Notifications.remove(Common, data);
} }
} catch (e) { } catch (e) {
@ -173,20 +176,24 @@ define([
execCommand('SUBSCRIBE', null, function () {}); execCommand('SUBSCRIBE', null, function () {});
subscribed = true; subscribed = true;
} }
var teams = types.indexOf('team') !== -1;
if (typeof(cfg.onViewed) === "function") { if (typeof(cfg.onViewed) === "function") {
onViewedHandlers.push(function (data) { onViewedHandlers.push(function (data) {
if (types.indexOf(data.type) === -1) { return; } var type = data.type;
if (types.indexOf(type) === -1 && !(teams && /^team-/.test(type))) { return; }
cfg.onViewed(data); cfg.onViewed(data);
}); });
} }
if (typeof(cfg.onMessage) === "function") { if (typeof(cfg.onMessage) === "function") {
onMessageHandlers.push(function (data, el) { onMessageHandlers.push(function (data, el) {
if (types.indexOf(data.type) === -1) { return; } var type = data.type;
if (types.indexOf(type) === -1 && !(teams && /^team-/.test(type))) { return; }
console.log('okokok');
cfg.onMessage(data, el); cfg.onMessage(data, el);
}); });
} }
Object.keys(history).forEach(function (type) { Object.keys(history).forEach(function (type) {
if (types.indexOf(type) === -1) { return; } if (types.indexOf(type) === -1 && !(teams && /^team-/.test(type))) { return; }
history[type].forEach(function (data) { history[type].forEach(function (data) {
pushMessage({ pushMessage({
type: type, type: type,

@ -1025,8 +1025,9 @@ MessengerUI, Messages) {
$button.addClass('fa-bell'); $button.addClass('fa-bell');
}; };
Common.mailbox.subscribe(['notifications'], { Common.mailbox.subscribe(['notifications', 'team'], {
onMessage: function (data, el) { onMessage: function (data, el) {
console.log(data, el, div);
if (el) { if (el) {
$(div).prepend(el); $(div).prepend(el);
} }

@ -148,7 +148,6 @@
"or": "o", "or": "o",
"tags_title": "Etiquetes (només vostres)", "tags_title": "Etiquetes (només vostres)",
"tags_add": "Actualitza les etiquetes d'aquesta pàgina", "tags_add": "Actualitza les etiquetes d'aquesta pàgina",
"tags_searchHint": "Inicieu una cerca amb # al vostre CryptDrive per trobar els vostres documents etiquetats.",
"tags_notShared": "Les vostres etiquetes no es comparteixen amb altres persones usuàries", "tags_notShared": "Les vostres etiquetes no es comparteixen amb altres persones usuàries",
"tags_duplicate": "Etiquetes duplicades: {0}", "tags_duplicate": "Etiquetes duplicades: {0}",
"tags_noentry": "No podeu etiquetar un document esborrat!", "tags_noentry": "No podeu etiquetar un document esborrat!",

@ -109,7 +109,7 @@
"newButton": "Neu", "newButton": "Neu",
"newButtonTitle": "Neues Pad erstellen", "newButtonTitle": "Neues Pad erstellen",
"uploadButton": "Hochladen", "uploadButton": "Hochladen",
"uploadButtonTitle": "Eine neue Datei in den aktuellen Ordner hochladen", "uploadButtonTitle": "Eine neue Datei zum CryptDrive hochladen",
"saveTemplateButton": "Als Vorlage speichern", "saveTemplateButton": "Als Vorlage speichern",
"saveTemplatePrompt": "Bitte gib einen Titel für die Vorlage ein", "saveTemplatePrompt": "Bitte gib einen Titel für die Vorlage ein",
"templateSaved": "Vorlage gespeichert!", "templateSaved": "Vorlage gespeichert!",
@ -145,8 +145,7 @@
"filePicker_filter": "Dateien nach Namen filtern", "filePicker_filter": "Dateien nach Namen filtern",
"or": "oder", "or": "oder",
"tags_title": "Tags (nur für dich)", "tags_title": "Tags (nur für dich)",
"tags_add": "Die Tags dieser Seite bearbeiten", "tags_add": "Tags der ausgewählten Pads bearbeiten",
"tags_searchHint": "Beginne die Suche in deinem CryptDrive mit #, um getaggte Dokumente zu finden.",
"tags_notShared": "Deine Tags werden nicht mit anderen Benutzern geteilt", "tags_notShared": "Deine Tags werden nicht mit anderen Benutzern geteilt",
"tags_duplicate": "Doppeltes Tag: {0}", "tags_duplicate": "Doppeltes Tag: {0}",
"tags_noentry": "Du kannst keine Tags zu einem gelöschten Pad hinzufügen!", "tags_noentry": "Du kannst keine Tags zu einem gelöschten Pad hinzufügen!",
@ -831,7 +830,7 @@
"help": { "help": {
"title": "Mit CryptPad anfangen", "title": "Mit CryptPad anfangen",
"generic": { "generic": {
"more": "Erfahre mehr über die Nutzung von CryptPad, indem du unsere <a href=\"/faq.html\" target=\"_blank\">FAQ</a> liest", "more": "Erfahre mehr über die Nutzung von CryptPad, indem du unsere <a href=\"/faq.html\" target=\"_blank\">FAQ</a> liest.",
"share": "Teile dieses Dokument mit der Schaltfläche <i class=\"fa fa-shhare-alt\"></i> <b>Teilen</b> und verwalte die Zugriffsrechte mit <i class=\"fa fa-unlock-alt\"></i> <b>Zugriff</b>.", "share": "Teile dieses Dokument mit der Schaltfläche <i class=\"fa fa-shhare-alt\"></i> <b>Teilen</b> und verwalte die Zugriffsrechte mit <i class=\"fa fa-unlock-alt\"></i> <b>Zugriff</b>.",
"save": "Alle Änderungen werden automatisch synchronisiert. Du musst sie also nicht selbst speichern" "save": "Alle Änderungen werden automatisch synchronisiert. Du musst sie also nicht selbst speichern"
}, },
@ -1377,5 +1376,10 @@
"toolbar_insert": "Einfügen", "toolbar_insert": "Einfügen",
"toolbar_savetodrive": "Als Bild speichern", "toolbar_savetodrive": "Als Bild speichern",
"slide_backCol": "Hintergrundfarbe", "slide_backCol": "Hintergrundfarbe",
"slide_textCol": "Textfarbe" "slide_textCol": "Textfarbe",
"support_languagesPreamble": "Das Support-Team spricht die folgenden Sprachen:",
"settings_safeLinkDefault": "Sichere Links sind nun standardmäßig aktiviert. Bitte verwende zum Kopieren von Links das Menü <i class=\"fa fa-shhare-alt\"></i> <b>Teilen</b> und nicht die Adressleiste des Browsers.",
"info_imprintFlavour": "<a>Rechtliche Informationen über die Administratoren dieses Servers</a>.",
"info_privacyFlavour": "Unsere <a>Datenschutzerklärung</a> beschreibt, wie wir deine Daten verarbeiten.",
"user_about": "Über CryptPad"
} }

@ -119,7 +119,6 @@
"or": "ή", "or": "ή",
"tags_title": "Ετικέτες (για εσάς μόνο)", "tags_title": "Ετικέτες (για εσάς μόνο)",
"tags_add": "Ενημερώστε τις ετικέτες αυτής της σελίδας", "tags_add": "Ενημερώστε τις ετικέτες αυτής της σελίδας",
"tags_searchHint": "Ξεκινήστε μια αναζήτηση με το σύμβολο # στο CryptDrive σας για να βρείτε pads με ετικέτες.",
"tags_notShared": "Οι ετικέτες σας δεν μοιράζονται με άλλους χρήστες", "tags_notShared": "Οι ετικέτες σας δεν μοιράζονται με άλλους χρήστες",
"tags_duplicate": "Διπλή ετικέτα: {0}", "tags_duplicate": "Διπλή ετικέτα: {0}",
"tags_noentry": "Δεν μπορείτε να βάλετε ετικέτα σε διεγραμένο pad!", "tags_noentry": "Δεν μπορείτε να βάλετε ετικέτα σε διεγραμένο pad!",

@ -472,7 +472,6 @@
"printBackgroundRemove": "Eliminar este fondo de pantalla", "printBackgroundRemove": "Eliminar este fondo de pantalla",
"tags_title": "Etiquetas (sólo para tí)", "tags_title": "Etiquetas (sólo para tí)",
"tags_add": "Actualizar las etiquetas de esta página", "tags_add": "Actualizar las etiquetas de esta página",
"tags_searchHint": "Comenzar una búsqueda con # en tú CryptDrive para encontrar las notas etiquetadas.",
"tags_notShared": "Tus etiquetas no están compartidas con otros usuarios", "tags_notShared": "Tus etiquetas no están compartidas con otros usuarios",
"tags_duplicate": "Duplicar etiquetas:{0}", "tags_duplicate": "Duplicar etiquetas:{0}",
"tags_noentry": "No puedes etiquetar una nota eliminada!", "tags_noentry": "No puedes etiquetar una nota eliminada!",

@ -151,7 +151,6 @@
"or": "tai", "or": "tai",
"tags_title": "Tunnisteet (vain sinulle)", "tags_title": "Tunnisteet (vain sinulle)",
"tags_add": "Päivitä sivun tunnisteet", "tags_add": "Päivitä sivun tunnisteet",
"tags_searchHint": "Aloita hakusi CryptDrivessa #-merkillä löytääksesi tunnisteita sisältävät padit.",
"tags_notShared": "Tunnisteitasi ei jaeta muiden käyttäjien kanssa", "tags_notShared": "Tunnisteitasi ei jaeta muiden käyttäjien kanssa",
"tags_duplicate": "Kaksinkertainen tunniste: {0}", "tags_duplicate": "Kaksinkertainen tunniste: {0}",
"tags_noentry": "Et voi lisätä tunnistetta poistettuun padiin!", "tags_noentry": "Et voi lisätä tunnistetta poistettuun padiin!",

@ -111,7 +111,7 @@
"newButtonTitle": "Créer un nouveau pad", "newButtonTitle": "Créer un nouveau pad",
"uploadButton": "Importer des fichiers", "uploadButton": "Importer des fichiers",
"uploadFolderButton": "Importer un dossier", "uploadFolderButton": "Importer un dossier",
"uploadButtonTitle": "Importer un nouveau fichier dans le dossier actuel", "uploadButtonTitle": "Importer un nouveau fichier dans votre CryptDrive",
"saveTemplateButton": "Sauver en tant que modèle", "saveTemplateButton": "Sauver en tant que modèle",
"saveTemplatePrompt": "Choisir un titre pour ce modèle", "saveTemplatePrompt": "Choisir un titre pour ce modèle",
"templateSaved": "Modèle enregistré !", "templateSaved": "Modèle enregistré !",
@ -147,8 +147,7 @@
"filePicker_filter": "Filtrez les fichiers par leur nom", "filePicker_filter": "Filtrez les fichiers par leur nom",
"or": "ou", "or": "ou",
"tags_title": "Mots-clés du pad (pour vous uniquement)", "tags_title": "Mots-clés du pad (pour vous uniquement)",
"tags_add": "Modifier les mots-clés du pad", "tags_add": "Modifier les mots-clés de la sélection",
"tags_searchHint": "Commencez une recherche par # dans votre CryptDrive pour retrouver vos pads par mot-clé.",
"tags_notShared": "Vos mots-clés ne sont pas partagés avec les autres utilisateurs", "tags_notShared": "Vos mots-clés ne sont pas partagés avec les autres utilisateurs",
"tags_duplicate": "Mot-clé déjà présent : {0}", "tags_duplicate": "Mot-clé déjà présent : {0}",
"tags_noentry": "Vous ne pouvez pas ajouter de mots-clés à un pad supprimé !", "tags_noentry": "Vous ne pouvez pas ajouter de mots-clés à un pad supprimé !",
@ -838,7 +837,7 @@
"help": { "help": {
"title": "Pour bien démarrer", "title": "Pour bien démarrer",
"generic": { "generic": {
"more": "Apprenez-en davantage sur le fonctionnement de CryptPad en lisant notre <a href=\"/faq.html\" target=\"_blank\">FAQ</a>", "more": "Apprenez-en davantage sur le fonctionnement de CryptPad en lisant notre <a href=\"/faq.html\" target=\"_blank\">FAQ</a>.",
"share": "Partagez ce document avec le bouton <i class=\"fa fa-shhare-alt\"></i> <b>Partager</b> et gérez les droits d'accès avec le bouton <i class=\"fa fa-unlock-alt\"></i> <b>Accès</b>.", "share": "Partagez ce document avec le bouton <i class=\"fa fa-shhare-alt\"></i> <b>Partager</b> et gérez les droits d'accès avec le bouton <i class=\"fa fa-unlock-alt\"></i> <b>Accès</b>.",
"save": "Tous les changements effectués sont enregistrés automatiquement" "save": "Tous les changements effectués sont enregistrés automatiquement"
}, },
@ -1377,5 +1376,10 @@
"toolbar_savetodrive": "Sauvegarder image", "toolbar_savetodrive": "Sauvegarder image",
"toolbar_insert": "Insérer", "toolbar_insert": "Insérer",
"toolbar_theme": "Thème", "toolbar_theme": "Thème",
"todo_move": "Votre liste de tâches est désormais dans le kanban <b>{0}</b> dans votre Drive." "todo_move": "Votre liste de tâches est désormais dans le kanban <b>{0}</b> dans votre Drive.",
"settings_safeLinkDefault": "Les liens sécurisés sont désormais activés par défaut. Veuillez utiliser le menu <i class=\"fa fa-shhare-alt\"></i> <b>Partager</b> pour copier les liens plutôt que la barre d'adresse de votre navigateur.",
"support_languagesPreamble": "L'équipe de support parle les langues suivantes :",
"info_privacyFlavour": "<a>Description de la confidentialité</a> de vos données.",
"user_about": "À propos de CryptPad",
"info_imprintFlavour": "<a>Informations légales sur les administateurs de cette instance</a>."
} }

@ -148,7 +148,6 @@
"or": "o", "or": "o",
"tags_title": "Tag (mostrati solo a te)", "tags_title": "Tag (mostrati solo a te)",
"tags_add": "Aggiorna i tag di questa pagina", "tags_add": "Aggiorna i tag di questa pagina",
"tags_searchHint": "Inizia una ricerca con # nel tuo CryptDrive per trovare i pad taggati.",
"tags_notShared": "I tuoi tag non sono condivisi con altri utenti", "tags_notShared": "I tuoi tag non sono condivisi con altri utenti",
"tags_duplicate": "Duplica tag: {0}", "tags_duplicate": "Duplica tag: {0}",
"tags_noentry": "Non puoi taggare un pad eliminato!", "tags_noentry": "Non puoi taggare un pad eliminato!",

@ -114,7 +114,7 @@
"newButtonTitle": "Create a new pad", "newButtonTitle": "Create a new pad",
"uploadButton": "Upload files", "uploadButton": "Upload files",
"uploadFolderButton": "Upload folder", "uploadFolderButton": "Upload folder",
"uploadButtonTitle": "Upload a new file to the current folder", "uploadButtonTitle": "Upload a new file to your CryptDrive",
"saveTemplateButton": "Save as template", "saveTemplateButton": "Save as template",
"saveTemplatePrompt": "Choose a title for the template", "saveTemplatePrompt": "Choose a title for the template",
"templateSaved": "Template saved!", "templateSaved": "Template saved!",
@ -150,8 +150,7 @@
"filePicker_filter": "Filter files by name", "filePicker_filter": "Filter files by name",
"or": "or", "or": "or",
"tags_title": "Tags (for you only)", "tags_title": "Tags (for you only)",
"tags_add": "Update this page's tags", "tags_add": "Update the tags for selected pads",
"tags_searchHint": "Start a search with # in your CryptDrive to find your tagged pads.",
"tags_notShared": "Your tags are not shared with other users", "tags_notShared": "Your tags are not shared with other users",
"tags_duplicate": "Duplicate tag: {0}", "tags_duplicate": "Duplicate tag: {0}",
"tags_noentry": "You can't tag a deleted pad!", "tags_noentry": "You can't tag a deleted pad!",
@ -856,7 +855,7 @@
"help": { "help": {
"title": "Getting started", "title": "Getting started",
"generic": { "generic": {
"more": "Learn more about how CryptPad can work for you by reading our <a href=\"/faq.html\" target=\"_blank\">FAQ</a>", "more": "Learn more about how CryptPad can work for you by reading our <a href=\"/faq.html\" target=\"_blank\">FAQ</a>.",
"share": "Share this document with the <i class=\"fa fa-shhare-alt\"></i> <b>Share</b> button, and manage access rights with <i class=\"fa fa-unlock-alt\"></i> <b>Access</b>.", "share": "Share this document with the <i class=\"fa fa-shhare-alt\"></i> <b>Share</b> button, and manage access rights with <i class=\"fa fa-unlock-alt\"></i> <b>Access</b>.",
"save": "All your changes are synced automatically so you never need to save" "save": "All your changes are synced automatically so you never need to save"
}, },
@ -1377,5 +1376,10 @@
"code_editorTheme": "Editor theme", "code_editorTheme": "Editor theme",
"toolbar_file": "File", "toolbar_file": "File",
"slide_backCol": "Background color", "slide_backCol": "Background color",
"slide_textCol": "Text color" "slide_textCol": "Text color",
"support_languagesPreamble": "The support team speaks the following languages:",
"settings_safeLinkDefault": "Safe Links are now turned on by default. Please use the <i class=\"fa fa-shhare-alt\"></i> <b>Share</b> menu to copy links rather than your browser's address bar.",
"info_imprintFlavour": "<a>Legal information about the administrators of this instance</a>.",
"user_about": "About CryptPad",
"info_privacyFlavour": "Our <a>privacy policy</a> describes how we treat your data."
} }

@ -140,7 +140,6 @@
"or": "eller", "or": "eller",
"tags_title": "Tags (kun for ditt bruk)", "tags_title": "Tags (kun for ditt bruk)",
"tags_add": "Oppdater tags for denne sida", "tags_add": "Oppdater tags for denne sida",
"tags_searchHint": "Søk med # i CryptDriven din for å finne pads med slike tags.",
"tags_notShared": "Dine tags deles ikke med andre brukerer", "tags_notShared": "Dine tags deles ikke med andre brukerer",
"tags_duplicate": "Dupliser tag:{0}", "tags_duplicate": "Dupliser tag:{0}",
"tags_noentry": "Du kan ikke tagge en sletta pad!", "tags_noentry": "Du kan ikke tagge en sletta pad!",

@ -74,7 +74,6 @@
"tags_noentry": "U kunt een verwijderde werkomgeving niet markeren!", "tags_noentry": "U kunt een verwijderde werkomgeving niet markeren!",
"tags_duplicate": "Gedupliceerde markering: {0}", "tags_duplicate": "Gedupliceerde markering: {0}",
"tags_notShared": "Uw markeringen worden niet gedeeld met andere gebruikers", "tags_notShared": "Uw markeringen worden niet gedeeld met andere gebruikers",
"tags_searchHint": "Begin een zoekopdracht met # in uw CryptDrive om gemarkeerde werkomgevingen te vinden.",
"tags_add": "Werk de markeringen van deze pagina bij", "tags_add": "Werk de markeringen van deze pagina bij",
"tags_title": "Markeringen (alleen voor u)", "tags_title": "Markeringen (alleen voor u)",
"or": "of", "or": "of",

@ -392,7 +392,6 @@
"or": "", "or": "",
"tags_title": "", "tags_title": "",
"tags_add": "", "tags_add": "",
"tags_searchHint": "",
"tags_notShared": "", "tags_notShared": "",
"tags_duplicate": "", "tags_duplicate": "",
"tags_noentry": "", "tags_noentry": "",

@ -382,7 +382,6 @@
"or": "sau", "or": "sau",
"tags_title": "Etichete (doar pentru tine)", "tags_title": "Etichete (doar pentru tine)",
"tags_add": "Updatează etichetele acestei pagini", "tags_add": "Updatează etichetele acestei pagini",
"tags_searchHint": "Începe o căutare cu # în CryptDrive-ul tău pentru a găsi pad-uri etichetate",
"tags_notShared": "Etichetele tale nu sunt împărțite cu alți utilizatori", "tags_notShared": "Etichetele tale nu sunt împărțite cu alți utilizatori",
"tags_duplicate": "Duplică eticheta: {0}", "tags_duplicate": "Duplică eticheta: {0}",
"tags_noentry": "Nu poți eticheta un pad șters", "tags_noentry": "Nu poți eticheta un pad șters",
@ -579,5 +578,6 @@
"fc_color": "Schimbă culoarea", "fc_color": "Schimbă culoarea",
"fm_morePads": "Mai mult", "fm_morePads": "Mai mult",
"uploadFolderButton": "Încarcă dosar", "uploadFolderButton": "Încarcă dosar",
"storageStatus": "Capacitate de stocare:<br /><b>{0}</b> utilizat din <b>{1}</b>" "storageStatus": "Capacitate de stocare:<br /><b>{0}</b> utilizat din <b>{1}</b>",
"fc_collapseAll": "Restrânge"
} }

@ -140,7 +140,6 @@
"or": "или", "or": "или",
"tags_title": "Теги (только для вас)", "tags_title": "Теги (только для вас)",
"tags_add": "Обновить теги страницы", "tags_add": "Обновить теги страницы",
"tags_searchHint": "Начните поиск в вашем CryptDrive при помощи # чтобы найти пэды с тегами.",
"tags_notShared": "Ваши теги не разделяются с другими пользователями", "tags_notShared": "Ваши теги не разделяются с другими пользователями",
"button_newsheet": "Новый Лист", "button_newsheet": "Новый Лист",
"newButtonTitle": "создать новую запись", "newButtonTitle": "создать новую запись",

@ -38,7 +38,6 @@
"tags_noentry": "Du kan inte tagga ett raderat dokument!", "tags_noentry": "Du kan inte tagga ett raderat dokument!",
"tags_duplicate": "Duplicera tagg: {0}", "tags_duplicate": "Duplicera tagg: {0}",
"tags_notShared": "Dina taggar är inte delade med andra användare", "tags_notShared": "Dina taggar är inte delade med andra användare",
"tags_searchHint": "Påbörja en sökning med # i din CryptDrive för att hitta dina taggade dokument.",
"tags_add": "Uppdatera taggar för denna sida", "tags_add": "Uppdatera taggar för denna sida",
"tags_title": "Taggar (endast för dig)", "tags_title": "Taggar (endast för dig)",
"or": "eller", "or": "eller",

@ -3,6 +3,7 @@
@import (reference) "../../customize/src/less2/include/tools.less"; @import (reference) "../../customize/src/less2/include/tools.less";
@import (reference) "../../customize/src/less2/include/markdown.less"; @import (reference) "../../customize/src/less2/include/markdown.less";
@import (reference) "../../customize/src/less2/include/avatar.less"; @import (reference) "../../customize/src/less2/include/avatar.less";
@import (reference) "../../customize/src/less2/include/buttons.less";
// body // body
&.cp-app-kanban { &.cp-app-kanban {
@ -309,28 +310,45 @@
position: relative; position: relative;
min-height: 50px; min-height: 50px;
.cp-kanban-filterTags { .cp-kanban-filterTags {
.buttons_main();
display: inline-flex; display: inline-flex;
align-items: baseline; align-items: center;
flex: 1; flex: 1;
max-width: 80%; //max-width: 80%;
min-width: 150px; min-width: 150px;
.cp-kanban-filterTags-toggle {
.cp-kanban-filterTags-reset { min-width: 100px;
cursor: pointer; display: flex;
margin-left: 10px; flex-flow: column;
flex-shrink: 0; flex-shrink: 0;
& > * {
visibility: hidden;
}
& > span {
display: inline-block;
height: 38px;
line-height: 38px;
}
& > button {
margin-top: -38px;
}
}
button.cp-kanban-filterTags-reset {
cursor: pointer;
white-space: normal !important;
.tools_unselectable(); .tools_unselectable();
i { i {
margin-right: 5px; margin-right: 5px;
} }
} }
.cp-kanban-filterTags-name {
flex-shrink: 0;
}
.cp-kanban-filterTags-list { .cp-kanban-filterTags-list {
margin-right: 10px;
margin-left: 10px; margin-left: 10px;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
&:not(:empty) {
margin-top: -5px;
}
em { em {
font-size: 14px; font-size: 14px;
color: lighten(@cryptpad_text_col, 10%); color: lighten(@cryptpad_text_col, 10%);
@ -421,14 +439,28 @@
} }
} }
#kanban-trash { #kanban-trash {
height: 60px; height: 1px;
font-size: 40px; font-size: 0px;
display: flex; /* CSS transitions are nice to look at, but it seems some interaction of "display: flex" here
makes the horizontal scrollbar stop working, so we need "display: none" for this state, but
CSS transitions are disabled when one state has "display: none". We can accomplish this in
js, but js animations are more prone to bugs and I'd rather live with a slight jank than
have the trash get stuck in some intermediary animation state under heavy use. --ansuz
*/
display: none; // flex;
//transition: opacity 400ms, height 400ms, font-size 400ms;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: relative; position: relative;
width: 100%; width: 100%;
//pointer-events: none; //pointer-events: none;
&.kanban-trash-active, &.kanban-trash-suggest {
display: flex;
height: 60px;
font-size: 40px;
}
i { i {
position: fixed; position: fixed;
} }

@ -874,14 +874,27 @@ define([
// Tags filter // Tags filter
var existing = getExistingTags(kanban.options.boards); var existing = getExistingTags(kanban.options.boards);
var list = h('div.cp-kanban-filterTags-list'); var list = h('div.cp-kanban-filterTags-list');
var reset = h('span.cp-kanban-filterTags-reset', [h('i.fa.fa-times'), Messages.kanban_clearFilter]); var reset = h('button.btn.btn-cancel.cp-kanban-filterTags-reset', [
h('i.fa.fa-times'),
Messages.kanban_clearFilter
]);
var hint = h('span.cp-kanban-filterTags-name', Messages.kanban_tags);
var tags = h('div.cp-kanban-filterTags', [ var tags = h('div.cp-kanban-filterTags', [
h('span.cp-kanban-filterTags-name', Messages.kanban_tags), h('span.cp-kanban-filterTags-toggle', [
hint,
reset,
]),
list, list,
reset
]); ]);
var $reset = $(reset); var $reset = $(reset);
var $list = $(list); var $list = $(list);
var $hint = $(hint);
var setTagFilterState = function (bool) {
$hint.css('visibility', bool? 'hidden': 'visible');
$reset.css('visibility', bool? 'visible': 'hidden');
};
setTagFilterState();
var getTags = function () { var getTags = function () {
return $list.find('span.active').map(function () { return $list.find('span.active').map(function () {
@ -890,11 +903,7 @@ define([
}; };
var commitTags = function () { var commitTags = function () {
var t = getTags(); var t = getTags();
if (t.length) { setTagFilterState(t.length);
$reset.css('visibility', '');
} else {
$reset.css('visibility', 'hidden');
}
//framework._.sfCommon.setPadAttribute('tagsFilter', t); //framework._.sfCommon.setPadAttribute('tagsFilter', t);
kanban.options.tags = t; kanban.options.tags = t;
kanban.setBoards(kanban.options.boards); kanban.setBoards(kanban.options.boards);
@ -938,14 +947,11 @@ define([
return $(this).data('tag') === t; return $(this).data('tag') === t;
}).addClass('active'); }).addClass('active');
}); });
if (tags.length) { setTagFilterState(tags.length);
$reset.css('visibility', '');
} else {
$reset.css('visibility', 'hidden');
}
//framework._.sfCommon.setPadAttribute('tagsFilter', tags); //framework._.sfCommon.setPadAttribute('tagsFilter', tags);
}; };
$reset.css('visibility', 'hidden').click(function () { setTagFilterState();
$reset.click(function () {
setTags([]); setTags([]);
commitTags(); commitTags();
}); });

@ -18,6 +18,15 @@
display: flex; display: flex;
flex-flow: column; flex-flow: column;
.cp-support-form-attachments {
.fa {
cursor: pointer;
}
&> span {
padding: 10px;
}
}
.cp-support-language-list { .cp-support-language-list {
.cp-support-language { .cp-support-language {
margin-left: 5px; margin-left: 5px;

@ -149,7 +149,6 @@ define([
}) })
); );
Messages.support_languagesPreamble = "This server's administrators speak the following languages:"; // XXX
var $div = $( var $div = $(
h('div.cp-support-language', [ h('div.cp-support-language', [
Messages.support_languagesPreamble, Messages.support_languagesPreamble,
@ -166,8 +165,6 @@ define([
var form = APP.support.makeForm(); var form = APP.support.makeForm();
$div.find('button').before(form);
var id = Util.uid(); var id = Util.uid();
$div.find('button').click(function () { $div.find('button').click(function () {
@ -183,6 +180,7 @@ define([
$('.cp-sidebarlayout-category[data-category="tickets"]').click(); $('.cp-sidebarlayout-category[data-category="tickets"]').click();
} }
}); });
$div.find('button').before(form);
return $div; return $div;
}; };

@ -6,8 +6,9 @@ define([
'/common/common-hash.js', '/common/common-hash.js',
'/common/common-util.js', '/common/common-util.js',
'/common/clipboard.js', '/common/clipboard.js',
'/common/common-ui-elements.js',
'/customize/messages.js', '/customize/messages.js',
], function ($, ApiConfig, h, UI, Hash, Util, Clipboard, Messages) { ], function ($, ApiConfig, h, UI, Hash, Util, Clipboard, UIElements, Messages) {
var send = function (ctx, id, type, data, dest) { var send = function (ctx, id, type, data, dest) {
var common = ctx.common; var common = ctx.common;
@ -61,9 +62,15 @@ define([
}; };
var sendForm = function (ctx, id, form, dest) { var sendForm = function (ctx, id, form, dest) {
var $title = $(form).find('.cp-support-form-title'); var $form = $(form);
var $content = $(form).find('.cp-support-form-msg'); var $cat = $form.find('.cp-support-form-category');
var $title = $form.find('.cp-support-form-title');
var $content = $form.find('.cp-support-form-msg');
// XXX block submission until pending uploads are complete?
var $attachments = $form.find('.cp-support-attachments');
var category = $cat.val().trim(); // XXX make category a required field?
var title = $title.val().trim(); var title = $title.val().trim();
if (!title) { if (!title) {
return void UI.alert(Messages.support_formTitleError); return void UI.alert(Messages.support_formTitleError);
@ -72,18 +79,61 @@ define([
if (!content) { if (!content) {
return void UI.alert(Messages.support_formContentError); return void UI.alert(Messages.support_formContentError);
} }
$cat.val('');
$content.val(''); $content.val('');
$title.val(''); $title.val('');
var attachments = [];
$attachments.find('> span').each(function (i, el) {
var $el = $(el);
attachments.push({
href: $el.attr('data-href'),
name: $el.attr('data-name')
});
});
$attachments.html('');
send(ctx, id, 'TICKET', { send(ctx, id, 'TICKET', {
category: category,
title: title, title: title,
attachments: attachments,
message: content, message: content,
}, dest); }, dest);
return true; return true;
}; };
var makeForm = function (cb, title) { Messages.support_cat_account = "User account"; // XXX
Messages.support_cat_data = "Loss of content"; // XXX
Messages.support_cat_bug = "Bug report"; // XXX
Messages.support_cat_other = "Other"; // XXX
Messages.support_cat_all = "All"; // XXX
Messages.support_category = "Category"; // XXX
Messages.support_attachments = "Attachments"; // XXX
Messages.support_addAttachment = "Add attachment"; // XXX
var makeCategoryDropdown = function (ctx, container, onChange, all) {
var categories = ['account', 'data', 'bug', 'other'];
if (all) { categories.push('all'); }
categories = categories.map(function (key) {
return {
tag: 'a',
content: h('span', Messages['support_cat_'+key]),
action: function () {
onChange(key);
}
};
});
var dropdownCfg = {
text: Messages.support_category,
options: categories,
container: $(container),
isSelect: true
};
return UIElements.createDropdown(dropdownCfg);
};
var makeForm = function (ctx, cb, title) {
var button; var button;
if (typeof(cb) === "function") { if (typeof(cb) === "function") {
@ -93,8 +143,21 @@ define([
var cancel = title ? h('button.btn.btn-secondary', Messages.cancel) : undefined; var cancel = title ? h('button.btn.btn-secondary', Messages.cancel) : undefined;
var category = h('input.cp-support-form-category', {
type: 'hidden',
value: ''
});
var catContainer = h('div.cp-dropdown-container' + (title ? '.cp-hidden': ''));
makeCategoryDropdown(ctx, catContainer, function (key) {
$(category).val(key);
});
var attachments, addAttachment;
var content = [ var content = [
h('hr'), h('hr'),
category,
catContainer,
h('input.cp-support-form-title' + (title ? '.cp-hidden' : ''), { h('input.cp-support-form-title' + (title ? '.cp-hidden' : ''), {
placeholder: Messages.support_formTitle, placeholder: Messages.support_formTitle,
type: 'text', type: 'text',
@ -104,11 +167,54 @@ define([
h('textarea.cp-support-form-msg', { h('textarea.cp-support-form-msg', {
placeholder: Messages.support_formMessage placeholder: Messages.support_formMessage
}), }),
h('label', Messages.support_attachments),
attachments = h('div.cp-support-attachments'),
addAttachment = h('button', Messages.support_addAttachment),
h('hr'), h('hr'),
button, button,
cancel cancel
]; ];
$(addAttachment).click(function () {
var $input = $('<input>', {
'type': 'file',
'style': 'display: none;',
'multiple': 'multiple',
'accept': 'image/*'
}).on('change', function (e) {
var files = Util.slice(e.target.files);
files.forEach(function (file) {
// XXX validate that the href is hosted on the same instance
// use relative URLs or compare it against a list or allowed domains?
var ev = {};
ev.callback = function (data) {
var x, a;
var span = h('span', {
'data-name': data.name,
'data-href': data.url
}, [
x = h('i.fa.fa-times'),
a = h('a', {
href: '#'
}, data.name)
]);
$(x).click(function () {
$(span).remove();
});
$(a).click(function (e) {
e.preventDefault();
ctx.common.openURL(data.url);
});
$(attachments).append(span);
};
// The empty object allows us to bypass the file upload modal
ctx.FM.handleFile(file, ev, {});
});
});
$input.click();
});
var form = h('div.cp-support-form-container', content); var form = h('div.cp-support-form-container', content);
$(cancel).click(function () { $(cancel).click(function () {
@ -125,6 +231,7 @@ define([
var privateData = metadataMgr.getPrivateData(); var privateData = metadataMgr.getPrivateData();
var ticketTitle = content.title + ' (#' + content.id + ')'; var ticketTitle = content.title + ' (#' + content.id + ')';
var ticketCategory;
var answer = h('button.btn.btn-primary.cp-support-answer', Messages.support_answer); var answer = h('button.btn.btn-primary.cp-support-answer', Messages.support_answer);
var close = h('button.btn.btn-danger.cp-support-close', Messages.support_close); var close = h('button.btn.btn-danger.cp-support-close', Messages.support_close);
var hide = h('button.btn.btn-danger.cp-support-hide', Messages.support_remove); var hide = h('button.btn.btn-danger.cp-support-hide', Messages.support_remove);
@ -137,6 +244,7 @@ define([
var url; var url;
if (ctx.isAdmin) { if (ctx.isAdmin) {
ticketCategory = Messages['support_cat_'+(content.category || 'all')] + ' - ';
url = h('button.btn.btn-primary.fa.fa-clipboard'); url = h('button.btn.btn-primary.fa.fa-clipboard');
$(url).click(function () { $(url).click(function () {
var link = privateData.origin + privateData.pathname + '#' + 'support-' + content.id; var link = privateData.origin + privateData.pathname + '#' + 'support-' + content.id;
@ -146,9 +254,10 @@ define([
} }
var $ticket = $(h('div.cp-support-list-ticket', { var $ticket = $(h('div.cp-support-list-ticket', {
'data-cat': content.category,
'data-id': content.id 'data-id': content.id
}, [ }, [
h('h2', [ticketTitle, url]), h('h2', [ticketCategory, ticketTitle, url]),
actions actions
])); ]));
@ -179,7 +288,7 @@ define([
$(answer).click(function () { $(answer).click(function () {
$ticket.find('.cp-support-form-container').remove(); $ticket.find('.cp-support-form-container').remove();
$(actions).hide(); $(actions).hide();
var form = makeForm(function () { var form = makeForm(ctx, function () {
var sent = sendForm(ctx, content.id, form, content.sender); var sent = sendForm(ctx, content.id, form, content.sender);
if (sent) { if (sent) {
$(actions).show(); $(actions).show();
@ -215,6 +324,21 @@ define([
ev.stopPropagation(); ev.stopPropagation();
}); });
var attachments = (content.attachments || []).map(function (obj) {
if (!obj || !obj.name || !obj.href) { return; }
var a = h('a', {
href: '#'
}, obj.name);
// XXX disallow remote URLs
$(a).click(function (e) {
e.preventDefault();
ctx.common.openURL(obj.href);
});
return h('span', [
a
]);
});
var adminClass = (fromAdmin? '.cp-support-fromadmin': ''); var adminClass = (fromAdmin? '.cp-support-fromadmin': '');
var premiumClass = (fromPremium && !fromAdmin? '.cp-support-frompremium': ''); var premiumClass = (fromPremium && !fromAdmin? '.cp-support-frompremium': '');
var name = Util.fixHTML(content.sender.name) || Messages.anonymous; var name = Util.fixHTML(content.sender.name) || Messages.anonymous;
@ -226,6 +350,7 @@ define([
h('span.cp-support-message-time', content.time ? new Date(content.time).toLocaleString() : '') h('span.cp-support-message-time', content.time ? new Date(content.time).toLocaleString() : '')
]), ]),
h('pre.cp-support-message-content', content.message), h('pre.cp-support-message-content', content.message),
h('div.cp-support-attachments', attachments),
isAdmin ? userData : undefined, isAdmin ? userData : undefined,
]); ]);
}; };
@ -257,10 +382,25 @@ define([
adminKeys: Array.isArray(ApiConfig.adminKeys)? ApiConfig.adminKeys.slice(): [], adminKeys: Array.isArray(ApiConfig.adminKeys)? ApiConfig.adminKeys.slice(): [],
}; };
var fmConfig = {
body: $('body'),
onUploaded: function (ev, data) {
if (ev.callback) {
ev.callback(data);
}
}
};
ctx.FM = common.createFileManager(fmConfig);
ui.sendForm = function (id, form, dest) { ui.sendForm = function (id, form, dest) {
return sendForm(ctx, id, form, dest); return sendForm(ctx, id, form, dest);
}; };
ui.makeForm = makeForm; ui.makeForm = function (cb, title) {
return makeForm(ctx, cb, title);
};
ui.makeCategoryDropdown = function (container, onChange, all) {
return makeCategoryDropdown(ctx, container, onChange, all);
};
ui.makeTicket = function ($div, content, onHide) { ui.makeTicket = function ($div, content, onHide) {
return makeTicket(ctx, $div, content, onHide); return makeTicket(ctx, $div, content, onHide);
}; };

Loading…
Cancel
Save