diff --git a/bower.json b/bower.json index 9c88d0033..cb8d01f07 100644 --- a/bower.json +++ b/bower.json @@ -41,6 +41,7 @@ "bootstrap": "#v4.0.0-alpha.6", "diff-dom": "2.1.1", "nthen": "^0.1.5", - "open-sans-fontface": "^1.4.2" + "open-sans-fontface": "^1.4.2", + "bootstrap-tokenfield": "^0.12.1" } } diff --git a/customize.dist/application_config.js b/customize.dist/application_config.js index 77962d8c4..e2ea77a02 100644 --- a/customize.dist/application_config.js +++ b/customize.dist/application_config.js @@ -49,6 +49,8 @@ define(function() { users. */ config.loginSalt = ''; + config.minimum_password_length = 8; + config.badStateTimeout = 30000; config.applicationsIcon = { diff --git a/customize.dist/pages.js b/customize.dist/pages.js index 7deb07262..ff5be71a6 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -1,11 +1,10 @@ define([ '/api/config', '/common/hyperscript.js', - '/common/cryptpad-common.js', + '/customize/messages.js', 'jquery' -], function (Config, h, Cryptpad, $) { +], function (Config, h, Msg, $) { var Pages = {}; - var Msg = Cryptpad.Messages; var urlArgs = Config.requireConf.urlArgs; var setHTML = function (e, html) { @@ -438,7 +437,7 @@ define([ ]; }; - var loadingScreen = function () { + var loadingScreen = Pages.loadingScreen = function () { return h('div#loading', h('div.loadingContainer', [ h('img.cryptofist', { @@ -451,6 +450,12 @@ define([ ); }; + var hiddenLoader = function () { + var loader = loadingScreen(); + loader.style.display = 'none'; + return loader; + }; + Pages['/user/'] = Pages['/user/index.html'] = function () { return h('div#container'); }; @@ -526,6 +531,7 @@ define([ ]), infopageFooter(), + hiddenLoader(), ])]; }; @@ -559,6 +565,7 @@ define([ ]), ]), infopageFooter(), + hiddenLoader(), ])]; }; diff --git a/customize.dist/src/less/loading.less b/customize.dist/src/less/loading.less index a9442d3cb..555b02ad8 100644 --- a/customize.dist/src/less/loading.less +++ b/customize.dist/src/less/loading.less @@ -3,7 +3,7 @@ #loading { position: fixed; - z-index: 9999; + z-index: 9999999; top: 0px; bottom: 0px; left: 0px; diff --git a/customize.dist/src/less/toolbar.less b/customize.dist/src/less/toolbar.less index 9ff435277..96f0cbc03 100644 --- a/customize.dist/src/less/toolbar.less +++ b/customize.dist/src/less/toolbar.less @@ -630,7 +630,7 @@ body .cryptpad-toolbar { .title { font-size: 25px; vertical-align: middle; - line-height: 25px; + line-height: 48px; //25px; white-space: nowrap; } .pageTitle { diff --git a/customize.dist/src/less2/include/alertify.less b/customize.dist/src/less2/include/alertify.less index 8dd7fc0cb..16688f179 100644 --- a/customize.dist/src/less2/include/alertify.less +++ b/customize.dist/src/less2/include/alertify.less @@ -65,6 +65,12 @@ height: 100%; z-index: 99999; + .message { + h1, h2, h3, h4, h5, h6 { + color: @alertify-fg; + } + } + h1, h2, h3 { margin-top: 0; } @@ -85,6 +91,20 @@ .dialog { padding: 12px; +/* + div.tokenfield { + .token { + //border: 1px solid red; + //color: red; + } + + color: @colortheme_light-base; + background-color: @alertify-dialog-bg; + + input[id$="tokenfield"][type="text"].token-input { + background-color: @alertify-dialog-bg !important; + } + }*/ } .dialog, .alert { @@ -308,6 +328,5 @@ pointer-events: auto; } } - } } diff --git a/customize.dist/src/less2/include/tokenfield.less b/customize.dist/src/less2/include/tokenfield.less new file mode 100644 index 000000000..2479e3330 --- /dev/null +++ b/customize.dist/src/less2/include/tokenfield.less @@ -0,0 +1,94 @@ +.tokenfield_main () { + .tokenfield { + .unselectable () { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + } + + .unselectable(); + height: auto; + min-height: 34px; + padding-bottom: 0px; + &.focus { + border-color: #66afe9; + outline: 0; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6); + } + .token { + box-sizing: border-box; + border-radius: 3px; + display: inline-block; + border: 1px solid #d9d9d9; + background-color: #ededed; + white-space: nowrap; + margin: -1px 5px 5px 0; + vertical-align: center; + cursor: default; + + color: #222; + + &:hover { + border-color: #b9b9b9; + } + &.invalid { + background: none; + border: 1px solid transparent; + border-radius: 0; + border-bottom: 1px dotted #d9534f; + } + &.invalid.active { + background: #ededed; + border: 1px solid #ededed; + border-radius: 3px; + } + .token-label { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + padding-left: 4px; + vertical-align: center; + } + .close { + font-family: Arial; + display: inline-block; + line-height: 100%; + font-size: 1.1em; + margin-left: 5px; + float: none; + height: 100%; + vertical-align: center; + padding-right: 4px; + } + &.active { + border-color: #52a8ec; + border-color: rgba(82, 168, 236, 0.8); + } + &.duplicate { + border-color: #ebccd1; + } + } + .token-input { + background: none; + width: 0%; //60px; + min-width: 60px; + border: 0; + padding: 0; + margin-bottom: 6px; + box-shadow: none; + max-width: 100%; + &:focus { + border-color: transparent; + outline: 0; + box-shadow: none; + } + } + &.disabled { + cursor: not-allowed; + background-color: #eeeeee; + } + } +} diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 35beb1f02..ad40a7989 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -129,6 +129,7 @@ define(function () { out.saveTemplatePrompt = "Choisir un titre pour ce modèle"; out.templateSaved = "Modèle enregistré !"; out.selectTemplate = "Sélectionner un modèle ou appuyer sur Échap"; + out.useTemplate = "Vous posséder des modèles pour ce type de pad, souhaitez-vous en utiliser un?"; out.previewButtonTitle = "Afficher ou cacher la prévisualisation de Markdown"; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index cdc13df2b..5415ae147 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -131,6 +131,7 @@ define(function () { out.saveTemplatePrompt = "Choose a title for the template"; out.templateSaved = "Template saved!"; out.selectTemplate = "Select a template or press escape"; + out.useTemplate = "You have available templates for that type of pad. Do you want to use one?"; out.previewButtonTitle = "Display or hide the Markdown preview mode"; @@ -409,6 +410,8 @@ define(function () { out.register_importRecent = "Import pad history (Recommended)"; out.register_acceptTerms = "I accept the terms of service"; out.register_passwordsDontMatch = "Passwords do not match!"; + out.register_passwordTooShort = "Passwords must be at least {0} characters long."; + out.register_mustAcceptTerms = "You must accept the terms of service."; out.register_mustRememberPass = "We cannot reset your password if you forget it. It's very important that you remember it! Please check the checkbox to confirm."; diff --git a/pinneddata.js b/pinneddata.js index 2ecf8605d..d7848c7df 100644 --- a/pinneddata.js +++ b/pinneddata.js @@ -81,6 +81,7 @@ nThen((waitFor) => { sema.take((returnAfter) => { Fs.stat(f, waitFor(returnAfter((err, st) => { if (err) { throw err; } + st.filename = f; dsFileStats[f.replace(/^.*\/([^\/\.]*)(\.ndjson)?$/, (all, a) => (a))] = st; }))); }); @@ -117,10 +118,28 @@ nThen((waitFor) => { }); }).nThen(() => { if (process.argv.indexOf('--unpinned') > -1) { + const ot = process.argv.indexOf('--olderthan'); + let before = Infinity; + if (ot > -1) { + before = new Date(process.argv[ot+1]); + if (isNaN(before)) { + throw new Error('--olderthan error [' + process.argv[ot+1] + '] not a valid date'); + } + } + const bot = process.argv.indexOf('--blobsolderthan'); + let blobsbefore = before; + if (bot > -1) { + blobsbefore = new Date(process.argv[bot+1]); + if (isNaN(blobsbefore)) { + throw new Error('--blobsolderthan error [' + process.argv[bot+1] + '] not a valid date'); + } + } Object.keys(dsFileStats).forEach((f) => { if (!(f in pinned)) { - console.log("./datastore/" + f.slice(0,2) + "/" + f + ".ndjson " + - dsFileStats[f].size + " " + (+dsFileStats[f].mtime)); + const isBlob = dsFileStats[f].filename.indexOf('.ndjson') === -1; + if ((+dsFileStats[f].mtime) >= ((isBlob) ? blobsbefore : before)) { return; } + console.log(dsFileStats[f].filename + " " + dsFileStats[f].size + " " + + (+dsFileStats[f].mtime)); } }); } else { diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 745a2b5aa..609317ea3 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -7,9 +7,11 @@ define([ '/common/notify.js', '/common/visible.js', '/common/tippy.min.js', + '/customize/pages.js', + '/common/hyperscript.js', + '/bower_components/bootstrap-tokenfield/dist/bootstrap-tokenfield.js', 'css!/common/tippy.css', -], function ($, Messages, Util, AppConfig, Alertify, Notify, Visible, Tippy) { - +], function ($, Messages, Util, AppConfig, Alertify, Notify, Visible, Tippy, Pages, h) { var UI = {}; /* @@ -20,11 +22,17 @@ define([ // set notification timeout Alertify._$$alertify.delay = AppConfig.notificationTimeout || 5000; - var findCancelButton = UI.findCancelButton = function () { + var findCancelButton = UI.findCancelButton = function (root) { + if (root) { + return $(root).find('button.cancel').last(); + } return $('button.cancel').last(); }; - var findOKButton = UI.findOKButton = function () { + var findOKButton = UI.findOKButton = function (root) { + if (root) { + return $(root).find('button.ok').last(); + } return $('button.ok').last(); }; @@ -33,7 +41,6 @@ define([ switch (e.which) { case 27: // cancel if (typeof(no) === 'function') { no(e); } - no(); break; case 13: // enter if (typeof(yes) === 'function') { yes(e); } @@ -49,21 +56,161 @@ define([ $(window).off('keyup', handler); }; - UI.alert = function (msg, cb, force) { - cb = cb || function () {}; - if (force !== true) { msg = Util.fixHTML(msg); } - var close = function () { - findOKButton().click(); + var dialog = UI.dialog = {}; + + dialog.selectable = function (value) { + var input = h('input', { + type: 'text', + readonly: 'readonly', + }); + $(input).val(value).click(function () { + input.select(); + }); + return input; + }; + + dialog.okButton = function () { + return h('button.ok', { tabindex: '2', }, Messages.okButton); + }; + + dialog.cancelButton = function () { + return h('button.cancel', { tabindex: '1'}, Messages.cancelButton); + }; + + dialog.message = function (text) { + return h('p.message', text); + }; + + dialog.textInput = function (opt) { + return h('input', opt || { + placeholder: '', + type: 'text', + 'class': 'cp-text-input', + }); + }; + + dialog.nav = function (content) { + return h('nav', content || [ + dialog.cancelButton(), + dialog.okButton(), + ]); + }; + + dialog.frame = function (content) { + return h('div.alertify', [ + h('div.dialog', [ + h('div', content), + ]) + ]); + }; + + UI.tokenField = function (target) { + var t = { + element: target || h('input'), }; - var keyHandler = listenForKeys(close, close); - Alertify - .okBtn(Messages.okButton || 'OK') - .alert(msg, function (ev) { - cb(ev); - stopListening(keyHandler); + var $t = t.tokenfield = $(t.element).tokenfield(); + t.getTokens = function () { + return $t.tokenfield('getTokens').map(function (token) { + return token.value; }); - window.setTimeout(function () { - findOKButton().focus(); + }; + + t.preventDuplicates = function (cb) { + $t.on('tokenfield:createtoken', function (ev) { + var val; + if (t.getTokens().some(function (t) { + if (t === ev.attrs.value) { return ((val = t)); } + })) { + ev.preventDefault(); + if (typeof(cb) === 'function') { cb(val); } + } + }); + return t; + }; + + t.setTokens = function (tokens) { + $t.tokenfield('setTokens', + tokens.map(function (token) { + return { + value: token, + label: token, + }; + })); + }; + + t.focus = function () { + var $temp = $t.closest('.tokenfield').find('.token-input'); + $temp.css('width', '20%'); + $t.tokenfield('focusInput', $temp[0]); + }; + + return t; + }; + + dialog.tagPrompt = function (tags, cb) { + var input = dialog.textInput(); + + var tagger = dialog.frame([ + dialog.message('make some tags'), // TODO translate + input, + dialog.nav(), + ]); + + var field = UI.tokenField(input).preventDuplicates(function (val) { + UI.warn('Duplicate tag: ' + val); // TODO translate + }); + + var close = Util.once(function () { + var $t = $(tagger).fadeOut(150, function () { $t.remove(); }); + }); + + var listener = listenForKeys(function () {}, function () { + close(); + stopListening(listener); + }); + + var CB = Util.once(cb); + findOKButton(tagger).click(function () { + var tokens = field.getTokens(); + close(); + CB(tokens); + }); + findCancelButton(tagger).click(function () { + close(); + CB(null); + }); + + // :( + setTimeout(function () { + field.setTokens(tags); + field.focus(); + }); + + return tagger; + }; + + UI.alert = function (msg, cb, force) { + cb = cb || function () {}; + if (typeof(msg) === 'string' && force !== true) { + msg = Util.fixHTML(msg); + } + var ok = dialog.okButton(); + var frame = dialog.frame([ + dialog.message(msg), + dialog.nav(ok), + ]); + + var listener; + var close = Util.once(function () { + $(frame).fadeOut(150, function () { $(this).remove(); }); + stopListening(listener); + }); + listener = listenForKeys(close, close); + var $ok = $(ok).click(close); + + document.body.appendChild(frame); + setTimeout(function () { + $ok.focus(); if (typeof(UI.notify) === 'function') { UI.notify(); } @@ -165,7 +312,7 @@ define([ */ UI.spinner = function (parent) { var $target = $('', { - 'class': 'fa fa-spinner fa-pulse fa-4x fa-fw' + 'class': 'fa fa-circle-o-notch fa-spin fa-4x fa-fw', }).hide(); $(parent).append($target); @@ -206,16 +353,15 @@ define([ } $container = $loading.find('.loadingContainer'); } else { - $loading = $('
', {id: LOADING}); - $container = $('
', {'class': 'loadingContainer'}); - if (!hideLogo) { - $container.append(''); + $loading = $(Pages.loadingScreen()); + $container = $loading.find('.loadingContainer'); + if (hideLogo) { + $loading.find('img').hide(); + } else { + $loading.find('img').show(); } - var $spinner = $('
', {'class': 'spinnerContainer'}); - UI.spinner($spinner).show(); - var $text = $('

').text(loadingText || Messages.loading); - $container.append($spinner).append($text); - $loading.append($container); + var $spinner = $loading.find('.spinnerContainer'); + $spinner.show(); $('body').append($loading); } if (Messages.tips && !hideTips) { diff --git a/www/common/common-util.js b/www/common/common-util.js index 8cbde420c..e42a0249c 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -170,5 +170,15 @@ define([], function () { return parts[0]; }; + /* for wrapping async functions such that they can only be called once */ + Util.once = function (f) { + var called; + return function () { + if (called) { return; } + called = true; + f.apply(this, Array.prototype.slice.call(arguments)); + }; + }; + return Util; }); diff --git a/www/common/credential.js b/www/common/credential.js index 432cc0511..ffab595c1 100644 --- a/www/common/credential.js +++ b/www/common/credential.js @@ -5,6 +5,13 @@ define([ var Cred = {}; var Scrypt = window.scrypt; + Cred.MINIMUM_PASSWORD_LENGTH = typeof(AppConfig.minimum_password_length) === 'number'? + AppConfig.minimum_password_length: 8; + + Cred.isLongEnoughPassword = function (passwd) { + return passwd.length >= Cred.MINIMUM_PASSWORD_LENGTH; + }; + var isString = Cred.isString = function (x) { return typeof(x) === 'string'; }; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 3f8e3ba2c..6f34a0325 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -81,6 +81,8 @@ define([ common.addTooltips = UI.addTooltips; common.clearTooltips = UI.clearTooltips; common.importContent = UI.importContent; + common.tokenField = UI.tokenField; + common.dialog = UI.dialog; // import common utilities for export common.find = Util.find; @@ -1391,6 +1393,21 @@ define([ }) .click(prepareFeedback(type)); break; + case 'hashtag': + button = $('