diff --git a/CHANGELOG.md b/CHANGELOG.md index 12df2ec6d..a4f63651f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,39 @@ +# GoldenFrog release (3.6.0) + +## Goals + +We're following up our last few releases of major core developments with an effort to improve reliability in some unstable areas and make some superficial tweaks to improve usability of some critical interfaces. + +## Update notes + +Update to 3.6.0 from 3.5.0 using the normal update procedure: + +1. stop your server +2. pull the latest code via git +3. run `bower update` +4. restart your server + +## Features + +* We've introduced a word-count feature in our rich text editor. +* The "share modal" which is accessible from both the "right-click menu" in the drive and the sharing button in the toolbar has been redesigned: + * different means of sharing access to documents have been split into different tabs to present users with less information to process + * each sharing method has an associated icon to make their actions easier to recognize at a glance + * various UI elements have been restyled to make their purpose and importance more obvious + * cancel buttons have a grey border to draw less attention + * OK buttons have a blue or grey background depending on whether they are active + * secondary buttons like "preview" have only a thin blue border so that they don't draw attention away from the primary button + * read-only text fields have a subtler appearance since they are shown primarily for the purpose of previewing your action + * text input fields (such as search) have a light background to suggest that you can use them +* We've made a minor adjustment to some of our styles for small screen to detect when a screen is very short in addition to when it is very narrow. As a result it should be somewhat easier to use on-screen keyboards. + +## Bug fixes + +* We found and fixed a subtle race condition which caused teams' quotas to be calculated incorrectly in certain circumstances. +* A minor bug in our login process caused users with premium accounts to incorrectly see an entry in their user menu as linking to our 'pricing' page instead of their 'subscription' management tools. This has since been fixed. +* We noticed that some of the rendered messages in the history mode of the notifications panel could fail to display text for some message types. These incorrect messages will be hidden from view wherever it is impossible to decide what should be displayed. We plan to address the issue in a deeper way in the near future. +* We've become aware of some odd behaviour in long-lived sessions where tabs seem to lose their connection to the sharedWorker which is common to all tabs open in a particular browser session. As far as we can tell the bug only affects Firefox browser. Unfortunately, debugging sharedWorkers in Firefox has been broken for a number of major versions, so we haven't been able to determine the cause of the issue. Until we're able to determine the underlying cause we've added extra checks to detect when particular features become isolated from the worker, where previously we assumed that if the worker was connected to the server then everything was behaving correctly. We recommend that you reload the tab if you notice that aspects of your shared folders or drives (for users or teams) display a read-only warning while your other tabs are behaving normally. + # FalklandWolf release (3.5.0) ## Goals diff --git a/bower.json b/bower.json index 86eb284b3..b67900338 100644 --- a/bower.json +++ b/bower.json @@ -18,7 +18,7 @@ "tests" ], "dependencies": { - "jquery": "~2.1.3", + "jquery": "2.2.4", "tweetnacl": "0.12.2", "components-font-awesome": "^4.6.3", "ckeditor": "4.7.3", @@ -51,6 +51,7 @@ "requirejs-plugins": "^1.0.3" }, "resolutions": { - "bootstrap": "^v4.0.0" + "bootstrap": "^v4.0.0", + "jquery": "2.2.4" } } diff --git a/customize.dist/src/less2/include/alertify.less b/customize.dist/src/less2/include/alertify.less index 9cd904c45..4bd5e298b 100644 --- a/customize.dist/src/less2/include/alertify.less +++ b/customize.dist/src/less2/include/alertify.less @@ -159,6 +159,9 @@ margin-bottom: @alertify_padding-base; margin: 0; overflow: auto; + :last-child { + margin-bottom: 0; + } } .alertify-tabs { max-height: 100%; @@ -222,14 +225,22 @@ background-color: @alertify-input-fg; color: @cryptpad_text_col; border: 1px solid @alertify-input-bg; - margin-bottom: 15px; + margin-bottom: @alertify_padding-base; width: 100%; font-size: 100%; padding: @alertify_padding-base; &[readonly] { background-color: @alertify-light-bg; color: @cryptpad_text_col; - border-color: @alertify-input-fg; + border-color: @alertify-light-bg; + } + } + + textarea { + overflow: hidden; + padding: 8px; + &[readonly] { + resize: none; } } @@ -509,5 +520,33 @@ overflow-x: auto; } } + // Bootstrap Alerts + .alert { + margin: 0px 0px @alertify_padding-base 0px; + font-size: 12px; + padding: 5px; + border-radius: 0px; + i { + margin-right: 10px; + } + &.alert-primary { + background-color: @alertify-base; + color: @alertify-fg; + border-color: @alertify-fg; + a { + color: @alertify-fg; + text-decoration: underline; + } + } + &.dismissable { + display: flex; + align-items: center; + span.fa-times { + font-size: @colortheme_app-font-size; + margin-left: 20px; + cursor: pointer; + } + } + } } diff --git a/customize.dist/src/less2/include/avatar.less b/customize.dist/src/less2/include/avatar.less index 87b4a3a32..c85c34877 100644 --- a/customize.dist/src/less2/include/avatar.less +++ b/customize.dist/src/less2/include/avatar.less @@ -1,9 +1,12 @@ @import (reference) "./tools.less"; +@import (reference) "./colortheme-all.less"; .avatar_vars( @width: 30px ) { @avatar-width: @width; @avatar-font-size: @width / 1.2; + @avatar-default-bg: #D9D8D8; + @avatar-default-fg: darken(@avatar-default-bg, 40%); } .avatar_main(@width: 30px) { --LessLoader_require: LessLoader_currentFile(); @@ -30,16 +33,16 @@ justify-content: center; align-items: center; - border-radius: 4px; overflow: hidden; box-sizing: content-box; } .cp-avatar-default { .tools_unselectable(); - background: white; - color: black; + background: @avatar-default-bg; + color: @avatar-default-fg; font-size: @avatar-font-size; font-size: var(--avatar-font-size); + text-transform: capitalize; } media-tag { min-height: @avatar-width; diff --git a/customize.dist/src/less2/include/modals-ui-elements.less b/customize.dist/src/less2/include/modals-ui-elements.less index 6e3921781..21707eab8 100644 --- a/customize.dist/src/less2/include/modals-ui-elements.less +++ b/customize.dist/src/less2/include/modals-ui-elements.less @@ -1,12 +1,16 @@ @import (reference) "./colortheme-all.less"; - +@import (reference) "./variables.less"; .modals-ui-elements_main() { --LessLoader_require: LessLoader_currentFile(); } & { + .cp-spacer { + height: @variables_padding; + } // Share modal .msg.cp-inline-radio-group { overflow: unset !important; + padding: 0px @variables_padding; .radio-group { display: flex; flex-direction: row; diff --git a/customize.dist/src/less2/include/password-input.less b/customize.dist/src/less2/include/password-input.less index 79824bac7..0f476390a 100644 --- a/customize.dist/src/less2/include/password-input.less +++ b/customize.dist/src/less2/include/password-input.less @@ -1,3 +1,4 @@ +@import (reference) "./colortheme-all.less"; .password_main() { --LessLoader_require: LessLoader_currentFile(); } @@ -17,7 +18,7 @@ justify-content: center; cursor: pointer; &:hover { - background-color: rgba(0,0,0,0.1); + color: darken(@colortheme_alertify-primary, 10%); } } } diff --git a/customize.dist/src/less2/include/usergrid.less b/customize.dist/src/less2/include/usergrid.less index 4ff807638..6ba8c2d07 100644 --- a/customize.dist/src/less2/include/usergrid.less +++ b/customize.dist/src/less2/include/usergrid.less @@ -6,16 +6,18 @@ --LessLoader_require: LessLoader_currentFile(); }; & { + .cp-usergrid-container { + margin-bottom: 12px !important; // even when last child of .msg .cp-usergrid-grid { display: flex; flex-wrap: wrap; + margin: -3px; margin-bottom: 6px; - } - &:not(.large) { - .cp-usergrid-grid { - margin: -3px; - margin-bottom: 6px; + max-height: 130px; + overflow-y: auto; + @media screen and (max-height: 515px) { + max-height: unset; // remove double scrollbar } } &.cp-usergrid-empty { @@ -28,17 +30,22 @@ input { flex: 1; min-width: 0; + margin: 0; margin-bottom: 0 !important; + height: 38px; &::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */ color: @cryptpad_color_grey; opacity: 1; /* Firefox */ } } - margin-bottom: 15px; + margin-bottom: 10px; &:empty { margin: 0; display: none; } + button:last-child { + margin-right: 0px !important; + } } .cp-usergrid-user { width: 70px; @@ -58,33 +65,48 @@ background-color: @colortheme_alertify-primary; color: @colortheme_alertify-primary-text; order: -1 !important; + .cp-usergrid-avatar { + media-tag, .cp-avatar-default { + opacity: 0.7; + } + } } .cp-usergrid-user-avatar { min-height: 40px; } + .cp-usergrid-user-name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; width: 100%; text-align: center; - line-height: 18px; + line-height: 20px; + flex: 1; } - border: 1px solid @colortheme_alertify-primary; &:not(.large) { .avatar_main(40px); } &.large { .avatar_main(25px); - width: 140px; + width: 145px; height: 35px; flex-flow: row; - margin: 0; - margin-right: 15px; - margin-bottom: 1px; - &:nth-child(3n) { - margin-right: 0; + margin: 3px; + flex-basis: calc(33.3333333% - 6px); + flex-shrink: 1; + min-width: 0; + .cp-usergrid-user-name { + margin-left: 5px; + text-align: left; + line-height: 150%; + color: @cryptpad_text_col; + } + } + &.cp-selected { + .cp-usergrid-user-name { + color: @colortheme_alertify-primary-text; } } } diff --git a/customize.dist/translations/messages.fi.js b/customize.dist/translations/messages.fi.js new file mode 100644 index 000000000..f01fa5160 --- /dev/null +++ b/customize.dist/translations/messages.fi.js @@ -0,0 +1,14 @@ +/* + * You can override the translation text using this file. + * The recommended method is to make a copy of this file (/customize.dist/translations/messages.{LANG}.js) + in a 'customize' directory (/customize/translations/messages.{LANG}.js). + * If you want to check all the existing translation keys, you can open the internal language file + but you should not change it directly (/common/translations/messages.{LANG}.js) +*/ +define(['/common/translations/messages.fi.js'], function (Messages) { + // Replace the existing keys in your copied file here: + // Messages.button_newpad = "New Rich Text Document"; + + return Messages; +}); + diff --git a/lib/batch-read.js b/lib/batch-read.js index 3e729e66d..2852a0579 100644 --- a/lib/batch-read.js +++ b/lib/batch-read.js @@ -8,6 +8,8 @@ If the result of IO or computation is requested while an identical request is already in progress, wait until the first one completes and provide its result to every routine that requested it. +Asynchrony is guaranteed. + ## Usage Provide: @@ -51,11 +53,12 @@ module.exports = function (/* task */) { var args = Array.prototype.slice.call(arguments); //if (map[id] && map[id].length > 1) { console.log("BATCH-READ DID ITS JOB for [%s][%s]", task, id); } - - map[id].forEach(function (h) { - h.apply(null, args); + setTimeout(function () { + map[id].forEach(function (h) { + h.apply(null, args); + }); + delete map[id]; }); - delete map[id]; }); }; }; diff --git a/lib/write-queue.js b/lib/write-queue.js index c1b64ebaf..c82b12b3b 100644 --- a/lib/write-queue.js +++ b/lib/write-queue.js @@ -4,7 +4,7 @@ q(id, function (next) { // whatever you need to do.... // when you're done - next(); + next(); // guaranteed to be asynchronous :D }); */ @@ -16,9 +16,11 @@ module.exports = function () { var map = {}; var next = function (id) { - if (map[id] && map[id].length === 0) { return void delete map[id]; } - var task = map[id].shift(); - task(fix1(next, id)); + setTimeout(function () { + if (map[id] && map[id].length === 0) { return void delete map[id]; } + var task = map[id].shift(); + task(fix1(next, id)); + }); }; return function (id, task) { diff --git a/package.json b/package.json index 4b62e5a37..a84f9ef6c 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,10 @@ "type": "git", "url": "git://github.com/xwiki-labs/cryptpad.git" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cryptpad" + }, "dependencies": { "chainpad-crypto": "^0.2.2", "chainpad-server": "^3.0.5", diff --git a/rpc.js b/rpc.js index c3d44e86c..8c7fe6c70 100644 --- a/rpc.js +++ b/rpc.js @@ -228,13 +228,15 @@ var truthyKeys = function (O) { }); }; -var getChannelList = function (Env, publicKey, cb) { +var getChannelList = function (Env, publicKey, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); loadUserPins(Env, publicKey, function (pins) { cb(truthyKeys(pins)); }); }; -var getFileSize = function (Env, channel, cb) { +var getFileSize = function (Env, channel, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); if (!isValidId(channel)) { return void cb('INVALID_CHAN'); } if (channel.length === 32) { if (typeof(Env.msgStore.getChannelSize) !== 'function') { @@ -416,20 +418,46 @@ var getDeletedPads = function (Env, channels, cb) { const batchTotalSize = BatchRead("GET_TOTAL_SIZE"); var getTotalSize = function (Env, publicKey, cb) { - batchTotalSize(publicKey, cb, function (done) { - var bytes = 0; - return void getChannelList(Env, publicKey, function (channels) { - if (!channels) { return done('INVALID_PIN_LIST'); } // unexpected + var unescapedKey = unescapeKeyCharacters(publicKey); + var limit = Env.limits[unescapedKey]; + + // Get a common key if multiple users share the same quota, otherwise take the public key + var batchKey = (limit && Array.isArray(limit.users)) ? limit.users.join('') : publicKey; - nThen(function (w) { - channels.forEach(function (channel) { // TODO semaphore? - getFileSize(Env, channel, w(function (e, size) { - if (!e) { bytes += size; } + batchTotalSize(batchKey, cb, function (done) { + var channels = []; + var bytes = 0; + nThen(function (waitFor) { + // Get the channels list for our user account + getChannelList(Env, publicKey, waitFor(function (_channels) { + if (!_channels) { + waitFor.abort(); + return done('INVALID_PIN_LIST'); + } + Array.prototype.push.apply(channels, _channels); + })); + // Get the channels list for users sharing our quota + if (limit && Array.isArray(limit.users) && limit.users.length > 1) { + limit.users.forEach(function (key) { + if (key === unescapedKey) { return; } // Don't count ourselves twice + getChannelList(Env, key, waitFor(function (_channels) { + if (!_channels) { return; } // Broken user, don't count their quota + Array.prototype.push.apply(channels, _channels); })); }); - }).nThen(function () { - done(void 0, bytes); + } + }).nThen(function (waitFor) { + // Get size of the channels + var list = []; // Contains the channels already counted in the quota to avoid duplicates + channels.forEach(function (channel) { // TODO semaphore? + if (list.indexOf(channel) !== -1) { return; } + list.push(channel); + getFileSize(Env, channel, waitFor(function (e, size) { + if (!e) { bytes += size; } + })); }); + }).nThen(function () { + done(void 0, bytes); }); }); }; diff --git a/www/auth/main.js b/www/auth/main.js index 7930e96e0..1b3667377 100644 --- a/www/auth/main.js +++ b/www/auth/main.js @@ -1,12 +1,17 @@ define([ 'jquery', - '/common/cryptpad-common.js', + '/common/cryptget.js', + '/common/pinpad.js', '/common/common-constants.js', '/common/outer/local-store.js', + '/common/outer/login-block.js', + '/common/outer/network-config.js', + '/customize/login.js', '/common/test.js', '/bower_components/nthen/index.js', + '/bower_components/netflux-websocket/netflux-client.js', '/bower_components/tweetnacl/nacl-fast.min.js' -], function ($, Cryptpad, Constants, LocalStore, Test, nThen) { +], function ($, Crypt, Pinpad, Constants, LocalStore, Block, NetConfig, Login, Test, nThen, Netflux) { var Nacl = window.nacl; var signMsg = function (msg, privKey) { @@ -27,9 +32,57 @@ define([ sessionStorage[Constants.userHashKey]; var proxy; + var rpc; + var network; + var rpcError; + + var loadProxy = function (hash) { + nThen(function (waitFor) { + var wsUrl = NetConfig.getWebsocketURL(); + var w = waitFor(); + Netflux.connect(wsUrl).then(function (_network) { + network = _network; + w(); + }, function (err) { + rpcError = err; + console.error(err); + }); + }).nThen(function (waitFor) { + Crypt.get(hash, waitFor(function (err, val) { + if (err) { + waitFor.abort(); + console.error(err); + return; + } + try { + var parsed = JSON.parse(val); + proxy = parsed; + } catch (e) { + console.log("Can't parse user drive", e); + } + }), { + network: network + }); + }).nThen(function (waitFor) { + if (!network) { return void waitFor.abort(); } + Pinpad.create(network, proxy, waitFor(function (e, call) { + if (e) { + rpcError = e; + return void waitFor.abort(); + } + rpc = call; + })); + }).nThen(function () { + Test(function () { + // This is only here to maybe trigger an error. + window.drive = proxy['drive']; + Test.passed(); + }); + }); + }; var whenReady = function (cb) { - if (proxy) { return void cb(); } + if (proxy && (rpc || rpcError)) { return void cb(); } console.log('CryptPad not ready...'); setTimeout(function () { whenReady(cb); @@ -45,6 +98,17 @@ define([ console.log('CP receiving', data); if (data.cmd === 'PING') { ret.res = 'PONG'; + } else if (data.cmd === 'LOGIN') { + Login.loginOrRegister(data.data.name, data.data.password, false, false, function (err) { + if (err) { + ret.error = 'LOGIN_ERROR'; + srcWindow.postMessage(JSON.stringify(ret), domain); + return; + } + loadProxy(LocalStore.getUserHash()); + srcWindow.postMessage(JSON.stringify(ret), domain); + }); + return; } else if (data.cmd === 'SIGN') { if (!AUTHORIZED_DOMAINS.filter(function (x) { return x.test(domain); }).length) { ret.error = "UNAUTH_DOMAIN"; @@ -63,7 +127,16 @@ define([ } } else if (data.cmd === 'UPDATE_LIMIT') { return void whenReady(function () { - Cryptpad.updatePinLimit(function (e, limit, plan, note) { + if (rpcError) { + // Tell the user on accounts that there was an issue and they need to wait maximum 24h or contact an admin + ret.warning = true; + srcWindow.postMessage(JSON.stringify(ret), domain); + return; + } + rpc.updatePinLimits(function (e, limit, plan, note) { + if (e) { + ret.warning = true; + } ret.res = [limit, plan, note]; srcWindow.postMessage(JSON.stringify(ret), domain); }); @@ -74,18 +147,8 @@ define([ srcWindow.postMessage(JSON.stringify(ret), domain); }); - nThen(function (waitFor) { - Cryptpad.ready(waitFor()); - }).nThen(function (waitFor) { - Cryptpad.getUserObject(null, waitFor(function (obj) { - proxy = obj; - })); - }).nThen(function () { - console.log('IFRAME READY'); - Test(function () { - // This is only here to maybe trigger an error. - window.drive = proxy['drive']; - Test.passed(); - }); - }); + var userHash = LocalStore.getUserHash(); + if (userHash) { + loadProxy(userHash); + } }); diff --git a/www/code/inner.js b/www/code/inner.js index 7c5e64c4f..e2ed086de 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -131,7 +131,7 @@ define([ if (['markdown', 'gfm'].indexOf(CodeMirror.highlightMode) === -1) { return; } if (!$previewButton.is('.cp-toolbar-button-active')) { return; } forceDrawPreview(); - }, 150); + }, 400); var previewTo; $previewButton.click(function () { diff --git a/www/code/mermaid.css b/www/code/mermaid.css index 769933f0a..60c366bb6 100644 --- a/www/code/mermaid.css +++ b/www/code/mermaid.css @@ -121,6 +121,10 @@ text.actor { font-size: 11px; text-height: 14px; } +.sectionTitle, .titleText { + font-weight: bold; +} + /* Grid and axis */ .grid .tick { stroke: lightgrey; diff --git a/www/code/orgmode.js b/www/code/orgmode.js index 3377ebb15..f36ffe9d6 100644 --- a/www/code/orgmode.js +++ b/www/code/orgmode.js @@ -6,22 +6,24 @@ define([ CodeMirror.defineSimpleMode("orgmode", { start: [ - {regex: /^(^\*{1,6}\s)(TODO|DOING|WAITING|NEXT){0,1}(CANCELLED|CANCEL|DEFERRED|DONE|REJECTED|STOP|STOPPED){0,1}(.*)$/, token: ["header org-level-star", "header org-todo", "header org-done", "header"]}, + {regex: /(\*\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, sol: true, token: ["header level1 org-level-star","header level1 org-todo","header level1 org-done", "header level1 org-priority", "header level1", "header level1 void", "header level1 comment"]}, + {regex: /(\*{1,}\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DEFERRED|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, sol: true, token: ["header org-level-star","header org-todo","header org-done", "header org-priority", "header", "header void", "header comment"]}, {regex: /(\+[^\+]+\+)/, token: ["strikethrough"]}, {regex: /(\*[^\*]+\*)/, token: ["strong"]}, {regex: /(\/[^\/]+\/)/, token: ["em"]}, {regex: /(\_[^\_]+\_)/, token: ["link"]}, {regex: /(\~[^\~]+\~)/, token: ["comment"]}, {regex: /(\=[^\=]+\=)/, token: ["comment"]}, - {regex: /\[\[[^\[\]]*\]\[[^\[\]]*\]\]/, token: "url"}, // links - {regex: /\[[xX\s]?\]/, token: 'qualifier'}, // checkbox - {regex: /\#\+BEGIN_[A-Z]*/, token: "comment", next: "env"}, // comments - {regex: /:?[A-Z_]+\:.*/, token: "comment"}, // property drawers - {regex: /(\#\+[A-Z_]*)(\:.*)/, token: ["keyword", 'qualifier']}, // environments - {regex: /(CLOCK\:|SHEDULED\:)(\s.+)/, token: ["comment", "keyword"]} + {regex: /\[\[[^\[\]]+\]\[[^\[\]]+\]\]/, token: "org-url"}, // links + {regex: /\[\[[^\[\]]+\]\]/, token: "org-image"}, // image + {regex: /\[[xX\s\-\_]\]/, token: 'qualifier org-toggle'}, // checkbox + {regex: /\#\+(?:(BEGIN|begin))_[a-zA-Z]*/, token: "comment", next: "env", sol: true}, // comments + {regex: /:?[A-Z_]+\:.*/, token: "comment", sol: true}, // property drawers + {regex: /(\#\+[a-zA-Z_]*)(\:.*)/, token: ["keyword", 'qualifier'], sol: true}, // environments + {regex: /(CLOCK\:|SHEDULED\:|DEADLINE\:)(\s.+)/, token: ["comment", "keyword"]} ], env: [ - {regex: /.*?\#\+END_[A-Z]*/, token: "comment", next: "start"}, + {regex: /\#\+(?:(END|end))_[a-zA-Z]*/, token: "comment", next: "start", sol: true}, {regex: /.*/, token: "comment"} ] }); diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 72438fec2..8dc14cc8c 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -127,6 +127,18 @@ define([ return input; }; + dialog.selectableArea = function (value, opt) { + var attrs = merge({ + readonly: 'readonly', + }, opt); + + var input = h('textarea', attrs); + $(input).val(value).click(function () { + input.select(); + }); + return input; + }; + dialog.okButton = function (content, classString) { var sel = typeof(classString) === 'string'? 'button.ok.' + classString:'button.ok.primary'; return h(sel, { tabindex: '2', }, content || Messages.okButton); @@ -187,7 +199,8 @@ define([ dialog.tabs = function (tabs) { var contents = []; var titles = []; - tabs.forEach(function (tab) { + var active = 0; + tabs.forEach(function (tab, i) { if (!tab.content || !tab.title) { return; } var content = h('div.alertify-tabs-content', tab.content); var title = h('span.alertify-tabs-title', tab.title); @@ -203,10 +216,11 @@ define([ }); titles.push(title); contents.push(content); + if (tab.active) { active = i; } }); if (contents.length) { - $(contents[0]).addClass('alertify-tabs-content-active'); - $(titles[0]).addClass('alertify-tabs-active'); + $(contents[active]).addClass('alertify-tabs-content-active'); + $(titles[active]).addClass('alertify-tabs-active'); } return h('div.alertify-tabs', [ h('div.alertify-tabs-titles', titles), diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index fd1b43bcd..2609286ee 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -147,6 +147,7 @@ define([ : Messages.owner_removeText; var removeCol = UIElements.getUserGrid(msg, { common: common, + large: true, data: _owners, noFilter: true }, function () { @@ -238,6 +239,7 @@ define([ }); var addCol = UIElements.getUserGrid(Messages.owner_addText, { common: common, + large: true, data: _friends }, function () { //console.log(arguments); @@ -254,6 +256,7 @@ define([ }); var teamsList = UIElements.getUserGrid(Messages.owner_addTeamText, { common: common, + large: true, noFilter: true, data: teamsData }, function () {}); @@ -739,12 +742,22 @@ define([ UIElements.getProperties = function (common, data, cb) { var c1; var c2; + var button = [{ + className: 'primary', + name: Messages.okButton, + onClick: function () {}, + keys: [13] + }]; NThen(function (waitFor) { getPadProperties(common, data, waitFor(function (e, c) { - c1 = c[0]; + c1 = UI.dialog.customModal(c[0], { + buttons: button + }); })); getRightsProperties(common, data, waitFor(function (e, c) { - c2 = c[0]; + c2 = UI.dialog.customModal(c[0], { + buttons: button + }); })); }).nThen(function () { var tabs = UI.dialog.tabs([{ @@ -784,8 +797,6 @@ define([ var noOthers = icons.length === 0 ? '.cp-usergrid-empty' : ''; - var buttonSelect = h('button', Messages.share_selectAll); - var buttonDeselect = h('button', Messages.share_deselectAll); var inputFilter = h('input', { placeholder: Messages.share_filterFriend }); @@ -793,9 +804,7 @@ define([ var div = h('div.cp-usergrid-container' + noOthers + (config.large?'.large':''), [ label ? h('label', label) : undefined, h('div.cp-usergrid-filter', (config.noFilter || config.noSelect) ? undefined : [ - inputFilter, - buttonSelect, - buttonDeselect + inputFilter ]), ]); var $div = $(div); @@ -808,23 +817,8 @@ define([ $div.find('.cp-usergrid-user:not(.cp-selected):not([data-name*="'+name+'"])').hide(); } }; - $(inputFilter).on('keydown keyup change', redraw); - $(buttonSelect).click(function () { - $div.find('.cp-usergrid-user:not(.cp-selected):visible').addClass('cp-selected'); - onSelect(); - }); - $(buttonDeselect).click(function () { - $div.find('.cp-usergrid-user.cp-selected').removeClass('cp-selected').each(function (i, el) { - var order = $(el).attr('data-order'); - if (!order) { return; } - $(el).attr('style', 'order:'+order); - }); - redraw(); - onSelect(); - }); - $(div).append(h('div.cp-usergrid-grid', icons)); if (!config.noSelect) { $div.on('click', '.cp-usergrid-user', function () { @@ -885,7 +879,8 @@ define([ var friendsList = UIElements.getUserGrid(null, { common: common, data: friends, - noFilter: false + noFilter: false, + large: true }, refreshButtons); var friendDiv = friendsList.div; $div.append(friendDiv); @@ -911,6 +906,7 @@ define([ var teamsList = UIElements.getUserGrid(Messages.share_linkTeam, { common: common, noFilter: true, + large: true, data: teams }, refreshButtons); $div.append(teamsList.div); @@ -1016,12 +1012,29 @@ define([ if (!hashes || (!hashes.editHash && !hashes.viewHash)) { return; } + // check if the pad is password protected + var hash = hashes.editHash || hashes.viewHash; + var href = origin + pathname + '#' + hash; + var parsedHref = Hash.parsePadUrl(href); + var hasPassword = parsedHref.hashData.password; + + var makeFaqLink = function () { + var link = h('span', [ + h('i.fa.fa-question-circle'), + h('a', {href: '#'}, Messages.passwordFaqLink) + ]); + $(link).click(function () { + common.openURL(config.origin + "/faq.html#security-pad_password"); + }); + return link; + }; + + var parsed = Hash.parsePadUrl(pathname); var canPresent = ['code', 'slide'].indexOf(parsed.type) !== -1; var rights = h('div.msg.cp-inline-radio-group', [ h('label', Messages.share_linkAccess), - h('br'), h('div.radio-group',[ UI.createRadio('accessRights', 'cp-share-editable-false', Messages.share_linkView, true, { mark: {tabindex:1} }), @@ -1068,9 +1081,42 @@ define([ h('br'), ] : [ UI.createCheckbox('cp-share-embed', Messages.share_linkEmbed, false, { mark: {tabindex:1} }), - h('br'), ]; - linkContent.push(UI.dialog.selectable('', { id: 'cp-share-link-preview', tabindex: 1 })); + linkContent.push(h('div.cp-spacer')); + linkContent.push(UI.dialog.selectableArea('', { id: 'cp-share-link-preview', tabindex: 1, rows:3})); + + // Show alert if the pad is password protected + if (hasPassword) { + linkContent.push(h('div.alert.alert-primary', [ + h('i.fa.fa-lock'), + Messages.share_linkPasswordAlert, h('br'), + makeFaqLink() + ])); + } + + // warning about sharing links + var localStore = window.cryptpadStore; + var dismissButton = h('span.fa.fa-times'); + var shareLinkWarning = h('div.alert.alert-warning.dismissable', + { style: 'display: none;' }, + [ + h('span.cp-inline-alert-text', Messages.share_linkWarning), + dismissButton + ]); + linkContent.push(shareLinkWarning); + + localStore.get('hide-alert-shareLinkWarning', function (val) { + if (val === '1') { return; } + $(shareLinkWarning).show(); + + $(dismissButton).on('click', function () { + localStore.put('hide-alert-shareLinkWarning', '1'); + $(shareLinkWarning).remove(); + }); + + }); + + var link = h('div.cp-share-modal', linkContent); var $link = $(link); @@ -1137,7 +1183,19 @@ define([ // XXX Don't display access rights if no contacts var contactsContent = h('div.cp-share-modal'); - $(contactsContent).append(friendsList); + var $contactsContent = $(contactsContent); + + $contactsContent.append(friendsList); + + // Show alert if the pad is password protected + if (hasPassword) { + $contactsContent.append(h('div.alert.alert-primary', [ + h('i.fa.fa-unlock'), + Messages.share_contactPasswordAlert, h('br'), + makeFaqLink() + ])); + } + var contactButtons = [makeCancelButton(), friendsObject.button]; @@ -1156,9 +1214,18 @@ define([ }; var embedContent = [ h('p', Messages.viewEmbedTag), - h('br'), - UI.dialog.selectable(getEmbedValue(), { id: 'cp-embed-link-preview', tabindex: 1 }) + UI.dialog.selectableArea(getEmbedValue(), { id: 'cp-embed-link-preview', tabindex: 1, rows: 3}) ]; + + // Show alert if the pad is password protected + if (hasPassword) { + embedContent.push(h('div.alert.alert-primary', [ + h('i.fa.fa-lock'), ' ', + Messages.share_embedPasswordAlert, h('br'), + makeFaqLink() + ])); + } + var embedButtons = [ makeCancelButton(), { className: 'primary', @@ -1187,13 +1254,15 @@ define([ // Create modal var tabs = [{ - title: Messages.share_linkCategory, - icon: "fa fa-link", - content: frameLink - }, { title: Messages.share_contactCategory, icon: "fa fa-address-book", - content: frameContacts + content: frameContacts, + active: hasFriends + }, { + title: Messages.share_linkCategory, + icon: "fa fa-link", + content: frameLink, + active: !hasFriends }, { title: Messages.share_embedCategory, icon: "fa fa-code", @@ -1261,6 +1330,21 @@ define([ if (!hashes.fileHash) { throw new Error("You must provide a file hash"); } var url = origin + pathname + '#' + hashes.fileHash; + // check if the file is password protected + var parsedHref = Hash.parsePadUrl(url); + var hasPassword = parsedHref.hashData.password; + + var makeFaqLink = function () { + var link = h('span', [ + h('i.fa.fa-question-circle'), + h('a', {href: '#'}, Messages.passwordFaqLink) + ]); + $(link).click(function () { + common.openURL(config.origin + "/faq.html#security-pad_password"); + }); + return link; + }; + var getLinkValue = function () { return url; }; var makeCancelButton = function() { @@ -1272,9 +1356,40 @@ define([ // Share link tab var linkContent = [ - UI.dialog.selectable(getLinkValue(), { id: 'cp-share-link-preview', tabindex: 1 }) + UI.dialog.selectableArea(getLinkValue(), { id: 'cp-share-link-preview', tabindex: 1, rows:2 }) ]; + // Show alert if the pad is password protected + if (hasPassword) { + linkContent.push(h('div.alert.alert-primary', [ + h('i.fa.fa-lock'), + Messages.share_linkPasswordAlert, h('br'), + makeFaqLink() + ])); + } + + // warning about sharing links + var localStore = window.cryptpadStore; + var dismissButton = h('span.fa.fa-times'); + var shareLinkWarning = h('div.alert.alert-warning.dismissable', + { style: 'display: none;' }, + [ + h('span.cp-inline-alert-text', Messages.share_linkWarning), + dismissButton + ]); + linkContent.push(shareLinkWarning); + + localStore.get('hide-alert-shareLinkWarning', function (val) { + if (val === '1') { return; } + $(shareLinkWarning).show(); + + $(dismissButton).on('click', function () { + localStore.put('hide-alert-shareLinkWarning', '1'); + $(shareLinkWarning).remove(); + }); + + }); + var link = h('div.cp-share-modal', linkContent); var linkButtons = [ @@ -1307,7 +1422,17 @@ define([ var friendsList = friendsObject.content; var contactsContent = h('div.cp-share-modal'); - $(contactsContent).append(friendsList); + var $contactsContent = $(contactsContent); + $contactsContent.append(friendsList); + + // Show alert if the pad is password protected + if (hasPassword) { + $contactsContent.append(h('div.alert.alert-primary', [ + h('i.fa.fa-unlock'), + Messages.share_contactPasswordAlert, h('br'), + makeFaqLink() + ])); + } var contactButtons = [makeCancelButton(), friendsObject.button]; @@ -1321,12 +1446,20 @@ define([ // Embed tab var embed = h('div.cp-share-modal', [ h('p', Messages.fileEmbedScript), - h('br'), UI.dialog.selectable(common.getMediatagScript()), h('p', Messages.fileEmbedTag), - h('br'), UI.dialog.selectable(common.getMediatagFromHref(fileData)), ]); + + // Show alert if the pad is password protected + if (hasPassword) { + embed.append(h('div.alert.alert-primary', [ + h('i.fa.fa-lock'), ' ', + Messages.share_embedPasswordAlert, h('br'), + makeFaqLink() + ])); + } + var embedButtons = [{ className: 'cancel', name: Messages.cancel, @@ -1349,13 +1482,15 @@ define([ // Create modal var tabs = [{ - title: Messages.share_linkCategory, - icon: "fa fa-link", - content: frameLink - }, { title: Messages.share_contactCategory, icon: "fa fa-address-book", - content: frameContacts + content: frameContacts, + active: hasFriends, + }, { + title: Messages.share_linkCategory, + icon: "fa fa-link", + content: frameLink, + active: !hasFriends }, { title: Messages.share_embedCategory, icon: "fa fa-code", @@ -1759,7 +1894,7 @@ define([ if (e) { return void console.error(e); } UIElements.getProperties(common, data, function (e, $prop) { if (e) { return void console.error(e); } - UI.alert($prop[0], undefined, true); + UI.openCustomModal($prop[0]); }); }); }); diff --git a/www/common/diffMarked.js b/www/common/diffMarked.js index 07044f5e2..0f92897bc 100644 --- a/www/common/diffMarked.js +++ b/www/common/diffMarked.js @@ -84,7 +84,7 @@ define([ var defaultCode = renderer.code; renderer.code = function (code, language) { - if (language === 'mermaid' && (code.match(/^sequenceDiagram/) || code.match(/^graph/))) { + if (language === 'mermaid' && code.match(/^(graph|pie|gantt|sequenceDiagram|classDiagram|gitGraph)/)) { return '
'+code+''; } else { return defaultCode.apply(renderer, arguments); @@ -197,7 +197,6 @@ define([ 'APPLET', 'VIDEO', // privacy implications of videos are the same as images 'AUDIO', // same with audio - 'SVG' ]; var unsafeTag = function (info) { /*if (info.node && $(info.node).parents('media-tag').length) { @@ -307,8 +306,39 @@ define([ var Dom = domFromHTML($('