Merge branch 'staging' into teamBox

pull/1/head
ansuz 4 years ago
commit b663c20233

@ -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)
Our next major release (3.19.0) is still a few weeks away.

@ -9,5 +9,8 @@ define(['/common/application_config_internal.js'], function (AppConfig) {
// Example: If you want to remove the survey link in the menu:
// AppConfig.surveyURL = "";
// To inform users of the support ticket panel which languages your admins speak:
//AppConfig.supportLanguages = [ 'en', 'fr' ];
return AppConfig;
});

@ -26,4 +26,6 @@
<glyph unicode="&#xe910;" glyph-name="new-template" d="M840.764 886.152h-655.119c-35.33 0-63.97-28.64-63.97-63.97v0-787.637c0-35.33 28.64-63.97 63.97-63.97v0h655.119c35.33 0 63.97 28.64 63.97 63.97v0 787.637c0 35.33-28.64 63.97-63.97 63.97v0zM844.499 34.545c0-2.063-1.672-3.735-3.735-3.735v0h-655.119c-2.063 0-3.735 1.672-3.735 3.735v0 787.637c0 2.063 1.672 3.735 3.735 3.735h655.119c2.063 0 3.735-1.672 3.735-3.735v0zM643.915 466.071h-93.365v93.003c0 10.313-8.36 18.673-18.673 18.673v0h-37.346c-0.036 0-0.078 0-0.121 0-10.246 0-18.552-8.306-18.552-18.552 0-0.042 0-0.085 0-0.127v0.006-93.003h-93.365c-0.036 0-0.078 0-0.121 0-10.246 0-18.552-8.306-18.552-18.552 0-0.042 0-0.085 0-0.127v0.006-37.346c0-10.313 8.36-18.673 18.673-18.673v0h93.365v-93.967c0-0.036 0-0.078 0-0.121 0-10.246 8.306-18.552 18.552-18.552 0.042 0 0.085 0 0.127 0h37.34c10.313 0 18.673 8.36 18.673 18.673v0 93.606h93.365c10.285 0.068 18.605 8.388 18.673 18.666v37.352c0.002 0.108 0.003 0.234 0.003 0.361 0 10.313-8.36 18.673-18.673 18.673-0.001 0-0.002 0-0.004 0v0z" />
<glyph unicode="&#xe911;" glyph-name="palette" d="M408.6 950c-198.8-38.8-359-198.6-398.2-396.8-74-374 263.4-652.8 517.6-613.4 82.4 12.8 122.8 109.2 85 183.4-46.2 90.8 19.8 196.8 121.8 196.8h159.4c71.6 0 129.6 59.2 129.8 130.6-1 315.2-287.8 563.2-615.4 499.4zM192 320c-35.4 0-64 28.6-64 64s28.6 64 64 64 64-28.6 64-64-28.6-64-64-64zM256 576c-35.4 0-64 28.6-64 64s28.6 64 64 64 64-28.6 64-64-28.6-64-64-64zM512 704c-35.4 0-64 28.6-64 64s28.6 64 64 64 64-28.6 64-64-28.6-64-64-64zM768 576c-35.4 0-64 28.6-64 64s28.6 64 64 64 64-28.6 64-64-28.6-64-64-64z" />
<glyph unicode="&#xe912;" glyph-name="folder-upload" d="M829.44 727.251h-296.84l-47.104 62.886c-25.923 34.066-66.406 55.898-111.999 56.139h-178.938c-77.457-0.137-140.211-62.891-140.348-140.335v-515.868c0.137-77.457 62.891-140.211 140.335-140.348h634.893c77.457 0.137 140.211 62.891 140.348 140.335v396.482c0 0.036 0 0.078 0 0.121 0 77.561-62.807 140.452-140.335 140.589h-0.013zM911.119 190.072c-0.068-45.083-36.597-81.611-81.673-81.679h-634.887c-45.083 0.068-81.611 36.597-81.679 81.673v515.862c0.068 45.083 36.597 81.611 81.673 81.679h178.906c26.48-0.030 50.004-12.656 64.908-32.207l0.146-0.199 47.104-62.765 17.709-24.094h326.114c45.125-0.069 81.679-36.665 81.679-81.799 0 0 0 0 0 0v0zM562.838 166.883h-102.039v203.957h-72.523l123.723 214.076 123.723-214.076h-72.885v-203.957z" />
<glyph unicode="&#xe913;" glyph-name="add-bottom" d="M108.793 271.501c-15.312 0-27.996 12.684-27.996 27.996v55.992c0 15.312 12.684 28.006 27.996 28.006h225.414v-111.993zM108.793 495.478c-15.312 0-27.996 12.694-27.996 28.006v55.992c0 15.312 12.684 27.996 27.996 27.996h403.491v-111.993zM108.793 719.465c-15.312 0-27.996 12.694-27.996 28.006v55.992c0 15.312 12.684 27.996 27.996 27.996h615.968c15.312 0 27.996-12.684 27.996-27.996v-55.992c0-15.312-12.684-28.006-27.996-28.006zM943.202 287.839c0-19.465-15.792-35.257-35.258-35.257h-152.782v-152.782c0-19.465-15.792-35.257-35.258-35.257h-70.515c-19.465 0-35.257 15.792-35.257 35.257v152.782h-152.782c-19.465 0-35.257 15.792-35.257 35.257v70.515c0 19.465 15.792 35.258 35.257 35.258h152.782v152.782c0 19.465 15.792 35.258 35.257 35.258h70.515c19.465 0 35.258-15.792 35.258-35.258v-152.782h152.782c19.465 0 35.258-15.792 35.258-35.258z" />
<glyph unicode="&#xe914;" glyph-name="add-top" d="M108.793 624.499c-15.312 0-27.996-12.684-27.996-27.996v-55.992c0-15.312 12.684-28.006 27.996-28.006h225.414v111.993zM108.793 400.522c-15.312 0-27.996-12.694-27.996-28.006v-55.992c0-15.312 12.684-27.996 27.996-27.996h403.491v111.993zM108.793 176.535c-15.312 0-27.996-12.694-27.996-28.006v-55.992c0-15.312 12.684-27.996 27.996-27.996h615.968c15.312 0 27.996 12.684 27.996 27.996v55.992c0 15.312-12.684 28.006-27.996 28.006zM943.202 608.161c0 19.465-15.792 35.257-35.258 35.257h-152.782v152.782c0 19.465-15.792 35.257-35.258 35.257h-70.515c-19.465 0-35.257-15.792-35.257-35.257v-152.782h-152.782c-19.465 0-35.257-15.792-35.257-35.257v-70.515c0-19.465 15.792-35.258 35.257-35.258h152.782v-152.782c0-19.465 15.792-35.258 35.257-35.258h70.515c19.465 0 35.258 15.792 35.258 35.258v152.782h152.782c19.465 0 35.258 15.792 35.258 35.258z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 24 KiB

