From 6cbdcdec651e00e63f9110c7983b46e31bf7417e Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Mon, 3 Nov 2014 16:07:39 +0100 Subject: [PATCH] Show users editing and lag, properly send message to indicate that all users have left channel and show basic error box if disconnected. --- ChainPadSrv.js | 23 ++++++++--- Storage.js | 2 +- bower.json | 3 +- www/chainpad.js | 5 +++ www/errorbox.js | 91 +++++++++++++++++++++++++++++++++++++++++ www/main.js | 21 ++-------- www/messages.js | 33 +++++++++++++++ www/realtime-wysiwyg.js | 71 +++++++++++++++++--------------- 8 files changed, 191 insertions(+), 58 deletions(-) create mode 100644 www/errorbox.js create mode 100644 www/messages.js diff --git a/ChainPadSrv.js b/ChainPadSrv.js index 1054670a8..1def6ee12 100644 --- a/ChainPadSrv.js +++ b/ChainPadSrv.js @@ -103,7 +103,7 @@ var dropClient = function (ctx, userpass) { var chanName = client.channels[i]; var chan = ctx.channels[chanName]; var idx = chan.indexOf(client); - if (idx < 0) { throw new Error(); } + if (idx < 0) { continue; } console.log("Removing ["+client.userName+"] from channel ["+chanName+"]"); chan.splice(idx, 1); if (chan.length === 0) { @@ -122,6 +122,7 @@ var handleMessage = function (ctx, socket, msg) { msg = popPassword(msg); if (parsed.content[0] === REGISTER) { + if (parsed.user.length === 0) { throw new Error(); } console.log("[" + userPass + "] registered"); var client = ctx.registeredClients[userPass] = ctx.registeredClients[userPass] || { channels: [parsed.channelId], @@ -131,17 +132,27 @@ console.log("[" + userPass + "] registered"); client.socket = socket; var chan = ctx.channels[parsed.channelId] = ctx.channels[parsed.channelId] || []; + var newChan = (chan.length === 0); chan.name = parsed.channelId; - chan.push(client); // we send a register ack right away but then we fallthrough // to let other users know that we were registered. sendMsg(mkMessage('', parsed.channelId, [1,0]), socket); - sendChannelMessage(ctx, chan, msg, function () { - ctx.store.getMessages(chan.name, function (msg) { - sendMsg(msg, socket); + + var sendMsgs = function () { + sendChannelMessage(ctx, chan, msg, function () { + chan.push(client); + ctx.store.getMessages(chan.name, function (msg) { + sendMsg(msg, socket); + }); }); - }); + }; + if (newChan) { + sendChannelMessage(ctx, chan, mkMessage('', chan.name, [DISCONNECT,0]), sendMsgs); + } else { + sendMsgs(); + } + return; } if (parsed.content[0] === PING) { diff --git a/Storage.js b/Storage.js index 91cf2bd87..23f070afb 100644 --- a/Storage.js +++ b/Storage.js @@ -31,7 +31,7 @@ var insert = function (coll, channelName, content, cb) { }; var getMessages = function (coll, channelName, cb) { - coll.find({chan:channelName}).forEach(function (doc) { + coll.find({chan:channelName}).sort( { _id: 1 } ).forEach(function (doc) { cb(doc.msg); }, function (err) { if (!err) { return; } diff --git a/bower.json b/bower.json index c5da7135c..c19dc8277 100644 --- a/bower.json +++ b/bower.json @@ -21,6 +21,7 @@ "jquery": "~2.1.1", "tweetnacl": "~0.12.2", "ckeditor": "~4.4.5", - "requirejs": "~2.1.15" + "requirejs": "~2.1.15", + "modalBox": "~1.0.2" } } diff --git a/www/chainpad.js b/www/chainpad.js index f6d5b59f8..9e7909616 100644 --- a/www/chainpad.js +++ b/www/chainpad.js @@ -933,6 +933,11 @@ var handleMessage = ChainPad.handleMessage = function (realtime, msgStr) { } if (msg.messageType === Message.DISCONNECT) { + if (msg.userName === '') { + realtime.userList = []; + userListChange(realtime); + return; + } var idx = realtime.userList.indexOf(msg.userName); if (Common.PARANOIA) { Common.assert(idx > -1); } if (idx > -1) { diff --git a/www/errorbox.js b/www/errorbox.js new file mode 100644 index 000000000..74a6b6f8d --- /dev/null +++ b/www/errorbox.js @@ -0,0 +1,91 @@ +/* + * Copyright 2014 XWiki SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +require.config({ + 'shim': { + 'bower/modalBox/modalBox-min': ['bower/jquery/dist/jquery.min'], + } +}); +define([ + 'messages', + 'bower/modalBox/modalBox-min' +], function (Messages) { + + var STYLE = [ + '' + ].join(''); + + var CONTENT = [ + '

', + '
', + '

' + ].join(''); + + var ERROR_ADDITIONAL = [ + '

', + '', + '', + ].join(''); + + var showError = function (errorType, docHtml, moreInfo) { + $('body').append(''); + var $modalbox = $('.modalBox') + $modalbox.append(CONTENT + STYLE); + + $modalbox.find('.errorType').text(Messages['errorBox_errorType_' + errorType]); + $modalbox.find('.errorExplanation').text(Messages['errorBox_errorExplanation_' + errorType]); + if (moreInfo) { + $modalbox.append(ERROR_ADDITIONAL); + $modalbox.find('.errorMoreExplanation').text(Messages.errorBox_moreExplanation); + $modalbox.find('.errorData').text(Messages['errorBox_' + errorType]); + } + + $modalbox.modalBox({ + onOpen: boxOpened, + onClose: function () { $('.modalBox').remove(); } + }); + $('.iw-modalOverlay').css({'z-index':10000}); + }; + + return { + show: showError + }; +}); diff --git a/www/main.js b/www/main.js index 6df37ad74..2b5da81ec 100644 --- a/www/main.js +++ b/www/main.js @@ -1,29 +1,15 @@ define([ 'api/config?cb=' + Math.random().toString(16).substring(2), 'realtime-wysiwyg', + 'messages', 'bower/jquery/dist/jquery.min', 'bower/ckeditor/ckeditor', 'bower/tweetnacl/nacl-fast.min', -], function (Config, RTWysiwyg) { +], function (Config, RTWysiwyg, Messages) { var Ckeditor = window.CKEDITOR; var Nacl = window.nacl; var $ = jQuery; - var INITIAL_STATE = [ - '

', - 'This is CryptPad, the zero knowledge realtime collaborative editor.', - '
', - 'What you type here is encrypted so only people who have the link can access it.', - '
', - 'Even the server cannot see what you type.', - '

', - '

', - '', - 'What you see here, what you hear here, when you leave here, let it stay here', - '', - '

', - ].join(''); - var module = { exports: {} }; var parseKey = function (str) { @@ -54,12 +40,11 @@ define([ editor.on('instanceReady', function () { editor.execCommand('maximize'); var ifr = window.ifr = $('iframe')[0]; - ifr.contentDocument.body.innerHTML = INITIAL_STATE; + ifr.contentDocument.body.innerHTML = Messages.initialState; var rtw = RTWysiwyg.start(Config.websocketURL, userName(), - {}, Nacl.util.encodeBase64(key.lookupKey).substring(0,10), key.cryptKey); editor.on('change', function () { rtw.onEvent(); }); diff --git a/www/messages.js b/www/messages.js new file mode 100644 index 000000000..f56ee82a3 --- /dev/null +++ b/www/messages.js @@ -0,0 +1,33 @@ +define(function () { + var out = {}; + + out.errorBox_errorType_disconnected = 'Connection Lost'; + out.errorBox_errorExplanation_disconnected = [ + 'Lost connection to server, you may reconnect by reloading the page or review your work ', + 'by clicking outside of this box.' + ].join(''); + + out.editingAlone = 'Editing alone'; + out.editingWithOneOtherPerson = 'Editing with one other person'; + out.editingWith = 'Editing with'; + out.otherPeople = 'other people'; + out.disconnected = 'Disconnected'; + out.lag = 'Lag'; + + out.initialState = [ + '

', + 'This is CryptPad, the zero knowledge realtime collaborative editor.', + '
', + 'What you type here is encrypted so only people who have the link can access it.', + '
', + 'Even the server cannot see what you type.', + '

', + '

', + '', + 'What you see here, what you hear here, when you leave here, let it stay here', + '', + '

', + ].join(''); + + return out; +}); diff --git a/www/realtime-wysiwyg.js b/www/realtime-wysiwyg.js index 33db9883f..aff3e1a61 100644 --- a/www/realtime-wysiwyg.js +++ b/www/realtime-wysiwyg.js @@ -16,12 +16,17 @@ */ define([ 'html-patcher', + 'errorbox', + 'messages', 'rangy', 'chainpad', 'otaml', 'bower/jquery/dist/jquery.min', 'bower/tweetnacl/nacl-fast.min' -], function (HTMLPatcher) { +], function (HTMLPatcher, ErrorBox, Messages) { + +window.ErrorBox = ErrorBox; + var $ = window.jQuery; var Rangy = window.rangy; Rangy.init(); @@ -29,12 +34,8 @@ define([ var Otaml = window.Otaml; var Nacl = window.nacl; -var ErrorBox = {}; - var PARANOIA = true; - - var module = { exports: {} }; /** @@ -106,26 +107,34 @@ var ErrorBox = {}; }; var isSocketDisconnected = function (socket, realtime) { - return socket._socket.readyState === socket.CLOSING - || socket._socket.readyState === socket.CLOSED + var sock = socket._socket; + return sock.readyState === sock.CLOSING + || sock.readyState === sock.CLOSED || (realtime.getLag().waiting && realtime.getLag().lag > MAX_LAG_BEFORE_DISCONNECT); }; - var updateUserList = function (myUserName, listElement, userList, messages) { + var updateUserList = function (myUserName, listElement, userList) { var meIdx = userList.indexOf(myUserName); if (meIdx === -1) { - listElement.text(messages.disconnected); + listElement.text(Messages.disconnected); return; } - listElement.text(messages.editingWith + ' ' + (userList.length - 1) + ' people'); + if (userList.length === 1) { + listElement.text(Messages.editingAlone); + } else if (userList.length === 2) { + listElement.text(Messages.editingWithOneOtherPerson); + } else { + listElement.text(Messages.editingWith + ' ' + (userList.length - 1) + + Messages.otherPeople); + } }; - var createUserList = function (realtime, myUserName, container, messages) { + var createUserList = function (realtime, myUserName, container) { var id = uid(); $(container).prepend('
'); var listElement = $('#'+id); realtime.onUserListChange(function (userList) { - updateUserList(myUserName, listElement, userList, messages); + updateUserList(myUserName, listElement, userList); }); return listElement; }; @@ -269,10 +278,10 @@ var ErrorBox = {}; return 'rtwysiwyg-uid-' + String(Math.random()).substring(2); }; - var checkLag = function (realtime, lagElement, messages) { + var checkLag = function (realtime, lagElement) { var lag = realtime.getLag(); var lagSec = lag.lag/1000; - var lagMsg = messages.lag + ' '; + var lagMsg = Messages.lag + ' '; if (lag.waiting && lagSec > 1) { lagMsg += "?? " + Math.floor(lagSec); } else { @@ -281,12 +290,12 @@ var ErrorBox = {}; lagElement.text(lagMsg); }; - var createLagElement = function (socket, realtime, container, messages) { + var createLagElement = function (socket, realtime, container) { var id = uid(); $(container).append('
'); var lagElement = $('#'+id); var intr = setInterval(function () { - checkLag(realtime, lagElement, messages); + checkLag(realtime, lagElement); }, 3000); socket.onClose.push(function () { clearTimeout(intr); }); return lagElement; @@ -306,11 +315,11 @@ var ErrorBox = {}; '.' + TOOLBAR_CLS + ' {', ' color: #666;', ' font-weight: bold;', - ' background-color: #f0f0ee;', - ' border-bottom: 1px solid #DDD;', - ' border-top: 3px solid #CCC;', - ' border-right: 2px solid #CCC;', - ' border-left: 2px solid #CCC;', +// ' background-color: #f0f0ee;', +// ' border-bottom: 1px solid #DDD;', +// ' border-top: 3px solid #CCC;', +// ' border-right: 2px solid #CCC;', +// ' border-left: 2px solid #CCC;', ' height: 26px;', ' margin-bottom: -3px;', ' display: inline-block;', @@ -319,7 +328,7 @@ var ErrorBox = {}; '.' + TOOLBAR_CLS + ' div {', ' padding: 0 10px;', ' height: 1.5em;', - ' background: #f0f0ee;', +// ' background: #f0f0ee;', ' line-height: 25px;', ' height: 22px;', '}', @@ -338,6 +347,7 @@ var ErrorBox = {}; '.' + DEBUG_LINK_CLS + ':link { color:transparent; }', '.' + DEBUG_LINK_CLS + ':link:hover { color:blue; }', '.gwt-TabPanelBottom { border-top: 0 none; }', + '' ].join('\n')); return toolbar; @@ -417,8 +427,7 @@ var ErrorBox = {}; return spl[0] + res.length + ':' + res; }; - var start = module.exports.start = - function (websocketUrl, userName, messages, channel, cryptKey) + var start = module.exports.start = function (websocketUrl, userName, channel, cryptKey) { var passwd = 'y'; var wysiwygDiv = document.getElementById('cke_1_contents'); @@ -427,10 +436,11 @@ var ErrorBox = {}; var socket = makeWebsocket(websocketUrl); var onEvent = function () { }; - var toolbar = createRealtimeToolbar('#xwikieditcontent'); + var toolbar = createRealtimeToolbar('#cke_1_toolbox'); socket.onClose.push(function () { $(toolbar).remove(); + checkSocket(); }); var allMessages = []; @@ -474,17 +484,15 @@ var ErrorBox = {}; getDocHTML(doc), { transformFunction: Otaml.transform }); - //createDebugLink(realtime, doc, allMessages, toolbar, messages); + //createDebugLink(realtime, doc, allMessages, toolbar); createLagElement(socket, realtime, - toolbar.find('.rtwysiwyg-toolbar-rightside'), - messages); + toolbar.find('.rtwysiwyg-toolbar-rightside')); createUserList(realtime, userName, - toolbar.find('.rtwysiwyg-toolbar-leftside'), - messages); + toolbar.find('.rtwysiwyg-toolbar-leftside')); onEvent = function () { if (isErrorState) { return; } @@ -522,11 +530,10 @@ var ErrorBox = {}; }; realtime.onUserListChange(function (userList) { - if (!initializing && userList.indexOf(userName) === -1) { return; } + if (!initializing || userList.indexOf(userName) === -1) { return; } // if we spot ourselves being added to the document, we'll switch // 'initializing' off because it means we're fully synced. initializing = false; - userDocBeforePatch = realtime.getUserDoc(); incomingPatch(); });