diff --git a/CHANGELOG.md b/CHANGELOG.md index dfa69006a..a47f5e760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/customize.dist/application_config.js b/customize.dist/application_config.js index a7ad90f11..4a8820682 100644 --- a/customize.dist/application_config.js +++ b/customize.dist/application_config.js @@ -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; }); diff --git a/customize.dist/fonts/cptools/fonts/cptools.svg b/customize.dist/fonts/cptools/fonts/cptools.svg index 93eef8d38..3540c5d06 100644 --- a/customize.dist/fonts/cptools/fonts/cptools.svg +++ b/customize.dist/fonts/cptools/fonts/cptools.svg @@ -26,4 +26,6 @@ + + \ No newline at end of file diff --git a/customize.dist/fonts/cptools/fonts/cptools.ttf b/customize.dist/fonts/cptools/fonts/cptools.ttf index 18338a9ee..2788cf010 100644 Binary files a/customize.dist/fonts/cptools/fonts/cptools.ttf and b/customize.dist/fonts/cptools/fonts/cptools.ttf differ diff --git a/customize.dist/fonts/cptools/fonts/cptools.woff b/customize.dist/fonts/cptools/fonts/cptools.woff index d8f56ba86..64b641b9d 100644 Binary files a/customize.dist/fonts/cptools/fonts/cptools.woff and b/customize.dist/fonts/cptools/fonts/cptools.woff differ diff --git a/customize.dist/fonts/cptools/style.css b/customize.dist/fonts/cptools/style.css index 349b62f2b..85ed350f5 100644 --- a/customize.dist/fonts/cptools/style.css +++ b/customize.dist/fonts/cptools/style.css @@ -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"; } diff --git a/customize.dist/pages.js b/customize.dist/pages.js index 6ba39e49a..0e42b9a3a 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -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)) ); }; diff --git a/customize.dist/src/less2/include/leftside-menu.less b/customize.dist/src/less2/include/leftside-menu.less index 4ffd2c4b8..1cf84e41b 100644 --- a/customize.dist/src/less2/include/leftside-menu.less +++ b/customize.dist/src/less2/include/leftside-menu.less @@ -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; diff --git a/customize.dist/src/less2/include/support.less b/customize.dist/src/less2/include/support.less index 890ba810d..d83746b51 100644 --- a/customize.dist/src/less2/include/support.less +++ b/customize.dist/src/less2/include/support.less @@ -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; diff --git a/lib/hk-util.js b/lib/hk-util.js index ccf8c4465..ed5f43297 100644 --- a/lib/hk-util.js +++ b/lib/hk-util.js @@ -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++; } diff --git a/package-lock.json b/package-lock.json index 9840bd099..ed2097eff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cryptpad", - "version": "3.18.1", + "version": "3.19.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 56e512b22..be6f77978 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/tests/test-lkh.js b/scripts/tests/test-lkh.js new file mode 100644 index 000000000..bf6ba12bb --- /dev/null +++ b/scripts/tests/test-lkh.js @@ -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, + }); +}); + diff --git a/www/admin/inner.js b/www/admin/inner.js index 2ff62cf1f..90048e76d 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -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(); diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 79951c3c5..4eba3755c 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -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, diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index acab5085b..abf98aa17 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -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) { - common.getPadAttribute('tags', waitFor(function (err, res) { - if (err) { - if (err === 'NO_ENTRY') { - UI.alert(Messages.tags_noentry); + 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); } - waitFor.abort(); - return void console.error(err); - } - tags = res || []; - }), href); + 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); - $innerblock.scrollTop($val.position().top + $innerblock.scrollTop()); + 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 = $('