@ -1,11 +1,12 @@
@font-face {
font-family: 'cptools';
src:
url('fonts/cptools.ttf?cljhos') format('truetype'),
url('fonts/cptools.woff?cljhos') format('woff'),
url('fonts/cptools.svg?cljhos#cptools') format('svg');
url('fonts/cptools.ttf?da4x1y') format('truetype'),
url('fonts/cptools.woff?da4x1y') format('woff'),
url('fonts/cptools.svg?da4x1y#cptools') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
}
.cptools {
@ -24,6 +25,12 @@
-moz-osx-font-smoothing: grayscale;
}
.cptools-add-bottom:before {
content: "\e913";
}
.cptools-add-top:before {
content: "\e914";
}
.cptools-folder-upload:before {
content: "\e912";
}

@ -62,7 +62,13 @@ define([
var imprintUrl = AppConfig.imprint && (typeof(AppConfig.imprint) === "boolean" ?
'/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 () {
return h('footer', [
@ -74,24 +80,14 @@ define([
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', [
footLink('https://cryptpad.fr/what-is-cryptpad.html', 'topbar_whatIsCryptpad'),
footLink('/faq.html', 'faq_link'),
footLink('https://github.com/xwiki-labs/cryptpad', null, 'GitHub'),
footLink('/what-is-cryptpad.html', 'topbar_whatIsCryptpad'),
Pages.faqLink,
Pages.githubLink,
footLink('https://opencollective.com/cryptpad/contribute/', 'footer_donate'),
]),
footerCol('footer_aboutUs', [
/*footLink('https://blog.cryptpad.fr', 'blog'),
footLink('https://labs.xwiki.com', null, 'XWiki Labs'),*/
/*footLink('https://blog.cryptpad.fr', 'blog'), */
footLink('http://www.xwiki.com', null, 'XWiki SAS'),
footLink('https://www.open-paas.org', null, 'OpenPaaS'),
footLink('/about.html', 'footer_team'),
@ -99,15 +95,9 @@ define([
]),
footerCol('footer_legal', [
footLink('/terms.html', 'footer_tos'),
footLink(AppConfig.privacy, 'privacy'),
AppConfig.imprint ? footLink(imprintUrl, 'imprint') : undefined,
Pages.privacyLink,
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)
@ -150,13 +140,9 @@ define([
h('a.navbar-brand', { href: '/index.html'}),
button,
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: '/features.html'}, Msg.pricing),
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))
);
};

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

@ -4,6 +4,9 @@
@msg-bg: #eee;
@fromme-bg: #ddd;
.cp-support-form-container {
div {
margin-bottom: 10px;
}
[type="text"] {
width: @sidebar_button-width;
margin-bottom: 10px;
@ -15,6 +18,18 @@
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-list-ticket {
display: flex;

@ -325,6 +325,9 @@ const storeMessage = function (Env, channel, msg, isCp, optionalMessageHash) {
}
}));
}).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) => {
if (err) {
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)
});
}
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.offsets++;
}

2
package-lock.json generated

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

@ -1,7 +1,7 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
"version": "3.18.1",
"version": "3.19.0",
"license": "AGPL-3.0+",
"repository": {
"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 $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 privateData = metadataMgr.getPrivateData();
var cat = privateData.category || '';
@ -277,6 +291,9 @@ define([
UI.alert(Messages.error);
});
});
if (category !== 'all' && $ticket.attr('data-cat') !== category) {
$ticket.hide();
}
}
$ticket.append(APP.support.makeMessage(content, hash));
reorder();

