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) {