diff --git a/customize.dist/src/less2/include/support.less b/customize.dist/src/less2/include/support.less index d83746b51..105599ada 100644 --- a/customize.dist/src/less2/include/support.less +++ b/customize.dist/src/less2/include/support.less @@ -78,7 +78,7 @@ } &.cp-support-list-closed { .cp-support-list-actions { - display: block !important; + display: flex !important; .cp-support-answer, .cp-support-close { display: none; } diff --git a/package-lock.json b/package-lock.json index 775f42f68..2c64d8307 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cryptpad", - "version": "3.24.0", + "version": "3.25.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -181,16 +181,16 @@ "optional": true }, "chainpad-crypto": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/chainpad-crypto/-/chainpad-crypto-0.2.4.tgz", - "integrity": "sha512-fWbVyeAv35vf/dkkQaefASlJcEfpEvfRI23Mtn+/TBBry7+LYNuJMXJiovVY35pfyw2+trKh1Py5Asg9vrmaVg==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/chainpad-crypto/-/chainpad-crypto-0.2.5.tgz", + "integrity": "sha512-K9vRsAspuX+uU1goXPz0CawpLIaOHq+1JP3WfDLqaz67LbCX/MLIUt9aMcSeIJcwZ9uMpqnbMGRktyVPoz6MCA==", "requires": { - "tweetnacl": "git://github.com/dchest/tweetnacl-js.git#v0.12.2" + "tweetnacl": "git+https://github.com/dchest/tweetnacl-js.git#v0.12.2" }, "dependencies": { "tweetnacl": { - "version": "git://github.com/dchest/tweetnacl-js.git#8a21381d696acdc4e99c9f706f1ad23285795f79", - "from": "git://github.com/dchest/tweetnacl-js.git#v0.12.2" + "version": "git+https://github.com/dchest/tweetnacl-js.git#8a21381d696acdc4e99c9f706f1ad23285795f79", + "from": "git+https://github.com/dchest/tweetnacl-js.git#v0.12.2" } } }, diff --git a/package.json b/package.json index 37fd6c686..86c7fc67b 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "cryptpad", "description": "realtime collaborative visual editor with zero knowlege server", - "version": "3.24.0", + "version": "3.25.0", "license": "AGPL-3.0+", "repository": { "type": "git", - "url": "git://github.com/xwiki-labs/cryptpad.git" + "url": "git+https://github.com/xwiki-labs/cryptpad.git" }, "funding": { "type": "opencollective", @@ -13,7 +13,7 @@ }, "dependencies": { "@mcrowe/minibloom": "^0.2.0", - "chainpad-crypto": "^0.2.2", + "chainpad-crypto": "^0.2.5", "chainpad-server": "^4.0.9", "express": "~4.16.0", "fs-extra": "^7.0.0", diff --git a/www/admin/app-admin.less b/www/admin/app-admin.less index 8fff2e528..77e13af43 100644 --- a/www/admin/app-admin.less +++ b/www/admin/app-admin.less @@ -65,14 +65,62 @@ .cp-support-container { display: flex; - flex-flow: column; + flex-wrap: wrap; + .cp-support-column { + min-width: 700px; + flex: 1 0 50%; + h1 { + display: flex; + align-items: center; + button { + margin-left: 50px !important; + } + } + .cp-support-count { + margin-left: 10px; + } + &.cp-support-column-collapsed { + .cp-support-list-ticket { + display: none; + } + } + } } .cp-support-list-actions { margin: 10px 0px 10px 2px; } + .cp-support-list-ticket { + h2 { + font-size: 1.5rem; + display: flex; + justify-content: space-between; + align-items: top; + .cp-support-title-buttons { + flex-shrink: 0; + } + } + .cp-support-collapsed { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + color: #666; + .cp-support-ispremium { + padding: 0 5px; + color: @colortheme_cp-red; + background-color: lighten(@colortheme_cp-red, 25%); + } + } + } + .cp-support-list-ticket:not(.cp-support-list-closed) { + .cp-support-list-actions { + .cp-button-confirm, .cp-support-close { + order: 20; + margin-left: auto !important; + } + } .cp-support-list-message { &:last-child:not(.cp-support-fromadmin) { color: @colortheme_cp-red; @@ -92,6 +140,28 @@ } } } + .cp-support-list-ticket:not(.cp-support-open) { + span:first-child { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + & > :not(h2):not(.cp-support-collapsed) { + display: none; + } + .cp-support-collapse { + display: none; + } + cursor: pointer; + } + .cp-support-list-ticket.cp-support-open { + .cp-support-collapsed { + display: none; + } + .cp-support-expand { + display: none; + } + } .cp-support-fromadmin { color: @colortheme_logo-2; diff --git a/www/admin/inner.js b/www/admin/inner.js index 955ba4faf..755b60ddc 100644 --- a/www/admin/inner.js +++ b/www/admin/inner.js @@ -552,7 +552,53 @@ define([ var $div = $(h('div.cp-support-container')).appendTo($container); var catContainer = h('div.cp-dropdown-container'); - $div.append(catContainer); + Messages.admin_support_premium = "Premium tickets:"; // XXX + Messages.admin_support_normal = "Unanswered tickets:"; + Messages.admin_support_answered = "Answered tickets:"; + Messages.admin_support_closed = "Closed tickets:"; + Messages.admin_support_open = "Show"; + Messages.admin_support_collapse = "Collapse"; + Messages.admin_support_first = "Created on: "; + Messages.admin_support_last = "Updated on: "; + var col1 = h('div.cp-support-column', h('h1', [ + h('span', Messages.admin_support_premium), + h('span.cp-support-count'), + h('button.btn.cp-support-column-button', Messages.admin_support_collapse) + ])); + var col2 = h('div.cp-support-column', h('h1', [ + h('span', Messages.admin_support_normal), + h('span.cp-support-count'), + h('button.btn.cp-support-column-button', Messages.admin_support_collapse) + ])); + var col3 = h('div.cp-support-column', h('h1', [ + h('span', Messages.admin_support_answered), + h('span.cp-support-count'), + h('button.btn.cp-support-column-button', Messages.admin_support_collapse) + ])); + var col4 = h('div.cp-support-column', h('h1', [ + h('span', Messages.admin_support_closed), + h('span.cp-support-count'), + h('button.btn.cp-support-column-button', Messages.admin_support_collapse) + ])); + var $col1 = $(col1), $col2 = $(col2), $col3 = $(col3), $col4 = $(col4); + $div.append([ + //catContainer + col1, + col2, + col3, + col4 + ]); + $div.find('.cp-support-column-button').click(function () { + var $col = $(this).closest('.cp-support-column'); + $col.toggleClass('cp-support-column-collapsed'); + if ($col.hasClass('cp-support-column-collapsed')) { + $(this).text(Messages.admin_support_open); + $(this).toggleClass('btn-primary'); + } else { + $(this).text(Messages.admin_support_collapse); + $(this).toggleClass('btn-primary'); + } + }); var category = 'all'; var $drop = APP.support.makeCategoryDropdown(catContainer, function (key) { category = key; @@ -572,37 +618,128 @@ define([ var hashesById = {}; - var reorder = function () { - var order = Object.keys(hashesById); - order.sort(function (id1, id2) { - var t1 = hashesById[id1]; - var t2 = hashesById[id2]; - if (!Array.isArray(t1)) { return 1; } - if (!Array.isArray(t2)) { return -1; } - var lastMsg1 = t1[t1.length - 1]; - var lastMsg2 = t2[t2.length - 1]; - var time1 = Util.find(lastMsg1, ['content', 'msg', 'content', 'time']); - var time2 = Util.find(lastMsg2, ['content', 'msg', 'content', 'time']); - var authorEd1 = Util.find(lastMsg1, ['content', 'msg', 'content', 'sender', 'edPublic']); - var authorEd2 = Util.find(lastMsg2, ['content', 'msg', 'content', 'sender', 'edPublic']); - var admin1 = ApiConfig.adminKeys.indexOf(authorEd1) !== -1; - var admin2 = ApiConfig.adminKeys.indexOf(authorEd2) !== -1; - // If one is answered and not the other, put the unanswered first - if (admin1 && !admin2) { return 1; } - if (!admin1 && admin2) { return -1; } - // Otherwise, sort them by time - return time2 - time1; + var getTicketData = function (id) { + var t = hashesById[id]; + if (!Array.isArray(t) || !t.length) { return; } + var ed = Util.find(t[0], ['content', 'msg', 'content', 'sender', 'edPublic']); + // If one of their ticket was sent as a premium user, mark them as premium + var premium = t.some(function (msg) { + var _ed = Util.find(msg, ['content', 'msg', 'content', 'sender', 'edPublic']); + if (ed !== _ed) { return; } + return Util.find(t[0], ['content', 'msg', 'content', 'sender', 'plan']); + }); + var lastMsg = t[t.length - 1]; + var lastMsgEd = Util.find(lastMsg, ['content', 'msg', 'content', 'sender', 'edPublic']); + return { + lastMsg: lastMsg, + time: Util.find(lastMsg, ['content', 'msg', 'content', 'time']), + lastMsgEd: lastMsgEd, + lastAdmin: lastMsgEd !== ed && ApiConfig.adminKeys.indexOf(lastMsgEd) !== -1, + premium: premium, + authorEd: ed, + closed: Util.find(lastMsg, ['content', 'msg', 'type']) === 'CLOSE' + }; + }; + + var addClickHandler = function ($ticket) { + $ticket.on('click', function () { + $ticket.toggleClass('cp-support-open', true); + $ticket.off('click'); }); - order.forEach(function (id, i) { - $div.find('[data-id="'+id+'"]').css('order', i); + }; + var makeOpenButton = function ($ticket) { + var button = h('button.btn.btn-primary.cp-support-expand', Messages.admin_support_open); + var collapse = h('button.btn.cp-support-collapse', Messages.admin_support_collapse); + $(button).click(function () { + $ticket.toggleClass('cp-support-open', true); + }); + addClickHandler($ticket); + $(collapse).click(function (e) { + $ticket.toggleClass('cp-support-open', false); + e.stopPropagation(); + setTimeout(function () { + addClickHandler($ticket); + }); + }); + $ticket.find('.cp-support-title-buttons').prepend([button, collapse]); + $ticket.append(h('div.cp-support-collapsed')); + }; + var updateTicketDetails = function ($ticket, isPremium) { + var $first = $ticket.find('.cp-support-message-from').first(); + var user = $first.find('span').first().html(); + var time = $first.find('.cp-support-message-time').text(); + var last = $ticket.find('.cp-support-message-from').last().find('.cp-support-message-time').text(); + var $c = $ticket.find('.cp-support-collapsed'); + var txtClass = isPremium ? ".cp-support-ispremium" : ""; + $c.html('').append([ + UI.setHTML(h('span'+ txtClass), user), + h('span', [ + h('b', Messages.admin_support_first), + h('span', time) + ]), + h('span', [ + h('b', Messages.admin_support_last), + h('span', last) + ]) + ]); + + }; + + var sort = function (id1, id2) { + var t1 = getTicketData(id1); + var t2 = getTicketData(id2); + if (!t1) { return 1; } + if (!t2) { return -1; } + /* + // If one is answered and not the other, put the unanswered first + if (t1.lastAdmin && !t2.lastAdmin) { return 1; } + if (!t1.lastAdmin && t2.lastAdmin) { return -1; } + */ + // Otherwise, sort them by time + return t1.time - t2.time; + }; + + var _reorder = function () { + var orderAnswered = Object.keys(hashesById).filter(function (id) { + var d = getTicketData(id); + return d && d.lastAdmin && !d.closed; + }).sort(sort); + var orderPremium = Object.keys(hashesById).filter(function (id) { + var d = getTicketData(id); + return d && d.premium && !d.lastAdmin && !d.closed; + }).sort(sort); + var orderNormal = Object.keys(hashesById).filter(function (id) { + var d = getTicketData(id); + return d && !d.premium && !d.lastAdmin && !d.closed; + }).sort(sort); + var orderClosed = Object.keys(hashesById).filter(function (id) { + var d = getTicketData(id); + return d && d.closed; + }).sort(sort); + var cols = [$col1, $col2, $col3, $col4]; + [orderPremium, orderNormal, orderAnswered, orderClosed].forEach(function (list, j) { + list.forEach(function (id, i) { + var $t = $div.find('[data-id="'+id+'"]'); + var d = getTicketData(id); + $t.css('order', i).appendTo(cols[j]); + updateTicketDetails($t, d.premium); + }); + if (!list.length) { + cols[j].hide(); + } else { + cols[j].show(); + cols[j].find('.cp-support-count').text(list.length); + } }); }; + var reorder = Util.throttle(_reorder, 150); var to = Util.throttle(function () { var $ticket = $div.find('.cp-support-list-ticket[data-id="'+linkedId+'"]'); + $ticket.addClass('cp-support-open'); $ticket[0].scrollIntoView(); linkedId = undefined; - }, 100); + }, 200); // Register to the "support" mailbox common.mailbox.subscribe(['supportadmin'], { @@ -630,6 +767,7 @@ define([ if (!$ticket.length) { return; } $ticket.addClass('cp-support-list-closed'); $ticket.append(APP.support.makeCloseMessage(content, hash)); + reorder(); return; } if (msg.type !== 'TICKET') { return; } @@ -650,13 +788,19 @@ define([ })); }); }).nThen(function () { - if (!error) { return void $ticket.remove(); } + if (!error) { + $ticket.remove(); + delete hashesById[id]; + reorder(); + return; + } // if deletion failed then reactivate the button and warn hideButton.removeAttribute('disabled'); // and show a generic error message UI.alert(Messages.error); }); }); + makeOpenButton($ticket); if (category !== 'all' && $ticket.attr('data-cat') !== category) { $ticket.hide(); } diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 3e9d848cd..bb84187b4 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -774,7 +774,8 @@ define([ $(originalBtn).show(); }; - $button.click(function () { + $button.click(function (e) { + e.stopPropagation(); done(true); }); @@ -792,7 +793,8 @@ define([ to = setTimeout(todo, INTERVAL); }; - $(originalBtn).addClass('cp-button-confirm-placeholder').click(function () { + $(originalBtn).addClass('cp-button-confirm-placeholder').click(function (e) { + e.stopPropagation(); // If we have a validation function, continue only if it's true if (config.validate && !config.validate()) { return; } i = 1; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 2c008a09e..717097b32 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1631,7 +1631,7 @@ define([ Store.leavePad(null, data, function () {}); }; var conf = { - //Cache: Cache, // XXX re-enable cache usage + Cache: Cache, // XXX re-enable cache usage onCacheStart: function () { postMessage(clientId, "PAD_CACHE"); }, @@ -2686,7 +2686,7 @@ define([ readOnly: false, validateKey: secret.keys.validateKey || undefined, crypto: Crypto.createEncryptor(secret.keys), - //Cache: Cache, // XXX re-enable cache usage + Cache: Cache, // XXX re-enable cache usage userName: 'fs', logLevel: 1, ChainPad: ChainPad, diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js index 518367826..2ac98cdb7 100644 --- a/www/common/outer/sharedfolder.js +++ b/www/common/outer/sharedfolder.js @@ -175,7 +175,7 @@ define([ ChainPad: ChainPad, classic: true, network: network, - //Cache: Cache, // XXX re-enable cache usage + Cache: Cache, // XXX re-enable cache usage metadata: { validateKey: secret.keys.validateKey || undefined, owners: owners diff --git a/www/common/outer/team.js b/www/common/outer/team.js index cba26189f..26bd32f3c 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -427,7 +427,7 @@ define([ channel: secret.channel, crypto: crypto, ChainPad: ChainPad, - //Cache: Cache, // XXX re-enable cache usage + Cache: Cache, // XXX re-enable cache usage metadata: { validateKey: secret.keys.validateKey || undefined, }, diff --git a/www/support/ui.js b/www/support/ui.js index a58fdf170..b85197f63 100644 --- a/www/support/ui.js +++ b/www/support/ui.js @@ -230,7 +230,7 @@ define([ var form = h('div.cp-support-form-container', content); $(cancel).click(function () { - $(form).closest('.cp-support-list-ticket').find('.cp-support-list-actions').show(); + $(form).closest('.cp-support-list-ticket').find('.cp-support-list-actions').css('display', ''); $(form).remove(); }); @@ -257,8 +257,9 @@ define([ var url; if (ctx.isAdmin) { ticketCategory = Messages['support_cat_'+(content.category || 'all')] + ' - '; - url = h('button.btn.btn-primary.fa.fa-clipboard'); - $(url).click(function () { + url = h('button.btn.fa.fa-clipboard'); + $(url).click(function (e) { + e.stopPropagation(); var link = privateData.origin + privateData.pathname + '#' + 'support-' + content.id; var success = Clipboard.copy(link); if (success) { UI.log(Messages.shareSuccess); } @@ -269,7 +270,10 @@ define([ 'data-cat': content.category, 'data-id': content.id }, [ - h('h2', [ticketCategory, ticketTitle, url]), + h('h2', [ + h('span', [ticketCategory, ticketTitle]), + h('span.cp-support-title-buttons',url) + ]), actions ])); @@ -303,7 +307,7 @@ define([ var form = makeForm(ctx, function () { var sent = sendForm(ctx, content.id, form, content.sender); if (sent) { - $(actions).show(); + $(actions).css('display', ''); $(form).remove(); } }, content.title);