diff --git a/CHANGELOG.md b/CHANGELOG.md index 60f86b192..b03ef9bc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,94 @@ +# 4.11.0 (WIP) + +## Goals + +## Update notes + +* warning about lack of support for internet explorer + * existing support will get worse over time. please update. + * we only support IE for the home page and related info pages. Apps with complex functionality assume you are using a regularly updated browser. +* this release includes new clientside dependencies. Don't forget to run `bower 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' +* 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) + * conditionally displayed sections depending on the state of previous answers + * nicer participant view without CryptPad toolbar or popups + * response page to with customizable message to thank those that have responded + * more granular form controls and clearer text + * anonymization settings for answers + * optional restriction of a form to registered users only + * real-time form authorship. + * changes are saved as you type, so you no longer need to "save" each question. + * co-author surveys with other users and edit the same question concurrently. + * avoid redrawing active parts of the UI when other authors make a change (datepicker UI, dropdowns, etc.) + * redraw no more than once every 500ms for performance reasons + * preserve current scroll position when other users make changes + * easier access to basic for form authors in the left sidebar: + * preview a form + * copy the participant link + * view existing responses + * more intuitive display of answers + * bar charts throughout, wherever applicable + * options with no answers are still displayed with zero results in the summary rather than not being displayed at all + * options are displayed according to the order of their appearance in the original question, rather than according to the order in which participants chose them + * the number of empty answers is displayed above the scrollable section of each answer's summary rather than at the bottom + * more intuitive controls and default options + * placeholders for text inputs instead of pre-filled fields + * "enter" creates a new field + * "esc" clears an empty field + * easy navigation using the tab key + * convert between related question types: + * radio, checkbox, ranked choices + * multi-radio, multi-check + * more form validation options: + * required questions + * validated question types + * summarize invalid answers at the bottom of the form. jump to the relevant question when clicked. + * CryptPad logo displayed at the bottom of the participant page which links to the home page + * we've pre-filled some options in our "simple scheduling poll" template. +* 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 +* our link creation UI from 4.9.0 now highlights the URL input field as you type to indicate whether the current URL value is valid +* the share menu now makes its primary actions more clear, with explicit text ("copy link" instead of just "copy") on its main buttons, as well as icons that better match button UI on the rest of the platform. +* we're working towards better accessibility for screen readers with better alt-text and `aria-` attributes to suppress descriptions of strictly visual UI features. + +## 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 +* links shared by contacts could be previewed in a modal when viewing their notification. The color of the previewed link was overridden by some bootstrap styles. we now use a better color. +* better validation for team invite links where badly formed invite content could have triggered a type error. + + # 4.10.0 ## Goals diff --git a/bower.json b/bower.json index efa70c1bb..81acde661 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", 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/colortheme-dark.less b/customize.dist/src/less2/include/colortheme-dark.less index 5c1649850..1c71d4f05 100644 --- a/customize.dist/src/less2/include/colortheme-dark.less +++ b/customize.dist/src/less2/include/colortheme-dark.less @@ -68,6 +68,25 @@ @cryptpad_color_yellow_fader: fade(#FFE69C, 15%); // not in light theme @cryptpad_color_lighter_blue: #d2e1f2; +@cp_palette: + #FFD4D4, + #FFDECA, + #FFE69C, + #DBFFB7, + #AFFDC2, + #C9FFFE, + #C8D6FF, + #E4CAFF; +@cp_palette-dark: + darken(desaturate(extract(@cp_palette, 1),60%), 60%), + darken(desaturate(extract(@cp_palette, 2),60%), 60%), + darken(desaturate(extract(@cp_palette, 3),55%), 60%), + darken(desaturate(extract(@cp_palette, 4),55%), 70%), + darken(desaturate(extract(@cp_palette, 5),60%), 65%), + darken(desaturate(extract(@cp_palette, 6),60%), 70%), + darken(desaturate(extract(@cp_palette, 7),60%), 60%), + darken(desaturate(extract(@cp_palette, 8),70%), 60%); + @cryptpad_color_link:@cryptpad_color_brand_300; // Premium plans colors @@ -353,15 +372,7 @@ @cp_kanban-add-hover: fade(@cryptpad_color_black, 10%); @cp_kanban-trash-bg: @cryptpad_color_warn_red; @cp_kanban-color0: @cryptpad_color_grey_600; -@cp_kanban-colors: - darken(desaturate(#FFD4D4,60%), 60%), - darken(desaturate(#FFDECA,60%), 60%), - darken(desaturate(#FFE69C,55%), 60%), - darken(desaturate(#DBFFB7,55%), 70%), - darken(desaturate(#AFFDC2,60%), 65%), - darken(desaturate(#C9FFFE,60%), 70%), - darken(desaturate(#C8D6FF,60%), 60%), - darken(desaturate(#E4CAFF,70%), 60%); +@cp_kanban-colors: @cp_palette-dark; // Notifications @cp_notif-hover: fade(@cryptpad_color_black, 10%); @@ -438,3 +449,5 @@ @cp_form-poll-maybe: @cryptpad_color_grey_700; @cp_form-poll-yes-color: @cryptpad_color_green; @cp_form-invalid: @cryptpad_color_light_red; +@cp_form-palette: @cp_palette-dark; +@cp_form-palette2: @cp_palette; diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less index 18dcb1c0a..dd6115ea1 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -67,6 +67,25 @@ @cryptpad_color_yellow_fade: fade(#FFE69C, 50%); // different from dark @cryptpad_color_lighter_blue: #d2e1f2; +@cp_palette: + #FFD4D4, + #FFDECA, + #FFE69C, + #DBFFB7, + #AFFDC2, + #C9FFFE, + #C8D6FF, + #E4CAFF; +@cp_palette-dark: + darken(extract(@cp_palette, 1), 50%), + darken(extract(@cp_palette, 2), 51%), + darken(extract(@cp_palette, 3), 52%), + darken(extract(@cp_palette, 4), 61%), + darken(extract(@cp_palette, 5), 57%), + darken(extract(@cp_palette, 6), 65%), + darken(extract(@cp_palette, 7), 50%), + darken(extract(@cp_palette, 8), 50%); + @cryptpad_color_link: @cryptpad_color_brand; // Premium plans colors @@ -299,7 +318,7 @@ @cp_usergrid-selected-fg: @cryptpad_color_white; // Other -@cp_shadow-color: fade(@cryptpad_color_black, 40%); +@cp_shadow-color: fade(@cryptpad_color_black, 30%); // Apps @cp_app-bg: @cryptpad_color_grey_100; @@ -352,15 +371,7 @@ @cp_kanban-add-hover: fade(@cryptpad_color_black, 10%); @cp_kanban-trash-bg: @cryptpad_color_warn_red; @cp_kanban-color0: @cryptpad_color_grey_400; -@cp_kanban-colors: - #FFD4D4, - #FFDECA, - #FFE69C, - #DBFFB7, - #AFFDC2, - #C9FFFE, - #C8D6FF, - #E4CAFF; +@cp_kanban-colors: @cp_palette; // Notifications @cp_notif-hover: fade(@cryptpad_color_black, 10%); @@ -428,8 +439,8 @@ @cp_calendar-now-fg: @cryptpad_color_grey_200; // Forms -@cp_form-bg1: @cryptpad_color_grey_200; -@cp_form-bg2: @cryptpad_color_grey_100; +@cp_form-bg1: @cryptpad_color_grey_50; +@cp_form-bg2: @cryptpad_color_grey_200; @cp_form-border: @cryptpad_color_grey_200; @cp_form-poll-color: @cryptpad_color_grey_800; @cp_form-poll-no: fade(@cryptpad_color_light_red, 75%); @@ -437,3 +448,5 @@ @cp_form-poll-maybe: @cryptpad_color_grey_300; @cp_form-poll-yes-color: @cryptpad_color_green; @cp_form-invalid: @cryptpad_color_red; +@cp_form-palette: @cp_palette; +@cp_form-palette2: @cp_palette-dark; 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..ffb6f95c9 100644 --- a/customize.dist/src/less2/include/modals-ui-elements.less +++ b/customize.dist/src/less2/include/modals-ui-elements.less @@ -273,4 +273,29 @@ } } } + #cp-upload-preview-container { + max-width: 100%; + max-height: 300px; + overflow: auto; + margin-bottom: 15px; + // FIXME 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/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 8315768f1..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'); @@ -649,7 +651,7 @@ define([ if (!AppConfig.enableTemplates) { return; } if (!common.isLoggedIn()) { return; } button = $('