diff --git a/customize.dist/messages.js b/customize.dist/messages.js index b365c275c..4c87d59a8 100755 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -172,6 +172,12 @@ define(req, function(AppConfig, Default, Language) { Messages.form_date_time = "Select date and time"; Messages.sent_chatMessage = "{0} has sent you a message"; // XXX + /// XXX + // To be edited for the Spring Release + // Messages.dontShowAgain = "Don't show again"; + // Messages.crowdfunding_popup_text = "

We need your help!

To ensure that CryptPad is actively developed, consider supporting the project via the OpenCollective page, where you can see our Roadmap and Funding goals." + Messages.crowdfunding_popup_text2 = "Alternatively, you can subscribe on this instance." // XXX + return Messages; }); diff --git a/customize.dist/src/less2/include/drive.less b/customize.dist/src/less2/include/drive.less index 4982d81ce..b17a61385 100644 --- a/customize.dist/src/less2/include/drive.less +++ b/customize.dist/src/less2/include/drive.less @@ -976,6 +976,12 @@ stroke-width: 3.5; vector-effect: non-scaling-stroke; } + &.cp-app-drive-content-grid { + li.cp-app-drive-element-row > svg.lucide * { + stroke-width: 3.5; + vector-effect: non-scaling-stroke; + } + } } #cp-app-drive-new-ghost-dialog.cp-modal-container { diff --git a/www/common/common-icons.js b/www/common/common-icons.js index a29e5d71e..3f2a2ee61 100644 --- a/www/common/common-icons.js +++ b/www/common/common-icons.js @@ -85,7 +85,7 @@ define([ "share": "share-2", "download": "hard-drive-download", "destroy": "shredder", - "donate": "hand-coins", + "donate": "hand-heart", "send": "send", "cloud-upload": "cloud-upload", "print": "printer", @@ -250,7 +250,10 @@ define([ "badge-error": "circle-alert", // Other "maintenance": "construction", - "release-notes": "notepad-text" + "release-notes": "notepad-text", + "crowdfunding-donate": "hand-heart", + "crowdfunding-snooze": "timer", + "crowdfunding-donate2": "ticket" }; Icons.add = (newIcons) => { diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 3c799738a..1c72119e4 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -3392,39 +3392,72 @@ define([ UIElements.displayCrowdfunding = function (common, force) { if (crowdfundingState) { return; } var priv = common.getMetadataMgr().getPrivateData(); - if (priv.app === 'form' && !priv.canEdit && !priv.form_auditorKey) { return; } + if (priv.app === 'drive') { return; } + if (!priv.channel) { return; } + if (priv.app === 'form' && priv.readOnly && !priv.form_auditorHash && !priv.form_auditorKey) { return; } var todo = function () { crowdfundingState = true; - // Display the popup - var text = Messages.crowdfunding_popup_text; - var yes = h('button.cp-corner-primary', [ - Icons.get('external-link'), - 'OpenCollective' - ]); - var no = h('button.cp-corner-cancel', Messages.crowdfunding_popup_no); - var actions = h('div', [no, yes]); - + var recordShown = function () { + common.getSframeChannel().query('Q_RECORD_CROWDFUNDING_SHOWN', {}, function () {}); + }; var dontShowAgain = function () { common.setAttribute(['general', 'crowdfunding'], false); Feedback.send('CROWDFUNDING_NEVER'); }; - var modal = UI.cornerPopup(text, actions, '', { - big: true, - alt: true, - dontShowAgain: dontShowAgain + var content = Messages.crowdfunding_popup_text; + var buttons = [ + { + name: Messages.crowdfunding_popup_no, + className: 'cancel', + iconClass: 'crowdfunding-snooze', + onClick: function () { + recordShown(); + Feedback.send('CROWDFUNDING_NO'); + } + } + ]; + if (!Config.removeDonateButton) { + buttons.push({ + name: Messages.crowdfunding_button2, + className: 'primary', + iconClass: 'crowdfunding-donate', + onClick: function () { + recordShown(); + common.openURL(priv.accounts.donateURL); + Feedback.send('CROWDFUNDING_YES'); + } + }); + } + if (Config.accounts_api && priv.accountName) { + content += ' ' + Messages.crowdfunding_popup_text2; + buttons.push({ + name: Messages.features_f_subscribe, + className: 'primary', + iconClass: 'crowdfunding-donate2', + onClick: function () { + recordShown(); + common.openURL('/accounts/'); + Feedback.send('CROWDFUNDING_SUBSCRIBE'); + } + }); + } + buttons.push({ + name: Messages.dontShowAgain, + className: 'cancel left', + iconClass: 'close', + onClick: function () { + recordShown(); + dontShowAgain(); + } }); - - $(yes).click(function () { - modal.delete(); - common.openURL(priv.accounts.donateURL); - Feedback.send('CROWDFUNDING_YES'); - }); - $(no).click(function () { - modal.delete(); - Feedback.send('CROWDFUNDING_NO'); + var modal = UI.dialog.customModal(content, { + force: true, + scrollable: true, + buttons: buttons }); + UI.openCustomModal(modal, { wide: true }); }; if (force) { @@ -3434,13 +3467,16 @@ define([ if (AppConfig.disableCrowdfundingMessages) { return; } if (priv.plan) { return; } + if (Config.removeDonateButton && !Config.accounts_api) { return; } crowdfundingState = true; common.getAttribute(['general', 'crowdfunding'], function (err, val) { - if (err || val === false) { return; } - common.getSframeChannel().query('Q_GET_PINNED_USAGE', null, function (err, obj) { - var quotaMb = obj.quota / (1024 * 1024); - if (quotaMb < 10) { return; } + if (err || val === false) { crowdfundingState = false; return; } + common.getSframeChannel().query('Q_CROWDFUNDING_SHOULD_SHOW', null, function (err, result) { + if (err || !result || !result.show) { + crowdfundingState = false; + return; + } todo(); }); }); @@ -3461,7 +3497,7 @@ define([ // This pad will be deleted automatically, it shouldn't be stored if (priv.burnAfterReading) { return; } - if (priv.app === 'form' && !priv.canEdit && !priv.form_auditorKey && !common.isLoggedIn()) { return; } + if (priv.app === 'form' && priv.readOnly && !priv.form_auditorHash && !priv.form_auditorKey && !common.isLoggedIn()) { return; } var typeMsg = priv.pathname.indexOf('/file/') !== -1 ? Messages.autostore_file : priv.pathname.indexOf('/drive/') !== -1 ? Messages.autostore_sf : Messages.autostore_pad; diff --git a/www/common/outer/local-store.js b/www/common/outer/local-store.js index a260f3def..cd14dc3b1 100644 --- a/www/common/outer/local-store.js +++ b/www/common/outer/local-store.js @@ -151,8 +151,8 @@ define([ sessionStorage.clear(); try { Object.keys(localStorage || {}).forEach(function (k) { - // Remvoe everything in localStorage except CACHE and FS_hash - if (/^CRYPTPAD_CACHE/.test(k) || /^LESS_CACHE/.test(k) || k === Constants.fileHashKey || /^CRYPTPAD_STORE|colortheme/.test(k)) { return; } + // Remove everything in localStorage except CACHE and FS_hash + if (/^CRYPTPAD_CACHE/.test(k) || /^LESS_CACHE/.test(k) || k === Constants.fileHashKey || /^CRYPTPAD_STORE|colortheme/.test(k) || (!isDeletion && /^cp_crowdfunding_/.test(k))) { return; } delete localStorage[k]; }); } catch (e) { console.error(e); } diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 499d8713a..e95389659 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -1028,6 +1028,110 @@ define([ cb({error:e}); }); }); + var CROWDFUNDING_PREFIX = 'cp_crowdfunding_'; + var CROWDFUNDING_DRIVE_KEY = ['general', 'crowdfunding_metrics']; + // First action (opening or creating a document) count threshold before showing the banner + var CROWDFUNDING_MIN_ACTIONS = 5; + // Additional actions required after each shown banner + var CROWDFUNDING_ACTIONS_INTERVAL = 10; + // Quota usage threshold for quota-based banner display + var CROWDFUNDING_MIN_QUOTA_MB = 50; + // Cooldown between banner displays based on last shown timestamp in milliseconds + var CROWDFUNDING_COOLDOWN_MS = 24 * 60 * 60 * 1000; + var crowdfundingGetLS = function () { + var get = function (suffix) { + var k = CROWDFUNDING_PREFIX + suffix; + var val = localStorage.getItem(k); + if (val !== null && val !== '') { return Number(val) || null; } + return null; + }; + return { + visitCount: get('visitCount') || 0, + firstSeen: get('firstSeen') || null, + lastShownAtCount: get('lastShownAtCount') || 0, + lastShownAtTime: get('lastShownAtTime') || 0 + }; + }; + // Read metrics: encrypted drive for logged-in users, localStorage for guests + var crowdfundingReadMetrics = function (cb) { + if (!Utils.LocalStore.isLoggedIn()) { return cb(crowdfundingGetLS()); } + Cryptpad.getAttribute(CROWDFUNDING_DRIVE_KEY, function (e, metrics) { + if (e || !metrics || typeof metrics !== 'object') { + return cb({ + visitCount: 0, + firstSeen: null, + lastShownAtCount: 0, + lastShownAtTime: 0 + }); + } + cb(metrics); + }); + }; + // Write metrics: encrypted drive for logged-in users, localStorage for guests + var crowdfundingWriteMetrics = function (metrics, cb) { + if (!Utils.LocalStore.isLoggedIn()) { + try { + ['visitCount', 'firstSeen', 'lastShownAtCount', 'lastShownAtTime'].forEach(function (k) { + if (metrics[k] !== null && metrics[k] !== undefined) { + localStorage.setItem(CROWDFUNDING_PREFIX + k, String(metrics[k])); + } + }); + } catch (e) {} + return cb && cb(); + } + Cryptpad.setAttribute(CROWDFUNDING_DRIVE_KEY, metrics, function () { + cb && cb(); + }); + }; + var crowdfundingIncrementAction = function (cb) { + crowdfundingReadMetrics(function (metrics) { + metrics.visitCount = (metrics.visitCount || 0) + 1; + if (!metrics.firstSeen) { metrics.firstSeen = Date.now(); } + crowdfundingWriteMetrics(metrics, cb); + }); + }; + + sframeChan.on('Q_CROWDFUNDING_SHOULD_SHOW', function (data, cb) { + crowdfundingReadMetrics(function (metrics) { + var actionCount = metrics.visitCount || 0; + var lastShownAtCount = metrics.lastShownAtCount || 0; + var lastShownAtTime = metrics.lastShownAtTime || 0; + var now = Date.now(); + var nextThreshold = lastShownAtCount === 0 ? CROWDFUNDING_MIN_ACTIONS : lastShownAtCount + CROWDFUNDING_ACTIONS_INTERVAL; + var enoughTimePassed = lastShownAtTime === 0 || (now - lastShownAtTime >= CROWDFUNDING_COOLDOWN_MS); + var showFromActions = actionCount >= nextThreshold && enoughTimePassed; + if (showFromActions) { + return cb({ + show: true, + actionCount: actionCount + }); + } + if (CROWDFUNDING_MIN_QUOTA_MB <= 0) { + return cb({ + show: false, + actionCount: actionCount + }); + } + Cryptpad.getPinnedUsage({}, function (e, used) { + var usedMb = (typeof used === 'number') ? (used / (1024 * 1024)) : 0; + cb({ + show: !e && usedMb >= CROWDFUNDING_MIN_QUOTA_MB && enoughTimePassed, + actionCount: actionCount + }); + }); + }); + }); + sframeChan.on('Q_RECORD_CROWDFUNDING_SHOWN', function (data, cb) { + crowdfundingReadMetrics(function (metrics) { + metrics.lastShownAtCount = (data && typeof data.count === 'number') ? data.count : (metrics.visitCount || 0); + metrics.lastShownAtTime = Date.now(); + crowdfundingWriteMetrics(metrics, cb); + }); + }); + sframeChan.on('Q_CROWDFUNDING_INCREMENT_OPEN', function (data, cb) { + if (readOnly) { return cb && cb(); } + crowdfundingIncrementAction(cb); + }); Cryptpad.mailbox.onEvent.reg(function (data, cb) { sframeChan.query('EV_MAILBOX_EVENT', data, function (err, obj) { diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 320140d9c..0e6d10fac 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -768,7 +768,7 @@ define([ // Ctrl+E: New pad modal var priv = ctx.metadataMgr.getPrivateData(); if (e.which === 69 && isApp) { - if (priv.app === 'form' && !priv.canEdit && !priv.form_auditorKey) { return; } + if (priv.app === 'form' && priv.readOnly && !priv.form_auditorHash && !priv.form_auditorKey) { return; } e.preventDefault(); return void funcs.createNewPadModal(); } @@ -1063,10 +1063,16 @@ define([ document.title = title; }); - funcs.isPadStored(function (err, val) { - if (err || !val) { return; } + var showCrowdfunding = function () { UIElements.displayCrowdfunding(funcs); - }); + }; + var privateData = ctx.metadataMgr.getPrivateData(); + var isDriveContext = privateData.app === 'drive'; + var isReadOnlyFormResponse = privateData.app === 'form' && privateData.readOnly && !privateData.form_auditorHash && !privateData.form_auditorKey; + var skipCrowdfunding = privateData.secureIframe === true || privateData.unsafeIframe === true || isReadOnlyFormResponse || isDriveContext; + if (!skipCrowdfunding) { + ctx.sframeChan.query('Q_CROWDFUNDING_INCREMENT_OPEN', {}, showCrowdfunding); + } ctx.sframeChan.ready(); diff --git a/www/unsafeiframe/main.js b/www/unsafeiframe/main.js index 0965b9c85..50e047c03 100644 --- a/www/unsafeiframe/main.js +++ b/www/unsafeiframe/main.js @@ -106,6 +106,7 @@ define([ origin: window.location.origin, pathname: window.location.pathname, feedbackAllowed: Utils.Feedback.state, + unsafeIframe: true, }; for (var k in additionalPriv) { metaObj.priv[k] = additionalPriv[k]; }