@ -328,11 +328,7 @@ define([
var input = dialog.textInput();
var tagger = dialog.frame([
dialog.message([
Messages.tags_add,
h('br'),
Messages.tags_searchHint,
]),
dialog.message([ Messages.tags_add ]),
input,
h('center', h('small', Messages.tags_notShared)),
dialog.nav(),
@ -377,6 +373,14 @@ define([
field.focus();
});
var $field = field.tokenfield.closest('.tokenfield').find('.token-input');
$field.on('keypress', function (e) {
if (!$field.val() && e.which === 13) { return void $ok.click(); }
});
$field.on('keydown', function (e) {
if (!$field.val() && e.which === 27) { return void $cancel.click(); }
});
return tagger;
};
@ -799,6 +803,7 @@ define([
UI.createHelper = function (href, text) {
var q = h('a.fa.fa-question-circle', {
'data-cptippy-html': true,
style: 'text-decoration: none !important;',
title: text,
href: href,

@ -46,28 +46,69 @@ define([
return mB + Messages.MB;
};
UIElements.updateTags = function (common, href) {
UIElements.updateTags = function (common, hrefs) {
var existing, tags;
var allTags = {};
if (!hrefs || typeof (hrefs) === "string") {
hrefs = [hrefs];
}
NThen(function(waitFor) {
common.getSframeChannel().query("Q_GET_ALL_TAGS", null, waitFor(function(err, res) {
if (err || res.error) { return void console.error(err || res.error); }
existing = Object.keys(res.tags).sort();
}));
}).nThen(function (waitFor) {
var _err;
hrefs.forEach(function (href) {
common.getPadAttribute('tags', waitFor(function (err, res) {
if (err) {
if (err === 'NO_ENTRY') {
UI.alert(Messages.tags_noentry);
}
waitFor.abort();
_err = err;
return void console.error(err);
}
allTags[href] = res || [];
if (tags) {
// Intersect with tags from previous pads
tags = (res || []).filter(function (tag) {
return tags.indexOf(tag) !== -1;
});
} else {
tags = res || [];
}
}), href);
});
}).nThen(function () {
UI.dialog.tagPrompt(tags, existing, function (newTags) {
if (!Array.isArray(newTags)) { return; }
common.setPadAttribute('tags', newTags, null, href);
var added = [];
var removed = [];
newTags.forEach(function (tag) {
if (tags.indexOf(tag) === -1) {
added.push(tag);
}
});
tags.forEach(function (tag) {
if (newTags.indexOf(tag) === -1) {
removed.push(tag);
}
});
var update = function (oldTags) {
Array.prototype.push.apply(oldTags, added);
removed.forEach(function (tag) {
var idx = oldTags.indexOf(tag);
oldTags.splice(idx, 1);
});
};
hrefs.forEach(function (href) {
var oldTags = allTags[href] || [];
update(oldTags);
common.setPadAttribute('tags', Util.deduplicateString(oldTags), null, href);
});
});
});
};
@ -2167,7 +2208,9 @@ define([
if (config.isSelect && value) {
var $val = $innerblock.find('[data-value="'+value+'"]');
setActive($val);
try {
$innerblock.scrollTop($val.position().top + $innerblock.scrollTop());
} catch (e) {}
}
if (config.feedback) { Feedback.send(config.feedback); }
};
@ -2261,6 +2304,58 @@ define([
return $container;
};
UIElements.displayInfoMenu = function (Common, metadataMgr) {
//var padType = metadataMgr.getMetadata().type;
var priv = metadataMgr.getPrivateData();
var origin = priv.origin;
// TODO link to the most recent changelog/release notes
// https://github.com/xwiki-labs/cryptpad/releases/latest/ ?
var template = function (line, link) {
if (!line || !link) { return; }
var p = $('<p>').html(line)[0];
var sub = link.cloneNode(true);
/* This is a hack to make relative URLs point to the main domain
instead of the sandbox domain. It will break if the admins have specified
some less common URL formats for their customizable links, such as if they've
used a protocal-relative absolute URL. The URL API isn't quite safe to use
because of IE (thanks, Bill). */
var href = sub.getAttribute('href');
if (/^\//.test(href)) { sub.setAttribute('href', origin + href); }
var a = p.querySelector('a');
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('hr'),
legalLine,
privacyLine,
faqLine,
]);
var buttons = [
{
className: 'primary',
name: Messages.filePicker_close,
onClick: function () {},
keys: [27],
},
];
var modal = UI.dialog.customModal(content, {buttons: buttons });
UI.openCustomModal(modal);
};
UIElements.createUserAdminMenu = function (Common, config) {
var metadataMgr = Common.getMetadataMgr();
@ -2293,15 +2388,21 @@ define([
content: $userAdminContent.html()
});
}
if (accountName && !AppConfig.disableProfile) {
options.push({
tag: 'a',
attributes: {
'target': '_blank',
'href': origin+'/index.html',
'class': 'fa fa-home'
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/';
}
},
content: h('span', Messages.homePage)
});
}
if (padType !== 'drive' || (!accountName && priv.newSharedFolder)) {
options.push({
tag: 'a',
@ -2335,29 +2436,6 @@ define([
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') {
options.push({
tag: 'a',
@ -2372,6 +2450,7 @@ define([
},
});
}
options.push({ tag: 'hr' });
// Add administration panel link if the user is an admin
if (priv.edPublic && Array.isArray(Config.adminKeys) && Config.adminKeys.indexOf(priv.edPublic) !== -1) {
@ -2402,57 +2481,72 @@ define([
},
});
}
options.push({ tag: 'hr' });
if (Config.allowSubscriptions) {
if (AppConfig.surveyURL) {
options.push({
tag: 'a',
attributes: {
'target': '_blank',
'href': priv.plan ? priv.accounts.upgradeURL : origin+'/features.html',
'class': 'fa fa-star-o'
'rel': 'noopener',
'href': AppConfig.surveyURL,
'class': 'cp-toolbar-survey fa fa-graduation-cap'
},
content: h('span', Messages.survey),
action: function () {
Feedback.send('SURVEY_CLICKED');
},
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'
'class': 'cp-toolbar-about fa fa-info',
},
content: h('span', Messages.user_about),
action: function () {
UIElements.displayInfoMenu(Common, metadataMgr);
},
content: h('span', Messages.crowdfunding_button2)
});
}
if (AppConfig.surveyURL) {
options.push({
tag: 'a',
attributes: {
'target': '_blank',
'rel': 'noopener',
'href': AppConfig.surveyURL,
'class': 'cp-toolbar-survey fa fa-graduation-cap'
'href': origin+'/index.html',
'class': 'fa fa-home'
},
content: h('span', Messages.survey),
action: function () {
Feedback.send('SURVEY_CLICKED');
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 (Pages.versionString) {
Messages.user_about = Messages.about; // XXX "About CryptPad"
if (!priv.plan && !Config.removeDonateButton) {
options.push({
tag: 'a',
attributes: {
'class': 'cp-toolbar-about fa fa-info',
},
content: h('span', Messages.user_about),
action: function () {
// XXX UIElements.createHelpButton
UI.alert(Pages.versionString);
'target': '_blank',
'rel': 'noopener',
'href': priv.accounts.donateURL,
'class': 'fa fa-gift'
},
content: h('span', Messages.crowdfunding_button2)
});
}

@ -526,6 +526,8 @@ define([
}
});
});
return $(menu);
};
@ -919,6 +921,11 @@ define([
if (e.ctrlKey) { ev.ctrlKey = true; }
if (e.shiftKey) { ev.shiftKey = true; }
// ESC
if (e.which === 27) {
return void APP.hideMenu();
}
// Enter
if (e.which === 13) {
var $allSelected = $content.find('.cp-app-drive-element.cp-app-drive-element-selected');
@ -1090,7 +1097,7 @@ define([
var priv = metadataMgr.getPrivateData();
var useUnsafe = Util.find(priv, ['settings', 'security', 'unsafeLinks']);
if (useUnsafe !== false) { // true of undefined: use unsafe links
if (useUnsafe === true) {
return void window.open(APP.origin + href);
}
@ -1294,7 +1301,6 @@ define([
hide.push('properties', 'access');
hide.push('rename');
hide.push('openparent');
hide.push('hashtag');
hide.push('download');
hide.push('share');
hide.push('savelocal');
@ -1309,6 +1315,7 @@ define([
if (!APP.loggedIn) {
hide.push('openparent');
hide.push('rename');
}
filter = function ($el, className) {
@ -4370,12 +4377,12 @@ define([
});
}
else if ($this.hasClass("cp-app-drive-context-hashtag")) {
if (paths.length !== 1) { return; }
el = manager.find(paths[0].path);
data = manager.getFileData(el);
if (!data) { return void console.error("Expected to find a file"); }
var href = data.href || data.roHref;
common.updateTags(href);
var hrefs = paths.map(function (p) {
var el = manager.find(p.path);
var data = manager.getFileData(el);
return data.href || data.roHref;
}).filter(Boolean);
common.updateTags(hrefs);
}
else if ($this.hasClass("cp-app-drive-context-empty")) {
if (paths.length !== 1 || !paths[0].element

@ -874,7 +874,7 @@ define([
// Use hidden hash if needed (we're an owner of this pad so we know it is stored)
var useUnsafe = Util.find(priv, ['settings', 'security', 'unsafeLinks']);
var href = (priv.readOnly && data.roHref) ? data.roHref : data.href;
if (useUnsafe === false) {
if (useUnsafe !== true) {
var newParsed = Hash.parsePadUrl(href);
var newSecret = Hash.getSecrets(newParsed.type, newParsed.hash, newPass);
var newHash = Hash.getHiddenHashFromKeys(parsed.type, newSecret, {});

@ -399,14 +399,16 @@ define([
e.stopPropagation();
m.hide();
var $mt = $menu.data('mediatag');
if ($(this).hasClass("cp-app-code-context-saveindrive")) {
var $this = $(this);
if ($this.hasClass("cp-app-code-context-saveindrive")) {
common.importMediaTag($mt);
}
else if ($(this).hasClass("cp-app-code-context-download")) {
var media = $mt[0]._mediaObject;
else if ($this.hasClass("cp-app-code-context-download")) {
var media = Util.find($mt, [0, '_mediaObject']);
if (!(media && media._blob)) { return void console.error($mt); }
window.saveAs(media._blob.content, media.name);
}
else if ($(this).hasClass("cp-app-code-context-open")) {
else if ($this.hasClass("cp-app-code-context-open")) {
$mt.trigger('preview');
}
});

@ -7,9 +7,10 @@ define([
'/common/cryptget.js',
'/common/outer/mailbox.js',
'/customize/messages.js',
'/common/common-realtime.js',
'/bower_components/nthen/index.js',
'/bower_components/chainpad-crypto/crypto.js',
], function (AppConfig, Feedback, Hash, Util, Messaging, Crypt, Mailbox, Messages, nThen, Crypto) {
], function (AppConfig, Feedback, Hash, Util, Messaging, Crypt, Mailbox, Messages, Realtime, nThen, Crypto) {
// Start migration check
// Versions:
// 1: migrate pad attributes
@ -456,6 +457,37 @@ define([
if (version < 10) {
fixTodo();
}
}).nThen(function (waitFor) {
if (version >= 11) { return; }
// Migration 11: alert users of safe links as the new default
var done = function () {
Feedback.send('Migrate-11', true);
userObject.version = version = 11;
};
/* userObject.settings.security.unsafeLinks
undefined => the user has never touched it
false => the user has explicitly enabled "safe links"
true => the user has explicitly disabled "safe links"
*/
var unsafeLinks = Util.find(userObject, [ 'settings', 'security', 'unsafeLinks' ]);
if (unsafeLinks !== undefined) { return void done(); }
var ctx = {
store: store,
};
var myData = Messaging.createData(userObject);
Mailbox.sendTo(ctx, 'SAFE_LINKS_DEFAULT', {
user: myData,
}, {
channel: myData.notifications,
curvePublic: myData.curvePublic
}, waitFor(function (obj) {
if (obj && obj.error) { return void console.error(obj); }
done();
}));
/*}).nThen(function (waitFor) {
// Test progress bar in the loading screen
var i = 0;
@ -467,7 +499,7 @@ define([
}, 500);
progress(0, 0);*/
}).nThen(function () {
setTimeout(cb);
Realtime.whenRealtimeSyncs(store.realtime, Util.bake(cb));
});
};
});

@ -406,6 +406,19 @@ define([
}
};
handlers['SAFE_LINKS_DEFAULT'] = function (common, data) {
var content = data.content;
content.getFormatText = function () {
return Messages.settings_safeLinkDefault;
};
content.handler = function () {
common.openURL('/settings/#security');
};
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
// NOTE: don't forget to fixHTML everything returned by "getFormatText"

@ -122,7 +122,6 @@ define([
if (!state && !readOnly) {
$('#cp-app-oo-editor').append(h('div#cp-app-oo-offline'));
}
debug(state);
};
var deleteOffline = function () {
@ -271,7 +270,7 @@ define([
var checkDrawings = function () {
var editor = getEditor();
if (!editor) { return false; }
if (!editor || !editor.GetSheets) { return false; }
var s = editor.GetSheets();
return s.some(function (obj) {
return obj.worksheet.Drawings.length;
@ -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 () {
content.saveLock = myOOId;
APP.onLocal();
@ -492,6 +505,89 @@ define([
}, 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) {
if (rtChannel.ready) { return void cb(); }
@ -515,6 +611,18 @@ define([
break;
case 'MESSAGE':
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.lastHash = obj.data.hash;
ooChannel.cpIndex++;
@ -775,6 +883,7 @@ define([
}
// Send the changes
content.locks = content.locks || {};
rtChannel.sendMsg({
type: "saveChanges",
changes: parseChanges(obj.changes),
@ -971,8 +1080,10 @@ define([
ooChannel.queue.forEach(function (data) {
ooChannel.send(data.msg);
});
if (!readOnly) {
var last = ooChannel.queue.pop();
if (last) { ooChannel.lastHash = last.hash; }
}
ooChannel.cpIndex += ooChannel.queue.length;
// Apply existing locks
deleteOfflineLocks();
@ -1003,7 +1114,7 @@ define([
UI.openCustomModal(UI.dialog.customModal(div, {buttons: []}));
setTimeout(function () {
makeCheckpoint(true);
}, 1000);
}, 5000);
}
}
}
@ -1432,41 +1543,6 @@ define([
}, 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) {
if (ooLoaded) { return; }
var type = common.getMetadataMgr().getPrivateData().ooType;
@ -1496,7 +1572,7 @@ define([
default:
newText = '';
}
var blob = new Blob([newText], {type: 'text/plain'});
var blob = loadInitDocument(type, useNewDefault);
startOO(blob, file);
};
@ -1583,6 +1659,14 @@ define([
$save.appendTo(toolbar.$bottomM);
}
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, {
name: 'restore',
icon: 'fa-history',
@ -1608,6 +1692,7 @@ define([
}
if (common.isLoggedIn()) {
window.CryptPad_deleteLastCp = deleteLastCp;
var $importXLSX = common.createButton('import', true, {
accept: accept,
binary : ["ods", "xlsx", "odt", "docx", "odp", "pptx"]
@ -1764,10 +1849,10 @@ define([
var latest = getLastCp(true);
var newLatest = getLastCp();
if (newLatest.index > latest.index) {
ooChannel.queue = [];
var hasDrawings = checkDrawings();
if (hasDrawings) {
ooChannel.ready = false;
ooChannel.queue = [];
}
// New checkpoint
sframeChan.query('Q_OO_SAVE', {

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

@ -515,6 +515,12 @@ define([
cb();
};
handlers["SAFE_LINKS_DEFAULT"] = function (ctx, box, data, cb) {
var curve = ctx.store.proxy.curvePublic;
if (data.msg.author !== curve) { return void cb(true); }
cb();
};
// Hide duplicates when receiving a SHARE_PAD notification:
// Keep only one notification per channel: the stronger and more recent one
var comments = {};

@ -806,16 +806,8 @@ define([
var title = currentTabTitle.replace(/\{title\}/g, currentTitle || 'CryptPad');
document.title = title;
};
sframeChan.on('Q_SET_PAD_TITLE_IN_DRIVE', function (newData, cb) {
var newTitle = newData.title || newData.defaultTitle;
currentTitle = newTitle;
setDocumentTitle();
var data = {
password: password,
title: newTitle,
channel: secret.channel,
path: initialPathInDrive // Where to store the pad if we don't have it in our drive
};
var setPadTitle = function (data, cb) {
Cryptpad.setPadTitle(data, function (err, obj) {
if (!err && !(obj && obj.notStored)) {
// No error and the pad was correctly stored
@ -823,13 +815,26 @@ define([
var opts = parsed.getOptions();
var hash = Utils.Hash.getHiddenHashFromKeys(parsed.type, secret, opts);
var useUnsafe = Utils.Util.find(settings, ['security', 'unsafeLinks']);
if (useUnsafe === false && window.history && window.history.replaceState) {
if (useUnsafe !== true && window.history && window.history.replaceState) {
if (!/^#/.test(hash)) { hash = '#' + hash; }
window.history.replaceState({}, window.document.title, hash);
}
}
cb({error: err});
});
};
sframeChan.on('Q_SET_PAD_TITLE_IN_DRIVE', function (newData, cb) {
var newTitle = newData.title || newData.defaultTitle;
currentTitle = newTitle;
setDocumentTitle();
var data = {
password: password,
title: newTitle,
channel: secret.channel,
path: initialPathInDrive // Where to store the pad if we don't have it in our drive
};
setPadTitle(data, cb);
});
sframeChan.on('EV_SET_TAB_TITLE', function (newTabTitle) {
currentTabTitle = newTabTitle;
@ -854,20 +859,7 @@ define([
path: initialPathInDrive, // Where to store the pad if we don't have it in our drive
forceSave: true
};
Cryptpad.setPadTitle(data, function (err) {
if (!err && !(obj && obj.notStored)) {
// No error and the pad was correctly stored
// hide the hash
var opts = parsed.getOptions();
var hash = Utils.Hash.getHiddenHashFromKeys(parsed.type, secret, opts);
var useUnsafe = Utils.Util.find(settings, ['security', 'unsafeLinks']);
if (useUnsafe === false && window.history && window.history.replaceState) {
if (!/^#/.test(hash)) { hash = '#' + hash; }
window.history.replaceState({}, window.document.title, hash);
}
}
cb({error: err});
});
setPadTitle(data, cb);
});
sframeChan.on('Q_IS_PAD_STORED', function (data, cb) {
Cryptpad.getPadAttribute('title', function (err, data) {
@ -1115,7 +1107,13 @@ define([
if (parsed.hashData) { currentPad.hash = parsed.hashData.getHash(opts); }
// Rendered (maybe hidden) hash
var hiddenParsed = Utils.Hash.parsePadUrl(window.location.href);
// Update the hash in the address bar
var ohc = window.onhashchange;
window.onhashchange = function () {};
window.location.href = hiddenParsed.getUrl(opts);
window.onhashchange = ohc;
ohc({reset: true});
});

@ -109,7 +109,7 @@
"newButton": "Neu",
"newButtonTitle": "Neues Pad erstellen",
"uploadButton": "Hochladen",
"uploadButtonTitle": "Eine neue Datei in den aktuellen Ordner hochladen",
"uploadButtonTitle": "Eine neue Datei zum CryptDrive hochladen",
"saveTemplateButton": "Als Vorlage speichern",
"saveTemplatePrompt": "Bitte gib einen Titel für die Vorlage ein",
"templateSaved": "Vorlage gespeichert!",
@ -145,7 +145,7 @@
"filePicker_filter": "Dateien nach Namen filtern",
"or": "oder",
"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_duplicate": "Doppeltes Tag: {0}",
@ -831,7 +831,7 @@
"help": {
"title": "Mit CryptPad anfangen",
"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>.",
"save": "Alle Änderungen werden automatisch synchronisiert. Du musst sie also nicht selbst speichern"
},
@ -1377,5 +1377,10 @@
"toolbar_insert": "Einfügen",
"toolbar_savetodrive": "Als Bild speichern",
"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"
}

@ -111,7 +111,7 @@
"newButtonTitle": "Créer un nouveau pad",
"uploadButton": "Importer des fichiers",
"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",
"saveTemplatePrompt": "Choisir un titre pour ce modèle",
"templateSaved": "Modèle enregistré !",
@ -147,7 +147,7 @@
"filePicker_filter": "Filtrez les fichiers par leur nom",
"or": "ou",
"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_duplicate": "Mot-clé déjà présent : {0}",
@ -838,7 +838,7 @@
"help": {
"title": "Pour bien démarrer",
"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>.",
"save": "Tous les changements effectués sont enregistrés automatiquement"
},
@ -1377,5 +1377,10 @@
"toolbar_savetodrive": "Sauvegarder image",
"toolbar_insert": "Insérer",
"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>."
}

@ -114,7 +114,7 @@
"newButtonTitle": "Create a new pad",
"uploadButton": "Upload files",
"uploadFolderButton": "Upload folder",
"uploadButtonTitle": "Upload a new file to the current folder",
"uploadButtonTitle": "Upload a new file to your CryptDrive",
"saveTemplateButton": "Save as template",
"saveTemplatePrompt": "Choose a title for the template",
"templateSaved": "Template saved!",
@ -150,7 +150,7 @@
"filePicker_filter": "Filter files by name",
"or": "or",
"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_duplicate": "Duplicate tag: {0}",
@ -856,7 +856,7 @@
"help": {
"title": "Getting started",
"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>.",
"save": "All your changes are synced automatically so you never need to save"
},
@ -1377,5 +1377,10 @@
"code_editorTheme": "Editor theme",
"toolbar_file": "File",
"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."
}

@ -14,7 +14,7 @@
"error": "Eroare",
"saved": "Salvat",
"synced": "Totul a fost salvat",
"deleted": "Pad șters din CryptDrive-ul tău",
"deleted": "Șters",
"disconnected": "Deconectat",
"synchronizing": "Se sincronizează",
"reconnecting": "Reconectare...",
@ -239,7 +239,7 @@
"settings_pinningNotAvailable": "Pad-urile fixate sunt disponibile doar utilizatorilor înregistrați.",
"settings_pinningError": "Ceva nu a funcționat",
"settings_usageAmount": "Pad-urile tale fixate ocupă {0}MB",
"settings_logoutEverywhereTitle": "Deloghează-te peste tot",
"settings_logoutEverywhereTitle": "Închide sesiunile remote",
"settings_logoutEverywhere": "Deloghează-te din toate sesiunile web",
"settings_logoutEverywhereConfirm": "Ești sigur? Va trebui să te loghezi, din nou, pe toate device-urile tale.",
"upload_serverError": "Eroare de server: fișierele tale nu pot fi încărcate la momentul acesta.",
@ -317,7 +317,8 @@
"todo": "De facut",
"contacts": "Contacte",
"sheet": "Foaie de calcul",
"kanban": "Foaie Kanban"
"kanban": "Foaie Kanban",
"teams": "Echipe"
},
"button_newkanban": "Kanban nou",
"padNotPinned": "Aceasta filă va expira după 3 luni de inactivitate, {0}autentifică-te{1} sau {2}înregistrează-te{3} pentru a o păstra.",
@ -455,7 +456,7 @@
"profile_create": "Crează un profil",
"profile_description": "Descriere",
"profile_fieldSaved": "Valoare nouă salvată: {0}",
"userlist_addAsFriendTitle": "Adaugă \"{0}\" în lista de contacte",
"userlist_addAsFriendTitle": "Trimite o cerere de contact către \"{0}\"",
"canvas_currentBrush": "Culoarea curentă",
"profile_viewMyProfile": "Vezi profilul tău",
"contacts_title": "Contacte",
@ -477,7 +478,7 @@
"contacts_confirmRemoveHistory": "Ești sigur că vrei să ștergi definitiv istoricul chat-ului? Datele nu vor putea fi recuperate",
"contacts_removeHistoryServerError": "A apărut o eroare în timpul ștergerii istoricului chat-ului. Te rugăm să încerci mai târziu",
"contacts_fetchHistory": "Recuperează istoricul mai vechi",
"contacts_friends": "Prieteni",
"contacts_friends": "Persoane de contact",
"contacts_rooms": "Camere",
"contacts_leaveRoom": "Părăsește această cameră",
"contacts_online": "Un alt utilizator din această cameră este online",
@ -492,7 +493,7 @@
"fm_removePermanentlyNote": "Pad-urile tale vor fi șterse de pe server dacă vei continua",
"fm_deleteOwnedPad": "Ești sigur că vrei să ștergi definitiv acest pad de pe server?",
"fm_deleteOwnedPads": "Ești sigur că vrei să ștergi definitiv aceste pad-uri de pe server?",
"fm_info_recent": "Listează pad-urile deschise sau modificate recent",
"fm_info_recent": "Aceste documente au fost deschise sau modificate recent de către tine sau de către colaboratorii tăi.",
"fm_info_sharedFolder": "Acesta este un dosar partajat. Deoarece nu ești logat, îl poți vizualiza doar în modul citire.<br><a href=\"/register/\">Înscrie-te</a> sau <a href=\"/loghează-te/\">Log in</a> pentru a-l putea importa in CryptDrive-ul tău si a-l modifica.",
"fm_info_owned": "Ești proprietarul pad-urilor afișate aici. Acest lucru înseamna că le poți șterge definitiv de pe server oricând vei dori. Dacă decizi să le ștergi, alți utilizatori nu le vor mai putea accesa.",
"fm_error_cantPin": "O eroare internă de server a apărut. Te rugăm să reîncarci pagina și să încerci din nou.",
@ -510,7 +511,7 @@
"fm_tags_used": "Numărul de utilizări",
"fm_restoreDrive": "Drive-ul tău va fi setat la o versiune aneterioară. Pentru a obține cele mai bune rezultate, te rugăm să eviți să aduci modificări în drive-ul tău până când procesul va fi terminat.",
"fm_moveNestedSF": "Nu poti plasa un folder partajat în interiorul altuia. Folderul {0} nu a fost mutat.",
"fm_passwordProtected": "Acest document este protejat cu o parolă",
"fm_passwordProtected": "Parolă protejată",
"fc_newsharedfolder": "Folder partajat nou",
"fc_delete_owned": "Șterge de pe server",
"fc_remove_sharedfolder": "Șterge",
@ -547,5 +548,37 @@
"settings_resetNewTitle": "Curățare CryptDrive",
"settings_resetButton": "Șterge",
"settings_resetTipsAction": "Resetează",
"settings_thumbnails": "Miniaturi"
"settings_thumbnails": "Miniaturi",
"settings_padWidth": "Lățimea maximă a editorului",
"settings_codeFontSize": "Dimensiunea font-ului in editorul de cod",
"settings_codeUseTabs": "Indentarea prin etichete (în loc de spații)",
"settings_codeIndentation": "Indentarea editorului de cod (spații)",
"settings_driveDuplicateLabel": "Ascunde duplicatele",
"settings_driveDuplicateHint": "Când deplasezi documentele tale într-un dosar comun, o copie este păstrată în CryptDrive-ul tău pentru asigura păstrarea drepturilor tale de control asupra lui. Poți ascunde fișierele duplicate. Doar versiunea partajata va fi vizibilă, dacă nu este ștearsă, în acest ultim caz originalul va fi afișat în locația precedentă.",
"settings_driveDuplicateTitle": "Documentele tale duplicate",
"register_emailWarning3": "Dacă înțelegeți aceste aspecte și dorești totuși să utilizezi adresa ta de e-mail ca și nume de utilizator, apasă OK.",
"padNotPinnedVariable": "Acest document va expira dupa {4} zile de inactivitate, {0}autentifică-te{1} sau {2}creează-ți un cont{3} pentru a-l pastra.",
"settings_logoutEverywhereButton": "Deconectează-te",
"settings_deleted": "Contul tău de utilizator este șters. Apasă OK pentru a reveni la pagina principală.",
"settings_deleteConfirm": "Dacă apeși OK, contul tău va fi șters permanent. Ești sigur?",
"settings_deleteModal": "Furnizează aceste informații administratorului CryptPad-ului tău astfel încât acestea să fie șterse de pe server.",
"settings_deleteButton": "Șterge contul tău",
"settings_deleteHint": "Ștergerea contului este permanentă. Cryptdrive-ul tău și lista ta de documente vor fi șterse de pe server. Restul documentelor vor fi șterse în timp de 90 de zile dacă nimeni nu le stochează în CryptDrive-ul său.",
"settings_deleteTitle": "Ștergere cont",
"settings_userFeedbackTitle": "Feedback",
"settings_autostoreMaybe": "Manual (întreabă întotdeauna)",
"settings_autostoreNo": "Manual (nu mai întreba)",
"settings_autostoreYes": "Automat",
"settings_autostoreHint": "<b>Automat</b> Toate documentele accesate sunt stocate în CryptDrive-ul dumneavoastră.<br><b>Manual (întreabă întotdeauna)</b> Dacă nu ai stocat încă un document, vei fi întrebat dacă dorești să îl stochezi în Cryptdrive-ul tău.<br><b>Manual (nu mai întreba)</b> Documentele nu sunt stocate automat în Cryptpad-ul tău. Opțiunea de a le stoca ulterior va fi ascunsă.",
"settings_autostoreTitle": "Capacitatea de stocare a documentului în CryptDrive",
"register_emailWarning2": "Nu vei putea reseta parola utilizând adresa ta de e-mail.",
"register_emailWarning1": "Poți proceda astfel dacă doreşti, însă nici o notificare nu va fi transmisă server-ului nostru.",
"register_emailWarning0": "Se pare că ai adăugat adresa ta de e-mail în loc de numele tău de utilizator.",
"fc_expandAll": "Extinde",
"fc_openInCode": "Deschide in editorul de cod",
"fc_color": "Schimbă culoarea",
"fm_morePads": "Mai mult",
"uploadFolderButton": "Încarcă dosar",
"storageStatus": "Capacitate de stocare:<br /><b>{0}</b> utilizat din <b>{1}</b>",
"fc_collapseAll": "Restrânge"
}

@ -283,13 +283,13 @@ define([
var onDisconnect = function (noAlert) {
setEditable(false);
if (drive.refresh) { drive.refresh(); }
APP.toolbar.failed();
toolbar.failed();
if (!noAlert) { UIElements.disconnectAlert(); }
};
var onReconnect = function () {
setEditable(true);
if (drive.refresh) { drive.refresh(); }
APP.toolbar.reconnecting();
toolbar.reconnecting();
UIElements.reconnectAlert();
};

@ -3,6 +3,7 @@
@import (reference) "../../customize/src/less2/include/tools.less";
@import (reference) "../../customize/src/less2/include/markdown.less";
@import (reference) "../../customize/src/less2/include/avatar.less";
@import (reference) "../../customize/src/less2/include/buttons.less";
// body
&.cp-app-kanban {
@ -277,13 +278,19 @@
border: 1px solid fade(@cryptpad_text_col, 70%);
color: fade(@cryptpad_text_col, 70%);
border-radius: 0px;
font-size: 20px;
font-size: 25px;
display: inline-flex;
justify-content: center;
align-items: center;
line-height: 1;
cursor: pointer;
height: 40px;
&:first-child {
margin-right: 5px;
}
&:last-child {
margin-left: 5px;
}
&:hover {
background-color: rgba(0,0,0,0.1);
}
@ -303,28 +310,45 @@
position: relative;
min-height: 50px;
.cp-kanban-filterTags {
.buttons_main();
display: inline-flex;
align-items: baseline;
align-items: center;
flex: 1;
max-width: 80%;
//max-width: 80%;
min-width: 150px;
.cp-kanban-filterTags-reset {
cursor: pointer;
margin-left: 10px;
.cp-kanban-filterTags-toggle {
min-width: 100px;
display: flex;
flex-flow: column;
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();
i {
margin-right: 5px;
}
}
.cp-kanban-filterTags-name {
flex-shrink: 0;
}
.cp-kanban-filterTags-list {
margin-right: 10px;
margin-left: 10px;
display: flex;
flex-wrap: wrap;
&:not(:empty) {
margin-top: -5px;
}
em {
font-size: 14px;
color: lighten(@cryptpad_text_col, 10%);
@ -415,14 +439,28 @@
}
}
#kanban-trash {
height: 60px;
font-size: 40px;
display: flex;
height: 1px;
font-size: 0px;
/* 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;
justify-content: center;
position: relative;
width: 100%;
//pointer-events: none;
&.kanban-trash-active, &.kanban-trash-suggest {
display: flex;
height: 60px;
font-size: 40px;
}
i {
position: fixed;
}
@ -460,12 +498,14 @@
width: 300px;
margin: 10px 5px;
border: 1px solid @cryptpad_text_col;
color: @cryptpad_text_col;
height: 40px;
display: inline-flex;
justify-content: center;
align-items: center;
align-self: flex-start;
font-size: 40px;
font-size: 25px;
line-height: 100%;
cursor: pointer;
.tools_unselectable();
&:hover {

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

@ -107,7 +107,7 @@ define([
boardContainerOuter.appendChild(boardContainer);
var addBoard = document.createElement('div');
addBoard.id = 'kanban-addboard';
addBoard.innerText = '+';
addBoard.innerHTML = '<i class="fa fa-plus"></i>';
boardContainer.appendChild(addBoard);
var trash = self.trashContainer = document.createElement('div');
trash.setAttribute('id', 'kanban-trash');
@ -675,17 +675,15 @@ define([
var footerBoard = document.createElement('footer');
footerBoard.classList.add('kanban-board-footer');
//add button
Messages.kanban_addTopButton = '<i class="fa fa-plus"></i> (top)'; // XXX
Messages.kanban_addBottomButton = '<i class="fa fa-plus"></i> (bottom)'; // XXX
var addTopBoardItem = document.createElement('span');
addTopBoardItem.classList.add('kanban-title-button');
addTopBoardItem.setAttribute('data-top', "1");
addTopBoardItem.innerHTML = Messages.kanban_addTopButton;
addTopBoardItem.innerHTML = '<i class="cptools cptools-add-top">';
footerBoard.appendChild(addTopBoardItem);
__onAddItemClickHandler(addTopBoardItem);
var addBoardItem = document.createElement('span');
addBoardItem.classList.add('kanban-title-button');
addBoardItem.innerHTML = Messages.kanban_addBottomButton;
addBoardItem.innerHTML = '<i class="cptools cptools-add-bottom">';
footerBoard.appendChild(addBoardItem);
__onAddItemClickHandler(addBoardItem);

@ -376,6 +376,7 @@ define([
todo();
});
var $upButton = common.createButton('upload', false, data);
$upButton.removeProp('title');
$upButton.text(Messages.profile_upload);
$upButton.prepend($('<span>', {'class': 'fa fa-upload'}));
$block.append($upButton);

@ -177,7 +177,7 @@ define([
});
// If file, display the upload button
if (types.indexOf('file') !== -1 && common.isLoggedIn()) {
if (types.indexOf('file') !== -1) {
var f = (filters && filters.filter) || {};
delete data.accept;
if (Array.isArray(f.fileType)) {
@ -188,7 +188,13 @@ define([
return val;
});
}
$filter.append(common.createButton('upload', false, data));
}
var $uploadButton = common.createButton('upload', false, data);
$filter.append($uploadButton);
if (!common.isLoggedIn()) {
$uploadButton.prop('disabled', true)
.prop('title', Messages.upload_mustLogin);
}
var $container = $(h('span.cp-filepicker-content', [

@ -348,7 +348,7 @@ define([
}
if (back) {
backColor = back;
//$modal.css('background-color', back);
$modal.find('.cp-app-slide-frame').css('background-color', back);
$('#' + SLIDE_BACKCOLOR_ID).find('i').css('color', back);
slideOptions.bgColor = back;
}
@ -362,6 +362,10 @@ define([
framework.localChange();
};
var $check = $("#cp-app-slide-colorpicker");
var $backgroundPicker = $('<input>', { type: 'color', value: backColor })
.css({ display: 'none', })
.on('change', function() { updateLocalColors(undefined, this.value); });
var $back = framework._.sfCommon.createButton(null, true, {
icon: 'fa-square',
text: Messages.slide_backCol,
@ -369,7 +373,14 @@ define([
hiddenReadOnly: true,
name: 'background',
id: SLIDE_BACKCOLOR_ID
}, function () {
$backgroundPicker.val(backColor);
$backgroundPicker.click();
});
var $foregroundPicker = $('<input>', { type: 'color', value: textColor })
.css({ display: 'none', })
.on('change', function() { updateLocalColors(this.value, undefined); });
var $text = framework._.sfCommon.createButton(null, true, {
icon: 'fa-i-cursor',
text: Messages.slide_textCol,
@ -377,28 +388,15 @@ define([
hiddenReadOnly: true,
name: 'color',
id: SLIDE_COLOR_ID
}, function () {
$foregroundPicker.val(textColor);
$foregroundPicker.click();
});
var $testColor = $('<input>', { type: 'color', value: '!' });
var $check = $("#cp-app-slide-colorpicker");
if ($testColor.attr('type') !== "color" || $testColor.val() === '!') { return; }
var $backgroundPicker = $('<input>', { type: 'color', value: backColor })
.css({ display: 'none', })
.on('change', function() { updateLocalColors(undefined, this.value); });
$check.append($backgroundPicker);
$back.on('click', function() {
$backgroundPicker.val(backColor);
$backgroundPicker.click();
});
var $foregroundPicker = $('<input>', { type: 'color', value: textColor })
.css({ display: 'none', })
.on('change', function() { updateLocalColors(this.value, undefined); });
$check.append($foregroundPicker);
$text.on('click', function() {
$foregroundPicker.val(textColor);
$foregroundPicker.click();
});
framework._.toolbar.$theme.append($text).append($back);
@ -524,6 +522,8 @@ define([
if (newPad) {
colors.updateLocalColors('#000', '#FFF');
} else {
colors.updateLocalColors('#FFF', '#000');
}
CodeMirror.setMode('markdown', function () { });

@ -17,5 +17,22 @@
display: flex;
flex-flow: column;
.cp-support-form-attachments {
.fa {
cursor: pointer;
}
&> span {
padding: 10px;
}
}
.cp-support-language-list {
.cp-support-language {
margin-left: 5px;
background-color: rgba(0, 0, 0, 0.1);
padding: 0 5px;
}
}
}

@ -11,6 +11,7 @@ define([
'/common/hyperscript.js',
'/support/ui.js',
'/api/config',
'/customize/application_config.js',
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
@ -27,7 +28,8 @@ define([
Messages,
h,
Support,
ApiConfig
ApiConfig,
AppConfig
)
{
var APP = window.APP = {};
@ -41,6 +43,7 @@ define([
'cp-support-list',
],
'new': [
'cp-support-language',
'cp-support-form',
],
};
@ -132,6 +135,29 @@ define([
return $div;
};
create['language'] = function () {
if (!Array.isArray(AppConfig.supportLanguages)) { return $(h('div')); }
var languages = AppConfig.supportLanguages;
var list = h('span.cp-support-language-list', languages
.map(function (lang) {
return Messages._languages[lang];
})
.filter(Boolean)
.map(function (lang) {
return h('span.cp-support-language', lang);
})
);
var $div = $(
h('div.cp-support-language', [
Messages.support_languagesPreamble,
list,
])
);
return $div;
};
// Create a new tickets
create['form'] = function () {
var key = 'form';
@ -139,8 +165,6 @@ define([
var form = APP.support.makeForm();
$div.find('button').before(form);
var id = Util.uid();
$div.find('button').click(function () {
@ -156,6 +180,7 @@ define([
$('.cp-sidebarlayout-category[data-category="tickets"]').click();
}
});
$div.find('button').before(form);
return $div;
};

@ -6,8 +6,9 @@ define([
'/common/common-hash.js',
'/common/common-util.js',
'/common/clipboard.js',
'/common/common-ui-elements.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 common = ctx.common;
@ -61,9 +62,15 @@ define([
};
var sendForm = function (ctx, id, form, dest) {
var $title = $(form).find('.cp-support-form-title');
var $content = $(form).find('.cp-support-form-msg');
var $form = $(form);
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();
if (!title) {
return void UI.alert(Messages.support_formTitleError);
@ -72,18 +79,60 @@ define([
if (!content) {
return void UI.alert(Messages.support_formContentError);
}
$cat.val('');
$content.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')
});
});
send(ctx, id, 'TICKET', {
category: category,
title: title,
attachments: attachments,
message: content,
}, dest);
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;
if (typeof(cb) === "function") {
@ -93,8 +142,21 @@ define([
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 = [
h('hr'),
category,
catContainer,
h('input.cp-support-form-title' + (title ? '.cp-hidden' : ''), {
placeholder: Messages.support_formTitle,
type: 'text',
@ -104,11 +166,53 @@ define([
h('textarea.cp-support-form-msg', {
placeholder: Messages.support_formMessage
}),
h('label', Messages.support_attachments),
attachments = h('div.cp-support-attachments'),
addAttachment = h('button', Messages.support_addAttachment),
h('hr'),
button,
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);
};
ctx.FM.handleFile(file, ev);
});
});
$input.click();
});
var form = h('div.cp-support-form-container', content);
$(cancel).click(function () {
@ -125,6 +229,7 @@ define([
var privateData = metadataMgr.getPrivateData();
var ticketTitle = content.title + ' (#' + content.id + ')';
var ticketCategory;
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 hide = h('button.btn.btn-danger.cp-support-hide', Messages.support_remove);
@ -137,6 +242,7 @@ define([
var url;
if (ctx.isAdmin) {
ticketCategory = Messages['support_cat_'+(content.category || 'other')] + ' - ';
url = h('button.btn.btn-primary.fa.fa-clipboard');
$(url).click(function () {
var link = privateData.origin + privateData.pathname + '#' + 'support-' + content.id;
@ -146,9 +252,10 @@ define([
}
var $ticket = $(h('div.cp-support-list-ticket', {
'data-cat': content.category,
'data-id': content.id
}, [
h('h2', [ticketTitle, url]),
h('h2', [ticketCategory, ticketTitle, url]),
actions
]));
@ -173,13 +280,13 @@ define([
classes: 'btn-danger'
}, function() {
if (typeof(onHide) !== "function") { return; }
onHide(hide); // XXX
onHide(hide);
});
$(answer).click(function () {
$ticket.find('.cp-support-form-container').remove();
$(actions).hide();
var form = makeForm(function () {
var form = makeForm(ctx, function () {
var sent = sendForm(ctx, content.id, form, content.sender);
if (sent) {
$(actions).show();
@ -215,6 +322,21 @@ define([
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 premiumClass = (fromPremium && !fromAdmin? '.cp-support-frompremium': '');
var name = Util.fixHTML(content.sender.name) || Messages.anonymous;
@ -226,6 +348,7 @@ define([
h('span.cp-support-message-time', content.time ? new Date(content.time).toLocaleString() : '')
]),
h('pre.cp-support-message-content', content.message),
h('div.cp-support-attachments', attachments),
isAdmin ? userData : undefined,
]);
};
@ -257,10 +380,25 @@ define([
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) {
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) {
return makeTicket(ctx, $div, content, onHide);
};

@ -220,6 +220,7 @@
width: 100%;
padding: 12px;
margin-bottom: 20px;
white-space: pre;
}
.cp-teams-invite-password {
margin-bottom: 20px;

@ -987,6 +987,7 @@ define([
});
});
var $upButton = common.createButton('upload', false, data);
$upButton.removeProp('title');
$upButton.text(Messages.profile_upload);
$upButton.prepend($('<span>', {'class': 'fa fa-upload'}));

@ -104,6 +104,8 @@ define([
};
SFCommonO.start({
getSecrets: getSecrets,
hash: hash,
href: href,
noHash: true,
noRealtime: true,
//driveEvents: true,

Loading…
Cancel
Save