diff --git a/.gitignore b/.gitignore index 50796e9bb..5e4e0e31b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ datastore tasks www/bower_components/* +www/accounts node_modules /config.js customization diff --git a/.jshintignore b/.jshintignore index 70cec6a0e..b9ed0f0ce 100644 --- a/.jshintignore +++ b/.jshintignore @@ -15,6 +15,7 @@ www/common/onlyoffice/v2* server.js www/common/old-media-tag.js www/scratch +www/accounts www/lib www/accounts diff --git a/CHANGELOG.md b/CHANGELOG.md index 890f8328d..1e30778d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +# WoollyMammoth (3.22.0) + +## Goals + +We've been working on some long-term projects that we hope to deliver over the course of the next few releases. In the meantime, this release includes a number of minor improvements. + +## Update notes + +To upgrade from 3.21.0 to 3.22.0: + +1. Stop your server +2. Get the latest platform code with git +3. Install client-side dependencies with `bower update` +4. Restart the CryptPad API server + +## Features + +* Contributors have helped by translating more of CryptPad into Finnish and traditional Chinese via [our weblate instance](https://weblate.cryptpad.fr/projects/cryptpad/app/) +* We've updated the syntax highlighting code that we use throughout the platform to include Rustlang (and possibly other languages that have been updated in the meantime). +* You can now use _ctrl-f_ in user or team drives to jump immediately to the search interface instead of possibly scrolling up to click on its entry in the sidebar. + +## Bug fixes + +* Some of the special behaviour implemented for Org-mode in our code editor sometimes failed when the document was first changed into Org-mode. +* We now clear some minor personal preferences like whether certain tooltips had been dismissed when you log out. +* We identified and addressed a number of issues with teams that caused valid teams to not be displayed and team member rights to fail to upgrade until a full session reload. +* We now display the number of days before an unregistered user's documents are considered inactive in their drive instead of hardcoding "3 months". + # VietnameseRhinoceros (3.21.0) ## Goals diff --git a/customize.dist/ckeditor-contents.css b/customize.dist/ckeditor-contents.css index 4d2abae08..000162c00 100644 --- a/customize.dist/ckeditor-contents.css +++ b/customize.dist/ckeditor-contents.css @@ -205,3 +205,11 @@ a > img { .cp-link-clicked a { cursor: pointer; } + +media-tag { + display: inline-block; +} +media-tag * { + width: 100%; + height: 100%; +} diff --git a/customize.dist/messages.js b/customize.dist/messages.js index d42d8fa46..11d814540 100755 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -5,7 +5,7 @@ var map = { 'de': 'Deutsch', 'el': 'Ελληνικά', 'es': 'Español', - 'fi': 'Suomalainen', + 'fi': 'Suomi', 'fr': 'Français', //'hi': 'हिन्दी', 'it': 'Italiano', diff --git a/customize.dist/pages.js b/customize.dist/pages.js index bd5627139..ab86aa7a8 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -62,7 +62,7 @@ define([ var imprintUrl = AppConfig.imprint && (typeof(AppConfig.imprint) === "boolean" ? '/imprint.html' : AppConfig.imprint); - Pages.versionString = "CryptPad v3.21.0 (VietnameseRhinoceros)"; + Pages.versionString = "CryptPad v3.22.0 (WoollyMammoth)"; // used for the about menu Pages.imprintLink = AppConfig.imprint ? footLink(imprintUrl, 'imprint') : undefined; diff --git a/customize.dist/src/less2/include/alertify.less b/customize.dist/src/less2/include/alertify.less index db951613c..c3767f073 100644 --- a/customize.dist/src/less2/include/alertify.less +++ b/customize.dist/src/less2/include/alertify.less @@ -153,12 +153,10 @@ max-width: 500px; margin: 0 auto; text-align: left; - padding: @alertify_padding-base; background: #fff; box-shadow: @alertify_box-shadow; &.wide { - width: 1000px; - max-width: 70%; + max-width: 850px; } } @@ -178,13 +176,14 @@ max-height: 100%; display: flex; flex-flow: column; + padding: @alertify_padding-base; .alertify-tabs-titles { height: 40px; display: flex; border-bottom: 1px solid @alertify-fore; margin-bottom: 10px; box-sizing: content-box; - span { + .alertify-tabs-title { .tools_unselectable(); font-size: 20px; height: 40px; @@ -201,6 +200,13 @@ color: #949494; cursor: not-allowed; } + &:not(.alertify-tabs-active) { + @media (max-width: @browser_media-medium-screen) { + .tab-title-text { + display: none; + } + } + } } span.alertify-tabs-active { background-color: @alertify-fore !important; @@ -393,9 +399,15 @@ .cp-share-columns { display: flex; flex-flow: row; + @media screen and (max-width: (@browser_media-medium-screen)) { + flex-flow: column; + } & > .cp-share-column { width: 50%; + @media screen and (max-width: (@browser_media-medium-screen)) { + width: unset; + } //padding: 0 10px; position: relative; &.contains-nav { @@ -414,19 +426,32 @@ } &:first-child { margin-right: @alertify_padding-base; + @media screen and (max-width: (@browser_media-medium-screen)) { + margin: 0 0 @alertify_padding-base 0; + } } &:last-child { margin-left: @alertify_padding-base; + @media screen and (max-width: (@browser_media-medium-screen)) { + margin: 0px; + } } } & > .cp-share-column-mid { display: flex; align-items: center; + justify-content: center; + @media screen and (max-width: (@browser_media-medium-screen)) { + margin-bottom: @alertify_padding-base; + } button { width: 50px; margin: 0; min-width: 0; font-size: 18px !important; + @media screen and (max-width: (@browser_media-medium-screen)) { + width: 100%; + } } } } diff --git a/customize.dist/src/less2/include/corner.less b/customize.dist/src/less2/include/corner.less index feec62165..04d503cc9 100644 --- a/customize.dist/src/less2/include/corner.less +++ b/customize.dist/src/less2/include/corner.less @@ -89,6 +89,7 @@ } &.cp-corner-big { width: 500px; + max-width: 95%; } .cp-corner-dontshow { diff --git a/customize.dist/src/less2/include/modal.less b/customize.dist/src/less2/include/modal.less index 8ed10a1b2..08c0121fd 100644 --- a/customize.dist/src/less2/include/modal.less +++ b/customize.dist/src/less2/include/modal.less @@ -41,6 +41,7 @@ box-shadow: @variables_shadow; padding: @variables_padding; + padding-top: @variables_padding * 2; position: relative; //top: 15vh; bottom: 15vh; @@ -57,6 +58,11 @@ margin-bottom: 1em; } + & > p:not(.cp-modal-form) { + text-align: left; // XXX needs testing + margin-right: 30px; + } + .cp-modal-form { display: flex; flex-wrap: wrap; diff --git a/customize.dist/src/less2/include/modals-ui-elements.less b/customize.dist/src/less2/include/modals-ui-elements.less index e8282f6e8..3855d15a8 100644 --- a/customize.dist/src/less2/include/modals-ui-elements.less +++ b/customize.dist/src/less2/include/modals-ui-elements.less @@ -1,5 +1,7 @@ @import (reference) "./colortheme-all.less"; @import (reference) "./variables.less"; +@import (reference) "./browser.less"; + .modals-ui-elements_main() { --LessLoader_require: LessLoader_currentFile(); } @@ -20,6 +22,15 @@ .cp-radio { margin-right: 30px; } + @media (max-width: @browser_media-medium-screen) { + flex-direction: column; + &:not(:last-child) { + margin-bottom: 0px; + } + .cp-radio { + margin: 0 0 5px 0; + } + } } } @@ -74,6 +85,24 @@ } // Access modal + button.cp-access-add { + i { + margin-right: 0px !important; + } + i.fa-arrow-left { + display: inline; + @media (max-width: @browser_media-medium-screen) { + display: none; + } + } + i.fa-arrow-up { + display: none; + @media (max-width: @browser_media-medium-screen) { + display: inline; + } + } + } + .cp-overlay-container { position: relative; .cp-overlay { diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js index c60cdcfbc..427d9c03c 100644 --- a/lib/commands/admin-rpc.js +++ b/lib/commands/admin-rpc.js @@ -146,6 +146,33 @@ var flushCache = function (Env, Server, cb) { cb(void 0, true); }; +// CryptPad_AsyncStore.rpc.send('ADMIN', ['ARCHIVE_DOCUMENT', documentID], console.log) +var archiveDocument = function (Env, Server, cb, data) { + var id = Array.isArray(data) && data[1]; + if (typeof(id) !== 'string' || id.length < 32) { return void cb("EINVAL"); } + + switch (id.length) { + case 32: + // TODO disconnect users from active sessions + return void Env.msgStore.archiveChannel(id, cb); + case 48: + return void Env.blobStore.archive.blob(id, cb); + default: + return void cb("INVALID_ID_LENGTH"); + } + + // archival for blob proofs isn't automated, but evict-inactive.js will + // clean up orpaned blob proofs + // Env.blobStore.archive.proof(userSafeKey, blobId, cb) +}; + +var restoreArchivedDocument = function (Env, Server, cb) { + // Env.msgStore.restoreArchivedChannel(channelName, cb) + // Env.blobStore.restore.blob(blobId, cb) + // Env.blobStore.restore.proof(userSafekey, blobId, cb) + + cb("NOT_IMPLEMENTED"); +}; // CryptPad_AsyncStore.rpc.send('ADMIN', ['SET_DEFAULT_STORAGE_LIMIT', 1024 * 1024 * 1024 /* 1GB */], console.log) var setDefaultStorageLimit = function (Env, Server, cb, data) { @@ -174,6 +201,9 @@ var commands = { GET_FILE_DESCRIPTOR_LIMIT: getFileDescriptorLimit, SET_DEFAULT_STORAGE_LIMIT: setDefaultStorageLimit, GET_CACHE_STATS: getCacheStats, + + ARCHIVE_DOCUMENT: archiveDocument, + RESTORE_ARCHIVED_DOCUMENT: restoreArchivedDocument, }; Admin.command = function (Env, safeKey, data, _cb, Server) { diff --git a/package-lock.json b/package-lock.json index ce127b9f9..b76dad661 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cryptpad", - "version": "3.21.0", + "version": "3.22.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0f79bc8af..aef342f36 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cryptpad", "description": "realtime collaborative visual editor with zero knowlege server", - "version": "3.21.0", + "version": "3.22.0", "license": "AGPL-3.0+", "repository": { "type": "git", diff --git a/www/code/inner.js b/www/code/inner.js index 05356a972..692bba8bb 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -134,6 +134,16 @@ define([ framework._.toolbar.$drawer.append(helpMenu.button); }; + + var previews = {}; + previews['gfm'] = function (val, $div, common) { + DiffMd.apply(DiffMd.render(val), $div, common); + }; + previews['markdown'] = previews['gfm']; + previews['htmlmixed'] = function (val, $div, common) { + DiffMd.apply(val, $div, common); + }; + var mkPreviewPane = function (editor, CodeMirror, framework, isPresentMode) { var $previewContainer = $('#cp-app-code-preview'); var $preview = $('#cp-app-code-preview-content'); @@ -149,17 +159,19 @@ define([ var $previewButton = framework._.sfCommon.createButton('preview', true); var forceDrawPreview = function () { + var f = previews[CodeMirror.highlightMode]; + if (!f) { return; } try { if (editor.getValue() === '') { $previewContainer.addClass('cp-app-code-preview-isempty'); return; } $previewContainer.removeClass('cp-app-code-preview-isempty'); - DiffMd.apply(DiffMd.render(editor.getValue()), $preview, framework._.sfCommon); + f(editor.getValue(), $preview, framework._.sfCommon); } catch (e) { console.error(e); } }; var drawPreview = Util.throttle(function () { - if (['markdown', 'gfm'].indexOf(CodeMirror.highlightMode) === -1) { return; } + if (!previews[CodeMirror.highlightMode]) { return; } if (!$previewButton.is('.cp-toolbar-button-active')) { return; } forceDrawPreview(); }, 400); @@ -171,7 +183,7 @@ define([ previewTo = setTimeout(function () { $codeMirror.removeClass('transition'); }, 500); - if (['markdown', 'gfm'].indexOf(CodeMirror.highlightMode) === -1) { + if (!previews[CodeMirror.highlightMode]) { $previewContainer.show(); } $previewContainer.toggle(); @@ -213,7 +225,7 @@ define([ }); var modeChange = function (mode) { - if (['markdown', 'gfm'].indexOf(mode) !== -1) { + if (previews[mode]) { $previewButton.show(); framework._.sfCommon.getPadAttribute('previewMode', function (e, data) { if (e) { return void console.error(e); } @@ -273,7 +285,7 @@ define([ // keep trying to draw until you're confident it has been drawn previewInt = setInterval(function () { // give up if it's not a valid preview mode - if (['markdown', 'gfm'].indexOf(CodeMirror.highlightMode) === -1) { return void clear(); } + if (!previews[CodeMirror.highlightMode]) { return void clear(); } // give up if content has been drawn if ($preview.text()) { return void clear(); } // only draw if there is actually content to display diff --git a/www/code/orgmode.js b/www/code/orgmode.js index c2af1bd5e..942d141f2 100644 --- a/www/code/orgmode.js +++ b/www/code/orgmode.js @@ -123,7 +123,10 @@ define([ }; }); + var init = false; CodeMirror.registerHelper("orgmode", "init", function (editor) { + if (init) { return; } + editor.setOption("extraKeys", { "Tab": function(cm) { org_cycle(cm); }, "Shift-Tab": function(cm){ org_shifttab(cm); }, @@ -139,6 +142,7 @@ define([ "Shift-Right": function(cm){ org_shiftright(cm); } }); + init = true; editor.on('mousedown', toggleHandler); editor.on('touchstart', toggleHandler); editor.on('gutterClick', foldLine); @@ -155,6 +159,9 @@ define([ }); CodeMirror.registerHelper("orgmode", "destroy", function (editor) { + if (!init) { return; } + + init = false; editor.off('mousedown', toggleHandler); editor.off('touchstart', toggleHandler); editor.off('gutterClick', foldLine); diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 37ef044ff..647036356 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -221,9 +221,9 @@ define([ tabs.forEach(function (tab, i) { if (!(tab.content || tab.disabled) || !tab.title) { return; } var content = h('div.alertify-tabs-content', tab.content); - var title = h('span.alertify-tabs-title'+ (tab.disabled ? '.disabled' : ''), tab.title); + var title = h('span.alertify-tabs-title'+ (tab.disabled ? '.disabled' : ''), h('span.tab-title-text',{id: 'cp-tab-' + tab.title.toLowerCase(), 'aria-hidden':"true"}, tab.title)); if (tab.icon) { - var icon = h('i', {class: tab.icon}); + var icon = h('i', {class: tab.icon, 'aria-labelledby': 'cp-tab-' + tab.title.toLowerCase()}); $(title).prepend(' ').prepend(icon); } $(title).click(function () { diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 39558e760..e0a12b4e8 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -229,181 +229,7 @@ define([ }; }; - - var createShareWithFriends = function (config, onShare, linkGetter) { - var common = config.common; - var sframeChan = common.getSframeChannel(); - var title = config.title; - var friends = config.friends || {}; - var teams = config.teams || {}; - var myName = common.getMetadataMgr().getUserData().name; - var order = []; - - var smallCurves = Object.keys(friends).map(function (c) { - return friends[c].curvePublic.slice(0,8); - }); - - var div = h('div.contains-nav'); - var $div = $(div); - // Replace "copy link" by "share with friends" if at least one friend is selected - // Also create the "share with friends" button if it doesn't exist - var refreshButtons = function () { - var $nav = $div.closest('.alertify').find('nav'); - - var friendMode = $div.find('.cp-usergrid-user.cp-selected').length; - if (friendMode) { - $nav.find('button.cp-share-with-friends').prop('disabled', ''); - } else { - $nav.find('button.cp-share-with-friends').prop('disabled', 'disabled'); - } - }; - - config.noInclude = true; - Object.keys(friends).forEach(function (curve) { - var data = friends[curve]; - if (curve.length > 40 && data.notifications) { return; } - delete friends[curve]; - }); - - var others = []; - if (Object.keys(friends).length) { - var friendsList = UIElements.getUserGrid(Messages.share_linkFriends, { - common: common, - data: friends, - noFilter: false, - large: true - }, refreshButtons); - var friendDiv = friendsList.div; - $div.append(friendDiv); - others = friendsList.icons; - } - - if (Object.keys(teams).length) { - var teamsList = UIElements.getUserGrid(Messages.share_linkTeam, { - common: common, - noFilter: true, - large: true, - data: teams - }, refreshButtons); - $div.append(teamsList.div); - } - - var shareButton = { - className: 'primary cp-share-with-friends', - name: Messages.share_withFriends, - onClick: function () { - var href; - NThen(function (waitFor) { - var w = waitFor(); - // linkGetter can be async if this is a burn after reading URL - var res = linkGetter({}, function (url) { - if (!url) { - waitFor.abort(); - return; - } - href = url; - setTimeout(w); - }); - if (res && /^http/.test(res)) { - href = Hash.getRelativeHref(res); - setTimeout(w); - return; - } - }).nThen(function () { - var $friends = $div.find('.cp-usergrid-user.cp-selected'); - $friends.each(function (i, el) { - var curve = $(el).attr('data-curve'); - var ed = $(el).attr('data-ed'); - var friend = curve && friends[curve]; - var team = teams[ed]; - // If the selected element is a friend or a team without edit right, - // send a notification - var mailbox = friend || ((team && team.viewer) ? team : undefined); - if (mailbox) { // Friend - if (friends[curve] && !mailbox.notifications) { return; } - if (mailbox.notifications && mailbox.curvePublic) { - common.mailbox.sendTo("SHARE_PAD", { - href: href, - password: config.password, - isTemplate: config.isTemplate, - name: myName, - title: title - }, { - viewed: team && team.id, - channel: mailbox.notifications, - curvePublic: mailbox.curvePublic - }); - return; - } - } - // If it's a team with edit right, add the pad directly - if (!team) { return; } - sframeChan.query('Q_STORE_IN_TEAM', { - href: href, - password: config.password, - path: config.isTemplate ? ['template'] : undefined, - title: title, - teamId: team.id - }, function (err) { - if (err) { return void console.error(err); } - }); - }); - - UI.findCancelButton().click(); - - // Update the "recently shared with" array: - // Get the selected curves - var curves = $friends.toArray().map(function (el) { - return ($(el).attr('data-curve') || '').slice(0,8); - }).filter(function (x) { return x; }); - // Prepend them to the "order" array - Array.prototype.unshift.apply(order, curves); - order = Util.deduplicateString(order); - // Make sure we don't have "old" friends and save - order = order.filter(function (curve) { - return smallCurves.indexOf(curve) !== -1; - }); - common.setAttribute(['general', 'share-friends'], order); - if (onShare) { - onShare.fire(); - } - }); - }, - keys: [13] - }; - - common.getAttribute(['general', 'share-friends'], function (err, val) { - order = val || []; - // Sort friends by "recently shared with" - others.sort(function (a, b) { - var ca = ($(a).attr('data-curve') || '').slice(0,8); - var cb = ($(b).attr('data-curve') || '').slice(0,8); - if (!ca && !cb) { return 0; } - if (!ca) { return 1; } - if (!cb) { return -1; } - var ia = order.indexOf(ca); - var ib = order.indexOf(cb); - if (ia === -1 && ib === -1) { return 0; } - if (ia === -1) { return 1; } - if (ib === -1) { return -1; } - return ia - ib; - }); - // Reorder the friend icons - others.forEach(function (el, i) { - $(el).attr('data-order', i).css('order', i); - }); - // Display them - $(friendDiv).find('.cp-usergrid-grid').detach(); - $(friendDiv).append(h('div.cp-usergrid-grid', others)); - refreshButtons(); - }); - return { - content: div, - buttons: [shareButton] - }; - }; - - var noContactsMessage = function(common){ + UIElements.noContactsMessage = function (common) { var metadataMgr = common.getMetadataMgr(); var data = metadataMgr.getUserData(); var origin = metadataMgr.getPrivateData().origin; @@ -446,655 +272,6 @@ define([ } }; - var getEditableTeams = function (common, config) { - var privateData = common.getMetadataMgr().getPrivateData(); - var teamsData = Util.tryParse(JSON.stringify(privateData.teams)) || {}; - var teams = {}; - Object.keys(teamsData).forEach(function (id) { - // config.teamId only exists when we're trying to share a pad from a team drive - // In this case, we don't want to share the pad with the current team - if (config.teamId && config.teamId === id) { return; } - var t = teamsData[id]; - teams[t.edPublic] = { - viewer: !teamsData[id].hasSecondaryKey, - notifications: t.notifications, - curvePublic: t.curvePublic, - displayName: t.name, - edPublic: t.edPublic, - avatar: t.avatar, - id: id - }; - }); - return teams; - }; - var makeBurnAfterReadingUrl = function (common, href, channel, cb) { - var keyPair = Hash.generateSignPair(); - var parsed = Hash.parsePadUrl(href); - var newHref = parsed.getUrl({ - ownerKey: keyPair.safeSignKey - }); - var sframeChan = common.getSframeChannel(); - var rtChannel; - NThen(function (waitFor) { - if (parsed.type !== "sheet") { return; } - common.getPadAttribute('rtChannel', waitFor(function (err, chan) { - rtChannel = chan; - })); - }).nThen(function (waitFor) { - sframeChan.query('Q_SET_PAD_METADATA', { - channel: channel, - command: 'ADD_OWNERS', - value: [keyPair.validateKey] - }, waitFor(function (err) { - if (err) { - waitFor.abort(); - UI.warn(Messages.error); - } - })); - if (rtChannel) { - sframeChan.query('Q_SET_PAD_METADATA', { - channel: rtChannel, - command: 'ADD_OWNERS', - value: [keyPair.validateKey] - }, waitFor(function (err) { - if (err) { - console.error(err); - } - })); - } - }).nThen(function () { - cb(newHref); - }); - }; - UIElements.createShareModal = function (config) { - var origin = config.origin; - var pathname = config.pathname; - var hashes = config.hashes; - var common = config.common; - - 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 canBAR = parsed.type !== 'drive'; - - var burnAfterReading = (hashes.viewHash && canBAR) ? - UI.createRadio('accessRights', 'cp-share-bar', Messages.burnAfterReading_linkBurnAfterReading, false, { - mark: {tabindex:1}, - label: {style: "display: none;"} - }) : undefined; - var rights = h('div.msg.cp-inline-radio-group', [ - h('label', Messages.share_linkAccess), - h('div.radio-group',[ - UI.createRadio('accessRights', 'cp-share-editable-false', - Messages.share_linkView, true, { mark: {tabindex:1} }), - canPresent ? UI.createRadio('accessRights', 'cp-share-present', - Messages.share_linkPresent, false, { mark: {tabindex:1} }) : undefined, - UI.createRadio('accessRights', 'cp-share-editable-true', - Messages.share_linkEdit, false, { mark: {tabindex:1} })]), - burnAfterReading - ]); - - // Burn after reading - // Check if we are an owner of this pad. If we are, we can show the burn after reading option. - // When BAR is selected, display a red message indicating the consequence and add - // the options to generate the BAR url - var barAlert = h('div.alert.alert-danger.cp-alertify-bar-selected', { - style: 'display: none;' - }, Messages.burnAfterReading_warningLink); - var channel = Hash.getSecrets('pad', hash, config.password).channel; - common.getPadMetadata({ - channel: channel - }, function (obj) { - if (!obj || obj.error) { return; } - var priv = common.getMetadataMgr().getPrivateData(); - // Not an owner: don't display the burn after reading option - if (!Array.isArray(obj.owners) || obj.owners.indexOf(priv.edPublic) === -1) { - $(burnAfterReading).remove(); - return; - } - // When the burn after reading option is selected, transform the modal buttons - $(burnAfterReading).css({ - display: 'flex' - }); - }); - - var $rights = $(rights); - - var saveValue = function () { - var edit = Util.isChecked($rights.find('#cp-share-editable-true')); - var present = Util.isChecked($rights.find('#cp-share-present')); - common.setAttribute(['general', 'share'], { - edit: edit, - present: present - }); - }; - - var burnAfterReadingUrl; - - var getLinkValue = function (initValue, cb) { - var val = initValue || {}; - var edit = val.edit !== undefined ? val.edit : Util.isChecked($rights.find('#cp-share-editable-true')); - var embed = val.embed; - var present = val.present !== undefined ? val.present : Util.isChecked($rights.find('#cp-share-present')); - var burnAfterReading = Util.isChecked($rights.find('#cp-share-bar')); - if (burnAfterReading && !burnAfterReadingUrl) { - if (cb) { // Called from the contacts tab, "share" button - var barHref = origin + pathname + '#' + (hashes.viewHash || hashes.editHash); - return makeBurnAfterReadingUrl(common, barHref, channel, function (url) { - cb(url); - }); - } - return Messages.burnAfterReading_generateLink; - } - var hash = (!hashes.viewHash || (edit && hashes.editHash)) ? hashes.editHash : hashes.viewHash; - var href = burnAfterReading ? burnAfterReadingUrl : (origin + pathname + '#' + hash); - var parsed = Hash.parsePadUrl(href); - return origin + parsed.getUrl({embed: embed, present: present}); - }; - - var makeCancelButton = function() { - return { - className: 'cancel', - name: Messages.cancel, - onClick: function () {}, - keys: [27] - }; - }; - - // Share link tab - var linkContent = config.sharedFolder ? [ - h('label', Messages.sharedFolders_share), - h('br'), - ] : [ - UI.createCheckbox('cp-share-embed', Messages.share_linkEmbed, false, { mark: {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(); - }); - - }); - - linkContent.push($(barAlert).clone()[0]); // Burn after reading - - var link = h('div.cp-share-modal', linkContent); - var $link = $(link); - - var linkButtons = [ - makeCancelButton(), - !config.sharedFolder && { - className: 'secondary cp-nobar', - name: Messages.share_linkOpen, - onClick: function () { - saveValue(); - var v = getLinkValue({ - embed: Util.isChecked($link.find('#cp-share-embed')) - }); - window.open(v); - return true; - }, - keys: [[13, 'ctrl']] - }, { - className: 'primary cp-nobar', - name: Messages.share_linkCopy, - onClick: function () { - saveValue(); - var v = getLinkValue({ - embed: Util.isChecked($link.find('#cp-share-embed')) - }); - var success = Clipboard.copy(v); - if (success) { UI.log(Messages.shareSuccess); } - }, - keys: [13] - }, { - className: 'primary cp-bar', - name: 'GENERATE LINK', - onClick: function () { - var barHref = origin + pathname + '#' + (hashes.viewHash || hashes.editHash); - makeBurnAfterReadingUrl(common, barHref, channel, function (url) { - burnAfterReadingUrl = url; - $rights.find('input[type="radio"]').trigger('change'); - }); - return true; - }, - keys: [] - } - ]; - - var frameLink = UI.dialog.customModal(link, { - buttons: linkButtons, - onClose: config.onClose, - }); - $(frameLink).find('.cp-bar').hide(); - - // Share with contacts tab - - var teams = getEditableTeams(common, config); - config.teams = teams; - var hasFriends = Object.keys(config.friends || {}).length || - Object.keys(teams).length; - var onFriendShare = Util.mkEvent(); - - - var friendsObject = hasFriends ? createShareWithFriends(config, onFriendShare, getLinkValue) : noContactsMessage(common); - var friendsList = friendsObject.content; - - onFriendShare.reg(saveValue); - - var contactsContent = h('div.cp-share-modal'); - 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() - ])); - } - - $(contactsContent).append($(barAlert).clone()); // Burn after reading - - var contactButtons = friendsObject.buttons; - contactButtons.unshift(makeCancelButton()); - - var onShowContacts = function () { - if (!hasFriends) { - $rights.hide(); - } - }; - - var frameContacts = UI.dialog.customModal(contactsContent, { - buttons: contactButtons, - onClose: config.onClose, - }); - - // Embed tab - var getEmbedValue = function () { - var url = getLinkValue({ - embed: true - }); - return ''; - }; - var embedContent = [ - h('p', Messages.viewEmbedTag), - 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', - name: Messages.share_linkCopy, - onClick: function () { - var v = getEmbedValue(); - var success = Clipboard.copy(v); - if (success) { UI.log(Messages.shareSuccess); } - }, - keys: [13] - }]; - - var onShowEmbed = function () { - $rights.find('#cp-share-bar').closest('label').hide(); - $rights.find('input[type="radio"]:enabled').first().prop('checked', 'checked'); - $rights.find('input[type="radio"]').trigger('change'); - }; - - var embed = h('div.cp-share-modal', embedContent); - var $embed = $(embed); - - var frameEmbed = UI.dialog.customModal(embed, { - buttons: embedButtons, - onClose: config.onClose, - }); - - // update values for link and embed preview when radio btns change - $embed.find('#cp-embed-link-preview').val(getEmbedValue()); - $link.find('#cp-share-link-preview').val(getLinkValue()); - $rights.find('input[type="radio"]').on('change', function () { - $link.find('#cp-share-link-preview').val(getLinkValue({ - embed: Util.isChecked($link.find('#cp-share-embed')) - })); - // Hide or show the burn after reading alert - if (Util.isChecked($rights.find('#cp-share-bar')) && !burnAfterReadingUrl) { - $('.cp-alertify-bar-selected').show(); - // Show burn after reading button - $('.alertify').find('.cp-bar').show(); - $('.alertify').find('.cp-nobar').hide(); - return; - } - $embed.find('#cp-embed-link-preview').val(getEmbedValue()); - // Hide burn after reading button - $('.alertify').find('.cp-nobar').show(); - $('.alertify').find('.cp-bar').hide(); - $('.cp-alertify-bar-selected').hide(); - }); - $link.find('input[type="checkbox"]').on('change', function () { - $link.find('#cp-share-link-preview').val(getLinkValue({ - embed: Util.isChecked($link.find('#cp-share-embed')) - })); - }); - - - // Create modal - var resetTab = function () { - $rights.show(); - $rights.find('label.cp-radio').show(); - }; - var tabs = [{ - title: Messages.share_contactCategory, - icon: "fa fa-address-book", - content: frameContacts, - active: hasFriends, - onShow: onShowContacts, - onHide: resetTab - }, { - title: Messages.share_linkCategory, - icon: "fa fa-link", - content: frameLink, - active: !hasFriends - }, { - title: Messages.share_embedCategory, - icon: "fa fa-code", - content: frameEmbed, - onShow: onShowEmbed, - onHide: resetTab - }]; - if (typeof(AppConfig.customizeShareOptions) === 'function') { - AppConfig.customizeShareOptions(hashes, tabs, { - type: 'DEFAULT', - origin: origin, - pathname: pathname - }); - } - - var modal = UI.dialog.tabs(tabs); - $(modal).find('.alertify-tabs-titles').after(rights); - - // disable edit share options if you don't have edit rights - if (!hashes.editHash) { - $rights.find('#cp-share-editable-false').attr('checked', true); - $rights.find('#cp-share-editable-true').removeAttr('checked').attr('disabled', true); - } else if (!hashes.viewHash) { - $rights.find('#cp-share-editable-false').removeAttr('checked').attr('disabled', true); - $rights.find('#cp-share-present').removeAttr('checked').attr('disabled', true); - $rights.find('#cp-share-editable-true').attr('checked', true); - } - - common.getAttribute(['general', 'share'], function (err, val) { - val = val || {}; - if (val.present && canPresent) { - $rights.find('#cp-share-editable-false').prop('checked', false); - $rights.find('#cp-share-editable-true').prop('checked', false); - $rights.find('#cp-share-present').prop('checked', true); - } else if ((val.edit === false && hashes.viewHash) || !hashes.editHash) { - $rights.find('#cp-share-editable-false').prop('checked', true); - $rights.find('#cp-share-editable-true').prop('checked', false); - $rights.find('#cp-share-present').prop('checked', false); - } else { - $rights.find('#cp-share-editable-true').prop('checked', true); - $rights.find('#cp-share-editable-false').prop('checked', false); - $rights.find('#cp-share-present').prop('checked', false); - } - delete val.embed; - if (!canPresent) { - delete val.present; - } - $link.find('#cp-share-link-preview').val(getLinkValue(val)); - }); - common.getMetadataMgr().onChange(function () { - // "hashes" is only available is the secure "share" app - var _hashes = common.getMetadataMgr().getPrivateData().hashes; - if (!_hashes) { return; } - hashes = _hashes; - $link.find('#cp-share-link-preview').val(getLinkValue()); - }); - return modal; - }; - - UIElements.createFileShareModal = function (config) { - var origin = config.origin; - var pathname = config.pathname; - var hashes = config.hashes; - var common = config.common; - var fileData = config.fileData; - - 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() { - return {className: 'cancel', - name: Messages.cancel, - onClick: function () {}, - keys: [27]}; - }; - - // Share link tab - var linkContent = [ - 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 = [ - makeCancelButton(), - { - className: 'primary', - name: Messages.share_linkCopy, - onClick: function () { - var v = getLinkValue(); - var success = Clipboard.copy(v); - if (success) { UI.log(Messages.shareSuccess); - } - }, - keys: [13] - } - ]; - - var frameLink = UI.dialog.customModal(link, { - buttons: linkButtons, - onClose: config.onClose, - }); - - // share with contacts tab - var teams = getEditableTeams(common, config); - config.teams = teams; - var hasFriends = Object.keys(config.friends || {}).length || - Object.keys(teams).length; - - var friendsObject = hasFriends ? createShareWithFriends(config, null, getLinkValue) : noContactsMessage(common); - var friendsList = friendsObject.content; - - var contactsContent = h('div.cp-share-modal'); - 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 = friendsObject.buttons; - contactButtons.unshift(makeCancelButton()); - - var frameContacts = UI.dialog.customModal(contactsContent, { - buttons: contactButtons, - onClose: config.onClose, - }); - - - // Embed tab - var embed = h('div.cp-share-modal', [ - h('p', Messages.fileEmbedScript), - UI.dialog.selectable(common.getMediatagScript()), - h('p', Messages.fileEmbedTag), - 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, - onClick: function () {}, - keys: [27] - }, { - className: 'primary', - name: Messages.share_mediatagCopy, - onClick: function () { - var v = common.getMediatagFromHref(fileData); - var success = Clipboard.copy(v); - if (success) { UI.log(Messages.shareSuccess); } - }, - keys: [13] - }]; - var frameEmbed = UI.dialog.customModal(embed, { - buttons: embedButtons, - onClose: config.onClose, - }); - - // Create modal - var tabs = [{ - title: Messages.share_contactCategory, - icon: "fa fa-address-book", - content: frameContacts, - active: hasFriends, - }, { - title: Messages.share_linkCategory, - icon: "fa fa-link", - content: frameLink, - active: !hasFriends - }, { - title: Messages.share_embedCategory, - icon: "fa fa-code", - content: frameEmbed - }]; - if (typeof(AppConfig.customizeShareOptions) === 'function') { - AppConfig.customizeShareOptions(hashes, tabs, { - type: 'FILE', - origin: origin, - pathname: pathname - }); - } - var modal = UI.dialog.tabs(tabs); - return modal; - }; - UIElements.createInviteTeamModal = function (config) { var common = config.common; var hasFriends = Object.keys(config.friends || {}).length !== 0; @@ -1159,7 +336,7 @@ define([ buttons: contactsButtons }; }; - var friendsObject = hasFriends ? getContacts() : noContactsMessage(common); + var friendsObject = hasFriends ? getContacts() : UIElements.noContactsMessage(common); var friendsList = friendsObject.content; var contactsButtons = friendsObject.buttons; contactsButtons.unshift({ @@ -1241,7 +418,7 @@ define([ var localStore = window.cryptpadStore; localStore.get('hide-alert-teamInvite', function (val) { if (val === '1') { return; } - $(linkWarning).show(); + $(linkWarning).css('display', 'flex'); $(dismissButton).on('click', function () { localStore.put('hide-alert-teamInvite', '1'); @@ -2104,6 +1281,9 @@ define([ cb(null, $container); return { $container: $container, + update: function () { + common.getPinUsage(teamId, todo); + }, stop: function () { clearInterval(interval); } diff --git a/www/common/diffMarked.js b/www/common/diffMarked.js index 6062b1d63..734551983 100644 --- a/www/common/diffMarked.js +++ b/www/common/diffMarked.js @@ -7,14 +7,15 @@ define([ '/common/hyperscript.js', '/common/inner/common-mediatag.js', '/common/media-tag.js', - '/common/highlight/highlight.pack.js', '/customize/messages.js', + '/common/highlight/highlight.pack.js', '/bower_components/diff-dom/diffDOM.js', '/bower_components/tweetnacl/nacl-fast.min.js', 'css!/common/highlight/styles/github.css' -],function ($, ApiConfig, Marked, Hash, Util, h, MT, MediaTag, Highlight, Messages) { +],function ($, ApiConfig, Marked, Hash, Util, h, MT, MediaTag, Messages) { var DiffMd = {}; + var Highlight = window.hljs; var DiffDOM = window.diffDOM; var renderer = new Marked.Renderer(); var restrictedRenderer = new Marked.Renderer(); diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 3447f5056..4165ef009 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -1,5 +1,6 @@ define([ 'jquery', + '/api/config', '/common/toolbar.js', 'json.sortify', '/common/common-util.js', @@ -9,6 +10,7 @@ define([ '/common/common-constants.js', '/common/common-feedback.js', + '/common/inner/share.js', '/common/inner/access.js', '/common/inner/properties.js', @@ -19,6 +21,7 @@ define([ '/customize/messages.js', ], function ( $, + ApiConfig, Toolbar, JSONSortify, Util, @@ -27,6 +30,7 @@ define([ UI, Constants, Feedback, + Share, Access, Properties, nThen, @@ -149,6 +153,17 @@ define([ var localStore = window.cryptpadStore; APP.store = {}; + $(window).keydown(function (e) { + if (e.which === 70 && e.ctrlKey) { + e.preventDefault(); + e.stopPropagation(); + if (APP.displayDirectory) { + APP.displayDirectory([SEARCH]); + } + return; + } + }); + var makeLS = function (teamId) { var suffix = teamId ? ('-' + teamId) : ''; var LS_LAST = "app-drive-lastOpened" + suffix; @@ -1374,7 +1389,7 @@ define([ } $button.show(); $button.css({ - background: '#000' + background: '#63b1f7' }); window.setTimeout(function () { $button.css({ @@ -2007,26 +2022,25 @@ define([ if (!parsed.hash && !roParsed.hash) { return void console.error("Invalid href: "+(data.href || data.roHref)); } var friends = common.getFriends(); var ro = folders[id] && folders[id].version >= 2; - var modal = UIElements.createShareModal({ - teamId: APP.team, - origin: APP.origin, - pathname: "/drive/", - friends: friends, - title: data.title, - password: data.password, - sharedFolder: true, - common: common, - hashes: { - editHash: parsed.hash, - viewHash: ro && roParsed.hash, - } - }); // If we're a viewer and this is an old shared folder (no read-only mode), we // can't share the read-only URL and we don't have access to the edit one. // We should hide the share button. - if (!modal) { return; } + if (!data.href && !ro) { return; } $shareBlock.click(function () { - UI.openCustomModal(modal); + Share.getShareModal(common, { + teamId: APP.team, + origin: APP.origin, + pathname: "/drive/", + friends: friends, + title: data.title, + password: data.password, + sharedFolder: true, + common: common, + hashes: { + editHash: parsed.hash, + viewHash: ro && roParsed.hash, + } + }); }); $container.append($shareBlock); return $shareBlock; @@ -2340,7 +2354,7 @@ define([ return $(common.fixLinks($box.html(msg))); } if (!APP.loggedIn) { - msg = APP.newSharedFolder ? Messages.fm_info_sharedFolder : Messages.fm_info_anonymous; + msg = APP.newSharedFolder ? Messages.fm_info_sharedFolder : Messages._getKey('fm_info_anonymous', [ApiConfig.inactiveTime || 90]); return $(common.fixLinks($box.html(msg))); } if (!msg || APP.store['hide-info-' + path[0]] === '1') { @@ -3660,14 +3674,6 @@ define([ if (!readOnlyFolder) { createNewButton(isInRoot, APP.toolbar.$bottomL); } - /* - // The share button is not displayed anymore in the toolbar: users can't know - // if they're going to share the current shared folder or the selected pad - if (sfId) { - createShareButton(sfId, APP.toolbar.$bottomL); - } - */ - if (APP.mobile()) { var $context = $('