From e414524eab6bfc3f06ed3cd705d67f01c9880d27 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 19 Aug 2021 16:17:59 +0530 Subject: [PATCH 001/125] refactor form results display * improve table component reusability for the rest of the platform * display count of empty results at the top of each section * remove unused styles * fix incorrect methods for counting empty answers for multi-checkbox questions --- customize.dist/src/less2/include/charts.less | 52 ++++++++ www/form/app-form.less | 64 +--------- www/form/inner.js | 120 ++++++++++--------- 3 files changed, 117 insertions(+), 119 deletions(-) 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/www/form/app-form.less b/www/form/app-form.less index 53acb92af..5ef211201 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -519,69 +519,9 @@ i { margin-right: 5px; } background: fade(@cryptpad_text_col, 15%); } - .cp-form-results-type-text { - max-height: 300px; + .cp-form-results-contained { + max-height: 350px; // enough for 10 table entries overflow: auto; - .cp-form-results-type-text-data { - padding: 5px 10px; - background: @cp_form-bg2; - &:not(:last-child) { margin-bottom: 1px; } - } - } - .cp-form-results-type-textarea-data { - white-space: pre-wrap; - padding: 5px 10px; - background: @cp_form-bg2; - &:not(:last-child) { margin-bottom: 1px; } - } - - .cp-form-results-cell() { - border: 1px solid @cp_form-border; - display: table-cell; - padding: 5px 10px; - background: @cp_form-bg2; - } - - .cp-form-results-type-multiradio-data { - .cp-mr-q { - font-weight: bold; - padding: 5px 10px; - .cp-form-results-cell(); - } - &:not(:first-child) { - .cp-mr-q { - margin-top: 15px; - } - } - } - - .cp-form-results-type-radio { - display: table; - width: 100%; - .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-form-results-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%; - } - } - } - } } .cp-form-individual { background: @cp_form-bg1; diff --git a/www/form/inner.js b/www/form/inner.js index ec329830f..cb85ae29c 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -803,10 +803,31 @@ define([ return total; }; - var getEmpty = function (empty) { // TODO don't include this in the scrollable area - if (empty) { - return UI.setHTML(h('div.cp-form-results-type-text-empty'), Messages._getKey('form_notAnswered', [empty])); - } + var multiAnswerSubHeading = function (content) { + return h('span.cp-charts-row', h('td.cp-charts-cell', { + colspan: 3, + style: 'font-weight: bold;', + }, content)); + }; + + var getEmpty = function (empty) { + if (!empty) { return; } + var msg = UI.setHTML(h('span.cp-form-results-empty-text'), Messages._getKey('form_notAnswered', [empty])); + return multiAnswerSubHeading(msg); + }; + + var barGraphic = function (itemScale) { + return h('span.cp-bar-container', h('div.cp-bar', { + style: 'width: ' + (itemScale * 100) + '%', + }, ' ')); + }; + + var barRow = function (value, count, max, showBar) { + return h('div.cp-charts-row', [ + h('span.cp-value', value), + h('span.cp-count', count), + showBar? barGraphic((count / max)): undefined, + ]); }; var findItem = function (items, uid) { @@ -957,27 +978,21 @@ define([ return Array.isArray(A)? Math.max.apply(null, A): NaN; }; - var barGraphic = function (itemScale) { - return h('span.cp-bar-container', h('div.cp-bar', { - style: 'width: ' + (itemScale * 100) + '%', - }, ' ')); - }; - var renderTally = function (tally, empty, showBar) { var rows = []; var counts = Util.values(tally); var max = arrayMax(counts); + if (empty) { rows.push(getEmpty(empty)); } Object.keys(tally).forEach(function (value) { var itemCount = tally[value]; var itemScale = (itemCount / max); - rows.push(h('div.cp-form-results-type-radio-data', [ + rows.push(h('div.cp-charts-row', [ h('span.cp-value', value), h('span.cp-count', itemCount), showBar? barGraphic(itemScale): undefined, ])); }); - if (empty) { rows.push(getEmpty(empty)); } return rows; }; @@ -1015,34 +1030,35 @@ define([ reset: function () { $tag.val(''); } }; }, - printResults: function (answers, uid) { + printResults: function (answers, uid) { // results text var results = []; var empty = 0; var tally = {}; + var isEmpty = function (answer) { + console.error("EMPTY?", JSON.stringify(answer)); + return !answer || !answer.trim(); + }; + Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; - if (!answer || !answer.trim()) { return empty++; } + if (isEmpty(answer)) { return empty++; } Util.inc(tally, answer); }); //var counts = Util.values(tally); //var max = arrayMax(counts); //if (max < 2) { // there are no duplicates, so just return text + results.push(getEmpty(empty)); Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; if (!answer || !answer.trim()) { return empty++; } - results.push(h('div.cp-form-results-type-text-data', answer)); + results.push(h('div.cp-charts-row', h('span.cp-value', answer))); }); - results.push(getEmpty(empty)); - return h('div.cp-form-results-type-text', results); + return h('div.cp-form-results-contained', h('div.cp-charts.cp-text-table', results)); //} -/* - var rendered = renderTally(tally, empty); - return h('div.cp-form-results-type-text', rendered); -*/ }, icon: h('i.cptools.cptools-form-text') }, @@ -1104,11 +1120,11 @@ define([ var obj = answers[author]; var answer = obj.msg[uid]; if (!answer || !answer.trim()) { return empty++; } - results.push(h('div.cp-form-results-type-textarea-data', answer)); + results.push(h('div.cp-charts-row', h('span.cp-value', answer))); }); - results.push(getEmpty(empty)); + results.unshift(getEmpty(empty)); - return h('div.cp-form-results-type-text', results); + return h('div.cp-form-results-contained', h('div.cp-charts.cp-text-table', results)); }, icon: h('i.cptools.cptools-form-paragraph') }, @@ -1179,7 +1195,7 @@ define([ }); var rendered = renderTally(count, empty, showBars); - return h('div.cp-form-results-type-radio', rendered); + return h('div.cp-charts.cp-bar-table', rendered); }, icon: h('i.cptools.cptools-form-list-radio') }, @@ -1285,26 +1301,17 @@ define([ max = arrayMax(counts); }); + results.push(getEmpty(empty)); count_keys.forEach(function (q_uid) { var q = findItem(opts.items, q_uid); var c = count[q_uid]; - - var values = Object.keys(c).map(function (res) { - var itemCount = c[res]; - return h('div.cp-form-results-type-radio-data', [ - h('span.cp-value', res), - h('span.cp-count', itemCount), - showBars? barGraphic((itemCount / max)): undefined, - ]); + results.push(multiAnswerSubHeading(q)); + Object.keys(c).forEach(function (res) { + results.push(barRow(res, c[res], max, showBars)); }); - results.push(h('div.cp-form-results-type-multiradio-data', [ - h('span.cp-mr-q', q), - h('span.cp-mr-value', values) - ])); }); - results.push(getEmpty(empty)); - return h('div.cp-form-results-type-radio', results); + return h('div.cp-charts.cp-bar-table', results); }, exportCSV: function (answer, form) { var opts = form.opts || {}; @@ -1401,7 +1408,7 @@ define([ }); var rendered = renderTally(count, empty, showBars); - return h('div.cp-form-results-type-radio', rendered); + return h('div.cp-charts.cp-bar-table', rendered); }, icon: h('i.cptools.cptools-form-list-check') }, @@ -1498,10 +1505,19 @@ define([ var empty = 0; var count = {}; var showBars = Boolean(content); + + var isEmpty = function (answer) { + if (!answer) { return true; } + return !Object.keys(answer).some(function (k) { + var A = answer[k]; + return Array.isArray(A) && A.length; + }); + }; + Object.keys(answers).forEach(function (author) { var obj = answers[author]; var answer = obj.msg[uid]; - if (!answer || !Object.keys(answer).length) { return empty++; } + if (isEmpty(answer)) { return empty++; } Object.keys(answer).forEach(function (q_uid) { var c = count[q_uid] = count[q_uid] || {}; var res = answer[q_uid]; @@ -1520,26 +1536,17 @@ define([ max = arrayMax(counts); }); + results.push(getEmpty(empty)); count_keys.forEach(function (q_uid) { var q = findItem(opts.items, q_uid); var c = count[q_uid]; - - var values = Object.keys(c).map(function (res) { - var val = c[res]; - return h('div.cp-form-results-type-radio-data', [ - h('span.cp-value', res), - h('span.cp-count', val), - showBars? barGraphic(val / max) : undefined, - ]); + results.push(multiAnswerSubHeading(q)); + Object.keys(c).forEach(function (res) { + results.push(barRow(res, c[res], max, showBars)); }); - results.push(h('div.cp-form-results-type-multiradio-data', [ - h('span.cp-mr-q', q), - h('span.cp-mr-value', values), - ])); }); - results.push(getEmpty(empty)); - return h('div.cp-form-results-type-radio', results); + return h('div.cp-charts.cp-bar-table', results); }, exportCSV: function (answer, form) { var opts = form.opts || {}; @@ -1658,7 +1665,6 @@ define([ // results sort var opts = form[uid].opts || TYPES.sort.defaultOpts; var l = (opts.values || []).length; - //var results = []; var empty = 0; var count = {}; var showBars = Boolean(content); @@ -1673,7 +1679,7 @@ define([ }); var rendered = renderTally(count, empty, showBars); - return h('div.cp-form-results-type-radio', rendered); + return h('div.cp-charts.cp-bar-table', rendered); }, icon: h('i.cptools.cptools-form-list-ordered') }, From bfdcf4ec0ce38f9e2930a8d988cc038a4f6f4df9 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 19 Aug 2021 18:20:14 +0530 Subject: [PATCH 002/125] fix user/display name rendering which I accidentally broke in the user admin menu --- www/common/toolbar.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 8bd0fcc32..d103fe410 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 @@ -1028,6 +1029,12 @@ MessengerUI, Messages, Pages) { var userMenuCfg = { $initBlock: $userAdmin, }; + if (!config.hideDisplayName) { + $.extend(true, userMenuCfg, { + displayNameCls: USERNAME_CLS, + changeNameButtonCls: USERBUTTON_CLS, + }); + } if (config.readOnly !== 1) { userMenuCfg.displayName = 1; userMenuCfg.displayChangeName = 1; From eafe27ffb46a00fa16e4ea9369c0b51e58b30dee Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 19 Aug 2021 18:21:48 +0530 Subject: [PATCH 003/125] reuse cp-charts to visualize server task running time as bar charts --- www/admin/app-admin.less | 24 +++++++++++++++++++ www/admin/inner.js | 52 +++++++++++++++++++++++++--------------- 2 files changed, 57 insertions(+), 19 deletions(-) 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); }); }; From 82101bcb9b16ee2a7c187f4eab310bc8789ed358 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 19 Aug 2021 21:16:49 +0530 Subject: [PATCH 004/125] use two characters for the default avatar --- customize.dist/src/less2/include/avatar.less | 2 +- customize.dist/src/less2/include/toolbar.less | 2 +- www/common/inner/common-mediatag.js | 11 ++++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/customize.dist/src/less2/include/avatar.less b/customize.dist/src/less2/include/avatar.less index 725c7748f..02abc4050 100644 --- a/customize.dist/src/less2/include/avatar.less +++ b/customize.dist/src/less2/include/avatar.less @@ -4,7 +4,7 @@ @width: 30px ) { @avatar-width: @width; - @avatar-font-size: @width / 1.2; + @avatar-font-size: @width / 1.8; } .avatar_main(@width: 30px) { --LessLoader_require: LessLoader_currentFile(); diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index b0f9b5e42..28b513095 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -855,7 +855,7 @@ span { text-align: center; width: 100%; - font-size: 48px; + font-size: 40px; display: inline-flex; justify-content: center; align-items: center; diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 1d88e1029..03d85fab0 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -82,7 +82,16 @@ define([ MT.displayAvatar = function (common, $container, href, name, _cb) { var cb = Util.once(Util.mkAsync(_cb || function () {})); var displayDefault = function () { - var text = Util.getFirstCharacter(name || Messages.anonymous); + name = (name || "").trim() || Messages.anonymous; + 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); + text += Util.getFirstCharacter(name.replace(text, '')); + } + var $avatar = $('', {'class': 'cp-avatar-default'}).text(text); $container.append($avatar); if (cb) { cb(); } From b8c847bccef0001553cfc09724f38939dd8cd74c Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 19 Aug 2021 22:25:51 +0530 Subject: [PATCH 005/125] prototype animal avatars for guests that haven't set a custom name --- www/common/inner/common-mediatag.js | 34 ++++++++++++++++++++++++++--- www/common/toolbar.js | 5 +++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 03d85fab0..2dc912e02 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -79,21 +79,48 @@ define([ }); }; - MT.displayAvatar = function (common, $container, href, name, _cb) { + // https://emojipedia.org/nature/ + var ANIMALS = [ '🙈', '🦀', '🐞', '🦋', '🐬', '🐋', '🐢', '🦉', '🦆', '🐧', '🦡', '🦘', '🦨', '🦦', '🦥', '🐼', '🐻', '🦝', '🦄', '🐄', '🐷', '🐐', '🦙', '🦒', '🐘', '🦏', '🐁', '🐹', '🐰', '🦫', '🦔', '🐨']; + + var getRandomAnimal = function () { + return ANIMALS[Math.floor(Math.random() * ANIMALS.length)]; + }; + + var getPseudorandomAnimal = function (seed) { + if (typeof(seed) !== 'string') { return getRandomAnimal(); } + seed = seed.replace(/\D/g, '').slice(0, 10); + seed = parseInt(seed); + if (!seed) { return getRandomAnimal(); } + return ANIMALS[seed % ANIMALS.length]; + }; + + MT.displayAvatar = function (common, $container, href, name, _cb, uid) { var cb = Util.once(Util.mkAsync(_cb || function () {})); var displayDefault = function () { + if (avatars[uid]) { + var nodes = $.parseHTML(avatars[uid]); + var $el = $(nodes[0]); + $container.append($el); + return void cb($el); + } + var animal = false; + name = (name || "").trim() || Messages.anonymous; var parts = name.split(/\s+/); var text; - if (parts.length > 1) { + if (name === Messages.anonymous) { + text = getPseudorandomAnimal(uid); + animal = true; + } else if (parts.length > 1) { text = parts.slice(0, 2).map(Util.getFirstCharacter).join(''); } else { text = Util.getFirstCharacter(name); text += Util.getFirstCharacter(name.replace(text, '')); } - var $avatar = $('', {'class': 'cp-avatar-default'}).text(text); + var $avatar = $('', {'class': 'cp-avatar-default' + (animal? ' animal': '')}).text(text); $container.append($avatar); + avatars[uid] = $avatar[0].outerHTML; if (cb) { cb(); } }; if (!window.Symbol) { return void displayDefault(); } // IE doesn't have Symbol @@ -106,6 +133,7 @@ define([ return void cb($el); } + var centerImage = function ($img, $image) { var img = $image[0]; var w = img.width; diff --git a/www/common/toolbar.js b/www/common/toolbar.js index d103fe410..791857049 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -361,9 +361,10 @@ MessengerUI, Messages, Pages) { Common.openURL(origin+'/profile/#' + data.profile); }); } - Common.displayAvatar($span, data.avatar, name, function () { + console.error("AVATAR", $span, data.uid); + Common.displayAvatar($span, data.avatar, name, function () { // XXX pass a little more info so we can display better (pseudo-random) defaults $span.append($rightCol); - }); + }, data.uid); $span.data('uid', data.uid); $editUsersList.append($span); }); From 904e06091da39b6f56ef88382091e2411a30ff0f Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 20 Aug 2021 11:29:16 +0200 Subject: [PATCH 006/125] Fix calendar .ics import (#784) --- www/calendar/export.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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); } }); From c5e6ca646eb0ab2868f3c2c31602537e6cbfed19 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 20 Aug 2021 15:57:14 +0530 Subject: [PATCH 007/125] adjust animal avatar caching system and adjust size in the toolbar --- customize.dist/src/less2/include/toolbar.less | 3 +++ www/common/inner/common-mediatag.js | 21 ++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index 28b513095..d8029f0d2 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -200,6 +200,9 @@ .avatar_main(30px); .cp-avatar-default, media-tag { margin-right: 5px; + &.animal { + font-size: 20px; + } } &.cp-userlist-clickable { cursor: pointer; diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 2dc912e02..ece9e3024 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -80,7 +80,7 @@ define([ }; // https://emojipedia.org/nature/ - var ANIMALS = [ '🙈', '🦀', '🐞', '🦋', '🐬', '🐋', '🐢', '🦉', '🦆', '🐧', '🦡', '🦘', '🦨', '🦦', '🦥', '🐼', '🐻', '🦝', '🦄', '🐄', '🐷', '🐐', '🦙', '🦒', '🐘', '🦏', '🐁', '🐹', '🐰', '🦫', '🦔', '🐨']; + var ANIMALS = '🙈 🦀 🐞 🦋 🐬 🐋 🐢 🦉 🦆 🐧 🦡 🦘 🦨 🦦 🦥 🐼 🐻 🦝 🦓 🐄 🐷 🐐 🦙 🦒 🐘 🦏 🐁 🐹 🐰 🦫 🦔 🐨 🐱 🐺 👺 👹 👽 👾 🤖'.split(/\s+/); var getRandomAnimal = function () { return ANIMALS[Math.floor(Math.random() * ANIMALS.length)]; @@ -94,14 +94,13 @@ define([ return ANIMALS[seed % ANIMALS.length]; }; + var animal_avatars = {}; MT.displayAvatar = function (common, $container, href, name, _cb, uid) { var cb = Util.once(Util.mkAsync(_cb || function () {})); var displayDefault = function () { - if (avatars[uid]) { - var nodes = $.parseHTML(avatars[uid]); - var $el = $(nodes[0]); - $container.append($el); - return void cb($el); + var animal_avatar; + if (uid && animal_avatars[uid]) { + animal_avatar = animal_avatars[uid] } var animal = false; @@ -109,7 +108,11 @@ define([ var parts = name.split(/\s+/); var text; if (name === Messages.anonymous) { - text = getPseudorandomAnimal(uid); + if (animal_avatar) { + text = animal_avatar; + } else { + text = animal_avatar = getPseudorandomAnimal(uid); + } animal = true; } else if (parts.length > 1) { text = parts.slice(0, 2).map(Util.getFirstCharacter).join(''); @@ -120,7 +123,9 @@ define([ var $avatar = $('', {'class': 'cp-avatar-default' + (animal? ' animal': '')}).text(text); $container.append($avatar); - avatars[uid] = $avatar[0].outerHTML; + if (uid && animal) { + animal_avatars[uid] = animal_avatar; + } if (cb) { cb(); } }; if (!window.Symbol) { return void displayDefault(); } // IE doesn't have Symbol From ff1c4c9a65971033f085957ea4c4c8c6f3786005 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 20 Aug 2021 16:02:00 +0530 Subject: [PATCH 008/125] update the calendar list when calendar removal is successful also fall back to team.name when team.displayName is not available --- www/calendar/inner.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/www/calendar/inner.js b/www/calendar/inner.js index 21cb12789..a1fd8c8e8 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.kanban_delete), // XXX delete key is misleading... "Remove" ? `poll_remove`, `fc_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) From f0fad8e95c005474f8a9f248d2140ed1e60ea3be Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 20 Aug 2021 16:03:59 +0530 Subject: [PATCH 009/125] avoid unnecessary use of scrollbars on sidebar apps (settings, admin, support, ...) --- customize.dist/src/less2/include/sidebar-layout.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/customize.dist/src/less2/include/sidebar-layout.less b/customize.dist/src/less2/include/sidebar-layout.less index ef93eae4a..1b0c75932 100644 --- a/customize.dist/src/less2/include/sidebar-layout.less +++ b/customize.dist/src/less2/include/sidebar-layout.less @@ -69,7 +69,7 @@ background: @cp_sidebar-right-bg; color: @cp_sidebar-right-fg; overflow: auto; - padding-bottom: 200px; + //padding-bottom: 200px; // XXX what was the intent behind this? // Following rules are only in settings .cp-sidebarlayout-element { From 0fec83b051f3c44e58b0de205b0d927261b1fe8f Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 20 Aug 2021 17:47:54 +0530 Subject: [PATCH 010/125] factor debugging data generation from support ticket transmission and fix two incorrectly set width/height properties which overwrote 'appVersion' --- www/support/ui.js | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/www/support/ui.js b/www/support/ui.js index fac6ef438..2f97ec412 100644 --- a/www/support/ui.js +++ b/www/support/ui.js @@ -10,14 +10,12 @@ define([ '/customize/messages.js', ], function ($, ApiConfig, h, UI, Hash, Util, Clipboard, UIElements, Messages) { - var send = function (ctx, id, type, data, dest) { + var getDebuggingData = function (ctx, data) { var common = ctx.common; - var supportKey = ApiConfig.supportMailbox; - var supportChannel = Hash.getChannelIdFromKey(supportKey); var metadataMgr = common.getMetadataMgr(); - var user = metadataMgr.getUserData(); var privateData = metadataMgr.getPrivateData(); - + var user = metadataMgr.getUserData(); + var teams = privateData.teams || {}; data = data || {}; data.sender = { @@ -34,16 +32,12 @@ define([ data.sender.quota = ctx.pinUsage; } - data.id = id; - data.time = +new Date(); - - var teams = privateData.teams || {}; if (!ctx.isAdmin) { data.sender.userAgent = Util.find(window, ['navigator', 'userAgent']); data.sender.vendor = Util.find(window, ['navigator', 'vendor']); data.sender.appVersion = Util.find(window, ['navigator', 'appVersion']); - data.sender.appVersion = Util.find(window, ['screen', 'width']); - data.sender.appVersion = Util.find(window, ['screen', 'height']); + data.sender.screenWidth = Util.find(window, ['screen', 'width']); + data.sender.screenHeight = Util.find(window, ['screen', 'height']); data.sender.blockLocation = privateData.blockLocation || ''; data.sender.teams = Object.keys(teams).map(function (key) { var team = teams[key]; @@ -57,7 +51,25 @@ define([ } return ret; }).filter(Boolean); + } + return data; + }; + + var send = function (ctx, id, type, data, dest) { + var common = ctx.common; + var supportKey = ApiConfig.supportMailbox; + var supportChannel = Hash.getChannelIdFromKey(supportKey); + var metadataMgr = common.getMetadataMgr(); + var user = metadataMgr.getUserData(); + var privateData = metadataMgr.getPrivateData(); + + data = getDebuggingData(ctx, data); + + data.id = id; + data.time = +new Date(); + + if (!ctx.isAdmin) { // "dest" is the recipient that is not the admin support mailbox. // In the support page, make sure dest is always ourselves. dest.channel = privateData.support; @@ -474,6 +486,10 @@ define([ ui.makeCloseMessage = function (content, hash) { return makeCloseMessage(ctx, content, hash); }; + ui.getDebuggingData = function (data) { + return getDebuggingData(ctx, data); + }; + return ui; }; From aef1b22291d36510ec31c755d8565671442be101 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Aug 2021 16:32:25 +0530 Subject: [PATCH 011/125] console.error instead of throwing when unregistering handlers --- www/common/common-util.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/www/common/common-util.js b/www/common/common-util.js index 3d2a8d0a4..fbed064dc 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 () { From 46e545a976ef9312f9aa0018aaca7dca614cedf1 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Aug 2021 16:34:51 +0530 Subject: [PATCH 012/125] lint compliance --- www/common/inner/common-mediatag.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index ece9e3024..3d4e82caf 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -100,7 +100,7 @@ define([ var displayDefault = function () { var animal_avatar; if (uid && animal_avatars[uid]) { - animal_avatar = animal_avatars[uid] + animal_avatar = animal_avatars[uid]; } var animal = false; From 4fe19c1ea4f67c41ab4d5fc34df4ac0da47b6dec Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Aug 2021 18:16:32 +0530 Subject: [PATCH 013/125] remove unnecessary example cards from default kanban board --- www/kanban/inner.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 38e933cf1..585c7c285 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -563,12 +563,12 @@ define([ "12": { "id": 12, "title": Messages.kanban_working, - "item": [3, 4] + "item": [], }, "13": { "id": 13, "title": Messages.kanban_done, - "item": [5, 6] + "item": [], } }, items: items From 9f52ec8dc713043cac8a41edc34acbeadb060658 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 23 Aug 2021 18:19:35 +0530 Subject: [PATCH 014/125] add new translation check to find duplicates and move all translation scripts into a dedicated folder --- package.json | 4 +- .../find-duplicate-translations.js | 55 +++++++++++++++++++ .../{ => translations}/lint-translations.js | 0 .../{ => translations}/unused-translations.js | 0 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 scripts/translations/find-duplicate-translations.js rename scripts/{ => translations}/lint-translations.js (100%) rename scripts/{ => translations}/unused-translations.js (100%) diff --git a/package.json b/package.json index a844167f5..2d1c3f626 100644 --- a/package.json +++ b/package.json @@ -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..f8b25497c --- /dev/null +++ b/scripts/translations/find-duplicate-translations.js @@ -0,0 +1,55 @@ +var Util = require("../lib/common-util"); +var EN = Util.clone(require("../www/common/translations/messages.json")); +var FR = Util.clone(require("../www/common/translations/messages.fr.json")); +var DE = Util.clone(require("../www/common/translations/messages.de.json")); +var JP = Util.clone(require("../www/common/translations/messages.ja.json")); + +var keys = Object.keys(EN); + +var duplicates = {}; +var addIfAbsent = function (A, e) { + if (A.includes(e)) { return; } + A.push(e); +}; +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 = EN[key]; + + //var duplicates = []; + keys.forEach(function (key2) { + if (key === key2) { return; } + var value2 = EN[key2]; + if (value === value2) { + markDuplicate(value, key, key2); + } + }); +}); + +// 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])); +}); + +// TODO iterate over all languages and + +// 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 + + + +// 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 100% rename from scripts/lint-translations.js rename to scripts/translations/lint-translations.js diff --git a/scripts/unused-translations.js b/scripts/translations/unused-translations.js similarity index 100% rename from scripts/unused-translations.js rename to scripts/translations/unused-translations.js From 385cd4e947aaa0494c21502fa7d92460a5b8ab2a Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 25 Aug 2021 12:05:25 +0530 Subject: [PATCH 015/125] handle single-character usernames when deriving initials from usernames and use emoji avatar in user admin button --- www/common/common-ui-elements.js | 7 +++++-- www/common/inner/common-mediatag.js | 23 +++++++++++++++++------ www/common/toolbar.js | 2 +- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 8315768f1..a4d84796d 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1993,9 +1993,11 @@ define([ var loadingAvatar; var to; var oldUrl = ''; + var oldUid = undefined; 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); @@ -2013,15 +2015,16 @@ define([ var newName = myData.name; var url = myData.avatar; $displayName.text(newName || Messages.anonymous); - if (accountName && oldUrl !== url) { + if ((accountName && oldUrl !== url) || !accountName && uid !== oldUid) { $avatar.html(''); Common.displayAvatar($avatar, url, newName || Messages.anonymous, function ($img) { oldUrl = url; + oldUid = uid; $userAdmin.find('> button').removeClass('cp-avatar'); if ($img) { $userAdmin.find('> button').addClass('cp-avatar'); } loadingAvatar = false; - }); + }, uid); return; } loadingAvatar = false; diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 3d4e82caf..bceb314b8 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -94,6 +94,21 @@ define([ return ANIMALS[seed % ANIMALS.length]; }; + var 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; + }; + var animal_avatars = {}; MT.displayAvatar = function (common, $container, href, name, _cb, uid) { var cb = Util.once(Util.mkAsync(_cb || function () {})); @@ -105,20 +120,16 @@ define([ var animal = false; name = (name || "").trim() || Messages.anonymous; - var parts = name.split(/\s+/); var text; - if (name === Messages.anonymous) { + if (name === Messages.anonymous && uid) { if (animal_avatar) { text = animal_avatar; } else { text = animal_avatar = getPseudorandomAnimal(uid); } animal = true; - } else if (parts.length > 1) { - text = parts.slice(0, 2).map(Util.getFirstCharacter).join(''); } else { - text = Util.getFirstCharacter(name); - text += Util.getFirstCharacter(name.replace(text, '')); + text = getPrettyInitials(name); } var $avatar = $('', {'class': 'cp-avatar-default' + (animal? ' animal': '')}).text(text); diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 791857049..7493d9996 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -362,7 +362,7 @@ MessengerUI, Messages, Pages) { }); } console.error("AVATAR", $span, data.uid); - Common.displayAvatar($span, data.avatar, name, function () { // XXX pass a little more info so we can display better (pseudo-random) defaults + Common.displayAvatar($span, data.avatar, name, function () { $span.append($rightCol); }, data.uid); $span.data('uid', data.uid); From 8d579c037645ff3b33bdb4c059b8ba1a90f7ba66 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 25 Aug 2021 12:57:24 +0530 Subject: [PATCH 016/125] Add avatars to rich text comments and mentions --- www/common/common-ui-elements.js | 21 ++++++++++++-------- www/common/sframe-common.js | 13 ++++++++++--- www/pad/comments.js | 33 +++++++++++++++++++++----------- 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index a4d84796d..943050e62 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -3456,7 +3456,8 @@ define([ name: f.displayName, curvePublic: f.curvePublic, profile: f.profile, - notifications: f.notifications + notifications: f.notifications, + uid: f.uid, }; }); }; @@ -3555,7 +3556,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 = (data.name.replace(/[^a-zA-Z0-9]+/g, "-") || "").trim() || Messages.anonymous; // XXX return "[@"+name+"|"+key+"]"; }; @@ -3608,18 +3609,20 @@ define([ var avatar = h('span.cp-avatar', { contenteditable: false }); - common.displayAvatar($(avatar), data.avatar, data.name); + + var displayName = (data.name || "").trim() || Messages.anonymous; + common.displayAvatar($(avatar), data.avatar, displayName); // XXX 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) ]); }; } @@ -3651,7 +3654,7 @@ define([ }).map(function (key) { var data = sources[key]; return { - label: data.name, + label: (data.name || "").trim() || Messages.anonymous, value: key }; }); @@ -3686,10 +3689,12 @@ define([ var obj = sources[key]; if (!obj) { return; } var avatar = h('span.cp-avatar'); - common.displayAvatar($(avatar), obj.avatar, obj.name); + var displayName = (obj.name || "").trim() || Messages.anonymous; + + common.displayAvatar($(avatar), obj.avatar, displayName, Util.noop, obj.uid); // XXX var li = h('li.cp-autocomplete-value', [ avatar, - h('span', obj.name) + h('span', displayName), ]); return $(li).appendTo(ul); }; diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 206137c86..01c48b540 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -249,11 +249,18 @@ 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; + if (!funcs.isLoggedIn()) { + existing.some(function (id) { + var author = authors[id] || {}; + if (author.uid !== tokenId) { return; } + uid = Number(id); + return true; + }); + return uid || authorUid(existing); + } existing.some(function(id) { var author = authors[id] || {}; if (author.curvePublic !== curve) { return; } diff --git a/www/pad/comments.js b/www/pad/comments.js index 6069aa1d0..6280a72bf 100644 --- a/www/pad/comments.js +++ b/www/pad/comments.js @@ -43,18 +43,21 @@ define([ var canonicalize = function(t) { return t.replace(/\r\n/g, '\n'); }; - var getAuthorId = function(Env, curve) { - return Env.common.getAuthorId(Env.comments.authors, curve); + var getAuthorId = function(Env, curve, uid) { + return Env.common.getAuthorId(Env.comments.authors, curve, uid); }; - // Return the author ID and add/update the data for registered users - // Return the username for unregistered users + // Return the author ID and add/update user data + // associate data with a curvePublic for registered users and the uid otherwise var updateAuthorData = function(Env, onChange) { var userData = Env.metadataMgr.getUserData(); + var myAuthorId; if (!Env.common.isLoggedIn()) { - return userData.name; + myAuthorId = getAuthorId(Env, undefined, userData.uid); + } else { + myAuthorId = getAuthorId(Env, userData.curvePublic); } - var myAuthorId = getAuthorId(Env, userData.curvePublic); + var data = Env.comments.authors[myAuthorId] = Env.comments.authors[myAuthorId] || {}; var old = Sortify(data); data.name = userData.name; @@ -62,6 +65,8 @@ define([ data.profile = userData.profile; data.curvePublic = userData.curvePublic; data.notifications = userData.notifications; + data.uid = userData.uid; + if (typeof(onChange) === "function" && Sortify(data) !== old) { onChange(); } @@ -82,6 +87,9 @@ define([ var userData = Env.metadataMgr.getUserData(); var privateData = Env.metadataMgr.getPrivateData(); var others = {}; + + + // XXX mentioned users should be excluded from the list of notified recipients to avoid notifying them twice // Get all the other registered users with a mailbox thread.m.forEach(function(obj) { var u = obj.u; @@ -93,7 +101,8 @@ define([ curvePublic: author.curvePublic, comment: obj.m, content: obj.v, - notifications: author.notifications + notifications: author.notifications, + uid: author.uid, }; }); // Send the notification @@ -146,7 +155,7 @@ define([ 'aria-required': true, contenteditable: true, }); - Env.common.displayAvatar($(avatar), userData.avatar, name); + Env.common.displayAvatar($(avatar), userData.avatar, name, Util.noop, userData.uid); var cancel = h('button.btn.btn-cancel', { tabindex: 1 @@ -224,7 +233,9 @@ define([ if (Env.common.isLoggedIn()) { var authors = {}; - Object.keys((Env.comments && Env.comments.authors) ||  {}).forEach(function(id) { + Object.keys((Env.comments && Env.comments.authors) ||  {}) + .filter(function (id) { return Util.find(Env, ['commments', 'authors', id, 'curvePublic']); }) + .forEach(function(id) { var obj = Util.clone(Env.comments.authors[id]); authors[obj.curvePublic] = obj; }); @@ -369,7 +380,7 @@ define([ var name = Util.fixHTML(author.name || Messages.anonymous); var date = new Date(msg.t); var avatar = h('span.cp-avatar'); - Env.common.displayAvatar($(avatar), author.avatar, name); + Env.common.displayAvatar($(avatar), author.avatar, name, Util.noop, author.uid); if (author.profile) { $(avatar).click(function(e) { Env.common.openURL(Hash.hashToHref(author.profile, 'profile')); @@ -393,7 +404,7 @@ define([ } cleanMentions($el); var avatar = h('span.cp-avatar'); - Env.common.displayAvatar($(avatar), avatarUrl, name); + Env.common.displayAvatar($(avatar), avatarUrl, name, Util.noop, author.uid); $el.append([ avatar, h('span.cp-mentions-name', name) From c630abb3c56abd451c2968800d49e9245557533a Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 25 Aug 2021 12:59:37 +0530 Subject: [PATCH 017/125] refactor animal avatar font-size to automatically scale with parents --- customize.dist/src/less2/include/avatar.less | 8 +++++++- customize.dist/src/less2/include/toolbar.less | 3 --- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/customize.dist/src/less2/include/avatar.less b/customize.dist/src/less2/include/avatar.less index 02abc4050..079ceb60d 100644 --- a/customize.dist/src/less2/include/avatar.less +++ b/customize.dist/src/less2/include/avatar.less @@ -40,7 +40,13 @@ color: @cp_avatar-fg; font-size: @avatar-font-size; font-size: var(--avatar-font-size); - text-transform: capitalize; + .animal { + font-size: 20px; + // 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 + font-size: calc(var(--avatar-width) * (6/5)); + } } media-tag { min-height: @avatar-width; diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index d8029f0d2..28b513095 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -200,9 +200,6 @@ .avatar_main(30px); .cp-avatar-default, media-tag { margin-right: 5px; - &.animal { - font-size: 20px; - } } &.cp-userlist-clickable { cursor: pointer; From c4fcc9f732ad3eb2b95c6291dc128d1305707c58 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 25 Aug 2021 13:03:27 +0530 Subject: [PATCH 018/125] WIP comments and non-functional changes --- www/common/common-ui-elements.js | 14 +++++++++----- www/common/inner/common-mediatag.js | 9 +++++++-- www/common/media-tag.js | 1 + www/common/toolbar.js | 3 ++- www/kanban/inner.js | 20 ++++++++++---------- www/profile/inner.js | 4 ++-- www/slide/inner.js | 2 +- 7 files changed, 32 insertions(+), 21 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 943050e62..6bb409222 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -157,8 +157,10 @@ 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 avatar = h('span.cp-usergrid-avatar.cp-avatar', { + 'aria-hidden': true, // XXX aria + }); + common.displayAvatar($(avatar), data.avatar, name); // XXX var removeBtn, el; if (config.remove) { removeBtn = h('span.fa.fa-times'); @@ -1989,11 +1991,11 @@ define([ var $displayName = $userAdmin.find('.'+displayNameCls); - var $avatar = $userAdmin.find('> button .cp-dropdown-button-title'); + var $avatar = $userAdmin.find('> button .cp-dropdown-button-title'); // XXX alt="User menu" var loadingAvatar; var to; var oldUrl = ''; - var oldUid = undefined; + var oldUid; var updateButton = function () { var myData = metadataMgr.getUserData(); var privateData = metadataMgr.getPrivateData(); @@ -2024,6 +2026,8 @@ define([ $userAdmin.find('> button').removeClass('cp-avatar'); if ($img) { $userAdmin.find('> button').addClass('cp-avatar'); } loadingAvatar = false; + + // XXX alt="User menu" }, uid); return; } @@ -2306,7 +2310,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'); - common.displayAvatar($(avatar), data.avatar, data.name); + common.displayAvatar($(avatar), data.avatar, data.name); // XXX return h('div.cp-creation-team', { 'data-id': id, title: data.name, diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index bceb314b8..54b20784a 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -132,7 +132,12 @@ define([ text = getPrettyInitials(name); } - var $avatar = $('', {'class': 'cp-avatar-default' + (animal? ' animal': '')}).text(text); + var $avatar = $('', { + 'class': 'cp-avatar-default' + (animal? ' animal': ''), + // XXX prevents screenreaders from trying to describe this + alt: '', + 'aria-hidden': true, + }).text(text); $container.append($avatar); if (uid && animal) { animal_avatars[uid] = animal_avatar; @@ -184,7 +189,7 @@ define([ var $img = $(mt).appendTo($container); MT.displayMediatagImage(common, $img, function (err, $image) { if (err) { return void console.error(err); } - centerImage($img, $image); + centerImage($img, $image); // XXX add alt="" (unless the media-tag has an alt attr) }); }); } diff --git a/www/common/media-tag.js b/www/common/media-tag.js index 15b038724..d1a5ebcda 100644 --- a/www/common/media-tag.js +++ b/www/common/media-tag.js @@ -73,6 +73,7 @@ var factory = function () { * @param {object} cfg Object {Plugins, allowed, download, pdf} containing infos about plugins * @param {function} cb Callback function: (err, pluginElement) => {} */ + // XXX add alt attributes if present in metadata text: function (metadata, url, content, cfg, cb) { var plainText = document.createElement('div'); plainText.className = "plain-text-reader"; diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 7493d9996..da8b1dad0 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -356,12 +356,13 @@ MessengerUI, Messages, Pages) { }); } if (data.profile) { + // XXX title to visit their profile "Visit {0}'s profile" + // Messages.contacts_info3 "Double-click their icon to view their profile", $span.addClass('cp-userlist-clickable'); $span.click(function () { Common.openURL(origin+'/profile/#' + data.profile); }); } - console.error("AVATAR", $span, data.uid); Common.displayAvatar($span, data.avatar, name, function () { $span.append($rightCol); }, data.uid); diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 38e933cf1..42b1e39fa 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -59,7 +59,7 @@ define([ verbose = function () {}; // comment out to enable verbose logging var onRedraw = Util.mkEvent(); var onCursorUpdate = Util.mkEvent(); - var remoteCursors = {}; + var remoteCursors = {}; // XXX var setValueAndCursor = function (input, val, _cursor) { if (!input) { return; } @@ -95,7 +95,7 @@ define([ var getAvatar = function (cursor, noClear) { // Tippy - var html = MT.getCursorAvatar(cursor); + var html = MT.getCursorAvatar(cursor); // XXX var l = Util.getFirstCharacter(cursor.name || Messages.anonymous); @@ -103,10 +103,10 @@ define([ if (cursor.color) { text = 'color:'+getTextColor(cursor.color)+';'; } - var avatar = h('span.cp-cursor.cp-tippy-html', { + var avatar = h('span.cp-cursor.cp-tippy-html', { // XXX style: "background-color: " + (cursor.color || 'red') + ";"+text, 'data-cptippy-html': true, - title: html + title: html, // XXX "{0} is editing" }, l); if (!noClear) { cursor.clear = function () { @@ -852,7 +852,7 @@ define([ getAvatar: getAvatar, openLink: openLink, getTags: getExistingTags, - cursors: remoteCursors, + cursors: remoteCursors, // XXX boards: boards, _boards: Util.clone(boards), }); @@ -1101,7 +1101,7 @@ define([ $container.find('.kanban-edit-item').remove(); }); - var getCursor = function () { + var getCursor = function () { // XXX if (!kanban || !kanban.inEditMode) { return; } try { var id = kanban.inEditMode; @@ -1204,7 +1204,7 @@ define([ var remoteContent = newContent.content; if (Sortify(currentContent) !== Sortify(remoteContent)) { - var cursor = getCursor(); + var cursor = getCursor(); // XXX verbose("Content is different.. Applying content"); kanban.options.boards = remoteContent; updateBoards(framework, kanban, remoteContent); @@ -1261,11 +1261,11 @@ define([ }); var myCursor = {}; - onCursorUpdate.reg(function (data) { + onCursorUpdate.reg(function (data) { // XXX myCursor = data; framework.updateCursor(); }); - framework.onCursorUpdate(function (data) { + framework.onCursorUpdate(function (data) { // XXX if (!data) { return; } if (data.reset) { Object.keys(remoteCursors).forEach(function (id) { @@ -1293,7 +1293,7 @@ define([ if (!cursor.item && !cursor.board) { return; } // Add new cursor - var avatar = getAvatar(cursor); + var avatar = getAvatar(cursor); // XXX var $item = $('.kanban-item[data-eid="'+cursor.item+'"]'); var $board = $('.kanban-board[data-id="'+cursor.board+'"]'); if ($item.length) { diff --git a/www/profile/inner.js b/www/profile/inner.js index 11abd6ae8..c63b72e99 100644 --- a/www/profile/inner.js +++ b/www/profile/inner.js @@ -349,7 +349,7 @@ define([ $('', { src: '/customize/images/avatar.png', title: Messages.profile_avatar, - alt: 'Avatar' + alt: 'Avatar' // XXX translate this "Default profile picture" }).appendTo($span); return; } @@ -391,7 +391,7 @@ define([ }, function () { sframeChan.query("Q_PROFILE_AVATAR_ADD", data.url, function (err, err2) { if (err || err2) { return void UI.log(err || err2); } - displayAvatar(data.url); + displayAvatar(data.url); // XXX add "Profile picture" }); }); }; diff --git a/www/slide/inner.js b/www/slide/inner.js index 9c9b9c70b..143165176 100644 --- a/www/slide/inner.js +++ b/www/slide/inner.js @@ -511,7 +511,7 @@ define([ framework.updateCursor(); }, 500); // 500ms to make sure it is sent after chainpad sync }; - framework.onCursorUpdate(CodeMirror.setRemoteCursor); + framework.onCursorUpdate(CodeMirror.setRemoteCursor); // XXX framework.setCursorGetter(CodeMirror.getCursor); editor.on('cursorActivity', updateCursor); From 4b0cebb0fd46f0d0339320160407b2859caa42ff Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 25 Aug 2021 13:19:51 +0530 Subject: [PATCH 019/125] animal emojis in the team roster and fall back to default username in teams when members are unnamed in various places where it was not handled --- www/teams/inner.js | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/www/teams/inner.js b/www/teams/inner.js index 373a27998..d1e383e44 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -693,6 +693,10 @@ define([ redrawRoster(common); }); }; + + var getDisplayName = function (name) { + return (typeof(name) === 'string'? name: "").trim() || Messages.anonymous; + }; var makeMember = function (common, data, me, roster) { if (!data.curvePublic) { return; } @@ -701,11 +705,12 @@ define([ return user.role === "OWNER" && user.curvePublic !== me.curvePublic && !user.pendingOwner; }); + var displayName = getDisplayName(data.displayName); // Avatar var avatar = h('span.cp-avatar.cp-team-member-avatar'); - common.displayAvatar($(avatar), data.avatar, data.displayName); + common.displayAvatar($(avatar), data.avatar, displayName, Util.noop, data.uid); // Name - var name = h('span.cp-team-member-name', data.displayName); + var name = h('span.cp-team-member-name', displayName); if (data.pendingOwner) { $(name).append(h('em', { title: Messages.team_pendingOwnerTitle @@ -789,7 +794,7 @@ define([ title: Messages.team_rosterKick }); $(remove).click(function () { - UI.confirm(Messages._getKey('team_kickConfirm', [Util.fixHTML(data.displayName)]), function (yes) { + UI.confirm(Messages._getKey('team_kickConfirm', [Util.fixHTML(displayName)]), function (yes) { if (!yes) { return; } APP.module.execCommand('REMOVE_USER', { pending: data.pending, @@ -1073,6 +1078,9 @@ define([ metadata: obj }, function () { $avatar.empty(); + // the UI is not supposed to allow admins to remove team names + // so we expect that it will be there. Failing that the initials + // from the default name will be displayed common.displayAvatar($avatar, data.url); }); }); @@ -1191,10 +1199,11 @@ define([ var displayUser = function (common, data) { var avatar = h('span.cp-teams-invite-from-avatar.cp-avatar'); - common.displayAvatar($(avatar), data.avatar, data.displayName); + var name = getDisplayName(data.displayName); + common.displayAvatar($(avatar), data.avatar, name); return h('div.cp-teams-invite-from-author', [ avatar, - h('span.cp-teams-invite-from-name', data.displayName) + h('span.cp-teams-invite-from-name', name) ]); }; @@ -1319,20 +1328,21 @@ define([ nThen(function (waitFor) { // Get preview content. sframeChan.query('Q_ANON_GET_PREVIEW_CONTENT', { seeds: seeds }, waitFor(function (err, json) { - if (json && (json.error || !Object.keys(json).length)) { + if (json && (json.error || !Object.keys(json).length)) { // XXX team invite links are triggering this every time for me? $(errorBlock).text(Messages.team_inviteInvalidLinkError).show(); waitFor.abort(); $div.empty(); return; } + // XXX nothing guarantees that author, teamName, or message exist in json $div.empty(); $div.append(h('div.cp-teams-invite-from', [ - Messages.team_inviteFrom || 'From:', + Messages.team_inviteFrom, displayUser(common, json.author) ])); $div.append(UI.setHTML(h('p.cp-teams-invite-to'), Messages._getKey('team_inviteFromMsg', - [Util.fixHTML(json.author.displayName), + [Util.fixHTML(getDisplayName(json.author.displayName)), Util.fixHTML(json.teamName)]))); if (json.message) { $div.append(h('div.cp-teams-invite-message', json.message)); @@ -1449,10 +1459,10 @@ define([ // Update the name in the user menu var $displayName = $bar.find('.' + Toolbar.constants.username); metadataMgr.onChange(function () { - var name = metadataMgr.getUserData().name || Messages.anonymous; + var name = getDisplayName(metadataMgr.getUserData().name); $displayName.text(name); }); - $displayName.text(user.name || Messages.anonymous); + $displayName.text(getDisplayName(user.name)); // Load the Team module var onEvent = function (obj) { From 95869b84c901ce48f69d1ad0fa49e9a6cfe31915 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 25 Aug 2021 13:38:05 +0530 Subject: [PATCH 020/125] emoji avatars in the contacts app and handling of empty display names --- www/common/common-interface.js | 4 ++++ www/common/common-messaging.js | 3 ++- www/common/messenger-ui.js | 30 ++++++++++++++++++++---------- www/teams/inner.js | 4 +--- 4 files changed, 27 insertions(+), 14 deletions(-) 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/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/teams/inner.js b/www/teams/inner.js index d1e383e44..bd7fca5ed 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -694,9 +694,7 @@ define([ }); }; - var getDisplayName = function (name) { - return (typeof(name) === 'string'? name: "").trim() || Messages.anonymous; - }; + var getDisplayName = UI.getDisplayName; var makeMember = function (common, data, me, roster) { if (!data.curvePublic) { return; } From 7bb3bc167cdd340ab9bb192031e39fb011b8f247 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 25 Aug 2021 15:12:28 +0530 Subject: [PATCH 021/125] use emoji avatars in share and access modals --- www/common/common-ui-elements.js | 36 +++++++++++++++++--------------- www/common/inner/access.js | 9 ++++++-- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 6bb409222..d0ef0af3f 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -156,11 +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 name = UI.getDisplayName(data.displayName || data.name); var avatar = h('span.cp-usergrid-avatar.cp-avatar', { 'aria-hidden': true, // XXX aria }); - common.displayAvatar($(avatar), data.avatar, name); // XXX + common.displayAvatar($(avatar), data.avatar, name, Util.noop, data.uid); var removeBtn, el; if (config.remove) { removeBtn = h('span.fa.fa-times'); @@ -2014,13 +2014,12 @@ define([ return; } loadingAvatar = true; - var newName = myData.name; + var newName = UI.getDisplayName(myData.name); var url = myData.avatar; - $displayName.text(newName || Messages.anonymous); + $displayName.text(newName); if ((accountName && oldUrl !== url) || !accountName && uid !== oldUid) { $avatar.html(''); - Common.displayAvatar($avatar, url, - newName || Messages.anonymous, function ($img) { + Common.displayAvatar($avatar, url, newName, function ($img) { oldUrl = url; oldUid = uid; $userAdmin.find('> button').removeClass('cp-avatar'); @@ -2310,7 +2309,8 @@ 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'); - common.displayAvatar($(avatar), data.avatar, data.name); // XXX + // 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, title: data.name, @@ -3113,7 +3113,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]); @@ -3245,7 +3245,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]); @@ -3360,13 +3360,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])); } @@ -3376,7 +3378,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); @@ -3560,7 +3562,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, "-") || "").trim() || Messages.anonymous; // XXX + var name = UI.getDisplayName(data.name.replace(/[^a-zA-Z0-9]+/g, "-")); return "[@"+name+"|"+key+"]"; }; @@ -3614,7 +3616,7 @@ define([ contenteditable: false }); - var displayName = (data.name || "").trim() || Messages.anonymous; + var displayName = UI.getDisplayName(data.name); common.displayAvatar($(avatar), data.avatar, displayName); // XXX return h('span.cp-mentions', { 'data-curve': data.curvePublic, @@ -3658,7 +3660,7 @@ define([ }).map(function (key) { var data = sources[key]; return { - label: (data.name || "").trim() || Messages.anonymous, + label: UI.getDisplayName(data.name), value: key }; }); @@ -3693,9 +3695,9 @@ define([ var obj = sources[key]; if (!obj) { return; } var avatar = h('span.cp-avatar'); - var displayName = (obj.name || "").trim() || Messages.anonymous; + var displayName = UI.getDisplayName(obj.name); - common.displayAvatar($(avatar), obj.avatar, displayName, Util.noop, obj.uid); // XXX + common.displayAvatar($(avatar), obj.avatar, displayName, Util.noop, obj.uid); var li = h('li.cp-autocomplete-value', [ avatar, h('span', displayName), diff --git a/www/common/inner/access.js b/www/common/inner/access.js index 3f281dc47..119714de3 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,10 @@ 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) }; strangers++; }); From 68efd549177bb5041dabce9170fc0bf8463182ee Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 25 Aug 2021 16:18:09 +0530 Subject: [PATCH 022/125] include uid in cursor data for animal avatars --- www/code/inner.js | 4 +++- www/common/inner/common-mediatag.js | 9 +++++++-- www/common/sframe-common-codemirror.js | 1 + www/slide/inner.js | 4 +++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/www/code/inner.js b/www/code/inner.js index 4f6c0308e..4328f0d8f 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -388,7 +388,9 @@ define([ var andThen2 = function (editor, CodeMirror, framework, isPresentMode) { var common = framework._.sfCommon; - var privateData = common.getMetadataMgr().getPrivateData(); + var metadataMgr = common.getMetadataMgr(); + var privateData = metadataMgr.getPrivateData(); + CodeMirror.uid = metadataMgr.getUserData().uid; var previewPane = mkPreviewPane(editor, CodeMirror, framework, isPresentMode); var markdownTb = mkMarkdownTb(editor, framework); diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 54b20784a..5e091a83c 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -43,9 +43,15 @@ define([ }); }; + var animal_avatars = {}; MT.getCursorAvatar = function (cursor) { + var uid = cursor.uid; var html = ''; - html += (cursor.avatar && avatars[cursor.avatar]) || ''; + if (cursor.avatar && avatars[cursor.avatar]) { + html += (cursor.avatar && avatars[cursor.avatar]) || ''; + } else if (animal_avatars[uid]) { + html += animal_avatars[uid] + ' '; + } html += Util.fixHTML(cursor.name) + ''; return html; }; @@ -109,7 +115,6 @@ define([ return text; }; - var animal_avatars = {}; MT.displayAvatar = function (common, $container, href, name, _cb, uid) { var cb = Util.once(Util.mkAsync(_cb || function () {})); var displayDefault = function () { diff --git a/www/common/sframe-common-codemirror.js b/www/common/sframe-common-codemirror.js index 58475bbed..4e490288d 100644 --- a/www/common/sframe-common-codemirror.js +++ b/www/common/sframe-common-codemirror.js @@ -509,6 +509,7 @@ define([ var cursor = {}; cursor.selectionStart = cursorToPos(editor.getCursor('from'), doc); cursor.selectionEnd = cursorToPos(editor.getCursor('to'), doc); + cursor.uid = exp.uid; // FIXME this is inefficient for the network but it's unlikely to trigger errors return cursor; }; diff --git a/www/slide/inner.js b/www/slide/inner.js index 143165176..9bbfc6e4d 100644 --- a/www/slide/inner.js +++ b/www/slide/inner.js @@ -459,7 +459,9 @@ define([ var andThen2 = function (editor, CodeMirror, framework, isPresentMode) { var common = framework._.sfCommon; - var privateData = common.getMetadataMgr().getPrivateData(); + var metadataMgr = common.getMetadataMgr(); + var privateData = metadataMgr.getPrivateData(); + CodeMirror.uid = metadataMgr.getUserData().uid; var $contentContainer = $('#cp-app-slide-editor'); var $modal = $('#cp-app-slide-modal'); From fe03da5bddfc1c5aba03144bf2c8d76854c630bc Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 25 Aug 2021 17:32:10 +0200 Subject: [PATCH 023/125] Conditional answers prototype --- www/form/app-form.less | 36 +++ www/form/inner.js | 543 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 554 insertions(+), 25 deletions(-) diff --git a/www/form/app-form.less b/www/form/app-form.less index e22f3030d..658784297 100644 --- a/www/form/app-form.less +++ b/www/form/app-form.less @@ -10,6 +10,7 @@ @bg-color: @colortheme_apps[form] ); + display: flex; flex-flow: column; font: @colortheme_app-font; @@ -161,6 +162,41 @@ margin-left: 5px; } } + + + .cp-form-conditional { + .cp-form-condition { + display: flex; + align-items: center; + justify-content: center; + & > *:not(:first-child) { + margin-left: 40px; + } + .cp-dropdown-container button { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + .cp-form-condition-rule { + padding-bottom: 20px; + margin-bottom: 20px; + border-bottom: 1px solid @cryptpad_text_col; + } + } + + div.cp-form-section-sortable { + min-height: 300px; + border: 4px dotted white; // XXX + padding: 20px; + background: @cp_app-bg; + .cp-form-creator-add-inline { + button[data-type="section"] { + display: none; + } + } + } } div.cp-form-creator-content, div.cp-form-creator-results { max-width: 1000px; diff --git a/www/form/inner.js b/www/form/inner.js index 1a776facc..f123f015d 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -74,6 +74,10 @@ define([ var is24h = UIElements.is24h(); var dateFormat = "Y-m-d H:i"; var timeFormat = "H:i"; + + var evCheckConditions = Util.mkEvent(); + var evShowConditions = Util.mkEvent(); + if (!is24h) { dateFormat = "Y-m-d h:i K"; timeFormat = "h:i K"; @@ -848,6 +852,7 @@ define([ }).filter(Boolean); }; + Messages.form_type_section = "Conditional section"; // XXX var STATIC_TYPES = { md: { defaultOpts: { @@ -964,6 +969,55 @@ define([ printResults: function () { return; }, icon: h('i.cptools.cptools-form-page-break') }, + section: { + defaultOpts: { + questions: [] + }, + get: function (opts, a, n, ev, block) { + var sortable = h('div.cp-form-section-sortable'); + var tag = h('div.cp-form-section-edit', [ + h('i.fa.fa-question'), + h('span', Messages.form_type_section), + h('hr'), + sortable + ]); + if (APP.isEditor) { + if (!opts) { opts = block.opts = STATIC_TYPES.section.defaultOpts; } + Sortable.create(sortable, { + group: { + name: 'nested', + put: function (to, from, el) { + // Make sure sections dan't be dropped into other sections + return $(el).attr('data-type') !== 'section'; + } + }, + direction: "vertical", + filter: "input, button, .CodeMirror, .cp-form-type-sort, .cp-form-block-type.editable", + preventOnFilter: false, + draggable: ".cp-form-block", + //forceFallback: true, + fallbackTolerance: 5, + onStart: function () { + var $container = $('div.cp-form-creator-content'); + $container.find('.cp-form-creator-add-inline').remove(); + }, + store: { + set: function (s) { + opts.questions = s.toArray(); + setTimeout(APP.framework.localChange); + if (APP.updateAddInline) { APP.updateAddInline(); } + } + } + }); + } + return { + tag: tag, + noViewMode: true + }; + }, + printResults: function () { return; }, + icon: h('i.fa.fa-question') + }, }; var arrayMax = function (A) { @@ -1157,7 +1211,7 @@ define([ var cursorGetter; var setCursorGetter = function (f) { cursorGetter = f; }; $(tag).find('input[type="radio"]').on('change', function () { - evOnChange.fire(); + evOnChange.fire(false, false, true); }); return { tag: tag, @@ -2435,6 +2489,64 @@ define([ reset, send ]); }; + var getSections = function (content) { + var uids = Object.keys(content.form).filter(function (uid) { + return content.form[uid].type === 'section'; + }); + return uids; + }; + var getSectionFromQ = function (content, uid) { + var arr = content.order; + var idx = content.order.indexOf(uid); + if (idx === -1) { // If it's not in the main array, check in sections + getSections(content).some(function (_uid) { + var block = content.form[_uid]; + if (!block.opts || !Array.isArray(block.opts.questions)) { return; } + var _idx = block.opts.questions.indexOf(uid); + if (_idx !== -1) { + arr = block.opts.questions; + idx = _idx; + return true; + } + }); + } + + return { + arr: arr, + idx: idx + }; + }; + var removeQuestion = function (content, uid) { + delete content.form[uid]; + var idx = content.order.indexOf(uid); + if (idx !== -1) { + content.order.splice(idx, 1); + } else { + getSections(content).some(function (_uid) { + var block = content.form[_uid]; + if (!block.opts || !Array.isArray(block.opts.questions)) { return; } + var _idx = block.opts.questions.indexOf(uid); + if (_idx !== -1) { + block.opts.questions.splice(_idx, 1); + return true; + } + }); + } + }; + var getFullOrder = function (content) { + var order = content.order.slice(); + getSections(content).forEach(function (uid) { + var block = content.form[uid]; + if (!block.opts || !Array.isArray(block.opts.questions)) { return; } + var idx = order.indexOf(uid); + if (idx === -1) { return; } + idx++; + block.opts.questions.forEach(function (el, i) { + order.splice(idx+i, 0, el); + }); + }); + return order; + }; var updateForm = function (framework, content, editable, answers, temp) { var $container = $('div.cp-form-creator-content'); if (!$container.length) { return; } // Not ready @@ -2479,28 +2591,45 @@ define([ } - var getFormCreator = function (uid) { + var getFormCreator = function (uid, inSection) { if (!APP.isEditor) { return; } var full = !uid; + var arr = content.order; var idx = content.order.indexOf(uid); + if (!full) { + if (inSection) { + var section = content.form[uid]; + section.opts = section.opts || STATIC_TYPES.section.opts; + arr = section.opts.questions; + } else { + var obj = getSectionFromQ(content, uid); + arr = obj.arr; + idx = obj.idx; + } + } var addControl = function (type) { var btn = h('button.btn.btn-secondary', { - title: full ? '' : Messages['form_type_'+type] + title: full ? '' : Messages['form_type_'+type], + 'data-type': type }, [ (TYPES[type] || STATIC_TYPES[type]).icon.cloneNode(), full ? h('span', Messages['form_type_'+type]) : undefined ]); $(btn).click(function () { var uid = Util.uid(); + + // Make sure we can't create a section inside another one + if (type === 'section' && arr !== content.order) { return; } + content.form[uid] = { //q: Messages.form_default, //opts: opts type: type, }; - if (full) { - content.order.push(uid); + if (full || inSection) { + arr.push(uid); } else { - content.order.splice(idx, 0, uid); + arr.splice(idx, 0, uid); } framework.localChange(); updateForm(framework, content, true); @@ -2541,19 +2670,29 @@ define([ }; - var updateAddInline = function () { + var updateAddInline = APP.updateAddInline = function () { $container.find('.cp-form-creator-add-inline').remove(); + // Add before existing question $container.find('.cp-form-block').each(function (i, el) { var $el = $(el); var uid = $el.attr('data-id'); $el.before(getFormCreator(uid)); }); + // Add to section + $container.find('.cp-form-section-sortable').each(function (i, el) { + var $el = $(el); + var uid = $el.closest('.cp-form-block').attr('data-id'); + $el.append(getFormCreator(uid, true)); + }); }; var elements = []; var n = 1; // Question number - content.order.forEach(function (uid) { + + var order = getFullOrder(content); + + order.forEach(function (uid, blockIdx) { var block = form[uid]; var type = block.type; var model = TYPES[type] || STATIC_TYPES[type]; @@ -2570,7 +2709,7 @@ define([ name = user.name; } - var data = model.get(block.opts, _answers, name, evOnChange); + var data = model.get(block.opts, _answers, name, evOnChange, block); if (!data) { return; } data.uid = uid; if (answers && answers[uid] && data.setValue) { data.setValue(answers[uid]); } @@ -2579,7 +2718,10 @@ define([ elements.push(data); return; } - + if (data.noViewMode && !editable) { + elements.push(data); + return; + } Messages.form_required = "Required"; // XXX var requiredTag; @@ -2606,7 +2748,7 @@ define([ Messages.form_required_on = "Required answer"; Messages.form_required_off = "Optional answer"; // Required radio displayed only for types that have an "isEmpty" function - var requiredDiv; + var requiredDiv, conditionalDiv; if (APP.isEditor && !isStatic && data.isEmpty) { if (!block.opts) { block.opts = TYPES[type].defaultOpts; } var isRequired = Boolean(block.opts.required); @@ -2633,6 +2775,272 @@ define([ }); } + if (APP.isEditor && blockIdx && type === "section") { + var getConditionsValues = function () { + order = getFullOrder(content); + var blockIdx = order.indexOf(uid); + var blocks = order.slice(0, blockIdx); // Get all previous questions + var values = blocks.map(function(uid) { + var block = form[uid]; + var type = block.type; + if (['radio'].indexOf(type) === -1) { return; } + if (type === 'radio') { + return { + uid: uid, + q: block.q || Messages.form_default, + values: block.opts ? block.opts.values : TYPES.radio.defaultOpts.values + }; + } + }).filter(Boolean); + return values; + }; + Messages.form_conditional_add = "Add condition OR"; + Messages.form_conditional_addAnd = "Add condition AND"; + var addCondition = h('button.btn.btn-default', [ + h('i.fa.fa-plus'), + h('span', Messages.form_conditional_add) + ]); + var $addC = $(addCondition); + var getConditions; + var getAddAndButton = function ($container, rules) { + var btn = h('button.btn.btn-default.cp-form-add-and', [ + h('i.fa.fa-plus'), + h('span', Messages.form_conditional_addAnd) + ]); + $(btn).click(function () { + getConditions($container, true, rules); + }); + $container.append(btn); + }; + getConditions = function ($container, isNew, rules, condition) { + var content = h('div.cp-form-condition'); + var $content = $(content); + var values = getConditionsValues(); + var qOptions = values.map(function (obj) { + return { + tag: 'a', + attributes: { + 'class': 'cp-form-condition-question', + 'data-value': obj.uid, + 'href': '#', + }, + content: obj.q + }; + }); + var qConfig = { + text: '', // Button initial text + options: qOptions, // Entries displayed in the menu + isSelect: true, + caretDown: true, + buttonCls: 'btn btn-secondary' + }; + var qSelect = UIElements.createDropdown(qConfig); + Messages.form_condition_is = 'is'; // XXX + Messages.form_condition_isnot = 'is not'; // XXX + + var isOn = !condition || condition.is !== 0; + var iOptions = [{ + tag: 'a', + attributes: { + 'data-value': 1, + 'href': '#', + }, + content: Messages.form_condition_is + }, { + tag: 'a', + attributes: { + 'data-value': 0, + 'href': '#', + }, + content: Messages.form_condition_isnot + }]; + var iConfig = { + options: iOptions, // Entries displayed in the menu + isSelect: true, + caretDown: true, + buttonCls: 'btn btn-secondary' + }; + var iSelect = UIElements.createDropdown(iConfig); + iSelect.setValue(isOn ? 1 : 0); + $(iSelect).hide(); + + $content.append(qSelect).append(iSelect); + if ($container.find('button.cp-form-add-and').length) { + $container.find('button.cp-form-add-and').before($content); + } else { + $container.append($content); + } + + var isChange; + qSelect.onChange.reg(function (prettyVal, val, init) { + $(iSelect).show(); + var res; + values.some(function (obj) { + if (String(obj.uid) === String(val)) { + res = obj.values; + return true; + } + }); + $content.find('.cp-form-condition-values').remove(); + if (!res) { return; } + var vOptions = res.map(function (str) { + return { + tag: 'a', + attributes: { + 'class': 'cp-form-condition-value', + 'data-value': str, + 'href': '#', + }, + content: str + }; + }); + var vConfig = { + text: '', // Button initial text + options: vOptions, // Entries displayed in the menu + //left: true, // Open to the left of the button + //container: $(type), + isSelect: true, + caretDown: true, + buttonCls: 'btn btn-secondary' + }; + var vSelect = UIElements.createDropdown(vConfig); + vSelect.addClass('cp-form-condition-values'); + if ($content.find('.cp-condition-remove').length) { + $content.find('.cp-condition-remove').before(vSelect); + } else { + $content.append(vSelect); + } + + var onChange = function () { + var w = block.opts.when = block.opts.when || []; + + condition = condition || {}; + condition.q = val; + condition.is = Number(iSelect.getValue()); + condition.v = vSelect.getValue(); + + var wasNew = isNew; + if (isNew) { + if (!Array.isArray(rules)) { // new set of rules (OR) + rules = [condition]; + w.push(rules); + getAddAndButton($container, rules); + } else { + rules.push(condition); + } + isNew = false; + } + + framework.localChange(); + framework._.cpNfInner.chainpad.onSettle(function () { + UI.log(Messages.saved); + if (wasNew) { $addC.show(); } + }); + + }; + + if (isChange) { iSelect.onChange.unreg(isChange); } + isChange = function () { + if (!vSelect.getValue()) { return; } + onChange(); + }; + iSelect.onChange.reg(isChange); + + vSelect.onChange.reg(function () { + if (!$content.find('.cp-condition-remove').length) { + var remove = h('button.btn.btn-danger-alt.cp-condition-remove', [ + h('i.fa.fa-times') + ]); + $(remove).click(function () { + var w = block.opts.when = block.opts.when || []; + var deleteRule = false; + if (rules.length === 1) { + var rIdx = w.indexOf(rules); + w.splice(rIdx, 1); + deleteRule = true; + } else { + var idx = rules.indexOf(condition); + rules.splice(idx, 1); + } + framework.localChange(); + framework._.cpNfInner.chainpad.onSettle(function () { + if (deleteRule) { + $content.closest('.cp-form-condition-rule').remove(); + return; + } + $content.remove(); + }); + }).appendTo($content); + } + onChange(); + }); + + if (condition && condition.v && init) { + vSelect.setValue(condition.v); + vSelect.onChange.fire(condition.v, condition.v); + } + + }); + if (condition && condition.q) { + qSelect.setValue(condition.q); // XXX check if exists? or integrity + qSelect.onChange.fire(condition.q, condition.q, true); + } + + + }; + Messages.form_conditional = "Only show this question when:"; // XXX + + conditionalDiv = h('div.cp-form-conditional', [ + h('div', Messages.form_conditional), + addCondition + ]); + var $condition = $(conditionalDiv); + var redraw = function () { + var w = block.opts.when = block.opts.when || []; + w.forEach(function (rules) { + var rulesC = h('div.cp-form-condition-rule'); + var $rulesC = $(rulesC); + getAddAndButton($rulesC, rules); + rules.forEach(function (obj) { + getConditions($rulesC, false, rules, obj); + }); + $addC.before($rulesC); // XXX + }); + }; + redraw(); + + $addC.click(function () { + $addC.hide(); + var rulesC = h('div.cp-form-condition-rule'); + var $rulesC = $(rulesC); + getConditions($rulesC, true); + $addC.before($rulesC); + }); + if (getConditionsValues().length) { + $condition.show(); + } else { + $condition.hide(); + } + evShowConditions.reg(function () { + if (getConditionsValues().length) { + $condition.show(); + } else { + $condition.hide(); + } + }); + evCheckConditions.reg(function (_uid) { + if (uid !== _uid) { return; } + // If our conditions are invalid, redraw them + if (getConditionsValues().length) { + $condition.show(); + } else { + $condition.hide(); + } + $condition.find('.cp-form-condition-rule').remove(); + redraw(); + }); + } + var changeType; if (editable) { // Drag handle @@ -2699,9 +3107,7 @@ define([ classes: 'btn-danger', new: true }, function () { - delete content.form[uid]; - var idx = content.order.indexOf(uid); - content.order.splice(idx, 1); + removeQuestion(content, uid); $('.cp-form-block[data-id="'+uid+'"]').remove(); framework.localChange(); updateAddInline(); @@ -2763,7 +3169,6 @@ define([ h('span', Messages['form_type_'+type]) ]); - //Messages.form_changeType = "Change type"; // XXX Messages.form_changeTypeConfirm = "Select the new type of this question and click OK."; // XXX Messages.form_breakAnswers = "Changing the type may corrupt existing answers"; if (Array.isArray(model.compatible)) { @@ -2822,13 +3227,15 @@ define([ } var editableCls = editable ? ".editable" : ""; elements.push(h('div.cp-form-block'+editableCls, { - 'data-id':uid + 'data-id':uid, + 'data-type':type }, [ APP.isEditor ? dragHandle : undefined, changeType, isStatic ? undefined : q, h('div.cp-form-block-content', [ APP.isEditor && !isStatic ? requiredDiv : undefined, + APP.isEditor ? conditionalDiv : undefined, APP.isEditor && !isStatic ? previewDiv : undefined, data.tag, editButtons @@ -2896,10 +3303,40 @@ define([ } $container.empty().append(_content); + + getSections(content).forEach(function (uid) { + var block = content.form[uid]; + if (!block.opts || !Array.isArray(block.opts.questions)) { return; } + var $block = $container.find('.cp-form-block[data-id="'+uid+'"] .cp-form-section-sortable'); + block.opts.questions.forEach(function (_uid) { + $container.find('.cp-form-block[data-id="'+_uid+'"]').appendTo($block); + }); + }); updateAddInline(); + // In view mode, hide sections when conditions aren't met + evOnChange.reg(function (reset, save, condition) { + if (!reset && !condition) { return; } + var results = getFormResults(); + getSections(content).forEach(function (uid) { + var block = content.form[uid]; + if (!block.opts || !Array.isArray(block.opts.questions) || !block.opts.when) { return; } + var w = block.opts.when; + var show = !w.length || w.some(function (rules) { + return rules.every(function (rule) { + return rule.is ? results[rule.q] === rule.v + : results[rule.q] !== rule.v; + }); + }); + block.opts.questions.forEach(function (_uid) { + $container.find('.cp-form-block[data-id="'+_uid+'"]').toggle(show); + }); + }); + }); + if (editable) { APP.mainSortable = Sortable.create($container[0], { + group: 'nested', direction: "vertical", filter: "input, button, .CodeMirror, .cp-form-type-sort, .cp-form-block-type.editable", preventOnFilter: false, @@ -2912,7 +3349,7 @@ define([ store: { set: function (s) { content.order = s.toArray(); - framework.localChange(); + setTimeout(framework.localChange); updateAddInline(); } } @@ -2931,7 +3368,7 @@ define([ // In view mode, add "Submit" and "reset" buttons $container.append(makeFormControls(framework, content, Boolean(answers), evOnChange)); - // In view mode, tell the user and answers are forced to be anonymous or authenticated + // In view mode, tell the user if answers are forced to be anonymous or authenticated if (!APP.isEditor) { var infoTxt; var loggedIn = framework._.sfCommon.isLoggedIn(); @@ -3313,21 +3750,77 @@ define([ var checkIntegrity = function (getter) { if (!content.order || !content.form) { return; } var changed = false; - content.order.forEach(function (uid) { - if (!content.form[uid]) { - var idx = content.order.indexOf(uid); - content.order.splice(idx, 1); - changed = true; - } + var deduplicate = []; + // Check if the questions in the lists (content.order or sections) exist in + // content.form and remove duplicates + var check1 = function (arr) { + arr.forEach(function (uid) { + if (!content.form[uid] || deduplicate.indexOf(uid) !== -1) { + var idx = arr.indexOf(uid); + arr.splice(idx, 1); + changed = true; + return; + } + deduplicate.push(uid); + }); + }; + check1(content.order); + getSections(content).forEach(function (uid) { + var block = content.form[uid]; + if (!block.opts || !Array.isArray(block.opts.questions)) { return; } + check1(block.opts.questions); }); + + // Make sure all the questions are displayed in the main list or a section and add + // the missing ones Object.keys(content.form).forEach(function (uid) { - var idx = content.order.indexOf(uid); + var idx = deduplicate.indexOf(uid); if (idx === -1) { changed = true; content.order.push(uid); } }); + // Check if conditions on sections are valid + var order = getFullOrder(content); + getSections(content).forEach(function (uid) { + var block = content.form[uid]; + if (!block.opts || !Array.isArray(block.opts.when)) { return; } + var sectionIdx = order.indexOf(uid); + if (sectionIdx === -1) { return; } + var available = order.slice(0, sectionIdx); + var errors = false; + block.opts.when.forEach(function (rules) { + if (!Array.isArray(rules)) { + var idx = block.opts.when.indexOf(rules); + block.opts.when.splice(idx, 1); + errors = true; + return; + } + rules.forEach(function (obj) { + var idx = available.indexOf(obj.q); + // If this question doesn't exist before the section, remove the condition + if (!obj.q || idx === -1) { + var cIdx = rules.indexOf(obj); + rules.splice(cIdx, 1); + errors = true; + return; + } + }); + if (!rules.length) { + var rIdx = block.opts.when.indexOf(rules); + block.opts.when.splice(rIdx, 1); + errors = true; + } + }); + if (errors) { + evCheckConditions.fire(uid); + changed = true; + } + }); + + evShowConditions.fire(); + if (!getter && changed) { framework.localChange(); } }; From dd8f70d6f457e2a2afa30a710139ddb16fe26417 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 26 Aug 2021 14:07:03 +0530 Subject: [PATCH 024/125] fix a bug I introduced by not preserving the base case of 'getAuthorId' --- www/common/sframe-common.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 01c48b540..000d049b3 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -252,10 +252,12 @@ define([ funcs.getAuthorId = function(authors, curve, tokenId) { var existing = Object.keys(authors || {}).map(Number); var uid; - if (!funcs.isLoggedIn()) { + var loggedIn = funcs.isLoggedIn(); + if (!loggedIn && !tokenId) { return authorUid(existing); } + if (!loggedIn) { existing.some(function (id) { - var author = authors[id] || {}; - if (author.uid !== tokenId) { return; } + var author = authors[id]; + if (!author || author.uid !== tokenId) { return; } uid = Number(id); return true; }); From 9304d7bb2a1cee58059807edd448255ce9957326 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 26 Aug 2021 10:54:18 +0200 Subject: [PATCH 025/125] Ignore required questions in hidden sections --- www/form/inner.js | 183 +++++++++++++++++++++++++++++----------------- 1 file changed, 115 insertions(+), 68 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index f123f015d..d67c23d3f 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2271,7 +2271,7 @@ define([ var getFormResults = function () { if (!Array.isArray(APP.formBlocks)) { return; } var results = {}; - APP.formBlocks.forEach(function (data) { + APP.formBlocks.some(function (data) { if (!data.getValue) { return; } results[data.uid] = data.getValue(); }); @@ -2281,6 +2281,104 @@ define([ Messages.form_anonAnswer = "All answers to this form are anonymous"; // XXX Messages.form_authAnswer = "You can't answer anonymously to this form"; // XXX + var getSections = function (content) { + var uids = Object.keys(content.form).filter(function (uid) { + return content.form[uid].type === 'section'; + }); + return uids; + }; + var getSectionFromQ = function (content, uid) { + var arr = content.order; + var idx = content.order.indexOf(uid); + var sectionUid; + if (idx === -1) { // If it's not in the main array, check in sections + getSections(content).some(function (_uid) { + var block = content.form[_uid]; + if (!block.opts || !Array.isArray(block.opts.questions)) { return; } + var _idx = block.opts.questions.indexOf(uid); + if (_idx !== -1) { + arr = block.opts.questions; + sectionUid = _uid; + idx = _idx; + return true; + } + }); + } + + return { + uid: sectionUid, + arr: arr, + idx: idx + }; + }; + var removeQuestion = function (content, uid) { + delete content.form[uid]; + var idx = content.order.indexOf(uid); + if (idx !== -1) { + content.order.splice(idx, 1); + } else { + getSections(content).some(function (_uid) { + var block = content.form[_uid]; + if (!block.opts || !Array.isArray(block.opts.questions)) { return; } + var _idx = block.opts.questions.indexOf(uid); + if (_idx !== -1) { + block.opts.questions.splice(_idx, 1); + return true; + } + }); + } + }; + var getFullOrder = function (content) { + var order = content.order.slice(); + getSections(content).forEach(function (uid) { + var block = content.form[uid]; + if (!block.opts || !Array.isArray(block.opts.questions)) { return; } + var idx = order.indexOf(uid); + if (idx === -1) { return; } + idx++; + block.opts.questions.forEach(function (el, i) { + order.splice(idx+i, 0, el); + }); + }); + return order; + }; + + var checkResults = {}; + var checkCondition = function (block) { + if (!block || block.type !== 'section') { return; } + if (!block.opts || !Array.isArray(block.opts.questions) || !block.opts.when) { + return true; + } + + // This function may be called multiple times synchronously to check the conditions + // of multiple sections. These sections may require the result of the same question + // so we store the results in an object outside. + // To make sure the results are cleared on the next change in the form, we + // clear it just after the current synchronous actions are done. + setTimeout(function () { checkResults = {}; }); + + var results = checkResults; + var findResult = function (uid) { + if (results.hasOwnProperty(uid)) { return results[uid]; } + APP.formBlocks.some(function (data) { + if (!data.getValue) { return; } + if (data.uid === uid) { + results[uid] = data.getValue(); + return true; + } + }); + return results[uid]; + }; + var w = block.opts.when; + return !w.length || w.some(function (rules) { + return rules.every(function (rule) { + var res = findResult(rule.q); + return rule.is ? res === rule.v + : res !== rule.v; + }); + }); + }; + var makeFormControls = function (framework, content, update, evOnChange) { var loggedIn = framework._.sfCommon.isLoggedIn(); var metadataMgr = framework._.cpNfInner.metadataMgr; @@ -2455,6 +2553,17 @@ define([ if (!data.isEmpty) { return; } if (!block) { return; } if (!block.opts || !block.opts.required) { return; } + + // Don't require questions that are in a hidden section + var section = getSectionFromQ(content, uid); + if (section.uid) { + // Check if section is hidden + var sBlock = form[section.uid]; + var visible = checkCondition(sBlock); + if (!visible) { return; } + } + + var isEmpty = data.isEmpty(); var $el = $(data.tag).closest('.cp-form-block'); $el.find('.cp-form-required-tag').toggleClass('cp-is-empty', isEmpty); @@ -2489,64 +2598,6 @@ define([ reset, send ]); }; - var getSections = function (content) { - var uids = Object.keys(content.form).filter(function (uid) { - return content.form[uid].type === 'section'; - }); - return uids; - }; - var getSectionFromQ = function (content, uid) { - var arr = content.order; - var idx = content.order.indexOf(uid); - if (idx === -1) { // If it's not in the main array, check in sections - getSections(content).some(function (_uid) { - var block = content.form[_uid]; - if (!block.opts || !Array.isArray(block.opts.questions)) { return; } - var _idx = block.opts.questions.indexOf(uid); - if (_idx !== -1) { - arr = block.opts.questions; - idx = _idx; - return true; - } - }); - } - - return { - arr: arr, - idx: idx - }; - }; - var removeQuestion = function (content, uid) { - delete content.form[uid]; - var idx = content.order.indexOf(uid); - if (idx !== -1) { - content.order.splice(idx, 1); - } else { - getSections(content).some(function (_uid) { - var block = content.form[_uid]; - if (!block.opts || !Array.isArray(block.opts.questions)) { return; } - var _idx = block.opts.questions.indexOf(uid); - if (_idx !== -1) { - block.opts.questions.splice(_idx, 1); - return true; - } - }); - } - }; - var getFullOrder = function (content) { - var order = content.order.slice(); - getSections(content).forEach(function (uid) { - var block = content.form[uid]; - if (!block.opts || !Array.isArray(block.opts.questions)) { return; } - var idx = order.indexOf(uid); - if (idx === -1) { return; } - idx++; - block.opts.questions.forEach(function (el, i) { - order.splice(idx+i, 0, el); - }); - }); - return order; - }; var updateForm = function (framework, content, editable, answers, temp) { var $container = $('div.cp-form-creator-content'); if (!$container.length) { return; } // Not ready @@ -3317,17 +3368,13 @@ define([ // In view mode, hide sections when conditions aren't met evOnChange.reg(function (reset, save, condition) { if (!reset && !condition) { return; } - var results = getFormResults(); getSections(content).forEach(function (uid) { var block = content.form[uid]; - if (!block.opts || !Array.isArray(block.opts.questions) || !block.opts.when) { return; } - var w = block.opts.when; - var show = !w.length || w.some(function (rules) { - return rules.every(function (rule) { - return rule.is ? results[rule.q] === rule.v - : results[rule.q] !== rule.v; - }); - }); + if (block.type !== 'section') { return; } + if (!block.opts || !Array.isArray(block.opts.questions) || !block.opts.when) { + return; + } + var show = checkCondition(block); block.opts.questions.forEach(function (_uid) { $container.find('.cp-form-block[data-id="'+_uid+'"]').toggle(show); }); From bf02ec7359bb6dc048bcac928ee47b8d230f8a73 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 26 Aug 2021 14:30:51 +0530 Subject: [PATCH 026/125] animal emojis in color-by-author 'written by' tooltips --- www/code/markers.js | 18 +++++++++++++++--- www/common/inner/common-mediatag.js | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/www/code/markers.js b/www/code/markers.js index b070e68fc..b8a53bdd9 100644 --- a/www/code/markers.js +++ b/www/code/markers.js @@ -3,7 +3,8 @@ 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', +], function (Util, SFCodeMirror, Messages, ChainPad, MT) { var Markers = {}; /* TODO Known Issues @@ -38,7 +39,17 @@ define([ }); } uid = Number(uid); - var name = Util.fixHTML(author.name || Messages.anonymous); + var name = Util.fixHTML((author.name || "").trim()); + 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 +531,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/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 5e091a83c..db15d83d4 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -92,7 +92,7 @@ define([ return ANIMALS[Math.floor(Math.random() * ANIMALS.length)]; }; - var getPseudorandomAnimal = function (seed) { + var getPseudorandomAnimal = MT.getPseudorandomAnimal = function (seed) { if (typeof(seed) !== 'string') { return getRandomAnimal(); } seed = seed.replace(/\D/g, '').slice(0, 10); seed = parseInt(seed); From 69e10df9e069ad199aa8e23807b4fe37b13647ec Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 26 Aug 2021 11:19:32 +0200 Subject: [PATCH 027/125] Form condition based on checkbox question --- www/common/common-ui-elements.js | 2 +- www/form/inner.js | 46 +++++++++++++++++++++++++------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index d8b399851..ac24b1ab9 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1588,7 +1588,7 @@ define([ }); }; $container.getValue = function () { - return value || ''; + return typeof(value) === "undefined" ? '' : value; }; } diff --git a/www/form/inner.js b/www/form/inner.js index d67c23d3f..230a6aea7 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -1455,7 +1455,7 @@ define([ }; $tag.find('input').on('change', function () { checkDisabled(); - evOnChange.fire(); + evOnChange.fire(false, false, true); }); var cursorGetter; var setCursorGetter = function (f) { cursorGetter = f; }; @@ -2373,8 +2373,13 @@ define([ return !w.length || w.some(function (rules) { return rules.every(function (rule) { var res = findResult(rule.q); - return rule.is ? res === rule.v - : res !== rule.v; + // Checkbox + if (Array.isArray(res)) { + var idx = res.indexOf(rule.v); + return rule.is ? idx !== -1 : idx === -1; + } + // Radio + return rule.is ? res === rule.v : res !== rule.v; }); }); }; @@ -2834,14 +2839,21 @@ define([ var values = blocks.map(function(uid) { var block = form[uid]; var type = block.type; - if (['radio'].indexOf(type) === -1) { return; } + if (['radio', 'checkbox'].indexOf(type) === -1) { return; } + var obj = { + uid: uid, + type: type, + q: block.q || Messages.form_default + }; if (type === 'radio') { - return { - uid: uid, - q: block.q || Messages.form_default, - values: block.opts ? block.opts.values : TYPES.radio.defaultOpts.values - }; + obj.values = block.opts ? block.opts.values + : TYPES.radio.defaultOpts.values } + if (type === 'checkbox') { + obj.values = block.opts ? block.opts.values + : TYPES.checkbox.defaultOpts.values + } + return obj; }).filter(Boolean); return values; }; @@ -2888,6 +2900,8 @@ define([ var qSelect = UIElements.createDropdown(qConfig); Messages.form_condition_is = 'is'; // XXX Messages.form_condition_isnot = 'is not'; // XXX + Messages.form_condition_has = 'has'; // XXX + Messages.form_condition_hasnot = 'has not'; // XXX var isOn = !condition || condition.is !== 0; var iOptions = [{ @@ -2925,13 +2939,25 @@ define([ var isChange; qSelect.onChange.reg(function (prettyVal, val, init) { $(iSelect).show(); - var res; + var res, type; values.some(function (obj) { if (String(obj.uid) === String(val)) { res = obj.values; + type = obj.type; return true; } }); + + var $selDiv = $(iSelect); + if (type === 'checkbox') { + $selDiv.find('[data-value="0"]').text(Messages.form_condition_hasnot); + $selDiv.find('[data-value="1"]').text(Messages.form_condition_has); + } else { + $selDiv.find('[data-value="0"]').text(Messages.form_condition_isnot); + $selDiv.find('[data-value="1"]').text(Messages.form_condition_is); + } + iSelect.setValue(iSelect.getValue()); + $content.find('.cp-form-condition-values').remove(); if (!res) { return; } var vOptions = res.map(function (str) { From a701522c61b822bef3238ae2a0d6be57fed976b2 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 26 Aug 2021 15:03:07 +0530 Subject: [PATCH 028/125] animal avatars for guests in join/part messages --- www/common/toolbar.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/www/common/toolbar.js b/www/common/toolbar.js index da8b1dad0..5b359a323 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -1217,18 +1217,31 @@ MessengerUI, Messages, Pages) { } }; + var getFancyGuestName = function (name, uid) { + name = UI.getDisplayName(name); + if (name === Messages.anonymous && uid) { + var animal = MT.getPseudorandomAnimal(uid); + if (animal) { + name = animal + ' ' + name; + } + } + return name; + }; + // Notifications var initNotifications = function (toolbar, config) { // Display notifications when users are joining/leaving the session var oldUserData; if (!config.metadataMgr) { return; } var metadataMgr = config.metadataMgr; - var notify = function(type, name, oldname) { + var notify = function(type, name, oldname, uid) { if (toolbar.isAlone) { return; } // type : 1 (+1 user), 0 (rename existing user), -1 (-1 user) if (typeof name === "undefined") { return; } - name = name || Messages.anonymous; if (Config.disableUserlistNotifications) { return; } + name = getFancyGuestName(name, uid); + oldname = getFancyGuestName(oldname, uid); + switch(type) { case 1: UI.log(Messages._getKey("notifyJoined", [name])); @@ -1277,7 +1290,7 @@ MessengerUI, Messages, Pages) { delete oldUserData[u]; if (temp && newdata[userNetfluxId] && temp.uid === newdata[userNetfluxId].uid) { return; } if (userPresent(u, temp, newdata || oldUserData) < 1) { - notify(-1, temp.name); + notify(-1, temp.name, undefined, temp.uid); } } } @@ -1297,10 +1310,10 @@ MessengerUI, Messages, Pages) { if (typeof oldUserData[k] === "undefined") { // if the same uid is already present in the userdata, don't notify if (!userPresent(k, newdata[k], oldUserData)) { - notify(1, newdata[k].name); + notify(1, newdata[k].name, undefined, newdata[k].uid); } } else if (oldUserData[k].name !== newdata[k].name) { - notify(0, newdata[k].name, oldUserData[k].name); + notify(0, newdata[k].name, oldUserData[k].name, newdata[k].uid); } } } From 8a1f6d66cf3e7a12d7ab8b1b3142ecfd7dd1ab82 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 26 Aug 2021 17:04:38 +0530 Subject: [PATCH 029/125] animal avatars in kanban cursors --- www/common/inner/common-mediatag.js | 3 ++- www/kanban/app-kanban.less | 8 ++++++ www/kanban/inner.js | 39 ++++++++++++++++++----------- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index db15d83d4..e061667ce 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -46,6 +46,7 @@ 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 = ''; if (cursor.avatar && avatars[cursor.avatar]) { html += (cursor.avatar && avatars[cursor.avatar]) || ''; @@ -100,7 +101,7 @@ define([ return ANIMALS[seed % ANIMALS.length]; }; - var getPrettyInitials = function (name) { + var getPrettyInitials = MT.getPrettyInitials = function (name) { var parts = name.split(/\s+/); var text; if (parts.length > 1) { diff --git a/www/kanban/app-kanban.less b/www/kanban/app-kanban.less index 69924ef77..098560548 100644 --- a/www/kanban/app-kanban.less +++ b/www/kanban/app-kanban.less @@ -159,6 +159,14 @@ margin-right: 5px; .tools_unselectable(); cursor: default; + &.cp-cursor.cp-tippy-html { + background-color: var(--red); + // XXX figure out how to inherit this from avatar.less + font-size: 11px; // 20px / 1.8 as per avatar.less.. + &.animal { + font-size: 14px; // 20px / 1.8 * (6/5)... + } + } } } .kanban-item { diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 42b1e39fa..fe80b1007 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -59,7 +59,7 @@ define([ verbose = function () {}; // comment out to enable verbose logging var onRedraw = Util.mkEvent(); var onCursorUpdate = Util.mkEvent(); - var remoteCursors = {}; // XXX + var remoteCursors = {}; var setValueAndCursor = function (input, val, _cursor) { if (!input) { return; } @@ -95,18 +95,27 @@ define([ var getAvatar = function (cursor, noClear) { // Tippy - var html = MT.getCursorAvatar(cursor); // XXX + var html = MT.getCursorAvatar(cursor); - var l = Util.getFirstCharacter(cursor.name || Messages.anonymous); + var name = UI.getDisplayName(cursor.name); + + var l; + var animal = ''; + if (cursor.name === Messages.anonymous && typeof(cursor.uid) === 'string') { + l = MT.getPseudorandomAnimal(cursor.uid); + animal = '.animal'; + } else { + l = MT.getPrettyInitials(name); + } var text = ''; if (cursor.color) { - text = 'color:'+getTextColor(cursor.color)+';'; + text = 'background-color:' + cursor.color + '; color:'+getTextColor(cursor.color)+';'; } - var avatar = h('span.cp-cursor.cp-tippy-html', { // XXX - style: "background-color: " + (cursor.color || 'red') + ";"+text, + var avatar = h('span.cp-cursor.cp-tippy-html' + animal, { + style: text, 'data-cptippy-html': true, - title: html, // XXX "{0} is editing" + title: html, }, l); if (!noClear) { cursor.clear = function () { @@ -852,7 +861,7 @@ define([ getAvatar: getAvatar, openLink: openLink, getTags: getExistingTags, - cursors: remoteCursors, // XXX + cursors: remoteCursors, boards: boards, _boards: Util.clone(boards), }); @@ -1062,6 +1071,7 @@ define([ var kanban; var $container = $('#cp-app-kanban-content'); + var myData = framework._.cpNfInner.metadataMgr.getUserData(); var privateData = framework._.cpNfInner.metadataMgr.getPrivateData(); if (!privateData.isEmbed) { mkHelpMenu(framework); @@ -1101,7 +1111,7 @@ define([ $container.find('.kanban-edit-item').remove(); }); - var getCursor = function () { // XXX + var getCursor = function () { if (!kanban || !kanban.inEditMode) { return; } try { var id = kanban.inEditMode; @@ -1204,7 +1214,7 @@ define([ var remoteContent = newContent.content; if (Sortify(currentContent) !== Sortify(remoteContent)) { - var cursor = getCursor(); // XXX + var cursor = getCursor(); verbose("Content is different.. Applying content"); kanban.options.boards = remoteContent; updateBoards(framework, kanban, remoteContent); @@ -1261,11 +1271,13 @@ define([ }); var myCursor = {}; - onCursorUpdate.reg(function (data) { // XXX + onCursorUpdate.reg(function (data) { + console.log('onCursorUpdate', data); myCursor = data; + myCursor.uid = myData.uid; framework.updateCursor(); }); - framework.onCursorUpdate(function (data) { // XXX + framework.onCursorUpdate(function (data) { if (!data) { return; } if (data.reset) { Object.keys(remoteCursors).forEach(function (id) { @@ -1293,9 +1305,8 @@ define([ if (!cursor.item && !cursor.board) { return; } // Add new cursor - var avatar = getAvatar(cursor); // XXX + var avatar = getAvatar(cursor); var $item = $('.kanban-item[data-eid="'+cursor.item+'"]'); - var $board = $('.kanban-board[data-id="'+cursor.board+'"]'); if ($item.length) { remoteCursors[id] = cursor; $item.find('.cp-kanban-cursors').append(avatar); From 840a16a563c2c1b96c6449db8b2adee398d70f7a Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 26 Aug 2021 17:39:56 +0530 Subject: [PATCH 030/125] add animal emojis to rich text cursors' tooltips --- www/pad/cursor.js | 21 ++++++++++++++++++--- www/pad/inner.js | 3 ++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/www/pad/cursor.js b/www/pad/cursor.js index 42569838f..fbca1a9cf 100644 --- a/www/pad/cursor.js +++ b/www/pad/cursor.js @@ -3,7 +3,9 @@ define([ '/common/common-ui-elements.js', '/common/common-interface.js', '/bower_components/chainpad/chainpad.dist.js', -], function ($, UIElements, UI, ChainPad) { + '/customize/messages.js', + '/common/inner/common-mediatag.js', +], function ($, UIElements, UI, ChainPad, Messages, MT) { var Cursor = {}; Cursor.isCursor = function (el) { @@ -35,13 +37,20 @@ define([ $(el).remove(); }; - Cursor.create = function (inner, hjsonToDom, cursorModule) { + Cursor.create = function (inner, hjsonToDom, cursorModule, uid) { var exp = {}; var cursors = {}; + // XXX despite the name of this function this doesn't actually render as a tippy tooltip + // that means that emojis will use the system font that shows up in native tooltips + // so this might be of limited value/aesthetic appeal compared to other apps' cursors var makeTippy = function (cursor) { - return cursor.name; + //return cursor.name; + if (typeof(cursor.uid) === 'string' && (!cursor.name || cursor.name === Messages.anonymous)) { + return MT.getPseudorandomAnimal(cursor.uid) + ' ' + Messages.anonymous; + } + return cursor.name || Messages.anonymous; }; var makeCursor = function (id, cursor) { @@ -138,6 +147,12 @@ define([ var cursorObj = data.cursor; if (!cursorObj.selectionStart) { return; } + if (cursorObj.name === Messages.anonymous) { + // save a little bit of data from going over the wire... + // remote clients will interpret this as Messages.anonymous (in their UI language) + cursorObj.name = ''; + cursorObj.uid = uid; + } // 1. Transform the cursor to get the offset relative to our doc // 2. Turn it into a range diff --git a/www/pad/inner.js b/www/pad/inner.js index 95e7c1bff..e89cd9a0a 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -678,6 +678,7 @@ define([ var metadataMgr = framework._.sfCommon.getMetadataMgr(); var privateData = metadataMgr.getPrivateData(); + var myData = metadataMgr.getUserData(); var common = framework._.sfCommon; var APP = window.APP; @@ -704,7 +705,7 @@ define([ var cursor = module.cursor = Cursor(inner); // Display other users cursor - var cursors = Cursors.create(inner, hjsonToDom, cursor); + var cursors = Cursors.create(inner, hjsonToDom, cursor, myData.uid); var openLink = function(e) { var el = e.currentTarget; From 1e1890dbe4a4dcaba56c703488ca9dec11c713fb Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 26 Aug 2021 17:42:22 +0530 Subject: [PATCH 031/125] replace a line that I accidentally removed --- www/kanban/inner.js | 1 + 1 file changed, 1 insertion(+) diff --git a/www/kanban/inner.js b/www/kanban/inner.js index fe80b1007..6934dd210 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -1312,6 +1312,7 @@ define([ $item.find('.cp-kanban-cursors').append(avatar); return; } + var $board = $('.kanban-board[data-id="'+cursor.board+'"]'); if ($board.length) { remoteCursors[id] = cursor; $board.find('header .cp-kanban-cursors').append(avatar); From 0ea300638adaa56f3b274611b459014357cd5320 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 26 Aug 2021 19:50:26 +0530 Subject: [PATCH 032/125] generate a report of consistently duplicated translation keys --- .../find-duplicate-translations.js | 116 +++++++++++++----- 1 file changed, 85 insertions(+), 31 deletions(-) diff --git a/scripts/translations/find-duplicate-translations.js b/scripts/translations/find-duplicate-translations.js index f8b25497c..8306fc70c 100644 --- a/scripts/translations/find-duplicate-translations.js +++ b/scripts/translations/find-duplicate-translations.js @@ -1,55 +1,109 @@ -var Util = require("../lib/common-util"); -var EN = Util.clone(require("../www/common/translations/messages.json")); -var FR = Util.clone(require("../www/common/translations/messages.fr.json")); -var DE = Util.clone(require("../www/common/translations/messages.de.json")); -var JP = Util.clone(require("../www/common/translations/messages.ja.json")); - -var keys = Object.keys(EN); - -var duplicates = {}; +var Assert = require("assert"); +var Util = require("../../lib/common-util"); var addIfAbsent = function (A, e) { if (A.includes(e)) { return; } A.push(e); }; -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 = EN[key]; +var findDuplicates = function (map) { + var keys = Object.keys(map); + - //var duplicates = []; - keys.forEach(function (key2) { - if (key === key2) { return; } - var value2 = EN[key2]; - if (value === value2) { - markDuplicate(value, key, key2); + 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); + } + }); }); -}); -// 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 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]); }); -// TODO iterate over all languages and +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 - From 34acded53874b08c5ebe07f3d6be2b4df5deb36f Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Aug 2021 14:07:23 +0530 Subject: [PATCH 033/125] clarify comment in example nginx config --- docs/example.nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From c416303e1d3f9d9fb74821abedf23141e23b8b44 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Aug 2021 18:23:07 +0530 Subject: [PATCH 034/125] set uid in cursor object in outer scope rather than within each app that uses cursors --- www/code/inner.js | 4 +--- www/code/markers.js | 5 +++-- www/common/outer/cursor.js | 1 + www/common/sframe-common-codemirror.js | 1 - www/kanban/inner.js | 2 -- www/pad/cursor.js | 8 +------- www/pad/inner.js | 3 +-- www/slide/inner.js | 6 ++---- 8 files changed, 9 insertions(+), 21 deletions(-) diff --git a/www/code/inner.js b/www/code/inner.js index 4328f0d8f..4f6c0308e 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -388,9 +388,7 @@ define([ var andThen2 = function (editor, CodeMirror, framework, isPresentMode) { var common = framework._.sfCommon; - var metadataMgr = common.getMetadataMgr(); - var privateData = metadataMgr.getPrivateData(); - CodeMirror.uid = metadataMgr.getUserData().uid; + var privateData = common.getMetadataMgr().getPrivateData(); var previewPane = mkPreviewPane(editor, CodeMirror, framework, isPresentMode); var markdownTb = mkMarkdownTb(editor, framework); diff --git a/www/code/markers.js b/www/code/markers.js index b8a53bdd9..48e5a25fb 100644 --- a/www/code/markers.js +++ b/www/code/markers.js @@ -4,7 +4,8 @@ define([ '/customize/messages.js', '/bower_components/chainpad/chainpad.dist.js', '/common/inner/common-mediatag.js', -], function (Util, SFCodeMirror, Messages, ChainPad, MT) { + '/common/common-interface.js', +], function (Util, SFCodeMirror, Messages, ChainPad, MT, UI) { var Markers = {}; /* TODO Known Issues @@ -39,7 +40,7 @@ define([ }); } uid = Number(uid); - var name = Util.fixHTML((author.name || "").trim()); + var name = Util.fixHTML(UI.getDisplayName(author.name)); var animal; if ((!name || name === Messages.anonymous) && typeof(author.uid) === 'string') { animal = MT.getPseudorandomAnimal(author.uid); 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/sframe-common-codemirror.js b/www/common/sframe-common-codemirror.js index 4e490288d..58475bbed 100644 --- a/www/common/sframe-common-codemirror.js +++ b/www/common/sframe-common-codemirror.js @@ -509,7 +509,6 @@ define([ var cursor = {}; cursor.selectionStart = cursorToPos(editor.getCursor('from'), doc); cursor.selectionEnd = cursorToPos(editor.getCursor('to'), doc); - cursor.uid = exp.uid; // FIXME this is inefficient for the network but it's unlikely to trigger errors return cursor; }; diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 6934dd210..368d18139 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -1272,9 +1272,7 @@ define([ var myCursor = {}; onCursorUpdate.reg(function (data) { - console.log('onCursorUpdate', data); myCursor = data; - myCursor.uid = myData.uid; framework.updateCursor(); }); framework.onCursorUpdate(function (data) { diff --git a/www/pad/cursor.js b/www/pad/cursor.js index fbca1a9cf..5b4c60ad7 100644 --- a/www/pad/cursor.js +++ b/www/pad/cursor.js @@ -37,7 +37,7 @@ define([ $(el).remove(); }; - Cursor.create = function (inner, hjsonToDom, cursorModule, uid) { + Cursor.create = function (inner, hjsonToDom, cursorModule) { var exp = {}; var cursors = {}; @@ -147,12 +147,6 @@ define([ var cursorObj = data.cursor; if (!cursorObj.selectionStart) { return; } - if (cursorObj.name === Messages.anonymous) { - // save a little bit of data from going over the wire... - // remote clients will interpret this as Messages.anonymous (in their UI language) - cursorObj.name = ''; - cursorObj.uid = uid; - } // 1. Transform the cursor to get the offset relative to our doc // 2. Turn it into a range diff --git a/www/pad/inner.js b/www/pad/inner.js index e89cd9a0a..95e7c1bff 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -678,7 +678,6 @@ define([ var metadataMgr = framework._.sfCommon.getMetadataMgr(); var privateData = metadataMgr.getPrivateData(); - var myData = metadataMgr.getUserData(); var common = framework._.sfCommon; var APP = window.APP; @@ -705,7 +704,7 @@ define([ var cursor = module.cursor = Cursor(inner); // Display other users cursor - var cursors = Cursors.create(inner, hjsonToDom, cursor, myData.uid); + var cursors = Cursors.create(inner, hjsonToDom, cursor); var openLink = function(e) { var el = e.currentTarget; diff --git a/www/slide/inner.js b/www/slide/inner.js index 9bbfc6e4d..9c9b9c70b 100644 --- a/www/slide/inner.js +++ b/www/slide/inner.js @@ -459,9 +459,7 @@ define([ var andThen2 = function (editor, CodeMirror, framework, isPresentMode) { var common = framework._.sfCommon; - var metadataMgr = common.getMetadataMgr(); - var privateData = metadataMgr.getPrivateData(); - CodeMirror.uid = metadataMgr.getUserData().uid; + var privateData = common.getMetadataMgr().getPrivateData(); var $contentContainer = $('#cp-app-slide-editor'); var $modal = $('#cp-app-slide-modal'); @@ -513,7 +511,7 @@ define([ framework.updateCursor(); }, 500); // 500ms to make sure it is sent after chainpad sync }; - framework.onCursorUpdate(CodeMirror.setRemoteCursor); // XXX + framework.onCursorUpdate(CodeMirror.setRemoteCursor); framework.setCursorGetter(CodeMirror.getCursor); editor.on('cursorActivity', updateCursor); From cd613f10377c655340d7534c61ee9868f1c81fd2 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Aug 2021 19:38:50 +0530 Subject: [PATCH 035/125] many small improvements for animal avatars * more consistent scaling for animal avatars relative to the font-size of username's initials * configurable emoji lists via AppConfig.emojiAvatars * various comments from code review * fixed a bug that caused the user admin menu button to not be redrawn on name changes * guard against empty animal emojis in case the admin sets the list to [] --- customize.dist/src/less2/include/avatar.less | 10 ++++---- customize.dist/src/less2/include/toolbar.less | 7 +++++- www/common/application_config_internal.js | 2 ++ www/common/common-messaging.js | 2 +- www/common/common-ui-elements.js | 4 ++- www/common/inner/access.js | 1 + www/common/inner/common-mediatag.js | 25 ++++++++++--------- www/kanban/app-kanban.less | 5 ++-- www/kanban/inner.js | 10 +++++--- www/pad/cursor.js | 6 +++-- 10 files changed, 44 insertions(+), 28 deletions(-) diff --git a/customize.dist/src/less2/include/avatar.less b/customize.dist/src/less2/include/avatar.less index 079ceb60d..acaa13351 100644 --- a/customize.dist/src/less2/include/avatar.less +++ b/customize.dist/src/less2/include/avatar.less @@ -5,6 +5,10 @@ ) { @avatar-width: @width; @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(); @@ -41,11 +45,7 @@ font-size: @avatar-font-size; font-size: var(--avatar-font-size); .animal { - font-size: 20px; - // 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 - font-size: calc(var(--avatar-width) * (6/5)); + font-size: @avatar-font-size-animal; } } media-tag { diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index 28b513095..2092d72cc 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -855,10 +855,15 @@ span { text-align: center; width: 100%; - font-size: 40px; + .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/www/common/application_config_internal.js b/www/common/application_config_internal.js index 3be979f17..2993edc52 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/common-messaging.js b/www/common/common-messaging.js index 4a3eb2a91..1e6e26ce3 100644 --- a/www/common/common-messaging.js +++ b/www/common/common-messaging.js @@ -18,7 +18,7 @@ define([ curvePublic: proxy.curvePublic, notifications: Util.find(proxy, ['mailboxes', 'notifications', 'channel']), avatar: proxy.profile && proxy.profile.avatar, - uid: proxy.uid, + uid: proxy.uid, // XXX test without this and see if it breaks things }; if (hash === false) { delete data.channel; } return data; diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index d0ef0af3f..abf45b0bd 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1996,6 +1996,7 @@ define([ var to; var oldUrl = ''; var oldUid; + var oldName; var updateButton = function () { var myData = metadataMgr.getUserData(); var privateData = metadataMgr.getPrivateData(); @@ -2017,11 +2018,12 @@ define([ var newName = UI.getDisplayName(myData.name); var url = myData.avatar; $displayName.text(newName); - if ((accountName && oldUrl !== url) || !accountName && uid !== oldUid) { + if ((accountName && oldUrl !== url) || !accountName && uid !== oldUid || oldName !== newName) { $avatar.html(''); 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; diff --git a/www/common/inner/access.js b/www/common/inner/access.js index 119714de3..0ba857a0a 100644 --- a/www/common/inner/access.js +++ b/www/common/inner/access.js @@ -787,6 +787,7 @@ define([ // 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 e061667ce..21cb25e82 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; @@ -49,7 +50,7 @@ define([ // TODO it would be nice to have "{0} is editing" instead of just their name var html = ''; if (cursor.avatar && avatars[cursor.avatar]) { - html += (cursor.avatar && avatars[cursor.avatar]) || ''; + html += avatars[cursor.avatar]; } else if (animal_avatars[uid]) { html += animal_avatars[uid] + ' '; } @@ -87,18 +88,20 @@ define([ }; // https://emojipedia.org/nature/ - var ANIMALS = '🙈 🦀 🐞 🦋 🐬 🐋 🐢 🦉 🦆 🐧 🦡 🦘 🦨 🦦 🦥 🐼 🐻 🦝 🦓 🐄 🐷 🐐 🦙 🦒 🐘 🦏 🐁 🐹 🐰 🦫 🦔 🐨 🐱 🐺 👺 👹 👽 👾 🤖'.split(/\s+/); + var ANIMALS = AppConfig.emojiAvatars || []; - var getRandomAnimal = function () { + var getRandomAnimal = function () { // XXX should never actually happen? + if (!ANIMALS.length) { return ''; } return ANIMALS[Math.floor(Math.random() * ANIMALS.length)]; }; var getPseudorandomAnimal = MT.getPseudorandomAnimal = function (seed) { + if (!ANIMALS.length) { return ''; } if (typeof(seed) !== 'string') { return getRandomAnimal(); } - seed = seed.replace(/\D/g, '').slice(0, 10); + seed = seed.replace(/\D/g, '').slice(0, 10); // XXX possible optimization for on-wire uid seed = parseInt(seed); if (!seed) { return getRandomAnimal(); } - return ANIMALS[seed % ANIMALS.length]; + return ANIMALS[seed % ANIMALS.length] || ''; }; var getPrettyInitials = MT.getPrettyInitials = function (name) { @@ -123,29 +126,27 @@ define([ if (uid && animal_avatars[uid]) { animal_avatar = animal_avatars[uid]; } - var animal = false; - name = (name || "").trim() || Messages.anonymous; + name = UI.getDisplayName(name); var text; - if (name === Messages.anonymous && uid) { + if (ANIMALS.length && name === Messages.anonymous && uid) { if (animal_avatar) { text = animal_avatar; } else { text = animal_avatar = getPseudorandomAnimal(uid); } - animal = true; } else { text = getPrettyInitials(name); } var $avatar = $('', { - 'class': 'cp-avatar-default' + (animal? ' animal': ''), + 'class': 'cp-avatar-default' + (animal_avatar? ' animal': ''), // XXX prevents screenreaders from trying to describe this alt: '', 'aria-hidden': true, }).text(text); $container.append($avatar); - if (uid && animal) { + if (uid && animal_avatar) { animal_avatars[uid] = animal_avatar; } if (cb) { cb(); } diff --git a/www/kanban/app-kanban.less b/www/kanban/app-kanban.less index 098560548..5996940e4 100644 --- a/www/kanban/app-kanban.less +++ b/www/kanban/app-kanban.less @@ -160,11 +160,12 @@ .tools_unselectable(); cursor: default; &.cp-cursor.cp-tippy-html { + .avatar_vars(20px); background-color: var(--red); // XXX figure out how to inherit this from avatar.less - font-size: 11px; // 20px / 1.8 as per avatar.less.. + font-size: @avatar-font-size; //var(11px; // 20px / 1.8 as per avatar.less.. &.animal { - font-size: 14px; // 20px / 1.8 * (6/5)... + font-size: @avatar-font-size-animal; //14px; // 20px / 1.8 * (6/5)... } } } diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 368d18139..add0ed693 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -99,12 +99,15 @@ define([ var name = UI.getDisplayName(cursor.name); - var l; + var l; // label? var animal = ''; if (cursor.name === Messages.anonymous && typeof(cursor.uid) === 'string') { l = MT.getPseudorandomAnimal(cursor.uid); - animal = '.animal'; - } else { + if (l) { + animal = '.animal'; + } + } + if (!l) { l = MT.getPrettyInitials(name); } @@ -1071,7 +1074,6 @@ define([ var kanban; var $container = $('#cp-app-kanban-content'); - var myData = framework._.cpNfInner.metadataMgr.getUserData(); var privateData = framework._.cpNfInner.metadataMgr.getPrivateData(); if (!privateData.isEmbed) { mkHelpMenu(framework); diff --git a/www/pad/cursor.js b/www/pad/cursor.js index 5b4c60ad7..ae91c80f1 100644 --- a/www/pad/cursor.js +++ b/www/pad/cursor.js @@ -46,9 +46,11 @@ define([ // that means that emojis will use the system font that shows up in native tooltips // so this might be of limited value/aesthetic appeal compared to other apps' cursors var makeTippy = function (cursor) { - //return cursor.name; if (typeof(cursor.uid) === 'string' && (!cursor.name || cursor.name === Messages.anonymous)) { - return MT.getPseudorandomAnimal(cursor.uid) + ' ' + Messages.anonymous; + var animal = MT.getPseudorandomAnimal(cursor.uid); + if (animal) { + return animal + ' ' + Messages.anonymous; + } } return cursor.name || Messages.anonymous; }; From 45c4765ce00950850dcb1ada86aac1836eae494d Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 27 Aug 2021 20:10:17 +0530 Subject: [PATCH 036/125] update changelog and make a note about a bug in getAuthorId --- CHANGELOG.md | 25 +++++++++++++++++++++++-- www/common/sframe-common.js | 7 ++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 185a9ae29..7325e2d21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,32 @@ ## 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 + ## 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/www/common/sframe-common.js b/www/common/sframe-common.js index 000d049b3..3575c95b0 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -235,7 +235,7 @@ define([ }; }; - funcs.getAuthorId = function () { + funcs.getAuthorId = function () { // XXX }; var authorUid = function(existing) { @@ -263,6 +263,11 @@ define([ }); return uid || authorUid(existing); } + // XXX 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; } From 79ca8778e78bdac05e4e3819d55421166623e3c0 Mon Sep 17 00:00:00 2001 From: Weblate Date: Mon, 30 Aug 2021 03:57:29 +0200 Subject: [PATCH 037/125] Translated using Weblate (Spanish) Currently translated at 45.9% (636 of 1384 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/es/ Translated using Weblate (Spanish) Currently translated at 45.9% (636 of 1384 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/es/ Translated using Weblate (Spanish) Currently translated at 45.9% (636 of 1384 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/es/ --- www/common/translations/messages.es.json | 41 +++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/www/common/translations/messages.es.json b/www/common/translations/messages.es.json index 2c6c4e98a..3864ea8a8 100644 --- a/www/common/translations/messages.es.json +++ b/www/common/translations/messages.es.json @@ -607,5 +607,44 @@ "share_linkEdit": "Editar", "share_linkAccess": "Permisos de acceso", "properties_passwordWarning": "La contraseña ha sido cambiada con éxito pero fue incapaz de actualizar tu CryptDrive con los nuevos datos. Es posible que tenga que eliminar la versión antigua del pad manualmente.
Pulse OK para recargar y actualizar sus derechos de acceso.", - "properties_passwordSuccess": "La contraseña ha sido cambiada con éxito.
Pulsa OK para recargar y actualizar tus derechos de acceso." + "properties_passwordSuccess": "La contraseña ha sido cambiada con éxito.
Pulsa OK para recargar y actualizar tus derechos de acceso.", + "admin_updateLimitTitle": "Actualizar cuotas de usuarios", + "admin_registeredHint": "Número de usuarios registrados en tu instancia", + "admin_registeredTitle": "Usuarios registrados", + "admin_activePadsHint": "Número de documentos únicos que se están viendo o editando ahora", + "admin_activePadsTitle": "Blocs activos", + "admin_activeSessionsHint": "Número de conexiones websocket activas (y direcciones IP únicas conectadas)", + "admin_activeSessionsTitle": "Conexiones activas", + "adminPage": "Administración", + "admin_cat_stats": "Estadísticas", + "admin_cat_general": "General", + "admin_authError": "Solo los administradores pueden acceder a esta página.", + "markdown_toc": "Contenido", + "survey": "Encuesta de CryptPad", + "crowdfunding_popup_no": "Ahora no", + "crowdfunding_popup_text": "

¡Necesitamos tu ayuda!

Para garantizar que CryptPad se desarrolle activamente, considera apoyar el proyecto a través de la página OpenCollective, donde puedes ver nuestra Hoja de ruta y Objetivos de financiación.", + "autostore_notAvailable": "Tienes que guardar el bloc en tu CryptDrive antes de usar esta función.", + "autostore_forceSave": "Guarda el fichero en tu CryptDrive", + "autostore_saved": "El bloc se guardó correctamente en tu CryptDrive!", + "autostore_error": "Error extraño: No pudimos guardar este bloc, inténtalo otra vez.", + "autostore_hide": "No guardar", + "autostore_store": "Guardar", + "autostore_settings": "Puedes habilitar el almacenamiento automático de blocs en su Página de configuración!", + "autostore_notstored": "Este {0} no está en su CryptDrive. ¿Quieres guardarlo ahora?", + "autostore_sf": "carpeta", + "chrome68": "Parece que estás usando el navegador Chrome o Chromium versión 68. Contiene un error que hace que la página se vuelva completamente blanca después de unos segundos o que la página no responda a los clics. Para solucionar este problema, puede cambiar a otra pestaña y volver, o tratar de desplazarse en la página. Este error debe corregirse en la próxima versión de su navegador.", + "convertFolderToSF_confirm": "Esta carpeta debe convertirse a carpeta compartida para que otros la puedan ver. ¿Continuar?", + "sharedFolders_create_owned": "Carpeta con propietario", + "sharedFolders_create_name": "Nombre de carpeta", + "share_embedCategory": "Embeber", + "share_linkPresent": "Presentar", + "share_linkEmbed": "Modo de inserción (oculta barra de herramientas y lista de usuarios)", + "settings_padOpenLinkLabel": "Habilita la apertura directa del enlace", + "settings_padOpenLinkHint": "Con esta opción puedes abrir enlaces embebidos haciendo click sin el popup de previsualización", + "properties_changePasswordButton": "Enviar", + "convertFolderToSF_SFChildren": "Esta carpeta no puede ser convertida a una carpeta compartida porque ya contiene carpetas compartidas. Mueva esas carpetas compartidas a otro lugar para continuar.", + "convertFolderToSF_SFParent": "Esta carpeta no puede ser convertida a una carpeta compartida en su actual localización. Mueva la carpeta fuera de la carperta compartida para continuar.", + "sharedFolders_share": "Comparte este enlace con otros usuarios registrados para darles accesso a la carpeta compartida. Una vez que ellos/as abran este enlace, la carpeta compartida será añadida a sus CryptDrive.", + "sharedFolders_create": "Crear una carpeta compartida", + "sharedFolders_duplicate": "Algunos de los documentos que intentaste mover ya estaban compartidos en la carpeta de destino" } From 86c9aaac233755b42a18e59fd8ad11b542dddade Mon Sep 17 00:00:00 2001 From: Weblate Date: Mon, 30 Aug 2021 03:57:29 +0200 Subject: [PATCH 038/125] Translated using Weblate (German) Currently translated at 100.0% (1384 of 1384 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/de/ Translated using Weblate (German) Currently translated at 99.9% (1383 of 1384 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/de/ --- www/common/translations/messages.de.json | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/www/common/translations/messages.de.json b/www/common/translations/messages.de.json index 0b5db20e7..35067f597 100644 --- a/www/common/translations/messages.de.json +++ b/www/common/translations/messages.de.json @@ -26,7 +26,7 @@ "typeError": "Dieses Dokument ist nicht mit der ausgewählten Anwendung kompatibel", "onLogout": "Du bist ausgeloggt. {0}Klicke hier{1}, um dich wieder einzuloggen,
oder drücke Escape, um dein Pad schreibgeschützt zu benutzen.", "padNotPinned": "Dieses Pad wird nach 3 Monaten ohne Aktivität auslaufen, {0}logge dich ein{1} oder {2}registriere dich{3}, um das Auslaufen zu verhindern.", - "anonymousStoreDisabled": "Der Webmaster dieses CryptPad-Servers hat die anonyme Verwendung des Speichers deaktiviert. Du musst dich einloggen, um CryptDrive zu verwenden.", + "anonymousStoreDisabled": "Der Administrator dieser CryptPad-Instanz hat die Verwendung des Speichers für Gäste deaktiviert. Logge dich ein, um dein persönliches CryptDrive zu verwenden.", "expiredError": "Dieses Pad ist abgelaufen und nicht mehr verfügbar.", "deletedError": "Dieses Dokument wurde gelöscht und ist nicht mehr verfügbar.", "inactiveError": "Dieses Pad ist wegen Inaktivität gelöscht worden. Drücke Esc, um ein neues Pad zu erstellen.", @@ -50,7 +50,7 @@ "forgotten": "In den Papierkorb verschoben", "errorState": "Kritischer Fehler: {0}", "readonly": "schreibgeschützt", - "anonymous": "Anonym", + "anonymous": "Gast", "users": "Nutzer", "viewer": "Betrachter", "viewers": "Betrachter", @@ -326,7 +326,7 @@ "login_invalUser": "Der Benutzername kann nicht leer sein", "login_invalPass": "Der Passwort kann nicht leer sein", "login_unhandledError": "Ein unerwarteter Fehler ist aufgetreten :(", - "register_importRecent": "Dokumente aus deiner nicht-registrierten Sitzung importieren", + "register_importRecent": "Dokumente aus deiner Gast-Sitzung importieren", "register_acceptTerms": "Ich bin mit den Nutzungsbedingungen einverstanden", "register_passwordsDontMatch": "Passwörter stimmen nicht überein!", "register_passwordTooShort": "Passwörter müssen mindestens {0} Zeichen haben.", @@ -498,7 +498,7 @@ "whatis_drive": "Organisieren mit CryptDrive", "features": "Funktionen", "features_title": "Funktionen", - "features_anon": "Nicht-registriert", + "features_anon": "Gast", "features_registered": "Registriert", "features_premium": "Premium", "features_f_apps": "Zugang zu allen Anwendungen", @@ -510,7 +510,7 @@ "features_f_cryptdrive0_note": "Du kannst besuchte Dokumente in deinem Browser speichern, damit du sie später öffnen kannst", "features_f_storage0": "Speicherung für eine begrenzte Zeit", "features_f_storage0_note": "Dokumente werden nach {0} Tagen ohne Aktivität gelöscht", - "features_f_anon": "Alle Funktionen für anonyme Benutzer", + "features_f_anon": "Alle Funktionen für Gäste", "features_f_anon_note": "Mit zusätzlichen Funktionen", "features_f_cryptdrive1": "Alle Funktionen des CryptDrives", "features_f_cryptdrive1_note": "Ordner, geteilte Ordner, Vorlagen, Tags", @@ -882,7 +882,7 @@ "oo_exportInProgress": "Export wird durchgeführt", "oo_sheetMigration_loading": "Deine Tabelle wird auf die neueste Version aktualisiert. Bitte warte etwa eine Minute.", "oo_sheetMigration_complete": "Eine aktualisierte Version ist verfügbar. Klicke auf OK, um neu zu laden.", - "oo_sheetMigration_anonymousEditor": "Die Bearbeitung dieser Tabelle ist für nicht-registrierte Benutzer deaktiviert, bis sie von einem registrierten Benutzer auf die neueste Version aktualisiert wird.", + "oo_sheetMigration_anonymousEditor": "Die Bearbeitung dieser Tabelle ist für Gäste deaktiviert, bis sie von einem registrierten Benutzer auf die neueste Version aktualisiert wird.", "imprint": "Impressum", "isContact": "{0} ist einer deiner Kontakte", "isNotContact": "{0} ist nicht einer deiner Kontakte", @@ -1120,7 +1120,7 @@ "whatis_apps": "Eine vollständige Anwendungs-Suite", "whatis_drive_info": "

Speichere und verwalte Dokumente mit CryptDrive. Erstelle Ordner, gemeinsame Ordner und Tags, um Dokumente zu organisieren. Lade Dateien hoch und teile sie (PDFs, Fotos, Video, Audio, etc.). Team-Drives werden zwischen Benutzern geteilt und ermöglichen eine gemeinsame Organisation und detaillierte Zugriffskontrolle.

", "whatis_apps_info": "

CryptPad bietet eine vollwertige Office-Suite mit allen notwendigen Werkzeugen für eine produktive Zusammenarbeit. Die Anwendungen umfassen: Rich Text, Tabellen, Code/Markdown, Kanban, Präsentationen, Whiteboard und Umfragen.

Die Anwendungen werden ergänzt durch eine Reihe von Funktionen zur Zusammenarbeit wie Chat, Kontakte, Farbe nach Autor (Code/Markdown) und Kommentare mit Erwähnungen (Rich Text).

", - "register_notes": "
  • Dein Passwort ist der geheime Schlüssel, der alle deine Dokumente verschlüsselt. Wenn du ihn verlierst, gibt es keine Möglichkeit, deine Daten wiederherzustellen.
  • Wenn du einen gemeinsam genutzten Computer verwendest, denke daran, dich abzumelden, wenn du fertig bist. Durch bloßes Schließen des Browserfensters bleibt das Konto ungeschützt.
  • Um die erstellten und/oder gespeicherten Dokumente zu behalten, ohne eingeloggt zu sein, setze einen Haken bei \"Dokumente aus deiner nicht-registrierten Sitzung importieren\".
", + "register_notes": "
  • Dein Passwort ist der geheime Schlüssel, der alle deine Dokumente verschlüsselt. Wenn du ihn verlierst, gibt es keine Möglichkeit, deine Daten wiederherzustellen.
  • Wenn du einen gemeinsam genutzten Computer verwendest, denke daran, dich abzumelden, wenn du fertig bist. Durch bloßes Schließen des Browserfensters bleibt das Konto ungeschützt.
  • Um die Dokumente zu behalten, die du erstellt und/oder gespeichert hast, ohne eingeloggt zu sein, setze einen Haken bei \"Dokumente aus deiner Gast-Sitzung importieren\".
", "settings_cacheTitle": "Cache", "settings_cacheButton": "Cache leeren", "settings_cacheCheckbox": "Cache auf diesem Gerät aktivieren", @@ -1382,5 +1382,7 @@ "fm_link_new": "Neuer Link", "form_totalResponses": "Antworten insgesamt: {0}", "ui_expand": "Ausklappen", - "ui_collapse": "Einklappen" + "ui_collapse": "Einklappen", + "support_premiumPriority": "Premium-Benutzer helfen bei der Verbesserung der Benutzerfreundlichkeit von CryptPad und profitieren von priorisierten Antworten auf ihre Support-Tickets.", + "support_premiumLink": "Abonnementoptionen ansehen" } From cea38b1ff4c23000ebd5698192a6271f3dd6442c Mon Sep 17 00:00:00 2001 From: Weblate Date: Mon, 30 Aug 2021 03:57:30 +0200 Subject: [PATCH 039/125] Translated using Weblate (Japanese) Currently translated at 100.0% (1384 of 1384 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/ja/ --- www/common/translations/messages.ja.json | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/www/common/translations/messages.ja.json b/www/common/translations/messages.ja.json index 55bb0b8c5..656239dec 100644 --- a/www/common/translations/messages.ja.json +++ b/www/common/translations/messages.ja.json @@ -123,7 +123,7 @@ "login_hashing": "パスワードをハッシュ化しています。この処理には時間がかかる場合があります。", "login_invalPass": "パスワードを入力してください", "login_invalUser": "ユーザー名を入力してください", - "register_importRecent": "匿名セッションのドキュメントをインポート", + "register_importRecent": "ゲストセッションのドキュメントをインポート", "importButton": "インポート", "main_catch_phrase": "コラボレーションスイート
端末間暗号化とオープンソース", "tos_3rdparties": "私たちは、法令に基づく場合を除き、個人情報を第三者に提供しません。", @@ -206,7 +206,7 @@ "reconnecting": "再接続中", "synchronizing": "同期中", "initializing": "初期化中...", - "anonymous": "匿名", + "anonymous": "ゲスト", "editor": "編集者", "typing": "編集中", "team_infoLabel": "チームについて", @@ -265,7 +265,7 @@ "features_f_cryptdrive1_note": "フォルダ、共有フォルダ、テンプレート、タグ", "features_f_cryptdrive1": "CryptDriveの全機能", "features_f_anon_note": "追加機能あり", - "features_f_anon": "匿名ユーザーの全機能", + "features_f_anon": "ゲストユーザーの全機能", "features_f_storage0_note": "ドキュメントは{0}日以上利用されないと削除されます", "features_f_storage0": "一時的な保存", "features_f_cryptdrive0_note": "アクセスしたパッドをブラウザに保存して、後で開くことができます", @@ -278,7 +278,7 @@ "features_premium": "プレミアム", "features_registered": "登録済", "features_title": "機能", - "features_anon": "未登録", + "features_anon": "ゲスト", "register_whyRegister": "登録するメリットをご紹介します", "historyText": "履歴", "help_button": "ヘルプ", @@ -481,7 +481,7 @@ "upload_pending": "保留中", "settings_resetTips": "ヒント", "chrome68": "バージョン68のChromeもしくはChromiumを使用しているようです。このバージョンには、数秒経過した後でページが白紙になったり、クリックにページが反応しなくなったりするバグがあります。この問題を解決するには、別のタブを表示して改めて表示するか、このページでスクロールを試みてください。このバグは次のバージョンで解決される予定となっています。", - "register_notes": "
  • ドキュメントは、パスワードによって暗号化されます。パスワードを紛失すると、データを復元することはできません。
  • 共有のコンピュータを使用している場合は、作業完了時に忘れずログアウトしてください。 ブラウザーのウインドウを閉じても、アカウントからはログアウトされません。
  • 未ログインで作成、共有したファイルを保存するには、 「匿名セッションのドキュメントをインポート」にチェックをしてください。
", + "register_notes": "
  • ドキュメントは、パスワードによって暗号化されます。パスワードを紛失すると、データを復元することはできません。
  • 共有のコンピュータを使用している場合は、作業完了時に忘れずログアウトしてください。 ブラウザーのウインドウを閉じても、アカウントからはログアウトされません。
  • 未ログインで作成、共有したファイルを保存するには、 「ゲストセッションのドキュメントをインポート」にチェックをしてください。
", "poll_commit": "送信", "admin_removeDonateButtonTitle": "クラウドファンディングへの参加", "mediatag_notReady": "ダウンロードを完了してください", @@ -774,7 +774,7 @@ "inactiveError": "このパッドは利用されていなかったため削除されました。Escキーを押すと新しいパッドを作成します。", "deletedError": "このドキュメントは削除されたため、利用できなくなりました。", "expiredError": "このパッドは利用期限を過ぎてしまったため、利用できなくなりました。", - "anonymousStoreDisabled": "このCryptPadのインスタンスの管理者は、匿名ユーザーによる保存を無効に設定しています。CryptDriveを使用するにはログインする必要があります。", + "anonymousStoreDisabled": "このCryptPadのインスタンスの管理者は、ゲストによる保存を無効に設定しています。あなたのCryptDriveにアクセスするにはログインが必要です。", "padNotPinnedVariable": "このパッドは{4}日間利用しないと有効期限が切れます。{0}ログイン{1}するか{2}登録{3}して保存してください。", "padNotPinned": "このパッドは3か月間利用しないと有効期限が切れます。{0}ログイン{1}するか{2}登録{3}して保存してください。", "onLogout": "ログアウトしました。{0}ここをクリック{1}するか
Escapeキーを押すと、閲覧モードでパッドにアクセスできます。", @@ -1095,7 +1095,7 @@ "trimHistory_getSizeError": "ドライブの履歴のサイズを計算している途中でエラーが発生しました", "profile_login": "このユーザーを連絡先に追加するにはログインする必要があります", "dontShowAgain": "再び表示しない", - "oo_sheetMigration_anonymousEditor": "登録ユーザーが最新のバージョンに更新するまで、このスプレッドシートを未登録ユーザーが編集することはできません。", + "oo_sheetMigration_anonymousEditor": "登録ユーザーが最新のバージョンに更新するまで、このスプレッドシートをゲストが編集することはできません。", "oo_invalidFormat": "このファイルはインポートできません", "burnAfterReading_warningDeleted": "このパッドは削除されました。ウインドウを閉じた後で再びアクセスすることはできません。", "burnAfterReading_proceed": "表示して削除", @@ -1382,5 +1382,7 @@ "form_answerAs": "名前を記入してください", "form_totalResponses": "回答数:{0}", "ui_expand": "広げる", - "ui_collapse": "折りたたむ" + "ui_collapse": "折りたたむ", + "support_premiumPriority": "プレミアムユーザーになると、CryptPadの使い勝手を改良する手助けができるほか、サポートチケットに対する優先サポートを受けることができます。", + "support_premiumLink": "定額利用のオプションを表示" } From e074b3676120e044b6dde86a0ff7a10d7583c3c9 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 Aug 2021 17:48:31 +0530 Subject: [PATCH 040/125] lint compliance --- scripts/translations/find-duplicate-translations.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/translations/find-duplicate-translations.js b/scripts/translations/find-duplicate-translations.js index 8306fc70c..a6bc53613 100644 --- a/scripts/translations/find-duplicate-translations.js +++ b/scripts/translations/find-duplicate-translations.js @@ -48,12 +48,14 @@ var findDuplicates = function (map) { 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")), From 7231fc6926c4d3b1b5a863e454149c976f91afd3 Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 Aug 2021 17:51:23 +0530 Subject: [PATCH 041/125] media-tag accessibility and UX improvements * preview content and prompt users to describe media when uploading * add 'alt' attributes to rendered media-tag content if it is included in the file's encrypted metadata * add alt attributes to some related UI elements --- .../src/less2/include/modals-ui-elements.less | 9 ++++ www/common/common-ui-elements.js | 6 ++- www/common/media-tag.js | 44 ++++++++++++---- www/common/sframe-common-file.js | 52 ++++++++++++++----- www/profile/inner.js | 4 +- 5 files changed, 92 insertions(+), 23 deletions(-) diff --git a/customize.dist/src/less2/include/modals-ui-elements.less b/customize.dist/src/less2/include/modals-ui-elements.less index 7ec19a699..3e449d495 100644 --- a/customize.dist/src/less2/include/modals-ui-elements.less +++ b/customize.dist/src/less2/include/modals-ui-elements.less @@ -273,4 +273,13 @@ } } } + #cp-upload-preview-container { + max-width: 100%; + media-tag { + max-width: 100%; + & > * { + max-width: 100%; + } + } + } } diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index abf45b0bd..f77a9d422 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1658,6 +1658,7 @@ define([ UI.openCustomModal(modal); }; + Messages.toolbar_userMenuAlt = "User menu"; // XXX UIElements.createUserAdminMenu = function (Common, config) { var metadataMgr = Common.getMetadataMgr(); @@ -1990,8 +1991,11 @@ define([ */ var $displayName = $userAdmin.find('.'+displayNameCls); + $userAdmin.attr({ // XXX is this on the right element? + alt: Messages.toolbar_userMenuAlt, + }); - var $avatar = $userAdmin.find('> button .cp-dropdown-button-title'); // XXX alt="User menu" + var $avatar = $userAdmin.find('> button .cp-dropdown-button-title'); var loadingAvatar; var to; var oldUrl = ''; diff --git a/www/common/media-tag.js b/www/common/media-tag.js index d1a5ebcda..e8b35a47e 100644 --- a/www/common/media-tag.js +++ b/www/common/media-tag.js @@ -73,7 +73,6 @@ var factory = function () { * @param {object} cfg Object {Plugins, allowed, download, pdf} containing infos about plugins * @param {function} cb Callback function: (err, pluginElement) => {} */ - // XXX add alt attributes if present in metadata text: function (metadata, url, content, cfg, cb) { var plainText = document.createElement('div'); plainText.className = "plain-text-reader"; @@ -88,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); }, @@ -95,15 +95,19 @@ 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) { + // XXX alt text var iframe = document.createElement('iframe'); if (cfg.pdf.viewer) { // PDFJS var viewerUrl = cfg.pdf.viewer + '?file=' + url; @@ -116,6 +120,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 () { @@ -543,7 +548,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); @@ -597,6 +602,15 @@ var factory = function () { }); }; + var initHandlers = function () { + return { + 'progress': [], + 'complete': [], + 'metadata': [], + 'error': [] + }; + }; + // Initialize a media-tag var init = function (el, cfg) { cfg = cfg || {}; @@ -614,13 +628,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 @@ -763,6 +771,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/sframe-common-file.js b/www/common/sframe-common-file.js index 1286da14d..a56313444 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,11 @@ define([ }); return manualStore; }; - var fileUploadModal = function (defaultFileName, cb) { + + Messages.upload_modal_alt = "Alt text"; // XXX + Messages.upload_addOptionalAlt = "Add descriptive text (optional)"; // XXX + + var fileUploadModal = function (defaultFileName, cb, preview) { var parsedName = /^(\.?.+?)(\.[^.]+)?$/.exec(defaultFileName) || []; var ext = parsedName[2] || ""; @@ -321,9 +327,15 @@ 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), // XXX alt text for uploads + 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 +347,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 +361,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 +380,8 @@ define([ name: newName, password: password, owned: owned, - forceSave: forceSave + forceSave: forceSave, + alt: alt, }); }); }; @@ -437,6 +452,8 @@ define([ } var thumb; + var preview; + var alt; var file_arraybuffer; var name = file.name; var password; @@ -447,6 +464,7 @@ define([ var metadata = { name: name, type: type, + alt: alt, }; if (thumb) { metadata.thumbnail = thumb; } queue.push({ @@ -486,8 +504,9 @@ define([ password = obj.password; owned = obj.owned; forceSave = obj.forceSave; + alt = obj.alt; finish(); - }); + }, preview); } }; @@ -495,11 +514,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/profile/inner.js b/www/profile/inner.js index c63b72e99..284b19114 100644 --- a/www/profile/inner.js +++ b/www/profile/inner.js @@ -341,6 +341,8 @@ define([ }); }; + Messages.profile_defaultAlt = "Default profile picture"; // XXX + var displayAvatar = function (val) { var sframeChan = common.getSframeChannel(); var $span = APP.$avatar; @@ -349,7 +351,7 @@ define([ $('', { src: '/customize/images/avatar.png', title: Messages.profile_avatar, - alt: 'Avatar' // XXX translate this "Default profile picture" + alt: Messages.profile_defaultAlt, }).appendTo($span); return; } From 02d9da502d4f376c8a76775da48fdd35f8aa3ddc Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 Aug 2021 17:56:53 +0530 Subject: [PATCH 042/125] remove dead code --- customize.dist/src/less2/include/sidebar-layout.less | 1 - 1 file changed, 1 deletion(-) diff --git a/customize.dist/src/less2/include/sidebar-layout.less b/customize.dist/src/less2/include/sidebar-layout.less index 1b0c75932..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; // XXX what was the intent behind this? // Following rules are only in settings .cp-sidebarlayout-element { From 147ddd88bf0aa32e7f144155c9eeadab794ed19a Mon Sep 17 00:00:00 2001 From: ansuz Date: Mon, 30 Aug 2021 18:40:42 +0530 Subject: [PATCH 043/125] adjust font size for initials and emoji avatar size on small screens --- customize.dist/src/less2/include/toolbar.less | 9 +++++++-- www/kanban/app-kanban.less | 5 ++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index 2092d72cc..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 { @@ -862,7 +868,6 @@ align-items: center; .animal { font-size: @avatar-font-size-animal; - } } &.cp-avatar { diff --git a/www/kanban/app-kanban.less b/www/kanban/app-kanban.less index 5996940e4..79d5a87a3 100644 --- a/www/kanban/app-kanban.less +++ b/www/kanban/app-kanban.less @@ -162,10 +162,9 @@ &.cp-cursor.cp-tippy-html { .avatar_vars(20px); background-color: var(--red); - // XXX figure out how to inherit this from avatar.less - font-size: @avatar-font-size; //var(11px; // 20px / 1.8 as per avatar.less.. + font-size: @avatar-font-size; &.animal { - font-size: @avatar-font-size-animal; //14px; // 20px / 1.8 * (6/5)... + font-size: @avatar-font-size-animal; } } } From 45d32f2294988f4f9652917c16b13e056b9ab5a9 Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 1 Sep 2021 11:36:47 +0200 Subject: [PATCH 044/125] Fix conditions based on the initial question --- www/form/inner.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/form/inner.js b/www/form/inner.js index 230a6aea7..f281d8452 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -3871,7 +3871,7 @@ define([ return; } rules.forEach(function (obj) { - var idx = available.indexOf(obj.q); + var idx = available.indexOf(String(obj.q)); // If this question doesn't exist before the section, remove the condition if (!obj.q || idx === -1) { var cIdx = rules.indexOf(obj); From 0ac34f1030971af15dc92f8be968fa2bc5a6069f Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 1 Sep 2021 12:11:52 +0200 Subject: [PATCH 045/125] Fix UI issues when editing responses is blocked in forms --- www/form/inner.js | 8 ++++++++ www/form/main.js | 3 +-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/www/form/inner.js b/www/form/inner.js index f281d8452..ea2ebd100 100644 --- a/www/form/inner.js +++ b/www/form/inner.js @@ -2436,6 +2436,10 @@ define([ $cbox.after(h('div.alert.alert-info', Messages.form_authAnswer)); }); } + if (update && content.answers.cantEdit) { + $cbox.hide(); + anonName = undefined; + } var send = h('button.cp-open.btn.btn-primary', update ? Messages.form_update : Messages.form_submit); var reset = h('button.cp-open.cp-reset-button.btn.btn-danger-alt', Messages.form_reset); @@ -2491,6 +2495,10 @@ define([ $send.text(Messages.form_update); APP.hasAnswered = true; showAnsweredPage(framework, content, { '_time': +new Date() }); + if (content.answers.cantEdit) { + $cbox.hide(); + if ($anonName) { $anonName.hide(); } + } }); }); diff --git a/www/form/main.js b/www/form/main.js index 7d81b1268..8c794a19a 100644 --- a/www/form/main.js +++ b/www/form/main.js @@ -221,8 +221,7 @@ define([ delete results[parsed._proof.key]; } } - // XXX If "allow edition" is disabled, don't override here? - // if (data.cantEdit && results[senderCurve]) { return; } + if (data.cantEdit && results[senderCurve]) { return; } results[senderCurve] = { msg: parsed, hash: hash, From 0ac368a5c9088ccfa274e90b348ce7b2985ae788 Mon Sep 17 00:00:00 2001 From: ansuz Date: Wed, 1 Sep 2021 16:07:16 +0530 Subject: [PATCH 046/125] resolve minor comments and clean up --- www/common/common-messaging.js | 2 +- www/common/common-ui-elements.js | 2 +- www/common/inner/common-mediatag.js | 15 +++++---------- www/common/sframe-common.js | 5 +---- www/common/toolbar.js | 8 +++++--- www/pad/cursor.js | 2 +- 6 files changed, 14 insertions(+), 20 deletions(-) diff --git a/www/common/common-messaging.js b/www/common/common-messaging.js index 1e6e26ce3..4a3eb2a91 100644 --- a/www/common/common-messaging.js +++ b/www/common/common-messaging.js @@ -18,7 +18,7 @@ define([ curvePublic: proxy.curvePublic, notifications: Util.find(proxy, ['mailboxes', 'notifications', 'channel']), avatar: proxy.profile && proxy.profile.avatar, - uid: proxy.uid, // XXX test without this and see if it breaks things + uid: proxy.uid, }; if (hash === false) { delete data.channel; } return data; diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index abf45b0bd..5bc8bf4f6 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1991,7 +1991,7 @@ define([ var $displayName = $userAdmin.find('.'+displayNameCls); - var $avatar = $userAdmin.find('> button .cp-dropdown-button-title'); // XXX alt="User menu" + var $avatar = $userAdmin.find('> button .cp-dropdown-button-title'); var loadingAvatar; var to; var oldUrl = ''; diff --git a/www/common/inner/common-mediatag.js b/www/common/inner/common-mediatag.js index 21cb25e82..33e5d74f7 100644 --- a/www/common/inner/common-mediatag.js +++ b/www/common/inner/common-mediatag.js @@ -90,17 +90,12 @@ define([ // https://emojipedia.org/nature/ var ANIMALS = AppConfig.emojiAvatars || []; - var getRandomAnimal = function () { // XXX should never actually happen? - if (!ANIMALS.length) { return ''; } - return ANIMALS[Math.floor(Math.random() * ANIMALS.length)]; - }; - var getPseudorandomAnimal = MT.getPseudorandomAnimal = function (seed) { if (!ANIMALS.length) { return ''; } - if (typeof(seed) !== 'string') { return getRandomAnimal(); } - seed = seed.replace(/\D/g, '').slice(0, 10); // XXX possible optimization for on-wire uid + 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 getRandomAnimal(); } + if (!seed) { return; } return ANIMALS[seed % ANIMALS.length] || ''; }; @@ -141,7 +136,7 @@ define([ var $avatar = $('', { 'class': 'cp-avatar-default' + (animal_avatar? ' animal': ''), - // XXX prevents screenreaders from trying to describe this + // this prevents screenreaders from trying to describe this alt: '', 'aria-hidden': true, }).text(text); @@ -196,7 +191,7 @@ define([ var $img = $(mt).appendTo($container); MT.displayMediatagImage(common, $img, function (err, $image) { if (err) { return void console.error(err); } - centerImage($img, $image); // XXX add alt="" (unless the media-tag has an alt attr) + centerImage($img, $image); }); }); } diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 3575c95b0..b7efd9f4f 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -235,9 +235,6 @@ define([ }; }; - funcs.getAuthorId = function () { // XXX - }; - var authorUid = function(existing) { if (!Array.isArray(existing)) { existing = []; } var n; @@ -263,7 +260,7 @@ define([ }); return uid || authorUid(existing); } - // XXX this should check for a matching curvePublic / uid if: + // 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 diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 5b359a323..5f1c2ece5 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -164,6 +164,7 @@ MessengerUI, Messages, Pages) { }); }; var showColors = false; + Messages.userlist_visitProfile = "Visit {0}'s profile"; // XXX "'s" is incorrect for names that end in "s" in English... don't care? var updateUserList = function (toolbar, config, forceOffline) { if (!config.displayed || config.displayed.indexOf('userlist') === -1) { return; } if (toolbar.isAlone) { return; } @@ -249,6 +250,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); @@ -323,7 +325,7 @@ MessengerUI, Messages, Pages) { $('