diff --git a/customize.dist/messages.js b/customize.dist/messages.js index 270747cb2..5fa0c6389 100755 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -5,6 +5,7 @@ var map = { 'es': 'Español', 'el': 'Ελληνικά', 'fr': 'Français', + 'nb': 'Norwegian Bokmål', 'pl': 'Polski', 'pt-br': 'Português do Brasil', 'ro': 'Română', diff --git a/customize.dist/translations/messages.nb.js b/customize.dist/translations/messages.nb.js new file mode 100644 index 000000000..8c2215a1a --- /dev/null +++ b/customize.dist/translations/messages.nb.js @@ -0,0 +1,14 @@ +/* + * You can override the translation text using this file. + * The recommended method is to make a copy of this file (/customize.dist/translations/messages.{LANG}.js) + in a 'customize' directory (/customize/translations/messages.{LANG}.js). + * If you want to check all the existing translation keys, you can open the internal language file + but you should not change it directly (/common/translations/messages.{LANG}.js) +*/ +define(['/common/translations/messages.nb.js'], function (Messages) { + // Replace the existing keys in your copied file here: + // Messages.button_newpad = "New Rich Text Document"; + + return Messages; +}); + diff --git a/www/code/mermaid.js b/www/code/mermaid.js index 69f55c03c..104bb7f82 100644 --- a/www/code/mermaid.js +++ b/www/code/mermaid.js @@ -58147,37 +58147,6 @@ function Log(level) { this.warn = function () {}; this.error = function () {}; this.log = function () {}; - return; - this.log = function () { - var args = Array.prototype.slice.call(arguments); - var level = args.shift(); - var logLevel = this.level; - if (typeof logLevel === 'undefined') { - logLevel = defaultLevel; - } - if (logLevel <= level) { - if (typeof console !== 'undefined') { - //eslint-disable-line no-console - if (typeof console.log !== 'undefined') { - //eslint-disable-line no-console - //return console.log('[' + formatTime(new Date()) + '] ' , str); //eslint-disable-line no-console - args.unshift('[' + formatTime(new Date()) + '] '); - console.log.apply(console, args.map(function (a) { - if (typeof a === "object") { - return a.toString() + JSON.stringify(a, null, 2); - } - return a; - })); - } - } - } - }; - - this.trace = window.console.debug.bind(window.console, format('TRACE', name), 'color:grey;', 'color: grey;'); - this.debug = window.console.debug.bind(window.console, format('DEBUG', name), 'color:grey;', 'color: green;'); - this.info = window.console.debug.bind(window.console, format('INFO', name), 'color:grey;', 'color: blue;'); - this.warn = window.console.debug.bind(window.console, format('WARN', name), 'color:grey;', 'color: orange;'); - this.error = window.console.debug.bind(window.console, format('ERROR', name), 'color:grey;', 'color: red;'); } exports.Log = Log; diff --git a/www/common/common-messenger.js b/www/common/common-messenger.js index b8ea783a5..c333112d9 100644 --- a/www/common/common-messenger.js +++ b/www/common/common-messenger.js @@ -965,6 +965,7 @@ define([ if (channel.padChan !== padChan) { return; } if (channel.wc) { channel.wc.leave(); } channel.stopped = true; + delete channels[chatChan]; return true; }); }; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 11b435b58..42fe7a55a 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -1058,14 +1058,12 @@ define([ }; var timeout = false; + common.onTimeoutEvent = Util.mkEvent(); var onTimeout = function () { - return; - /* timeout = true; common.onNetworkDisconnect.fire(); - // FIXME: no UI in outer... - window.alert("Timeout error, please reload this tab"); - */ + common.padRpc.onDisconnectEvent.fire(); + common.onTimeoutEvent.fire(); }; var queries = { diff --git a/www/common/metadata-manager.js b/www/common/metadata-manager.js index 684ddcdd2..2b504d39a 100644 --- a/www/common/metadata-manager.js +++ b/www/common/metadata-manager.js @@ -18,6 +18,25 @@ define(['json.sortify'], function (Sortify) { var lazyChangeHandlers = []; var titleChangeHandlers = []; + // When someone leaves the document, their metadata is removed from our metadataObj + // but it is not removed instantly from the chainpad document metadata. This is + // the result of the lazy object: if we had to remove the metadata instantly, all + // the remaining members would try to push a patch to do it, and it could create + // conflicts. Their metadata is instead removed from the chainpad doc only when + // someone calls onLocal to make another change. + // The leaving user is not visible in the userlist UI because we filter it using + // the list of "members" (netflux ID currently online). + // Our Problem: + // With the addition of shared workers, a user can leave and join back with the same + // netflux ID (just reload the pad). If nobody has made any change in the mean time, + // their metadata will still be in the document, but they won't be in our metadataObj. + // This causes the presence of a "viewer" instead of an editor, because they don't + // have user data. + // To fix this problem, the metadata manager can request "syncs" from a chainpad app, + // and the app should trigger a "metadataMgr.updateMetadata(data)" in the handler. + // See "metadataMgr.onRequestSync" in sframe-app-framework for an example. + var syncHandlers = []; + var rememberedTitle; var checkUpdate = function (lazy) { @@ -41,26 +60,25 @@ define(['json.sortify'], function (Sortify) { var mdo = {}; // We don't want to add our user data to the object multiple times. - //var containsYou = false; - //console.log(metadataObj); Object.keys(metadataObj.users).forEach(function (x) { if (members.indexOf(x) === -1) { return; } mdo[x] = metadataObj.users[x]; - /*if (metadataObj.users[x].uid === meta.user.uid) { - //console.log('document already contains you'); - containsYou = true; - }*/ }); - //if (!containsYou) { mdo[meta.user.netfluxId] = meta.user; } if (!priv.readOnly) { mdo[meta.user.netfluxId] = meta.user; } metadataObj.users = mdo; + + // Always update the userlist in the lazy object, otherwise it may be outdated + // and metadataMgr.updateMetadata() won't do anything, and so we won't push events + // to the userlist UI ==> phantom viewers var lazyUserStr = Sortify(metadataLazyObj.users[meta.user.netfluxId]); dirty = false; if (lazy || lazyUserStr !== Sortify(meta.user)) { metadataLazyObj = JSON.parse(JSON.stringify(metadataObj)); lazyChangeHandlers.forEach(function (f) { f(); }); + } else { + metadataLazyObj.users = JSON.parse(JSON.stringify(mdo)); } if (metadataObj.title !== rememberedTitle) { @@ -127,6 +145,7 @@ define(['json.sortify'], function (Sortify) { members.push(ev); if (!meta.user) { return; } change(false); + syncHandlers.forEach(function (f) { f(); }); }); sframeChan.on('EV_RT_LEAVE', function (ev) { var idx = members.indexOf(ev); @@ -171,13 +190,14 @@ define(['json.sortify'], function (Sortify) { onTitleChange: function (f) { titleChangeHandlers.push(f); }, onChange: function (f) { changeHandlers.push(f); }, onChangeLazy: function (f) { lazyChangeHandlers.push(f); }, + onRequestSync: function (f) { syncHandlers.push(f); }, isConnected : function () { return members.indexOf(meta.user.netfluxId) !== -1; }, getViewers : function () { checkUpdate(false); var list = members.slice().filter(function (m) { return m.length === 32; }); - return list.length - Object.keys(metadataObj.users).length; + return list.length - Object.keys(metadataLazyObj.users).length; }, getChannelMembers: function () { return members.slice(); }, getPrivateData : function () { diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 06ab0f089..3dca7febf 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1262,9 +1262,15 @@ define([ var messengerEventClients = []; var dropChannel = function (chanId) { - store.messenger.leavePad(chanId); - store.cursor.leavePad(chanId); - store.onlyoffice.leavePad(chanId); + try { + store.messenger.leavePad(chanId); + } catch (e) { console.error(e); } + try { + store.cursor.leavePad(chanId); + } catch (e) { console.error(e); } + try { + store.onlyoffice.leavePad(chanId); + } catch (e) { console.error(e); } if (!Store.channels[chanId]) { return; } @@ -1283,8 +1289,12 @@ define([ if (messengerIdx !== -1) { messengerEventClients.splice(messengerIdx, 1); } - store.cursor.removeClient(clientId); - store.onlyoffice.removeClient(clientId); + try { + store.cursor.removeClient(clientId); + } catch (e) { console.error(e); } + try { + store.onlyoffice.removeClient(clientId); + } catch (e) { console.error(e); } Object.keys(Store.channels).forEach(function (chanId) { var chanIdx = Store.channels[chanId].clients.indexOf(clientId); @@ -1602,12 +1612,11 @@ define([ broadcast([], 'NETWORK_RECONNECT', {myId: info.myId}); }); - /* // Ping clients regularly to make sure one tab was not closed without sending a removeClient() // command. This allow us to avoid phantom viewers in pads. var PING_INTERVAL = 30000; - var MAX_PING = 1000; - var MAX_FAILED_PING = 5; + var MAX_PING = 5000; + var MAX_FAILED_PING = 2; setInterval(function () { var clients = []; @@ -1635,7 +1644,6 @@ define([ ping(); }); }, PING_INTERVAL); - */ }; /** diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index 263781130..b905c9ffd 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -533,6 +533,12 @@ define([ } }; cpNfInner.metadataMgr.onChange(checkReady); + cpNfInner.metadataMgr.onRequestSync(function () { + var newContentStr = cpNfInner.chainpad.getUserDoc(); + var newContent = JSON.parse(newContentStr); + var meta = extractMetadata(newContent); + cpNfInner.metadataMgr.updateMetadata(meta); + }); checkReady(); var infiniteSpinnerModal = false; diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index b74a743d8..dc1031a83 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -269,6 +269,9 @@ define([ sessionStorage[Utils.Constants.displayPadCreationScreen]; delete sessionStorage[Utils.Constants.displayPadCreationScreen]; var updateMeta = function () { + // TODO availableHashes in privateData may need updates once we have + // a better privileges workflow + //console.log('EV_METADATA_UPDATE'); var metaObj, isTemplate; nThen(function (waitFor) { @@ -874,6 +877,10 @@ define([ Cryptpad.cursor.execCommand(data, cb); }); + Cryptpad.onTimeoutEvent.reg(function () { + sframeChan.event('EV_WORKER_TIMEOUT'); + }); + if (cfg.messaging) { Notifier.getPermission(); diff --git a/www/common/sframe-common-title.js b/www/common/sframe-common-title.js index b406a5dd0..3e98b853e 100644 --- a/www/common/sframe-common-title.js +++ b/www/common/sframe-common-title.js @@ -51,14 +51,16 @@ define([ metadataMgr.onChange(function () { var md = metadataMgr.getMetadata(); if ($title) { - $title.find('span.cp-toolbar-title-value').text(md.title || md.defaultTitle); - $title.find('input').val(md.title || md.defaultTitle); $title.find('input').prop('placeholder', md.defaultTitle); } exp.defaultTitle = md.defaultTitle; - exp.title = md.title; }); metadataMgr.onTitleChange(function (title, defaultTitle) { + if ($title) { + $title.find('span.cp-toolbar-title-value').text(title || defaultTitle); + $title.find('input').val(title || defaultTitle); + } + exp.title = title; sframeChan.query('Q_SET_PAD_TITLE_IN_DRIVE', { title: title, defaultTitle: defaultTitle diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 17783f460..c321af294 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -613,6 +613,10 @@ define([ }); }); + ctx.sframeChan.on('EV_WORKER_TIMEOUT', function () { + UI.errorLoadingScreen(Messages.timeoutError); + }); + ctx.sframeChan.on('EV_CHROME_68', function () { UI.alert(Messages.chrome68); }); diff --git a/www/common/toolbar3.js b/www/common/toolbar3.js index 15a993c81..41a4f0731 100644 --- a/www/common/toolbar3.js +++ b/www/common/toolbar3.js @@ -475,11 +475,11 @@ MessengerUI, Messages) { }; $closeIcon.click(function () { Common.setAttribute(['toolbar', 'chat-drawer'], false); - hide(); + hide(true); }); $button.click(function () { var visible = $content.is(':visible'); - if (visible) { hide(); } + if (visible) { hide(true); } else { show(); } visible = !visible; Common.setAttribute(['toolbar', 'chat-drawer'], visible); diff --git a/www/common/translations/messages.nb.json b/www/common/translations/messages.nb.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/www/common/translations/messages.nb.json @@ -0,0 +1,2 @@ +{ +} diff --git a/www/common/translations/messages.ru.json b/www/common/translations/messages.ru.json index f0e2a2633..3d0a623b4 100644 --- a/www/common/translations/messages.ru.json +++ b/www/common/translations/messages.ru.json @@ -143,7 +143,7 @@ "tags_notShared": "Ваши теги не разделяются с другими пользователями", "button_newsheet": "Новый Лист", "newButtonTitle": "Создать новый блокнот", - "useTemplateCancel": "Начать без образца (Esc)", + "useTemplateCancel": "Начать заново (Esc)", "previewButtonTitle": "Отобразить или скрыть режим предпросмотра разметки", "printOptions": "Опции расположения", "printBackgroundValue": "Текущий фон: {0}", @@ -163,7 +163,7 @@ "editOpen": "Открыть редактируемую ссылку в новой вкладке", "editOpenTitle": "Открыть блокнот в режиме редактирования в новой вкладке", "viewShare": "Ссылка только для чтения", - "viewShareTitle": "Копировать ссылку для чтения", + "viewShareTitle": "Скопировать ссылку для чтения в буфер обмена", "viewOpen": "Открыть ссылку в режиме чтения в новой вкладке", "viewOpenTitle": "Открыть блокнот в режиме чтения в новой вкладке", "fileShare": "Скопировать ссылку", @@ -204,10 +204,10 @@ "kanban_working": "В процессе", "kanban_deleteBoard": "Вы уверены, что хотите удалить эту доску?", "kanban_addBoard": "Добавить доску", - "kanban_removeItem": "", - "poll_p_save": "Ваши настройки применяются мгновенно, так что вам не нужно их сохранять", + "kanban_removeItem": "Удалить этот элемент", + "poll_p_save": "Ваши настройки применяются мгновенно, так что вам не нужно их сохранять.", "wizardTitle": "Используйте помощник, чтобы создать опрос", - "wizardConfirm": "Вы хотите добавить эти варианты в опрос?", + "wizardConfirm": "Вы действительно хотите добавить эти варианты в ваш опрос?", "poll_publish_button": "Опубликовать", "poll_admin_button": "Админ", "poll_create_user": "Добавить нового пользователя", @@ -305,5 +305,17 @@ "crowdfunding_popup_no": "Не сейчас", "crowdfunding_popup_never": "Не спрашивать меня снова", "markdown_toc": "Содержимое", - "fm_expirablePad": "Этот блокнот удалится через {0}" + "fm_expirablePad": "Этот блокнот удалится через {0}", + "fileEmbedTitle": "Вставить файл во внешнюю страницу", + "kanban_removeItemConfirm": "Вы уверенны, что хотите удалить этот пункт?", + "settings_backup2": "Скачать мой CryptDrive", + "settings_backup2Confirm": "Это позволит скачать все пэды и файлы с вашего CryptDrive. Если вы хотите продолжить, выберите имя и нажмите OK", + "settings_exportTitle": "Экспортировать Ваш CryptDrive", + "fileEmbedScript": "Чтобы вставить этот файл, включите этот скрипт один раз на своей странице, чтобы загрузить медиатег:", + "fileEmbedTag": "Затем поместите медиатег в любое место на странице, куда вы хотите его вставить:", + "pad_mediatagRatio": "Оставить соотношение", + "kanban_item": "Элемент {0}", + "poll_p_encryption": "Все ваши данные зашифрованы, доступ к ним имеют только пользователи, имеющие доступ к этой ссылке. Даже сервер не видит, что вы меняете.", + "wizardLog": "Нажмите кнопку в левом верхнем углу, чтобы вернуться к опросу", + "poll_bookmark_col": "Добавить этот столбец в закладку, чтобы он всегда был разблокирован и отображался для вас в начале" } diff --git a/www/poll/inner.js b/www/poll/inner.js index b487fd5ab..19d3daf9c 100644 --- a/www/poll/inner.js +++ b/www/poll/inner.js @@ -1158,6 +1158,10 @@ define([ var md = copyObject(metadataMgr.getMetadata()); APP.proxy.metadata = md; }); + metadataMgr.onRequestSync(function () { + var meta = JSON.parse(JSON.stringify(APP.proxy.metadata)); + metadataMgr.updateMetadata(meta); + }); /* add a forget button */ var forgetCb = function (err) {