diff --git a/customize.dist/src/less2/include/colortheme-dark.less b/customize.dist/src/less2/include/colortheme-dark.less index 07117b0f9..1f5aee211 100644 --- a/customize.dist/src/less2/include/colortheme-dark.less +++ b/customize.dist/src/less2/include/colortheme-dark.less @@ -164,6 +164,7 @@ @cp_buttons-hover: @cryptpad_color_brand_fadest; @cp_buttons-default: @cryptpad_color_grey_700; @cp_buttons-default-color: @cryptpad_text_col; +@cp_buttons-default-alt-color: @cryptpad_color_grey_700; @cp_buttons-default-border: @cryptpad_text_col; @cp_buttons-red: #E55236; @cp_buttons-red-text: @cryptpad_color_light_red; diff --git a/customize.dist/src/less2/include/colortheme.less b/customize.dist/src/less2/include/colortheme.less index faaca5285..ece9f6fe0 100644 --- a/customize.dist/src/less2/include/colortheme.less +++ b/customize.dist/src/less2/include/colortheme.less @@ -164,6 +164,7 @@ @cp_buttons-default: #CCC; @cp_buttons-default-color: @cryptpad_text_col; @cp_buttons-default-border: @cryptpad_text_col; +@cp_buttons-default-alt-color: @cryptpad_color_grey_50; @cp_buttons-red: #E55236; @cp_buttons-red-text: @cp_buttons-red; @cp_buttons-red-color: #FFF; diff --git a/customize.dist/src/less2/include/forms.less b/customize.dist/src/less2/include/forms.less index 3c0b206db..745d24ab0 100644 --- a/customize.dist/src/less2/include/forms.less +++ b/customize.dist/src/less2/include/forms.less @@ -199,6 +199,16 @@ background-color: @cp_buttons-default; } } + &.btn-default-alt { + border-color: @cp_buttons-default-alt-color; + color: @cp_buttons-default-alt-color; + background-color: @cp_buttons-default-color; + &:hover, &:not(:disabled):active, &:focus { + border-color: @cp_buttons-default-color; + color: @cp_buttons-default-color; + background-color: @cp_toolbar-fade3; + } + } &.danger, &.btn-danger { background-color: @cp_buttons-red; diff --git a/lib/hk-util.js b/lib/hk-util.js index 479f10ee3..4cbb89c4d 100644 --- a/lib/hk-util.js +++ b/lib/hk-util.js @@ -767,15 +767,16 @@ const handleGetHistoryRange = function (Env, Server, seq, userId, parsed) { var channelName = parsed[1]; var map = parsed[2]; const HISTORY_KEEPER_ID = Env.id; + const store = Env.store; if (!(map && typeof(map) === 'object')) { return void Server.send(userId, [seq, 'ERROR', 'INVALID_ARGS', HISTORY_KEEPER_ID]); } - var oldestKnownHash = map.from; - var untilHash = map.to; - var desiredMessages = map.count; - var desiredCheckpoint = map.cpCount; + var oldestKnownHash = map.from; // last known hash + var untilHash = map.to; // oldest hash (unknown), start point if defined + var desiredMessages = map.count; // nb messages before lkh + var desiredCheckpoint = map.cpCount; // nb cp before lkh var txid = map.txid; if (typeof(desiredMessages) !== 'number' && typeof(desiredCheckpoint) !== 'number' && !untilHash) { return void Server.send(userId, [seq, 'ERROR', 'UNSPECIFIED_COUNT', HISTORY_KEEPER_ID]); @@ -786,6 +787,43 @@ const handleGetHistoryRange = function (Env, Server, seq, userId, parsed) { } Server.send(userId, [seq, 'ACK']); + + if (untilHash) { + // Get all messages between untilHash (oldest but unknown) + // and oldestKnownHesh (last known hash) (or until the end if undefined) + // Messages can be streamed since we instantly know the start point + let found = false; + store.readMessagesBin(channelName, 0, (msgObj, readMore, abort) => { + const parsed = tryParse(Env, msgObj.buff.toString('utf8')); + if (!parsed) { return void readMore(); } + if (isMetadataMessage(parsed)) { return void readMore(); } + const content = parsed[4]; + if (typeof(content) !== 'string') { return void readMore(); } + + const hash = getHash(content); + if (hash === untilHash || untilHash === 'NONE') { found = true; } + let then = hash === oldestKnownHash ? abort : readMore; + if (found) { + Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, + JSON.stringify(['HISTORY_RANGE', txid, parsed])], then); + } + return void readMore(); + }, function (err, reason) { + if (err) { + Env.Log.error("HK_GET_OLDER_HISTORY", channelName, err, reason); + Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, + JSON.stringify(['HISTORY_RANGE_ERROR', txid, err]) + ]); + return; + } + Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, + JSON.stringify(['HISTORY_RANGE_END', txid, channelName]) + ]); + }); + return; + } + // If desiredCp or desiredMsg are defined, we can't stream and must + // get a list of messages to send from a worker Env.getOlderHistory(channelName, oldestKnownHash, untilHash, desiredMessages, desiredCheckpoint, function (err, toSend) { if (err && err.code !== 'ENOENT') { Env.Log.error("HK_GET_OLDER_HISTORY", err); diff --git a/lib/storage/file.js b/lib/storage/file.js index ff57d922b..4ea2284f2 100644 --- a/lib/storage/file.js +++ b/lib/storage/file.js @@ -366,7 +366,7 @@ var readMessages = function (path, msgHandler, _cb) { return readFileBin(stream, function (msgObj, readMore) { collector.keepAlive(); msgHandler(msgObj.buff.toString('utf8')); - readMore(); + setTimeout(readMore); }, function (err) { cb(err); }); diff --git a/lib/workers/db-worker.js b/lib/workers/db-worker.js index 88fcd5989..e8bc31cae 100644 --- a/lib/workers/db-worker.js +++ b/lib/workers/db-worker.js @@ -40,7 +40,7 @@ Logger.levels.forEach(function (level) { }; }); -const HISTORY_SIZE_LIMIT = 1024 * 1024 * 1024; // XXX 1GB +//const HISTORY_SIZE_LIMIT = 1024 * 1024 * 1024; // XXX 1GB var DETAIL = 1000; var round = function (n) { @@ -396,11 +396,50 @@ const getFileSize = function (data, cb) { const getOlderHistory = function (data, cb) { const oldestKnownHash = data.hash; - const untilHash = data.toHash; const channelName = data.channel; const desiredMessages = data.desiredMessages; const desiredCheckpoint = data.desiredCheckpoint; + let messages = []; + store.readMessagesBin(channelName, 0, (msgObj, readMore, abort) => { + const parsed = HK.tryParse(Env, msgObj.buff.toString('utf8')); + if (!parsed) { return void readMore(); } + if (HK.isMetadataMessage(parsed)) { return void readMore(); } + const content = parsed[4]; + if (typeof(content) !== 'string') { return void readMore(); } + const hash = HK.getHash(content); + + messages.push(parsed); + + // "X" messages before oldestKnownHash + if (typeof (desiredMessages) === "number") { + messages = messages.slice(-desiredMessages); + if (hash === oldestKnownHash) { return void abort(); } + return void readMore(); + } + + // "X" checkpoints before oldestKnownHash + if (hash === oldestKnownHash) { return void abort(); } + if (/^cp\|/.test(content)) { // clean whenever we push a cp + let foundCp = 0; + const idx = messages.findLastIndex(parsed => { + let isCp = /^cp\|/.test(parsed[4]); + if (!isCp) { return; } + foundCp++; + return foundCp >= desiredCheckpoint; + }); + if (idx > 0) { + messages = messages.slice(idx); + } + } + readMore(); + }, function (err, reason) { + if (err) { return void cb(err, reason); } + cb(void 0, messages); + }); + + /* + const untilHash = data.toHash; var next = () => { var messages = []; var found = false; @@ -456,6 +495,7 @@ const getOlderHistory = function (data, cb) { } next(); }); + */ }; const getPinState = function (data, cb) { diff --git a/lib/workers/index.js b/lib/workers/index.js index bc1780cc5..4d0473918 100644 --- a/lib/workers/index.js +++ b/lib/workers/index.js @@ -53,16 +53,20 @@ Workers.initialize = function (Env, config, _cb) { //return Object.keys(workers[index].tasks || {}).length; }; + const WORKER_TASK_LIMIT = 1000; // XXX + //const WORKER_TASK_LIMIT = 100; // XXX + var workerOffset = -1; var queue = []; - var getAvailableWorkerIndex = function () { + var getAvailableWorkerIndex = function (isQueue) { // If there is already a backlog of tasks you can avoid some work -// by going to the end of the line - if (queue.length) { return -1; } +// by going to the end of the line (unless we're trying to +// empty the queue) + if (queue.length && !isQueue) { return -1; } var L = workers.length; if (L === 0) { - Log.error('NO_WORKERS_AVAILABLE', { + Log.warn('NO_WORKERS_AVAILABLE', { queue: queue.length, }); return -1; @@ -94,7 +98,7 @@ Workers.initialize = function (Env, config, _cb) { }; var drained = true; - var sendCommand = function (msg, _cb, opt) { + var sendCommand = function (msg, _cb, opt, isQueue) { if (!_cb) { return void Log.error('WORKER_COMMAND_MISSING_CB', { msg: msg, @@ -103,7 +107,7 @@ Workers.initialize = function (Env, config, _cb) { } opt = opt || {}; - var index = getAvailableWorkerIndex(); + var index = getAvailableWorkerIndex(isQueue); var state = workers[index]; // if there is no worker available: @@ -115,7 +119,7 @@ Workers.initialize = function (Env, config, _cb) { }); if (drained) { drained = false; - Log.error('WORKER_QUEUE_BACKLOG', { + Log.warn('WORKER_QUEUE_BACKLOG', { workers: workers.length, }); } @@ -128,13 +132,14 @@ Workers.initialize = function (Env, config, _cb) { var cb = Util.once(Util.mkAsync(Util.both(_cb, function (err /*, value */) { incrementTime(msg && msg.command, start); if (err !== 'TIMEOUT') { return; } - Log.debug("WORKER_TIMEOUT_CAUSE", msg); + Log.warn("WORKER_TIMEOUT_CAUSE", msg); // in the event of a timeout the user will receive an error // but the state used to resend a query in the event of a worker crash // won't be cleared. This also leaks a slot that could be used to keep // an upper bound on the amount of parallelism for any given worker. // if you run out of slots then the worker locks up. delete state.tasks[txid]; + state.checkTasks(); }))); if (!msg) { @@ -165,6 +170,12 @@ Workers.initialize = function (Env, config, _cb) { msg._cb = _cb; msg._opt = opt; }); + + state.count++; + if (state.count > WORKER_TASK_LIMIT) { + // Remove from list and spawn new one + if (state.replaceWorker) { state.replaceWorker(); } + } }; const pluginsResponses = {}; @@ -208,6 +219,8 @@ Workers.initialize = function (Env, config, _cb) { if (!res.txid) { return; } response.handle(res.txid, [res.error, res.value]); delete state.tasks[res.txid]; + state.checkTasks(); + if (!queue.length) { if (!drained) { drained = true; @@ -235,7 +248,7 @@ Workers.initialize = function (Env, config, _cb) { to the back because the following msg took its place. OR, in an even worse scenario, we cycle through the queue but don't run anything. */ - sendCommand(nextMsg.msg, nextMsg.cb); + sendCommand(nextMsg.msg, nextMsg.cb, {}, true); }; const initWorker = function (worker, cb) { @@ -244,13 +257,60 @@ Workers.initialize = function (Env, config, _cb) { const state = { worker: worker, tasks: {}, + count: Math.floor(Math.random()*(WORKER_TASK_LIMIT/10)), pid: worker.pid, // store the child process's id in an easily accessible location }; + state.replaceWorker = () => { + let index = workers.indexOf(state); + if (index === -1) { return; } + // Remove old + workers.splice(index, 1); + // Create new + state.complete = true; + const w = fork(DB_PATH); + Log.info('WORKER_REPLACE_START', { + from: state.worker.pid, + to: w.pid + }); + initWorker(w, function (err) { + if (err) { + throw new Error(err); + } + }); + }; + + // If we've reached the limit, kill the worker once + // all the tasks are complete or timed out + state.checkTasks = () => { + // Check limit + if (!state.complete || !state.worker) { return; } + // Check remaining tasks + if (Object.keys(state.tasks).length) { return; } + // Kill + Log.info('WORKER_KILL', { + worker: state.worker.pid, + count: state.count + }); + delete state.worker; + worker.kill(); + }; + response.expect(txid, function (err) { if (err) { return void cb(err); } workers.push(state); cb(void 0, state); + // We just pushed a new worker, available to receive + // a task, so we can empty the queue if necessary + if (queue.length) { + const nextMsg = queue.shift(); + if (!nextMsg || !nextMsg.msg) { + return Log.error('WORKER_QUEUE_EMPTY_MESSAGE', { + item: nextMsg, + }); + } + sendCommand(nextMsg.msg, nextMsg.cb, {}, true); + } }, 15000); worker.send({ @@ -298,18 +358,21 @@ Workers.initialize = function (Env, config, _cb) { }); worker.on('exit', function () { + if (!state.worker) { return; } // Manually killed substituteWorker(); Env.Log.error("DB_WORKER_EXIT", { pid: state.pid, }); }); worker.on('close', function () { + if (!state.worker) { return; } // Manually killed substituteWorker(); Env.Log.error("DB_WORKER_CLOSE", { pid: state.pid, }); }); worker.on('error', function (err) { + if (!state.worker) { return; } // Manually killed substituteWorker(); Env.Log.error("DB_WORKER_ERROR", { pid: state.pid, diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index 0c94aca71..631aff970 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -1720,11 +1720,14 @@ define([ "documentType": file.doc, "editorConfig": { customization: { + compactHeader: true, chat: false, logo: { url: "/bounce/#" + encodeURIComponent('https://www.onlyoffice.com') }, - comments: !lock && !readOnly + comments: !lock && !readOnly, + hideRightMenu: true, + uiTheme: window.CryptPad_theme === "dark" ? "theme-dark" : "theme-classic-light" }, "user": { "id": String(myOOId), //"c0c3bf82-20d7-4663-bf6d-7fa39c598b1d", @@ -2183,8 +2186,11 @@ Uncaught TypeError: Cannot read property 'calculatedType' of null } c.forcesave = true; } - console.error('updated config', APP.ooconfig); + // Always hide right menu + localStorage?.original.removeItem('sse-hide-right-settings'); + localStorage?.original.removeItem('de-hide-right-settings'); + localStorage?.original.removeItem('pe-hide-right-settings'); APP.docEditor = new window.DocsAPI.DocEditor("cp-app-oo-placeholder-a", APP.ooconfig); ooLoaded = true; diff --git a/www/common/sframe-boot2.js b/www/common/sframe-boot2.js index 4df642678..359424a5d 100644 --- a/www/common/sframe-boot2.js +++ b/www/common/sframe-boot2.js @@ -25,16 +25,19 @@ define([ delete ls[k]; }); }; - var mkFakeStore = function () { + var mkFakeStore = function (original) { var fakeStorage = { getItem: function (k) { return fakeStorage[k]; }, setItem: function (k, v) { fakeStorage[k] = v; return v; }, - removeItem: function (k) { delete fakeStorage[k]; } + removeItem: function (k) { delete fakeStorage[k]; }, + original }; return fakeStorage; }; - window.__defineGetter__('localStorage', function () { return mkFakeStore(); }); - window.__defineGetter__('sessionStorage', function () { return mkFakeStore(); }); + let loc = localStorage; + let ses = sessionStorage; + window.__defineGetter__('localStorage', function () { return mkFakeStore(loc); }); + window.__defineGetter__('sessionStorage', function () { return mkFakeStore(ses); }); window.CRYPTPAD_INSIDE = true; diff --git a/www/kanban/app-kanban.less b/www/kanban/app-kanban.less index 86e1052b5..b050791b7 100644 --- a/www/kanban/app-kanban.less +++ b/www/kanban/app-kanban.less @@ -145,6 +145,17 @@ margin-bottom: 15px; } + .cp-kanban-toggle-container.cp-kanban-container-flex { + flex: 1; + } + .cp-kanban-toggle-tags { + text-transform: unset; + margin-right: 0.5rem; + padding: 3px 10px; + span { + font: @colortheme_app-font; + } + } #cp-kanban-edit-tags { .tokenfield { margin: 0; @@ -152,10 +163,6 @@ } margin-bottom: 15px; } - .kanban-tag-btn-toggle { - margin-top: 10px; - margin-left: 10px - } #cp-app-kanban-container { flex: 1; display: flex; @@ -430,12 +437,10 @@ justify-content: space-between; position: relative; min-height: 50px; + align-items: center; .cp-kanban-filterTags { @media (min-width: 505px) { display: inline-flex; - .kanban-tag-btn-toggle { - margin-right: 10px; - } } align-items: center; flex: 1; @@ -449,16 +454,8 @@ } flex-flow: column; flex-shrink: 0; - & > * { - visibility: hidden; - } & > span { display: inline-block; - height: 38px; - line-height: 38px; - } - & > button { - margin-top: -38px; } } button.cp-kanban-filterTags-reset { @@ -571,7 +568,7 @@ display: flex; min-height: 0; .kanban-container { - padding: 30px 5px; + padding: 0px 5px; flex: 1; display: flex; max-height: 100%; @@ -700,6 +697,15 @@ } } + @media (pointer: none), (pointer:coarse) { + .kanban-container-outer { + .kanban-container { + padding: 30px 5px; + } + } + } + + &.cp-app-readonly { .kanban-item, .kanban-title-board { cursor: default !important; diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 947092c59..fa7103aa5 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -65,6 +65,9 @@ define([ var onCursorUpdate = Util.mkEvent(); var remoteCursors = {}; + let getCursor = () => {}; + let restoreCursor = () => {}; + var setValueAndCursor = function (input, val, _cursor) { if (!input) { return; } var $input = $(input); @@ -149,13 +152,17 @@ define([ var addEditItemButton = function () {}; + var onRemoteChange = Util.mkEvent(); var now = function () { return +new Date(); }; var _lastUpdate = 0; var _updateBoards = function (framework, kanban, boards) { _lastUpdate = now(); + var cursor = getCursor(); kanban.setBoards(Util.clone(boards)); kanban.inEditMode = false; addEditItemButton(framework, kanban); + restoreCursor(cursor); + onRemoteChange.fire(); }; var _updateBoardsThrottle = Util.throttle(_updateBoards, 1000); var updateBoards = function (framework, kanban, boards) { @@ -166,7 +173,6 @@ define([ _updateBoardsThrottle(framework, kanban, boards); }; - var onRemoteChange = Util.mkEvent(); var editModal; var PROPERTIES = ['title', 'body', 'tags', 'color']; var BOARD_PROPERTIES = ['title', 'color']; @@ -700,8 +706,11 @@ define([ var item = kanban.getItemJSON(eid); item.title = name; kanban.onChange(); - // Unlock edit mode - kanban.inEditMode = false; + // Unlock edit mode unless we're already editing + // something else + if (kanban.inEditMode === eid) { + kanban.inEditMode = false; + } onCursorUpdate.fire({}); }; $input.blur(save); @@ -764,7 +773,9 @@ define([ kanban.getBoardJSON(boardId).title = name; kanban.onChange(); // Unlock edit mode - kanban.inEditMode = false; + if (kanban.inEditMode === boardId) { + kanban.inEditMode = false; + } onCursorUpdate.fire({}); }; $input.blur(save); @@ -819,7 +830,9 @@ define([ }); var save = function () { $item.remove(); - kanban.inEditMode = false; + if (kanban.inEditMode === "new") { + kanban.inEditMode = false; + } onCursorUpdate.fire({}); if (!$input.val()) { return; } var id = Util.createRandomInteger(); @@ -930,18 +943,15 @@ define([ //framework._.sfCommon.setPadAttribute('quickMode', false); }); - var toggleTagsButton = h('button.btn.btn-default.kanban-tag-btn-toggle', Messages.kanban_showTags); - // Tags filter var existing = getExistingTags(kanban.options.boards); var list = h('div.cp-kanban-filterTags-list'); - var reset = h('button.btn.btn-cancel.cp-kanban-filterTags-reset', [ + var reset = h('button.btn.btn-cancel.cp-kanban-filterTags-reset.cp-kanban-toggle-tags', [ h('i.fa.fa-times'), - Messages.kanban_clearFilter + h('span', Messages.kanban_clearFilter) ]); var hint = h('span.cp-kanban-filterTags-name', Messages.kanban_tags); var tags = h('div.cp-kanban-filterTags', [ - h('span.cp-kanban-filterTags-toggle', [ hint, reset, @@ -954,8 +964,16 @@ define([ var $hint = $(hint); var setTagFilterState = function (bool) { + //$hint.toggle(!bool); + //$reset.toggle(!!bool); $hint.css('visibility', bool? 'hidden': 'visible'); + $hint.css('height', bool ? 0 : ''); + $hint.css('padding-top', bool ? 0 : ''); + $hint.css('padding-bottom', bool ? 0 : ''); $reset.css('visibility', bool? 'visible': 'hidden'); + $reset.css('height', !bool ? 0 : ''); + $reset.css('padding-top', !bool ? 0 : ''); + $reset.css('padding-bottom', !bool ? 0 : ''); }; setTagFilterState(); @@ -1022,41 +1040,48 @@ define([ commitTags(); }); + let toggleTagsButton = h('button.btn.btn-default.cp-kanban-toggle-tags', [ + h('i.fa.fa-tags'), + h('span', Messages.fm_tagsName) + ]); + let toggleContainer = h('div.cp-kanban-toggle-container', toggleTagsButton); - if ($(window).width() < 500) { + let toggleClicked = false; + let $tags = $(tags); + let $toggleBtn = $(toggleTagsButton); + let toggle = () => { + $tags.toggle(); + let visible = $tags.is(':visible'); + $(toggleContainer).toggleClass('cp-kanban-container-flex', !visible); + $toggleBtn.toggleClass('btn-default', visible); + $toggleBtn.toggleClass('btn-default-alt', !visible); + }; + $toggleBtn.click(function() { + toggleClicked = true; + toggle(); + }); - $(tags).append(toggleTagsButton); - - var hideTags = function () { - for (var tag of list.children) { - if (existing.indexOf(tag.innerHTML) > 10) { - $(tag).hide(); - } + const resizeTags = () => { + if (toggleClicked) { return; } + let visible = $tags.is(':visible'); + // Small screen and visible: hide + if ($(window).width() < 600) { + if (visible) { + $(tags).show(); + toggle(); } - }; - hideTags(); - - var toggleTags = function () { - for (var tag of list.children) { - if (existing.indexOf(tag.innerHTML) > 10 && kanban.options.tags.indexOf(tag.innerHTML) === -1) { - if ($(tag).is(":visible")) { - $(tag).hide(); - $(toggleTagsButton).text(Messages.kanban_showTags); - } else { - $(tag).show(); - $(toggleTagsButton).text(Messages.kanban_hideTags); - } - } - } - }; - - $(toggleTagsButton).click(function() { - toggleTags(); - }); - - } + return; + } + // Large screen: make visible by default + if (visible) { return; } + $(tags).hide(); + toggle(); + }; + + $(window).on('resize', resizeTags); var container = h('div#cp-kanban-controls', [ + toggleContainer, tags, h('div.cp-kanban-changeView', [ small, @@ -1065,18 +1090,6 @@ define([ ]); $container.before(container); - var common = framework._.sfCommon; - var $button = common.createButton('toggle', true, { - element: $(container), - icon: 'fa-tags', - text: Messages.fm_tagsName, - }, function () { - $button.toggleClass('cp-toolbar-button-active'); - - }); - $button.addClass('cp-toolbar-button-active'); - framework._.toolbar.$bottomL.append($button); - onRedraw.reg(function () { // Redraw if new tags have been added to items var old = Sortify(existing); @@ -1191,7 +1204,7 @@ define([ $container.find('.kanban-edit-item').remove(); }); - var getCursor = function () { + getCursor = function () { if (!kanban || !kanban.inEditMode) { return; } try { var id = kanban.inEditMode; @@ -1232,7 +1245,7 @@ define([ return {}; } }; - var restoreCursor = function (data) { + restoreCursor = function (data) { if (!data) { return; } try { var id = data.id; @@ -1296,12 +1309,9 @@ define([ var remoteContent = newContent.content; if (Sortify(currentContent) !== Sortify(remoteContent)) { - var cursor = getCursor(); verbose("Content is different.. Applying content"); kanban.options.boards = remoteContent; updateBoards(framework, kanban, remoteContent); - restoreCursor(cursor); - onRemoteChange.fire(); } });