diff --git a/CHANGELOG.md b/CHANGELOG.md index 185a9ae29..670d71eca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,48 @@ ## Update notes +* warning about lack of support for internet explorer + * existing support will get worse over time. please update. + +To update from 4.10.0 to 4.11.0: + +1. Stop your server +2. Get the latest code with git +3. Install the latest dependencies with `bower update` and `npm i` +4. Restart your server +5. Confirm that your instance is passing all the tests included on the `/checkup/` page (on whatever devices you intend to support) + ## Features * unify unregistered/non-registered/anonymous terminology as 'guest' -* prompt users that need support to subscribe -* include bar graphs for multiple-answer form questions +* support + * prompt users that need support to subscribe + * refactor debugging data generation to easily show users what data is included +* form improvements + * include bar graphs for multiple-answer form questions + * move the tally of empty responses to the top of each question's summary (rather than the bottom) +* bar charts on the admin page's 'Performance' tab +* enhancements for guest users and registered users without names or avatars + * two initials for users with a custom name but no avatar (previously one initial, always capitalized) + * animal avatars as defaults instead of indistinguishable initials (A for Anonymous, G for Guest) + * configurable via `AppConfig.emojiAvatars = []` + * authorship data for guests in rich text comments, code editor author data + * emojis in cursor tooltips for guests (rich text, code, slide, kanban) + * emojis in the share and access modals for contacts with empty names +* script to identify unnecessary duplication of translations +* improvements to upload and media-tag UI + * support for adding descriptive text at upload time + * preview of uploaded media in the upload modal ## Bug fixes +* fix empty name fields in various places across the platform where we did not fall back to "anonymous/guest" + * teams + * contacts + * ??? +* clarified a comment in the nginx config about _professional support_ +* handled an edge case in ICS import to calendars where DTEND was not defined (use duration or consider it an "all-day" event + # 4.10.0 diff --git a/bower.json b/bower.json index efa70c1bb..0f8337867 100644 --- a/bower.json +++ b/bower.json @@ -30,7 +30,7 @@ "secure-fabric.js": "secure-v1.7.9", "hyperjson": "~1.4.0", "chainpad-crypto": "^0.2.0", - "chainpad-listmap": "^0.10.0", + "chainpad-listmap": "^1.0.0", "chainpad": "^5.2.0", "file-saver": "1.3.1", "alertifyjs": "1.0.11", @@ -49,7 +49,8 @@ "jszip": "Stuk/jszip#^3.1.5", "requirejs-plugins": "^1.0.3", "dragula.js": "3.7.2", - "MathJax": "3.0.5" + "MathJax": "3.0.5", + "es6-promise ": "4.2.4" }, "resolutions": { "bootstrap": "^v4.0.0", diff --git a/customize.dist/fonts/cptools/fonts/cptools.eot b/customize.dist/fonts/cptools/fonts/cptools.eot new file mode 100644 index 000000000..dbfc79bf7 Binary files /dev/null and b/customize.dist/fonts/cptools/fonts/cptools.eot differ diff --git a/customize.dist/fonts/cptools/fonts/cptools.svg b/customize.dist/fonts/cptools/fonts/cptools.svg index 12d2fe9d6..29461075f 100644 --- a/customize.dist/fonts/cptools/fonts/cptools.svg +++ b/customize.dist/fonts/cptools/fonts/cptools.svg @@ -7,28 +7,28 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - + + + + + @@ -38,4 +38,5 @@ + \ No newline at end of file diff --git a/customize.dist/fonts/cptools/fonts/cptools.ttf b/customize.dist/fonts/cptools/fonts/cptools.ttf index 788b7f881..41ae6ef37 100644 Binary files a/customize.dist/fonts/cptools/fonts/cptools.ttf and b/customize.dist/fonts/cptools/fonts/cptools.ttf differ diff --git a/customize.dist/fonts/cptools/fonts/cptools.woff b/customize.dist/fonts/cptools/fonts/cptools.woff index f2faf5aa2..dca533fcd 100644 Binary files a/customize.dist/fonts/cptools/fonts/cptools.woff and b/customize.dist/fonts/cptools/fonts/cptools.woff differ diff --git a/customize.dist/fonts/cptools/style.css b/customize.dist/fonts/cptools/style.css index 874b6068f..7b3227453 100644 --- a/customize.dist/fonts/cptools/style.css +++ b/customize.dist/fonts/cptools/style.css @@ -1,10 +1,10 @@ @font-face { font-family: 'cptools'; - src: url('fonts/cptools.eot?chd5a1'); - src: url('fonts/cptools.eot?chd5a1#iefix') format('embedded-opentype'), - url('fonts/cptools.ttf?chd5a1') format('truetype'), - url('fonts/cptools.woff?chd5a1') format('woff'), - url('fonts/cptools.svg?chd5a1#cptools') format('svg'); + src: url('fonts/cptools.eot?6tk5ck'); + src: url('fonts/cptools.eot?6tk5ck#iefix') format('embedded-opentype'), + url('fonts/cptools.ttf?6tk5ck') format('truetype'), + url('fonts/cptools.woff?6tk5ck') format('woff'), + url('fonts/cptools.svg?6tk5ck#cptools') format('svg'); font-weight: normal; font-style: normal; font-display: block; @@ -14,7 +14,7 @@ /* use !important to prevent issues with browser extensions that change fonts */ font-family: 'cptools' !important; display: inline-block; - speak: none; + speak: never; font-style: normal; font-weight: normal; font-variant: normal; @@ -26,6 +26,9 @@ -moz-osx-font-smoothing: grayscale; } +.cptools-form-conditional:before { + content: "\e900"; +} .cptools-form-poll:before { content: "\e910"; } @@ -57,65 +60,65 @@ content: "\e91d"; } .cptools-folder-no-color:before { - content: "\e900"; + content: "\e901"; } .cptools-whiteboard:before { - content: "\e901"; + content: "\e902"; } .cptools-new-template:before { - content: "\e902"; + content: "\e903"; } .cptools-shared-folder:before { - content: "\e903"; + content: "\e904"; } .cptools-file-upload:before { - content: "\e904"; + content: "\e905"; } .cptools-template:before { - content: "\e905"; + content: "\e906"; } .cptools-poll:before { - content: "\e906"; + content: "\e907"; } .cptools-slide:before { - content: "\e907"; + content: "\e908"; } .cptools-sheet:before { - content: "\e908"; + content: "\e909"; } .cptools-folder-open:before { - content: "\e909"; + content: "\e90a"; } .cptools-kanban:before { - content: "\e90a"; + content: "\e90b"; } .cptools-folder:before { - content: "\e90b"; + content: "\e90c"; } .cptools-shared-folder-open:before { - content: "\e90c"; + content: "\e90d"; } .cptools-code:before { - content: "\e90d"; + content: "\e90e"; } .cptools-richtext:before { - content: "\e90e"; + content: "\e90f"; } .cptools-file:before { - content: "\e90f"; + content: "\e911"; } .cptools-palette:before { - content: "\e911"; + content: "\e912"; } .cptools-folder-upload:before { - content: "\e912"; + content: "\e913"; } .cptools-add-bottom:before { - content: "\e913"; + content: "\e914"; } .cptools-add-top:before { - content: "\e914"; + content: "\e915"; } .cptools-destroy:before { - content: "\e915"; + content: "\e91f"; } diff --git a/customize.dist/login.js b/customize.dist/login.js index 5cab49934..1ba0eb470 100644 --- a/customize.dist/login.js +++ b/customize.dist/login.js @@ -1,6 +1,6 @@ define([ 'jquery', - '/bower_components/chainpad-listmap/chainpad-listmap.js', + 'chainpad-listmap', '/bower_components/chainpad-crypto/crypto.js', '/common/common-util.js', '/common/outer/network-config.js', diff --git a/customize.dist/pages.js b/customize.dist/pages.js index fa183f90c..7a520c3c0 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -105,7 +105,7 @@ define([ var imprintUrl = AppConfig.imprint && (typeof(AppConfig.imprint) === "boolean" ? '/imprint.html' : AppConfig.imprint); - Pages.versionString = "v4.10.0"; + Pages.versionString = "v4.11.0"; // used for the about menu diff --git a/customize.dist/src/less2/include/alertify.less b/customize.dist/src/less2/include/alertify.less index 96b8e7cde..5c829cec4 100644 --- a/customize.dist/src/less2/include/alertify.less +++ b/customize.dist/src/less2/include/alertify.less @@ -166,6 +166,9 @@ color: @cryptpad_text_col; text-decoration: underline; } + pre.cp-link-preview { + color: @cryptpad_text_col; + } .cp-info-menu-container { .logo-block { text-align: center; diff --git a/customize.dist/src/less2/include/avatar.less b/customize.dist/src/less2/include/avatar.less index 725c7748f..acaa13351 100644 --- a/customize.dist/src/less2/include/avatar.less +++ b/customize.dist/src/less2/include/avatar.less @@ -4,7 +4,11 @@ @width: 30px ) { @avatar-width: @width; - @avatar-font-size: @width / 1.2; + @avatar-font-size: @width / 1.8; + // scale animal avatar to be somewhat larger, because: + // 1. emojis are wider than most latin characters + // 2. they should occupy the width of two average characters + @avatar-font-size-animal: @avatar-font-size * (6/5); } .avatar_main(@width: 30px) { --LessLoader_require: LessLoader_currentFile(); @@ -40,7 +44,9 @@ color: @cp_avatar-fg; font-size: @avatar-font-size; font-size: var(--avatar-font-size); - text-transform: capitalize; + .animal { + font-size: @avatar-font-size-animal; + } } media-tag { min-height: @avatar-width; diff --git a/customize.dist/src/less2/include/charts.less b/customize.dist/src/less2/include/charts.less index c8b983707..ea402094f 100644 --- a/customize.dist/src/less2/include/charts.less +++ b/customize.dist/src/less2/include/charts.less @@ -50,4 +50,56 @@ } } } + + .cp-charts-cell { + border: 1px solid @cp_form-border; + display: table-cell; + padding: 5px 10px; + background: @cp_form-bg2; + } + + .cp-form-results-type-radio { + .cp-form-results-type-multiradio-data { + display: flex; + flex-flow: column; + } + .cp-form-results-type-radio-data { + display: table-row; + border: 1px solid @cp_form-border; + & > span { + .cp-charts-cell(); + } + } + } + + .cp-charts.cp-bar-table, .cp-charts.cp-text-table { + display: table; + width: 100%; + .cp-charts-row { + display: table-row; + border: 1px solid @cp_form-border; + &.full { + display: flex; + flex-flow: column; + } + + & > span { + .cp-charts-cell(); + display: table-cell; + &.cp-value { + min-width: 200px; + } + &.cp-bar-container { + width: 99%; + padding: 0px; + position: relative; + .cp-bar { + position: absolute; + background: @cryptpad_color_brand; + height: 100%; + } + } + } + } + } } diff --git a/customize.dist/src/less2/include/creation.less b/customize.dist/src/less2/include/creation.less index f4f4d0193..b15863c57 100644 --- a/customize.dist/src/less2/include/creation.less +++ b/customize.dist/src/less2/include/creation.less @@ -364,9 +364,9 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - min-height: 20px; - height: 20px; - line-height: 20px; + min-height: 25px; + height: 25px; + line-height: 25px; max-width: 100%; } .fa, .cptools { diff --git a/customize.dist/src/less2/include/forms.less b/customize.dist/src/less2/include/forms.less index 4fb799e13..2eace9f29 100644 --- a/customize.dist/src/less2/include/forms.less +++ b/customize.dist/src/less2/include/forms.less @@ -112,8 +112,10 @@ border-radius: 0; transition: none; - .fa, .cptools { - margin-right: 5px; + i, .fa, .cptools { + &:not(.nomargin) { + margin-right: 5px; + } } .cptools { vertical-align: middle; diff --git a/customize.dist/src/less2/include/modals-ui-elements.less b/customize.dist/src/less2/include/modals-ui-elements.less index 7ec19a699..e8d9695c5 100644 --- a/customize.dist/src/less2/include/modals-ui-elements.less +++ b/customize.dist/src/less2/include/modals-ui-elements.less @@ -273,4 +273,28 @@ } } } + #cp-upload-preview-container { + max-width: 100%; + max-height: 300px; + overflow: auto; + // XXX these styles yield weird results for tall, thin images + // maybe img { object-fit: contain; } or scale-down are options + // but they have problems too + media-tag { + max-width: 100%; + iframe { + // pdfs don't take the full width unless we tell them to + width: 100%; + } + & > * { + max-width: 100%; + } + } + } + input#cp-app-drive-link-url { + &.cp-input-invalid { + border: 2px solid @cryptpad_color_red; + color: @cp_form-invalid; + } + } } diff --git a/customize.dist/src/less2/include/sidebar-layout.less b/customize.dist/src/less2/include/sidebar-layout.less index ef93eae4a..564067fb7 100644 --- a/customize.dist/src/less2/include/sidebar-layout.less +++ b/customize.dist/src/less2/include/sidebar-layout.less @@ -69,7 +69,6 @@ background: @cp_sidebar-right-bg; color: @cp_sidebar-right-fg; overflow: auto; - padding-bottom: 200px; // Following rules are only in settings .cp-sidebarlayout-element { diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index b0f9b5e42..6ccec507d 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -532,7 +532,13 @@ &> button { height: @toolbar_line-height; width: @toolbar_line-height; - span { font-size: unset; } + span { + .avatar_vars(36px); + font-size: @avatar-font-size; + .animal { + font-size: @avatar-font-size-animal; + } + } } &> button.cp-avatar.cp-avatar { media-tag { @@ -855,10 +861,14 @@ span { text-align: center; width: 100%; - font-size: 48px; + .avatar_vars(72px); + font-size: @avatar-font-size; display: inline-flex; justify-content: center; align-items: center; + .animal { + font-size: @avatar-font-size-animal; + } } &.cp-avatar { .avatar_main(64px); diff --git a/docs/example.nginx.conf b/docs/example.nginx.conf index 8bc47d9f8..14a3d4fc2 100644 --- a/docs/example.nginx.conf +++ b/docs/example.nginx.conf @@ -2,7 +2,7 @@ # to work with CryptPad. This example WILL NOT WORK AS IS. For best results, # compare the sections of this configuration file against a working CryptPad # installation (http server by the Nodejs process). If you are using CryptPad -# in production, contact sales@cryptpad.fr +# in production and require professional support please contact sales@cryptpad.fr server { listen 443 ssl http2; diff --git a/package-lock.json b/package-lock.json index 790052c6e..c795956c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cryptpad", - "version": "4.10.0", + "version": "4.11.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a844167f5..26fbc968a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cryptpad", "description": "realtime collaborative visual editor with zero knowlege server", - "version": "4.10.0", + "version": "4.11.0", "license": "AGPL-3.0+", "repository": { "type": "git", @@ -45,8 +45,8 @@ "lint:js": "jshint --config .jshintrc --exclude-path .jshintignore .", "lint:server": "jshint --config .jshintrc lib", "lint:less": "./node_modules/lesshint/bin/lesshint -c ./.lesshintrc ./customize.dist/src/less2/", - "lint:translations": "node ./scripts/lint-translations.js", - "unused-translations": "node ./scripts/unused-translations.js", + "lint:translations": "node ./scripts/translations/lint-translations.js", + "unused-translations": "node ./scripts/translations/unused-translations.js", "test": "node scripts/TestSelenium.js", "test-rpc": "cd scripts/tests && node test-rpc", "template": "cd customize.dist/src && for page in ../index.html ../privacy.html ../terms.html ../contact.html ../what-is-cryptpad.html ../features.html ../../www/login/index.html ../../www/register/index.html ../../www/user/index.html;do echo $page; cp template.html $page; done;", diff --git a/scripts/translations/find-duplicate-translations.js b/scripts/translations/find-duplicate-translations.js new file mode 100644 index 000000000..a6bc53613 --- /dev/null +++ b/scripts/translations/find-duplicate-translations.js @@ -0,0 +1,111 @@ +var Assert = require("assert"); +var Util = require("../../lib/common-util"); +var addIfAbsent = function (A, e) { + if (A.includes(e)) { return; } + A.push(e); +}; + +var findDuplicates = function (map) { + var keys = Object.keys(map); + + + var duplicates = {}; + var markDuplicate = function (value, key1, key2) { + //console.log("[%s] === [%s] (%s)", key1, key2, value); + if (!Array.isArray(duplicates[value])) { + duplicates[value] = []; + } + addIfAbsent(duplicates[value], key1); + addIfAbsent(duplicates[value], key2); + }; + + keys.forEach(function (key) { + var value = map[key]; + + //var duplicates = []; + keys.forEach(function (key2) { + if (key === key2) { return; } + var value2 = map[key2]; + if (value === value2) { + markDuplicate(value, key, key2); + } + }); + }); + + var temp = {}; + // sort keys and construct a new index using the first key in the sorted array + Object.keys(duplicates).forEach(function (key) { + var val = duplicates[key]; // should be an array + val.sort(); // default js sort + var new_key = val[0]; + temp[new_key] = val; + }); + + var canonical = {}; + Object.keys(temp).sort().forEach(function (key) { + canonical[key] = temp[key]; + }); + return canonical; +}; + +/* +var logDuplicates = function (duplicates) { + // indicate which strings are duplicated and could potentially be changed to use one key + Object.keys(duplicates).forEach(function (val) { + console.log('\"%s\" => %s', val, JSON.stringify(duplicates[val])); + }); +}; +*/ + +var FULL_LANGUAGES = { + EN: Util.clone(require("../../www/common/translations/messages.json")), + FR: Util.clone(require("../../www/common/translations/messages.fr.json")), + DE: Util.clone(require("../../www/common/translations/messages.de.json")), + JP: Util.clone(require("../../www/common/translations/messages.ja.json")), +}; + +var DUPLICATES = {}; + +Object.keys(FULL_LANGUAGES).forEach(function (code) { + DUPLICATES[code] = findDuplicates(FULL_LANGUAGES[code]); +}); + +var extraneousKeys = 0; + +// 1) check whether the same mapping exists across languages +// ie. English has "Open" (verb) and "Open" (adjective) +// while French has "Ouvrir" and "Ouvert(s)" +// such keys should not be simplified/deduplicated +Object.keys(DUPLICATES.EN).forEach(function (key) { + var reference = DUPLICATES.EN[key]; + if (!['FR', 'DE', 'JP'].every(function (code) { + try { + Assert.deepEqual(reference, DUPLICATES[code][key]); + } catch (err) { + return false; + } + return true; + })) { + return; + } + console.log("The key [%s] (\"%s\") is duplicated identically across all fully supported languages", key, FULL_LANGUAGES.EN[key]); + console.log("Values:", JSON.stringify(['EN', 'FR', 'DE', 'JP'].map(function (code) { + return FULL_LANGUAGES[code][key]; + }))); + console.log("Keys:", JSON.stringify(reference)); + console.log(); + extraneousKeys += reference.length - 1; + + //console.log("\n" + code + "\n==\n"); + //logDuplicates(map); +}); + +console.log("Total extraneous keys: %s", extraneousKeys); + + +// TODO +// find instances where +// one of the duplicated keys is not translated +// perhaps we could automatically use the translated one everywhere +// and improve the completeness of translations + diff --git a/scripts/lint-translations.js b/scripts/translations/lint-translations.js similarity index 94% rename from scripts/lint-translations.js rename to scripts/translations/lint-translations.js index 5e7f81b17..e02bca46a 100644 --- a/scripts/lint-translations.js +++ b/scripts/translations/lint-translations.js @@ -1,4 +1,4 @@ -var EN = require("../www/common/translations/messages.json"); +var EN = require("../../www/common/translations/messages.json"); var simpleTags = [ '
', @@ -118,7 +118,7 @@ processLang(EN, 'en', true); 'zh', ].forEach(function (lang) { try { - var map = require("../www/common/translations/messages." + lang + ".json"); + var map = require("../../www/common/translations/messages." + lang + ".json"); if (!Object.keys(map).length) { return; } processLang(map, lang); } catch (err) { diff --git a/scripts/unused-translations.js b/scripts/translations/unused-translations.js similarity index 98% rename from scripts/unused-translations.js rename to scripts/translations/unused-translations.js index 26156cd03..941835cba 100644 --- a/scripts/unused-translations.js +++ b/scripts/translations/unused-translations.js @@ -1,4 +1,4 @@ -var Messages = require("../www/common/translations/messages.json"); +var Messages = require("../../www/common/translations/messages.json"); var Exec = require("child_process").exec; var ignoreLines = function (source, pattern) { diff --git a/temp.md b/temp.md new file mode 100644 index 000000000..e69de29bb diff --git a/www/admin/app-admin.less b/www/admin/app-admin.less index 227792540..f66eb75f7 100644 --- a/www/admin/app-admin.less +++ b/www/admin/app-admin.less @@ -1,12 +1,14 @@ @import (reference) '../../customize/src/less2/include/framework.less'; @import (reference) '../../customize/src/less2/include/sidebar-layout.less'; @import (reference) '../../customize/src/less2/include/support.less'; +@import (reference) '../../customize/src/less2/include/charts.less'; &.cp-app-admin { .framework_min_main(); .sidebar-layout_main(); .support_main(); + .charts_main(); .cp-hidden { display: none !important; @@ -294,5 +296,27 @@ } } } + span.cp-bar.profiling-percentage { + text-align: center; + padding: 5px; + } + span.profiling-label { + position: absolute; + z-index: 1; + width: 100%; + text-align: center; + padding: 5px; + } + #profiling-chart { + .cp-bar-container { + max-width: 400px; + } + } + .width-constrained { + max-width: 800px; + } + .cp-charts-row.heading { + font-weight: bold; + } } diff --git a/www/admin/inner.js b/www/admin/inner.js index e8d567d41..63c516614 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -1673,34 +1673,51 @@ define([ var $div = makeBlock('performance-profiling'); // Msg.admin_performanceProfilingHint, .admin_performanceProfilingTitle var onRefresh = function () { - var body = h('tbody'); + var createBody = function () { + return h('div#profiling-chart.cp-charts.cp-bar-table', [ + h('span.cp-charts-row.heading', [ + h('span', Messages.admin_performanceKeyHeading), + h('span', Messages.admin_performanceTimeHeading), + h('span', Messages.admin_performancePercentHeading), + //h('span', ''), //Messages.admin_performancePercentHeading), + ]), + ]); + }; - var table = h('table#cp-performance-table', [ - h('thead', [ - h('th', Messages.admin_performanceKeyHeading), - h('th', Messages.admin_performanceTimeHeading), - h('th', Messages.admin_performancePercentHeading), - ]), - body, - ]); - var appendRow = function (key, time, percent) { - console.log("[%s] %ss running time (%s%)", key, time, percent); - body.appendChild(h('tr', [ key, time, percent ].map(function (x) { - return h('td', x); - }))); + var body = createBody(); + var appendRow = function (key, time, percent, scaled) { + //console.log("[%s] %ss running time (%s%)", key, time, percent); + body.appendChild(h('span.cp-charts-row', [ + h('span', key), + h('span', time), + //h('span', percent), + h('span.cp-bar-container', [ + h('span.cp-bar.profiling-percentage', { + style: 'width: ' + scaled + '%', + }, ' ' ), + h('span.profiling-label', percent + '%'), + ]), + ])); }; var process = function (_o) { + $('#profiling-chart').remove(); + body = createBody(); var o = _o[0]; var sorted = Object.keys(o).sort(function (a, b) { if (o[b] - o[a] <= 0) { return -1; } return 1; }); + + var values = sorted.map(function (k) { return o[k]; }); var total = 0; - sorted.forEach(function (k) { total += o[k]; }); + values.forEach(function (value) { total += value; }); + var max = Math.max.apply(null, values); + sorted.forEach(function (k) { var percent = Math.floor((o[k] / total) * 1000) / 10; - appendRow(k, o[k], percent); + appendRow(k, o[k], percent, (o[k] / max) * 100); }); + $div.append(h('div.width-constrained', body)); }; sFrameChan.query('Q_ADMIN_RPC', { @@ -1710,10 +1727,7 @@ define([ UI.warn(Messages.error); return void console.error(e, data); } - //console.info(data); - $div.find("table").remove(); process(data); - $div.append(table); }); }; diff --git a/www/calendar/export.js b/www/calendar/export.js index 64775762b..5967e2027 100644 --- a/www/calendar/export.js +++ b/www/calendar/export.js @@ -123,6 +123,7 @@ define([ var jcalData = ICAL.parse(content); vcalendar = new ICAL.Component(jcalData); } catch (e) { + console.error(e); return void cb(e); } @@ -147,6 +148,18 @@ define([ var isAllDay = false; var start = ev.getFirstPropertyValue('dtstart'); var end = ev.getFirstPropertyValue('dtend'); + var duration = ev.getFirstPropertyValue('duration'); + if (!end && !duration) { + if (start.isDate) { + end = start.clone(); + end.adjust(1); // Add one day + } else { + end = start.clone(); + } + } else if (!end) { + end = start.clone(); + end.addDuration(duration); + } if (start.isDate && end.isDate) { isAllDay = true; start = String(start); @@ -175,7 +188,7 @@ define([ hidden.push(al.toString()); } var trigger = al.getFirstPropertyValue('trigger'); - var minutes = -trigger.toSeconds() / 60; + var minutes = trigger ? (-trigger.toSeconds() / 60) : 0; if (reminders.indexOf(minutes) === -1) { reminders.push(minutes); } }); diff --git a/www/calendar/inner.js b/www/calendar/inner.js index 21cb12789..fbc910bdb 100644 --- a/www/calendar/inner.js +++ b/www/calendar/inner.js @@ -566,7 +566,7 @@ define([ attributes: { 'class': 'fa fa-trash-o', }, - content: h('span', Messages.kanban_delete), + content: h('span', Messages.poll_remove), action: function (e) { e.stopPropagation(); var cal = APP.calendars[id]; @@ -586,8 +586,9 @@ define([ }, function (err) { if (err) { console.error(err); - UI.warn(Messages.error); + return void UI.warn(Messages.error); } + renderCalendar(); }); }); } @@ -722,7 +723,7 @@ define([ if (!calendars.length) { return; } var team = privateData.teams[teamId]; var avatar = h('span.cp-avatar'); - common.displayAvatar($(avatar), team.avatar, team.displayName); + common.displayAvatar($(avatar), team.avatar, team.displayName || team.name); APP.$calendars.append(h('div.cp-calendar-team', [ avatar, h('span.cp-name', {title: team.name}, team.name) diff --git a/www/code/markers.js b/www/code/markers.js index b070e68fc..48e5a25fb 100644 --- a/www/code/markers.js +++ b/www/code/markers.js @@ -3,7 +3,9 @@ define([ '/common/sframe-common-codemirror.js', '/customize/messages.js', '/bower_components/chainpad/chainpad.dist.js', -], function (Util, SFCodeMirror, Messages, ChainPad) { + '/common/inner/common-mediatag.js', + '/common/common-interface.js', +], function (Util, SFCodeMirror, Messages, ChainPad, MT, UI) { var Markers = {}; /* TODO Known Issues @@ -38,7 +40,17 @@ define([ }); } uid = Number(uid); - var name = Util.fixHTML(author.name || Messages.anonymous); + var name = Util.fixHTML(UI.getDisplayName(author.name)); + var animal; + if ((!name || name === Messages.anonymous) && typeof(author.uid) === 'string') { + animal = MT.getPseudorandomAnimal(author.uid); + if (animal) { + name = animal + ' ' + Messages.anonymous; + } else { + name = Messages.anonymous; + } + } + var col = Util.hexToRGB(author.color); var rgba = 'rgba('+col[0]+','+col[1]+','+col[2]+','+Env.opacity+');'; return Env.editor.markText(from, to, { @@ -520,7 +532,8 @@ define([ Env.authormarks.authors[Env.myAuthorId] = { name: userData.name, curvePublic: userData.curvePublic, - color: userData.color + color: userData.color, + uid: userData.uid, }; if (!old || (old.name === userData.name && old.color === userData.color)) { return; } return true; diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js index 3be979f17..4a58085ab 100644 --- a/www/common/application_config_internal.js +++ b/www/common/application_config_internal.js @@ -208,5 +208,7 @@ define(function() { // the driveless mode by changing the following value to "false" AppConfig.allowDrivelessMode = true; + AppConfig.emojiAvatars = '🙈 🦀 🐞 🦋 🐬 🐋 🐢 🦉 🦆 🐧 🦡 🦘 🦨 🦦 🦥 🐼 🐻 🦝 🦓 🐄 💮️ 🐙️ 🌸️ 🌻️ 🐝️ 🐐 🦙 🦒 🐘 🦏 🐁 🐹 🐰 🦫 🦔 🐨 🐱 🐺 👺 👹 👽 👾 🤖'.split(/\s+/); + return AppConfig; }); diff --git a/www/common/boot2.js b/www/common/boot2.js index ada5b794c..d143da87d 100644 --- a/www/common/boot2.js +++ b/www/common/boot2.js @@ -34,6 +34,7 @@ try { define([ '/common/requireconfig.js' ], function (RequireConfig) { + require.config(RequireConfig()); // most of CryptPad breaks if you don't support isArray @@ -91,4 +92,10 @@ define([ } catch (e) { console.error(e); failStore(); } require([document.querySelector('script[data-bootload]').getAttribute('data-bootload')]); + if (typeof(Promise) !== 'function') { + setTimeout(function () { + var s = "Internet Explorer is not supported anymore, including by Microsoft.\n\nMost of CryptPad's collaborative functionality requires a modern browser to work.\n\nWe recommend Mozilla Firefox."; + window.alert(s); + }); + } }); diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 71a1ed8af..5c2431efd 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -41,6 +41,10 @@ define([ return e; }; + UI.getDisplayName = function (name) { + return (typeof(name) === 'string'? name: "").trim() || Messages.anonymous; + }; + // FIXME almost everywhere this is used would also be // a good candidate for sframe-common's getMediatagFromHref UI.mediaTag = function (src, key) { diff --git a/www/common/common-messaging.js b/www/common/common-messaging.js index 65f05961e..4a3eb2a91 100644 --- a/www/common/common-messaging.js +++ b/www/common/common-messaging.js @@ -17,7 +17,8 @@ define([ edPublic: proxy.edPublic, curvePublic: proxy.curvePublic, notifications: Util.find(proxy, ['mailboxes', 'notifications', 'channel']), - avatar: proxy.profile && proxy.profile.avatar + avatar: proxy.profile && proxy.profile.avatar, + uid: proxy.uid, }; if (hash === false) { delete data.channel; } return data; diff --git a/www/common/common-thumbnail.js b/www/common/common-thumbnail.js index 01e89e5d1..e3b328dc1 100644 --- a/www/common/common-thumbnail.js +++ b/www/common/common-thumbnail.js @@ -196,7 +196,7 @@ define([ reader.readAsText(blob); }; Thumb.fromBlob = function (blob, _cb) { - var cb = Util.once(_cb); + var cb = Util.once(Util.mkAsync(_cb)); // The blob is already in memory, it should be super-fast to make a thumbnail // ==> 1s timeout setTimeout(function () { diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index d8b399851..08e0bcf07 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -156,9 +156,11 @@ define([ var icons = Object.keys(users).map(function (key, i) { var data = users[key]; - var name = data.displayName || data.name || Messages.anonymous; - var avatar = h('span.cp-usergrid-avatar.cp-avatar'); - common.displayAvatar($(avatar), data.avatar, name); + var name = UI.getDisplayName(data.displayName || data.name); + var avatar = h('span.cp-usergrid-avatar.cp-avatar', { + 'aria-hidden': true, + }); + common.displayAvatar($(avatar), data.avatar, name, Util.noop, data.uid); var removeBtn, el; if (config.remove) { removeBtn = h('span.fa.fa-times'); @@ -753,7 +755,7 @@ define([ //title: Messages.previewButtonTitle, // TODO display if the label text is collapsed }, [ h('i.fa.fa-eye'), - h('span.cp-toolbar-name', Messages.share_linkOpen) + h('span.cp-toolbar-name', Messages.toolbar_preview) ])).click(common.prepareFeedback(type)); break; case 'print': @@ -1437,18 +1439,25 @@ define([ window.setTimeout(function () { $innerblock.hide(); }, 0); }; - config.options.forEach(function (o) { - if (!isValidOption(o)) { return; } - if (isElement(o)) { return $innerblock.append($(o)); } - var $el = $('<' + o.tag + '>', o.attributes || {}).html(o.content || ''); - $el.appendTo($innerblock); - if (typeof(o.action) === 'function') { - $el.click(function (e) { - var close = o.action(e); - if (close) { hide(); } - }); - } - }); + var setOptions = function (options) { + options.forEach(function (o) { + if (!isValidOption(o)) { return; } + if (isElement(o)) { return $innerblock.append($(o)); } + var $el = $('<' + o.tag + '>', o.attributes || {}).html(o.content || ''); + $el.appendTo($innerblock); + if (typeof(o.action) === 'function') { + $el.click(function (e) { + var close = o.action(e); + if (close) { hide(); } + }); + } + }); + }; + setOptions(config.options); + $container.setOptions = function (options) { + $innerblock.empty(); + setOptions(options); + }; $container.append($button).append($innerblock); @@ -1579,16 +1588,20 @@ define([ }, 1000); }); - $container.setValue = function (val, name) { + $container.setValue = function (val, name, sync) { value = val; var $val = $innerblock.find('[data-value="'+val+'"]'); var textValue = name || $val.html() || val; + if (sync) { + $button.find('.cp-dropdown-button-title').html(textValue); + return; + } setTimeout(function () { $button.find('.cp-dropdown-button-title').html(textValue); }); }; $container.getValue = function () { - return value || ''; + return typeof(value) === "undefined" ? '' : value; }; } @@ -1993,9 +2006,12 @@ define([ var loadingAvatar; var to; var oldUrl = ''; + var oldUid; + var oldName; var updateButton = function () { var myData = metadataMgr.getUserData(); var privateData = metadataMgr.getPrivateData(); + var uid = myData.uid; if (!priv.plan && privateData.plan) { config.$initBlock.empty(); metadataMgr.off('change', updateButton); @@ -2010,18 +2026,19 @@ define([ return; } loadingAvatar = true; - var newName = myData.name; + var newName = UI.getDisplayName(myData.name); var url = myData.avatar; - $displayName.text(newName || Messages.anonymous); - if (accountName && oldUrl !== url) { + $displayName.text(newName); + if ((accountName && oldUrl !== url) || !accountName && uid !== oldUid || oldName !== newName) { $avatar.html(''); - Common.displayAvatar($avatar, url, - newName || Messages.anonymous, function ($img) { + Common.displayAvatar($avatar, url, newName, function ($img) { oldUrl = url; + oldUid = uid; + oldName = newName; $userAdmin.find('> button').removeClass('cp-avatar'); if ($img) { $userAdmin.find('> button').addClass('cp-avatar'); } loadingAvatar = false; - }); + }, uid); return; } loadingAvatar = false; @@ -2303,6 +2320,7 @@ define([ var teams = Object.keys(privateData.teams).map(function (id) { var data = privateData.teams[id]; var avatar = h('span.cp-creation-team-avatar.cp-avatar'); + // We assume that teams always have a non-empty name, so we don't need a UID common.displayAvatar($(avatar), data.avatar, data.name); return h('div.cp-creation-team', { 'data-id': id, @@ -2440,7 +2458,6 @@ define([ }); }*/ if (!privateData.newTemplate) { - Messages.creation_noTemplate = "Blank document"; // XXX update key allData.unshift({ name: Messages.creation_noTemplate, id: 0, @@ -3037,22 +3054,10 @@ define([ var name = Util.fixHTML(data.title); var url = data.href; var user = data.name; - //Messages.link_open = "Open URL"; - // openLinkInNewTab ("Open Link in New Tab") - // fc_open ("Open") - // share_linkOpen ("Preview") - // resources_openInNewTab ("Open it in a new tab") - Messages.link_open = Messages.fc_open; // XXX 4.11.0 - - //Messages.link_store = "Store link in drive"; - // toolbar_storeInDrive ? ("Store in CryptDrive") - // autostore_store ? ("Store") - Messages.link_store = Messages.toolbar_storeInDrive; // XXX 4.11.0 - var content = h('div', [ UI.setHTML(h('p'), Messages._getKey('notification_openLink', [name, user])), - h('pre', url), + h('pre.cp-link-preview', url), UIElements.getVerifiedFriend(common, data.curve, user) ]); var clicked = false; @@ -3067,7 +3072,7 @@ define([ keys: [27] }, { className: 'primary', - name: Messages.link_open, + name: Messages.fc_open, onClick: function () { if (clicked) { return true; } clicked = true; @@ -3077,7 +3082,7 @@ define([ keys: [13] }, { className: 'primary', - name: Messages.link_store, + name: Messages.toolbar_storeInDrive, onClick: function () { if (clicked) { return; } clicked = true; @@ -3107,7 +3112,7 @@ define([ var sframeChan = common.getSframeChannel(); var msg = data.content.msg; - var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous; + var name = Util.fixHTML(UI.getDisplayName(msg.content.user.displayName)); var title = Util.fixHTML(msg.content.title); var text = Messages._getKey('owner_add', [name, title]); @@ -3239,7 +3244,7 @@ define([ var sframeChan = common.getSframeChannel(); var msg = data.content.msg; - var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous; + var name = Util.fixHTML(UI.getDisplayName(msg.content.user.displayName)); var title = Util.fixHTML(msg.content.title); var text = Messages._getKey('owner_team_add', [name, title]); @@ -3354,13 +3359,15 @@ define([ var verified = h('p'); var $verified = $(verified); + name = UI.getDisplayName(name); if (priv.friends && priv.friends[curve]) { $verified.addClass('cp-notifications-requestedit-verified'); var f = priv.friends[curve]; $verified.append(h('span.fa.fa-certificate')); var $avatar = $(h('span.cp-avatar')).appendTo($verified); - $verified.append(h('p', Messages._getKey('isContact', [f.displayName]))); - common.displayAvatar($avatar, f.avatar, f.displayName); + name = UI.getDisplayName(f.displayName); + $verified.append(h('p', Messages._getKey('isContact', [name]))); + common.displayAvatar($avatar, f.avatar, name, Util.noop, f.uid); } else { $verified.append(Messages._getKey('isNotContact', [name])); } @@ -3370,7 +3377,7 @@ define([ UIElements.displayInviteTeamModal = function (common, data) { var msg = data.content.msg; - var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous; + var name = Util.fixHTML(UI.getDisplayName(msg.content.user.displayName)); var teamName = Util.fixHTML(Util.find(msg, ['content', 'team', 'metadata', 'name']) || ''); var verified = UIElements.getVerifiedFriend(common, msg.author, name); @@ -3454,7 +3461,8 @@ define([ name: f.displayName, curvePublic: f.curvePublic, profile: f.profile, - notifications: f.notifications + notifications: f.notifications, + uid: f.uid, }; }); }; @@ -3553,7 +3561,7 @@ define([ }; // Set the value to receive from the autocomplete var toInsert = function (data, key) { - var name = data.name.replace(/[^a-zA-Z0-9]+/g, "-"); + var name = UI.getDisplayName(data.name.replace(/[^a-zA-Z0-9]+/g, "-")); return "[@"+name+"|"+key+"]"; }; @@ -3606,18 +3614,20 @@ define([ var avatar = h('span.cp-avatar', { contenteditable: false }); - common.displayAvatar($(avatar), data.avatar, data.name); + + var displayName = UI.getDisplayName(data.name); + common.displayAvatar($(avatar), data.avatar, displayName); return h('span.cp-mentions', { 'data-curve': data.curvePublic, 'data-notifications': data.notifications, 'data-profile': data.profile, - 'data-name': Util.fixHTML(data.name), + 'data-name': Util.fixHTML(displayName), 'data-avatar': data.avatar || "", }, [ avatar, h('span.cp-mentions-name', { contenteditable: false - }, data.name) + }, displayName) ]); }; } @@ -3649,7 +3659,7 @@ define([ }).map(function (key) { var data = sources[key]; return { - label: data.name, + label: UI.getDisplayName(data.name), value: key }; }); @@ -3684,10 +3694,12 @@ define([ var obj = sources[key]; if (!obj) { return; } var avatar = h('span.cp-avatar'); - common.displayAvatar($(avatar), obj.avatar, obj.name); + var displayName = UI.getDisplayName(obj.name); + + common.displayAvatar($(avatar), obj.avatar, displayName, Util.noop, obj.uid); var li = h('li.cp-autocomplete-value', [ avatar, - h('span', obj.name) + h('span', displayName), ]); return $(li).appendTo(ul); }; diff --git a/www/common/common-util.js b/www/common/common-util.js index ea8d54256..bec6cb125 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -75,7 +75,9 @@ handlers.push(cb); }, unreg: function (cb) { - if (handlers.indexOf(cb) === -1) { throw new Error("Not registered"); } + if (handlers.indexOf(cb) === -1) { + return void console.error("event handler was already unregistered"); + } handlers.splice(handlers.indexOf(cb), 1); }, fire: function () { diff --git a/www/common/cryptget.js b/www/common/cryptget.js index a1d0eef92..c4c17eb70 100644 --- a/www/common/cryptget.js +++ b/www/common/cryptget.js @@ -1,7 +1,7 @@ define([ '/bower_components/chainpad-crypto/crypto.js', - '/bower_components/chainpad-netflux/chainpad-netflux.js', - '/bower_components/netflux-websocket/netflux-client.js', + 'chainpad-netflux', + 'netflux-client', '/common/common-util.js', '/common/common-hash.js', '/common/common-realtime.js', diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 15234c35e..65839290e 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -161,7 +161,7 @@ define([ common.makeNetwork = function (cb) { require([ - '/bower_components/netflux-websocket/netflux-client.js', + 'netflux-client', '/common/outer/network-config.js' ], function (Netflux, NetConfig) { var wsUrl = NetConfig.getWebsocketURL(); diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index fe87e23da..d2f76ef8d 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -2759,6 +2759,7 @@ define([ var $warning = $(warning).hide(); var $url = $(url).on('change keypress keyup keydown', function () { var v = $url.val().trim(); + $url.toggleClass('cp-input-invalid', !Util.isValidURL(v)); if (v.length > 200) { $warning.show(); return; @@ -2783,7 +2784,6 @@ define([ var u = $url.val().trim(); if (!n || !u) { return true; } if (!Util.isValidURL(u)) { - // XXX 4.11.0 add style for invalid input? input:invalid UI.warn(Messages.fm_link_invalid); return true; } diff --git a/www/common/inner/access.js b/www/common/inner/access.js index 3f281dc47..0ba857a0a 100644 --- a/www/common/inner/access.js +++ b/www/common/inner/access.js @@ -171,7 +171,7 @@ define([ if (!Object.keys(_friends).length) { var friendText; if (!friendKeys.length) { - console.error(UIElements.noContactsMessage(common)); + //console.error(UIElements.noContactsMessage(common)); var findContacts = UIElements.noContactsMessage(common); friendText = h('span.cp-app-prop-content', findContacts.content @@ -772,7 +772,8 @@ define([ if (friend.edPublic !== ed || c === 'me') { return; } _owners[friend.edPublic] = { name: friend.displayName, - avatar: friend.avatar + avatar: friend.avatar, + uid: friend.uid, }; return true; })) { @@ -782,6 +783,11 @@ define([ _owners[ed] = { avatar: '?', name: Messages.owner_unknownUser, + // TODO a possible enhancement is to use data from the context + // ie. if you have opened the access modal from within the pad + // its owner might be present or they might have left some data + // in the pad itself (as is the case of the uid in rich text comments) + // TODO or just implement "Acquaintances" }; strangers++; }); diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 1d88e1029..33e5d74f7 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -6,12 +6,13 @@ define([ '/common/hyperscript.js', '/common/media-tag.js', '/customize/messages.js', + '/customize/application_config.js', '/bower_components/tweetnacl/nacl-fast.min.js', '/bower_components/croppie/croppie.min.js', '/bower_components/file-saver/FileSaver.min.js', 'css!/bower_components/croppie/croppie.css', -], function ($, Util, Hash, UI, h, MediaTag, Messages) { +], function ($, Util, Hash, UI, h, MediaTag, Messages, AppConfig) { var MT = {}; var Nacl = window.nacl; @@ -43,9 +44,16 @@ define([ }); }; + var animal_avatars = {}; MT.getCursorAvatar = function (cursor) { + var uid = cursor.uid; + // TODO it would be nice to have "{0} is editing" instead of just their name var html = ''; - html += (cursor.avatar && avatars[cursor.avatar]) || ''; + if (cursor.avatar && avatars[cursor.avatar]) { + html += avatars[cursor.avatar]; + } else if (animal_avatars[uid]) { + html += animal_avatars[uid] + ' '; + } html += Util.fixHTML(cursor.name) + ''; return html; }; @@ -79,12 +87,63 @@ define([ }); }; - MT.displayAvatar = function (common, $container, href, name, _cb) { + // https://emojipedia.org/nature/ + var ANIMALS = AppConfig.emojiAvatars || []; + + var getPseudorandomAnimal = MT.getPseudorandomAnimal = function (seed) { + if (!ANIMALS.length) { return ''; } + if (typeof(seed) !== 'string') { return; } + seed = seed.replace(/\D/g, '').slice(0, 10); // TODO possible optimization for on-wire uid + seed = parseInt(seed); + if (!seed) { return; } + return ANIMALS[seed % ANIMALS.length] || ''; + }; + + var getPrettyInitials = MT.getPrettyInitials = function (name) { + var parts = name.split(/\s+/); + var text; + if (parts.length > 1) { + text = parts.slice(0, 2).map(Util.getFirstCharacter).join(''); + } else { + text = Util.getFirstCharacter(name); + var second = Util.getFirstCharacter(name.replace(text, '')); + if (second && second !== '?') { + text += second; + } + } + return text; + }; + + MT.displayAvatar = function (common, $container, href, name, _cb, uid) { var cb = Util.once(Util.mkAsync(_cb || function () {})); var displayDefault = function () { - var text = Util.getFirstCharacter(name || Messages.anonymous); - var $avatar = $('', {'class': 'cp-avatar-default'}).text(text); + var animal_avatar; + if (uid && animal_avatars[uid]) { + animal_avatar = animal_avatars[uid]; + } + + name = UI.getDisplayName(name); + var text; + if (ANIMALS.length && name === Messages.anonymous && uid) { + if (animal_avatar) { + text = animal_avatar; + } else { + text = animal_avatar = getPseudorandomAnimal(uid); + } + } else { + text = getPrettyInitials(name); + } + + var $avatar = $('', { + 'class': 'cp-avatar-default' + (animal_avatar? ' animal': ''), + // this prevents screenreaders from trying to describe this + alt: '', + 'aria-hidden': true, + }).text(text); $container.append($avatar); + if (uid && animal_avatar) { + animal_avatars[uid] = animal_avatar; + } if (cb) { cb(); } }; if (!window.Symbol) { return void displayDefault(); } // IE doesn't have Symbol @@ -97,6 +156,7 @@ define([ return void cb($el); } + var centerImage = function ($img, $image) { var img = $image[0]; var w = img.width; diff --git a/www/common/inner/share.js b/www/common/inner/share.js index 4bcf0d621..73d736346 100644 --- a/www/common/inner/share.js +++ b/www/common/inner/share.js @@ -76,6 +76,7 @@ define([ var shareButton = { className: 'primary cp-share-with-friends', name: Messages.share_withFriends, + iconClass: '.fa.fa-shhare-alt', onClick: function () { var href; nThen(function (waitFor) { @@ -420,12 +421,12 @@ define([ embed: Util.isChecked($link.find('#cp-share-embed')) })); }); - var linkButtons = [ makeCancelButton(), !opts.sharedFolder && { className: 'secondary cp-nobar', name: Messages.share_linkOpen, + iconClass: '.fa.fa-eye', onClick: function () { opts.saveValue(); var v = opts.getLinkValue({ @@ -442,6 +443,7 @@ define([ }, { className: 'primary cp-nobar', name: Messages.share_linkCopy, + iconClass: '.fa.fa-link', onClick: function () { opts.saveValue(); var v = opts.getLinkValue({ @@ -496,6 +498,7 @@ define([ { className: 'primary', name: Messages.share_linkCopy, + iconClass: '.fa.fa-link', onClick: function () { Feedback.send('SHARE_EMBED'); var v = opts.getEmbedValue(); @@ -870,6 +873,7 @@ define([ { className: 'primary', name: Messages.share_linkCopy, + iconClass: '.fa.fa-link', onClick: function () { var v = opts.getLinkValue(); var success = Clipboard.copy(v); @@ -915,6 +919,7 @@ define([ }, { className: 'primary', name: Messages.share_mediatagCopy, + iconClass: '.fa.fa-link', onClick: function () { var v = common.getMediatagFromHref(opts.fileData); var success = Clipboard.copy(v); diff --git a/www/common/media-tag.js b/www/common/media-tag.js index 15b038724..6b5735294 100644 --- a/www/common/media-tag.js +++ b/www/common/media-tag.js @@ -87,6 +87,7 @@ var factory = function () { image: function (metadata, url, content, cfg, cb) { var img = document.createElement('img'); img.setAttribute('src', url); + img.setAttribute('alt', metadata.alt || ""); img.blob = content; cb(void 0, img); }, @@ -94,12 +95,15 @@ var factory = function () { var video = document.createElement('video'); video.setAttribute('src', url); video.setAttribute('controls', true); + // https://discuss.codecademy.com/t/can-we-use-an-alt-attribute-with-the-video-tag/300322/4 + video.setAttribute('title', metadata.alt || ""); cb(void 0, video); }, audio: function (metadata, url, content, cfg, cb) { var audio = document.createElement('audio'); audio.setAttribute('src', url); audio.setAttribute('controls', true); + audio.setAttribute('alt', metadata.alt || ""); cb(void 0, audio); }, pdf: function (metadata, url, content, cfg, cb) { @@ -115,6 +119,7 @@ var factory = function () { download: function (metadata, url, content, cfg, cb) { var btn = document.createElement('button'); btn.setAttribute('class', 'btn btn-default'); + btn.setAttribute('alt', metadata.alt || ""); btn.innerHTML = '' + cfg.download.text + '
' + (metadata.name ? '' + fixHTML(metadata.name) + '' : ''); btn.addEventListener('click', function () { @@ -542,7 +547,7 @@ var factory = function () { // Process var process = function (mediaObject, decrypted, cfg, cb) { - var metadata = decrypted.metadata; + var metadata = decrypted.metadata || {}; var blob = decrypted.content; var mediaType = getType(mediaObject, metadata, cfg); @@ -596,6 +601,15 @@ var factory = function () { }); }; + var initHandlers = function () { + return { + 'progress': [], + 'complete': [], + 'metadata': [], + 'error': [] + }; + }; + // Initialize a media-tag var init = function (el, cfg) { cfg = cfg || {}; @@ -613,13 +627,7 @@ var factory = function () { }; } - var handlers = cfg.handlers || { - 'progress': [], - 'complete': [], - 'metadata': [], - 'error': [] - }; - + var handlers = cfg.handlers || initHandlers(); var mediaObject = el._mediaObject = { handlers: handlers, tag: el @@ -762,6 +770,24 @@ var factory = function () { init.fetchDecryptedMetadata = fetchDecryptedMetadata; + init.preview = function (content, metadata, cfg, cb) { + cfg = cfg || {}; + addMissingConfig(cfg, config); + var handlers = cfg.handlers || initHandlers(); + var el = document.createElement('media-tag'); + var mediaObject = el._mediaObject = { + handlers: handlers, + tag: el, + }; + process(mediaObject, { + metadata: metadata, + content: content + }, cfg, function (err) { + if (err) { return void cb(err); } + cb(void 0, el); + }); + }; + return init; }; diff --git a/www/common/messenger-ui.js b/www/common/messenger-ui.js index 76a756a11..494001624 100644 --- a/www/common/messenger-ui.js +++ b/www/common/messenger-ui.js @@ -190,9 +190,11 @@ define([ markup.message = function (msg) { if (msg.type !== 'MSG') { return; } var curvePublic = msg.author; + // FIXME this assignment looks like it has some holes in its logic + // but I'm scared to touch it because it looks like it was hacked to fix some bugs var name = (typeof msg.name !== "undefined" || !contactsData[msg.author]) ? (msg.name || Messages.anonymous) : - contactsData[msg.author].displayName; + contactsData[msg.author].displayName || Messages.anonymous; var d = msg.time ? new Date(msg.time) : undefined; var day = d ? d.toLocaleDateString() : ''; var hour = d ? d.toLocaleTimeString() : ''; @@ -239,7 +241,7 @@ define([ }); var chan = state.channels[id]; - var displayName = chan.name; + var displayName = UI.getDisplayName(chan.name || chan.displayName); var fetching = false; var $moreHistory = $(moreHistory).click(function () { @@ -364,7 +366,7 @@ define([ avatars[friend.avatar] = $img[0].outerHTML; } $(rightCol).insertAfter($avatar); - }); + }, friend.uid); } var sending = false; @@ -544,7 +546,7 @@ define([ title: Messages.contacts_online }); var rightCol = h('span.cp-app-contacts-right-col', [ - h('span.cp-app-contacts-name', [room.name]), + h('span.cp-app-contacts-name', [room.isFriendChat? UI.getDisplayName(room.name): room.name]), h('span.cp-app-contacts-icons', [ room.isFriendChat ? mute : undefined, room.isFriendChat ? unmute : undefined, @@ -609,7 +611,7 @@ define([ avatars[friendData.avatar] = $img[0].outerHTML; } $room.append(rightCol); - }); + }, friendData.uid); } $room.append(status); return $room; @@ -631,9 +633,9 @@ define([ var el_message = markup.message(message); if (message.type === 'MSG') { - var name = typeof message.name !== "undefined" ? - (message.name || Messages.anonymous) : - contactsData[message.author].displayName; + var name = UI.getDisplayName(typeof message.name !== "undefined" ? + message.name: + contactsData[message.author].displayName); common.notify({ title: name, msg: message.text, @@ -826,6 +828,11 @@ define([ } }; +/* The following block is for a disabled feature which allows users to switch + between pad chat (when in the context of a pad) and direct chats with their + contacts. +*/ +/* common.getMetadataMgr().onTitleChange(function () { var padChat = common.getPadChat(); var md = common.getMetadataMgr().getMetadata(); @@ -839,11 +846,14 @@ define([ $lAvatar.find('.cp-avatar-default, media-tag').remove(); var $div = $('
'); + // There should always be a title here (defaultTitle if nothing else) + // so we don't ever need to supply a uid for an animal avatar common.displayAvatar($div, null, name, function () { $mAvatar.html($div.html()); $lAvatar.find('.cp-app-contacts-right-col').before($div.html()); }); }); +*/ // TODO room // var onJoinRoom @@ -878,7 +888,7 @@ define([ h('i.fa.fa-bell'), Messages.contacts_unmute || 'unmute' ]); - common.displayAvatar($(avatar), data.avatar, data.name); + common.displayAvatar($(avatar), data.avatar, data.name, Util.noop, data.uid); $(button).click(function () { unmuteUser(curve, button); execCommand('UNMUTE_USER', curve, function (e, data) { @@ -894,7 +904,7 @@ define([ }); return h('div.cp-contacts-muted-user', [ h('span', avatar), - h('span', data.name), + h('span', UI.getDisplayName(data.name)), button ]); }); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 130926e6a..cd8887a15 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -26,9 +26,9 @@ define([ '/bower_components/chainpad-crypto/crypto.js', '/bower_components/chainpad/chainpad.dist.js', - '/bower_components/chainpad-netflux/chainpad-netflux.js', - '/bower_components/chainpad-listmap/chainpad-listmap.js', - '/bower_components/netflux-websocket/netflux-client.js', + 'chainpad-netflux', + 'chainpad-listmap', + 'netflux-client', '/bower_components/nthen/index.js', '/bower_components/saferphore/index.js', ], function (ApiConfig, Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback, diff --git a/www/common/outer/calendar.js b/www/common/outer/calendar.js index bea22db6b..a0b990e89 100644 --- a/www/common/outer/calendar.js +++ b/www/common/outer/calendar.js @@ -6,7 +6,7 @@ define([ '/common/outer/cache-store.js', '/customize/messages.js', '/bower_components/nthen/index.js', - '/bower_components/chainpad-listmap/chainpad-listmap.js', + 'chainpad-listmap', '/bower_components/chainpad-crypto/crypto.js', '/bower_components/chainpad/chainpad.dist.js', ], function (Util, Hash, Constants, Realtime, Cache, Messages, nThen, Listmap, Crypto, ChainPad) { diff --git a/www/common/outer/cursor.js b/www/common/outer/cursor.js index 95b2b34c6..b1a5e7cd0 100644 --- a/www/common/outer/cursor.js +++ b/www/common/outer/cursor.js @@ -187,6 +187,7 @@ define([ data.color = Util.find(proxy, ['settings', 'general', 'cursor', 'color']); data.name = proxy[Constants.displayNameKey] || ctx.store.noDriveName || Messages.anonymous; data.avatar = Util.find(proxy, ['profile', 'avatar']); + data.uid = Util.find(proxy, ['uid']) || ctx.store.noDriveUid; c.cursor = data; sendMyCursor(ctx, client); cb(); diff --git a/www/common/outer/mailbox.js b/www/common/outer/mailbox.js index a7269c6e5..dbe95e84d 100644 --- a/www/common/outer/mailbox.js +++ b/www/common/outer/mailbox.js @@ -7,7 +7,7 @@ define([ '/common/common-messaging.js', '/common/notify.js', '/common/outer/mailbox-handlers.js', - '/bower_components/chainpad-netflux/chainpad-netflux.js', + 'chainpad-netflux', '/bower_components/chainpad-crypto/crypto.js', ], function (Config, BCast, Util, Hash, Realtime, Messaging, Notify, Handlers, CpNetflux, Crypto) { var Mailbox = {}; diff --git a/www/common/outer/profile.js b/www/common/outer/profile.js index 733b2b4c4..7cba963c7 100644 --- a/www/common/outer/profile.js +++ b/www/common/outer/profile.js @@ -3,7 +3,7 @@ define([ '/common/common-hash.js', '/common/common-constants.js', '/common/common-realtime.js', - '/bower_components/chainpad-listmap/chainpad-listmap.js', + 'chainpad-listmap', '/bower_components/chainpad-crypto/crypto.js', '/bower_components/chainpad/chainpad.dist.js', ], function (Util, Hash, Constants, Realtime, Listmap, Crypto, ChainPad) { diff --git a/www/common/outer/roster.js b/www/common/outer/roster.js index e395891b7..6aa80e5d8 100644 --- a/www/common/outer/roster.js +++ b/www/common/outer/roster.js @@ -918,7 +918,7 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto, Feedback) define([ '/common/common-util.js', '/common/common-hash.js', - '/bower_components/chainpad-netflux/chainpad-netflux.js', + 'chainpad-netflux', 'json.sortify', '/bower_components/nthen/index.js', '/bower_components/chainpad-crypto/crypto.js', diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js index 6735330b4..4579492ba 100644 --- a/www/common/outer/sharedfolder.js +++ b/www/common/outer/sharedfolder.js @@ -6,7 +6,7 @@ define([ '/bower_components/nthen/index.js', '/bower_components/chainpad-crypto/crypto.js', - '/bower_components/chainpad-listmap/chainpad-listmap.js', + 'chainpad-listmap', '/bower_components/chainpad/chainpad.dist.js', ], function (Hash, Util, UserObject, Cache, nThen, Crypto, Listmap, ChainPad) { diff --git a/www/common/outer/team.js b/www/common/outer/team.js index 94f91c807..32ce35f3b 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -14,9 +14,9 @@ define([ '/common/cryptget.js', '/common/outer/cache-store.js', - '/bower_components/chainpad-listmap/chainpad-listmap.js', + 'chainpad-listmap', '/bower_components/chainpad-crypto/crypto.js', - '/bower_components/chainpad-netflux/chainpad-netflux.js', + 'chainpad-netflux', '/bower_components/chainpad/chainpad.dist.js', '/bower_components/nthen/index.js', '/bower_components/saferphore/index.js', diff --git a/www/common/requireconfig.js b/www/common/requireconfig.js index 4e3baf857..7ed772575 100644 --- a/www/common/requireconfig.js +++ b/www/common/requireconfig.js @@ -16,6 +16,9 @@ define([ cm: '/bower_components/codemirror', 'tui-code-snippet': '/lib/calendar/tui-code-snippet.min', 'tui-date-picker': '/lib/calendar/date-picker', + 'netflux-client': '/bower_components/netflux-websocket/netflux-client', + 'chainpad-netflux': '/bower_components/chainpad-netflux/chainpad-netflux', + 'chainpad-listmap': '/bower_components/chainpad-listmap/chainpad-listmap', }, map: { '*': { diff --git a/www/common/sframe-common-file.js b/www/common/sframe-common-file.js index 1286da14d..facd27f22 100644 --- a/www/common/sframe-common-file.js +++ b/www/common/sframe-common-file.js @@ -11,10 +11,12 @@ define([ '/common/hyperscript.js', '/customize/messages.js', '/customize/pages.js', + '/bower_components/nthen/index.js', + '/common/media-tag.js', '/bower_components/file-saver/FileSaver.min.js', '/bower_components/tweetnacl/nacl-fast.min.js', -], function ($, ApiConfig, FileCrypto, MakeBackup, Thumb, UI, UIElements, Util, Hash, h, Messages, Pages) { +], function ($, ApiConfig, FileCrypto, MakeBackup, Thumb, UI, UIElements, Util, Hash, h, Messages, Pages, nThen, MT) { var Nacl = window.nacl; var module = {}; @@ -312,7 +314,8 @@ define([ }); return manualStore; }; - var fileUploadModal = function (defaultFileName, cb) { + + var fileUploadModal = function (defaultFileName, cb, preview) { var parsedName = /^(\.?.+?)(\.[^.]+)?$/.exec(defaultFileName) || []; var ext = parsedName[2] || ""; @@ -321,9 +324,14 @@ define([ // Ask for name, password and owner var content = h('div', [ h('h4', Messages.upload_modal_title), + (preview? h('div#cp-upload-preview-container', preview): undefined), UIElements.setHTML(h('label', {for: 'cp-upload-name'}), Messages._getKey('upload_modal_filename', [ext])), h('input#cp-upload-name', {type: 'text', placeholder: defaultFileName, value: defaultFileName}), + + h('label', {for: 'cp-upload-alt'}, Messages.upload_addOptionalAlt), + h('input#cp-upload-alt', {type: 'text', placeholder: Messages.upload_modal_alt}), + h('label', {for: 'cp-upload-password'}, Messages.addOptionalPassword), UI.passwordInput({id: 'cp-upload-password'}), h('span', { @@ -335,7 +343,8 @@ define([ manualStore ]); - $(content).find('#cp-upload-owned').on('change', function () { + var $content = $(content); + $content.find('#cp-upload-owned').on('change', function () { var val = Util.isChecked($(content).find('#cp-upload-owned')); if (val) { $(content).find('#cp-upload-store').prop('checked', true).prop('disabled', true); @@ -348,8 +357,9 @@ define([ if (!yes) { return void cb(); } // Get the values - var newName = $(content).find('#cp-upload-name').val(); - var password = $(content).find('#cp-upload-password').val() || undefined; + var newName = $content.find('#cp-upload-name').val(); + var password = $content.find('#cp-upload-password').val() || undefined; + var alt = $content.find('#cp-upload-alt').val() || undefined; var owned = Util.isChecked($(content).find('#cp-upload-owned')); var forceSave = owned || Util.isChecked($(content).find('#cp-upload-store')); @@ -366,7 +376,8 @@ define([ name: newName, password: password, owned: owned, - forceSave: forceSave + forceSave: forceSave, + alt: alt, }); }); }; @@ -437,6 +448,8 @@ define([ } var thumb; + var preview; + var alt; var file_arraybuffer; var name = file.name; var password; @@ -447,6 +460,7 @@ define([ var metadata = { name: name, type: type, + alt: alt, }; if (thumb) { metadata.thumbnail = thumb; } queue.push({ @@ -486,8 +500,9 @@ define([ password = obj.password; owned = obj.owned; forceSave = obj.forceSave; + alt = obj.alt; finish(); - }); + }, preview); } }; @@ -495,11 +510,20 @@ define([ if (e) { console.error(e); } file_arraybuffer = buffer; if (!Thumb.isSupportedType(file)) { return getName(); } - // make a resized thumbnail from the image.. - Thumb.fromBlob(file, function (e, thumb64) { - if (e) { console.error(e); } - if (!thumb64) { return getName(); } - thumb = thumb64; + nThen(function (w) { + // make a resized thumbnail from the image.. + Thumb.fromBlob(file, w(function (e, thumb64) { + if (e) { console.error(e); } + if (!thumb64) { return; } + thumb = thumb64; + })); + MT.preview(buffer, { + type: file.type, + }, void 0, w(function (err, el) { + if (err) { return void console.error(err); } + preview = el; + })); + }).nThen(function () { getName(); }); }); diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 206137c86..b7efd9f4f 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -235,9 +235,6 @@ define([ }; }; - funcs.getAuthorId = function () { - }; - var authorUid = function(existing) { if (!Array.isArray(existing)) { existing = []; } var n; @@ -249,11 +246,25 @@ define([ if (existing.indexOf(n) !== -1) { n = 0; } return n; }; - funcs.getAuthorId = function(authors, curve) { + funcs.getAuthorId = function(authors, curve, tokenId) { var existing = Object.keys(authors || {}).map(Number); - if (!funcs.isLoggedIn()) { return authorUid(existing); } - var uid; + var loggedIn = funcs.isLoggedIn(); + if (!loggedIn && !tokenId) { return authorUid(existing); } + if (!loggedIn) { + existing.some(function (id) { + var author = authors[id]; + if (!author || author.uid !== tokenId) { return; } + uid = Number(id); + return true; + }); + return uid || authorUid(existing); + } + // TODO this should check for a matching curvePublic / uid if: + // 1. you are logged in OR + // 2. you have a token + // so that users that register recognize comments from before + // they registered as their own (same uid) existing.some(function(id) { var author = authors[id] || {}; if (author.curvePublic !== curve) { return; } diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 31a40ffeb..8931c57f5 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -53,6 +53,7 @@ MessengerUI, Messages, Pages) { var USERADMIN_CLS = Bar.constants.user = 'cp-toolbar-user-dropdown'; var USERNAME_CLS = Bar.constants.username = 'cp-toolbar-user-name'; /*var READONLY_CLS = */Bar.constants.readonly = 'cp-toolbar-readonly'; + var USERBUTTON_CLS = Bar.constants.changeUsername = "cp-toolbar-user-rename"; // Create the toolbar element @@ -250,6 +251,7 @@ MessengerUI, Messages, Pages) { var friendRequests = Common.getFriendRequests(); // Friend requests received editUsersNames.forEach(function (data) { var name = data.name || Messages.anonymous; + var safeName = Util.fixHTML(name); var $span = $('', {'class': 'cp-avatar'}); if (data.color && showColors) { $span.css('border-color', data.color); @@ -324,7 +326,7 @@ MessengerUI, Messages, Pages) { $('