').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() }); } - options.push({ - tag: 'a', - attributes: { - 'target': '_blank', - 'href': origin+'/index.html', - 'class': 'fa fa-home' - }, - content: h('span', Messages.homePage) - }); + + 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 !== '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,6 +2481,50 @@ define([ }, }); } + if (AppConfig.surveyURL) { + options.push({ + tag: 'a', + attributes: { + 'target': '_blank', + 'rel': 'noopener', + 'href': AppConfig.surveyURL, + 'class': 'cp-toolbar-survey fa fa-graduation-cap' + }, + content: h('span', Messages.survey), + action: function () { + Feedback.send('SURVEY_CLICKED'); + }, + }); + } + options.push({ + tag: 'a', + attributes: { + 'class': 'cp-toolbar-about fa fa-info', + }, + content: h('span', Messages.user_about), + action: function () { + UIElements.displayInfoMenu(Common, metadataMgr); + }, + }); + + 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({ @@ -2426,35 +2549,6 @@ define([ 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' - }, - content: h('span', Messages.survey), - action: function () { - Feedback.send('SURVEY_CLICKED'); - }, - }); - } - if (Pages.versionString) { - Messages.user_about = Messages.about; // XXX "About CryptPad" - 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); - }, - }); - } options.push({ tag: 'hr' }); // Add login or logout button depending on the current status diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index a92e383d1..93c36597a 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -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 diff --git a/www/common/inner/access.js b/www/common/inner/access.js index 55a4dbbe9..190af4279 100644 --- a/www/common/inner/access.js +++ b/www/common/inner/access.js @@ -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, {}); diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 3c1f340af..9009d2557 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -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'); } }); diff --git a/www/common/migrate-user-object.js b/www/common/migrate-user-object.js index 18bc9c61b..231c39ebd 100644 --- a/www/common/migrate-user-object.js +++ b/www/common/migrate-user-object.js @@ -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)); }); }; }); diff --git a/www/common/notifications.js b/www/common/notifications.js index 348b0cc44..57ac38f8b 100644 --- a/www/common/notifications.js +++ b/www/common/notifications.js @@ -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" diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index 19adc46b2..7534b552b 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -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); }); - var last = ooChannel.queue.pop(); - if (last) { ooChannel.lastHash = last.hash; } + 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', { diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 885bc1131..c87edc17d 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -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(); diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js index a55a2d63d..4f0543885 100644 --- a/www/common/outer/mailbox-handlers.js +++ b/www/common/outer/mailbox-handlers.js @@ -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 = {}; diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 4c240a2ec..ef983503c 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -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}); }); diff --git a/www/common/translations/messages.ar.json b/www/common/translations/messages.ar.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/www/common/translations/messages.ar.json @@ -0,0 +1 @@ +{} diff --git a/www/common/translations/messages.de.json b/www/common/translations/messages.de.json index 8b0638aac..0ed8f0f91 100644 --- a/www/common/translations/messages.de.json +++ b/www/common/translations/messages.de.json @@ -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 FAQ liest", + "more": "Erfahre mehr über die Nutzung von CryptPad, indem du unsere FAQ liest.", "share": "Teile dieses Dokument mit der Schaltfläche Teilen und verwalte die Zugriffsrechte mit Zugriff.", "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ü Teilen und nicht die Adressleiste des Browsers.", + "info_imprintFlavour": "Rechtliche Informationen über die Administratoren dieses Servers.", + "info_privacyFlavour": "Unsere Datenschutzerklärung beschreibt, wie wir deine Daten verarbeiten.", + "user_about": "Über CryptPad" } diff --git a/www/common/translations/messages.fr.json b/www/common/translations/messages.fr.json index e1c72098c..f38f3f5ef 100644 --- a/www/common/translations/messages.fr.json +++ b/www/common/translations/messages.fr.json @@ -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 FAQ", + "more": "Apprenez-en davantage sur le fonctionnement de CryptPad en lisant notre FAQ.", "share": "Partagez ce document avec le bouton Partager et gérez les droits d'accès avec le bouton Accès.", "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 {0} dans votre Drive." + "todo_move": "Votre liste de tâches est désormais dans le kanban {0} dans votre Drive.", + "settings_safeLinkDefault": "Les liens sécurisés sont désormais activés par défaut. Veuillez utiliser le menu Partager 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": "Description de la confidentialité de vos données.", + "user_about": "À propos de CryptPad", + "info_imprintFlavour": "Informations légales sur les administateurs de cette instance." } diff --git a/www/common/translations/messages.json b/www/common/translations/messages.json index 637b05bef..5cad912da 100644 --- a/www/common/translations/messages.json +++ b/www/common/translations/messages.json @@ -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 FAQ", + "more": "Learn more about how CryptPad can work for you by reading our FAQ.", "share": "Share this document with the Share button, and manage access rights with Access.", "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 Share menu to copy links rather than your browser's address bar.", + "info_imprintFlavour": "Legal information about the administrators of this instance.", + "user_about": "About CryptPad", + "info_privacyFlavour": "Our privacy policy describes how we treat your data." } diff --git a/www/common/translations/messages.ro.json b/www/common/translations/messages.ro.json index 5b744e4a7..190f3250a 100644 --- a/www/common/translations/messages.ro.json +++ b/www/common/translations/messages.ro.json @@ -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.
Înscrie-te sau Log in 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": "Automat Toate documentele accesate sunt stocate în CryptDrive-ul dumneavoastră.
Manual (întreabă întotdeauna) Dacă nu ai stocat încă un document, vei fi întrebat dacă dorești să îl stochezi în Cryptdrive-ul tău.
Manual (nu mai întreba) 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:
{0} utilizat din {1}", + "fc_collapseAll": "Restrânge" } diff --git a/www/common/translations/messages.tr.json b/www/common/translations/messages.tr.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/www/common/translations/messages.tr.json @@ -0,0 +1 @@ +{} diff --git a/www/drive/inner.js b/www/drive/inner.js index d5380b8d0..5d60e9a4c 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -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(); }; diff --git a/www/kanban/app-kanban.less b/www/kanban/app-kanban.less index e83ef3a29..f0af6eeca 100644 --- a/www/kanban/app-kanban.less +++ b/www/kanban/app-kanban.less @@ -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 { diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 84b7ea762..10db6261b 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -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(); }); diff --git a/www/kanban/jkanban_cp.js b/www/kanban/jkanban_cp.js index 7967fa5e4..54fb71f46 100644 --- a/www/kanban/jkanban_cp.js +++ b/www/kanban/jkanban_cp.js @@ -107,7 +107,7 @@ define([ boardContainerOuter.appendChild(boardContainer); var addBoard = document.createElement('div'); addBoard.id = 'kanban-addboard'; - addBoard.innerText = '+'; + addBoard.innerHTML = ''; 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 = ' (top)'; // XXX - Messages.kanban_addBottomButton = ' (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 = ''; footerBoard.appendChild(addTopBoardItem); __onAddItemClickHandler(addTopBoardItem); var addBoardItem = document.createElement('span'); addBoardItem.classList.add('kanban-title-button'); - addBoardItem.innerHTML = Messages.kanban_addBottomButton; + addBoardItem.innerHTML = ''; footerBoard.appendChild(addBoardItem); __onAddItemClickHandler(addBoardItem); diff --git a/www/profile/inner.js b/www/profile/inner.js index 535da563e..12ab5e607 100644 --- a/www/profile/inner.js +++ b/www/profile/inner.js @@ -376,6 +376,7 @@ define([ todo(); }); var $upButton = common.createButton('upload', false, data); + $upButton.removeProp('title'); $upButton.text(Messages.profile_upload); $upButton.prepend($('', {'class': 'fa fa-upload'})); $block.append($upButton); diff --git a/www/secureiframe/inner.js b/www/secureiframe/inner.js index a42cd5e7a..1aa94d9cd 100644 --- a/www/secureiframe/inner.js +++ b/www/secureiframe/inner.js @@ -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', [ diff --git a/www/slide/inner.js b/www/slide/inner.js index b051724e2..71f25f4e7 100644 --- a/www/slide/inner.js +++ b/www/slide/inner.js @@ -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 = $('', { 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 = $('', { 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 = $('', { type: 'color', value: '!' }); - var $check = $("#cp-app-slide-colorpicker"); if ($testColor.attr('type') !== "color" || $testColor.val() === '!') { return; } - var $backgroundPicker = $('', { 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 = $('', { 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 () { }); diff --git a/www/support/app-support.less b/www/support/app-support.less index c033332eb..41af016fd 100644 --- a/www/support/app-support.less +++ b/www/support/app-support.less @@ -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; + } + } } diff --git a/www/support/inner.js b/www/support/inner.js index 740109bd3..ed1c16955 100644 --- a/www/support/inner.js +++ b/www/support/inner.js @@ -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; }; diff --git a/www/support/ui.js b/www/support/ui.js index ac61bfe7f..8451cf55c 100644 --- a/www/support/ui.js +++ b/www/support/ui.js @@ -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 = $('', { + '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); }; diff --git a/www/teams/app-team.less b/www/teams/app-team.less index eb49ec21f..c59d0dcb9 100644 --- a/www/teams/app-team.less +++ b/www/teams/app-team.less @@ -220,6 +220,7 @@ width: 100%; padding: 12px; margin-bottom: 20px; + white-space: pre; } .cp-teams-invite-password { margin-bottom: 20px; diff --git a/www/teams/inner.js b/www/teams/inner.js index 1c376962b..86b8acf47 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -987,6 +987,7 @@ define([ }); }); var $upButton = common.createButton('upload', false, data); + $upButton.removeProp('title'); $upButton.text(Messages.profile_upload); $upButton.prepend($('', {'class': 'fa fa-upload'})); diff --git a/www/teams/main.js b/www/teams/main.js index 6c780a8fd..65292b1f6 100644 --- a/www/teams/main.js +++ b/www/teams/main.js @@ -104,6 +104,8 @@ define([ }; SFCommonO.start({ getSecrets: getSecrets, + hash: hash, + href: href, noHash: true, noRealtime: true, //driveEvents: true,