From fa6914037caca8fd11d74c9c6a903131149d82a4 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 26 Apr 2016 17:34:57 +0200 Subject: [PATCH 1/5] start rewriting codepad to use realtime-input --- www/code/main.js | 62 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/www/code/main.js b/www/code/main.js index 26cd0b1e9..f65453622 100644 --- a/www/code/main.js +++ b/www/code/main.js @@ -1,30 +1,32 @@ define([ '/api/config?cb=' + Math.random().toString(16).substring(2), - '/code/rt_codemirror.js', +// '/code/rt_codemirror.js', '/common/messages.js', '/common/crypto.js', '/common/realtime-input.js', '/common/TextPatcher.js', '/bower_components/jquery/dist/jquery.min.js' -], function (Config, RTCode, Messages, Crypto, Realtime, TextPatcher) { +], function (Config, /*RTCode,*/ Messages, Crypto, Realtime, TextPatcher) { var $ = window.jQuery; - var ifrw = $('#pad-iframe')[0].contentWindow; - var module = {}; + var module = window.APP = {}; + var ifrw = module.ifrw = $('#pad-iframe')[0].contentWindow; $(function () { var userName = Crypto.rand64(8); var key; var channel = ''; + var hash = false; if (!/#/.test(window.location.href)) { key = Crypto.genKey(); } else { - var hash = window.location.hash.slice(1); + hash = window.location.hash.slice(1); channel = hash.slice(0, 32); key = hash.slice(32); } var config = { + //initialState: Messages.codeInitialState, userName: userName, websocketURL: Config.websocketURL, channel: channel, @@ -36,7 +38,7 @@ define([ var $pad = $('#pad-iframe'); var $textarea = $pad.contents().find('#editor1'); - var editor = CMeditor.fromTextArea($textarea[0], { + var editor = module.editor = CMeditor.fromTextArea($textarea[0], { lineNumbers: true, lineWrapping: true, autoCloseBrackets: true, @@ -50,52 +52,86 @@ define([ gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], mode: "javascript" }); - editor.setValue(Messages.codeInitialState); // TODO lock editor until chain is synced // then unlock var setEditable = function () { }; + var canonicalize = function (t) { return t.replace(/\r\n/g, '\n'); }; var initializing = true; var onInit = config.onInit = function (info) { window.location.hash = info.channel + key; - var realtime = info.realtime; + var realtime = module.realtime = info.realtime; module.patchText = TextPatcher.create({ realtime: realtime, logging: true, + //initialState: Messages.codeInitialState }); - $(window).on('hashchange', function() { - window.location.reload(); - }); + + + if (!hash) { + editor.setValue(Messages.codeInitialState); + module.patchText(Messages.codeInitialState); + module.patchText(Messages.codeInitialState); + editor.setValue(Messages.codeInitialState); + } + + //$(window).on('hashchange', function() { window.location.reload(); }); }; var onReady = config.onReady = function (info) { console.log("READY!"); - + + var userDoc = module.realtime.getUserDoc(); + + editor.setValue(userDoc); + initializing = false; }; var onRemote = config.onRemote = function (info) { if (initializing) { return; } + console.log("REMOTE"); + + var oldDoc = $textarea.val(); + var userDoc = module.realtime.getUserDoc(); + + $textarea.val(userDoc); + editor.setValue(userDoc); + + editor.save(); + // check cursor // apply changes to textarea // replace cursor }; var onLocal = config.onLocal = function () { + if (initializing) { return; } + console.log("LOCAL"); + module.patchText(canonicalize($textarea.val())); editor.save(); - //rtw.onEvent(); }; var onAbort = config.onAbort = function (info) { // TODO alert the user // inform of network disconnect + window.alert("Network Connection Lost!"); }; var realtime = module.realtime = Realtime.start(config); editor.on('change', onLocal); + + ['keydown', /*'keyup',*/ 'select', 'mousedown', 'mouseup', 'click'].forEach(function (evt) { + $textarea.on(evt, function () { + onRemote(); + onLocal(); + }); + // onLocal? + }); + }; var interval = 100; From f5f8f6e1eb3057aeaeea3336f6e2ba65b57ae443 Mon Sep 17 00:00:00 2001 From: Yann Flory Date: Tue, 26 Apr 2016 17:50:54 +0200 Subject: [PATCH 2/5] Remove the delay between a change and its propagation to chainpad --- www/code/main.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/www/code/main.js b/www/code/main.js index f65453622..6b9aa65fa 100644 --- a/www/code/main.js +++ b/www/code/main.js @@ -110,8 +110,8 @@ define([ var onLocal = config.onLocal = function () { if (initializing) { return; } console.log("LOCAL"); - module.patchText(canonicalize($textarea.val())); editor.save(); + module.patchText(canonicalize($textarea.val())); }; var onAbort = config.onAbort = function (info) { @@ -123,15 +123,6 @@ define([ var realtime = module.realtime = Realtime.start(config); editor.on('change', onLocal); - - ['keydown', /*'keyup',*/ 'select', 'mousedown', 'mouseup', 'click'].forEach(function (evt) { - $textarea.on(evt, function () { - onRemote(); - onLocal(); - }); - // onLocal? - }); - }; var interval = 100; From cc51e6d6edf5576d830795ce7b61bbeaa2cf4321 Mon Sep 17 00:00:00 2001 From: Yann Flory Date: Wed, 27 Apr 2016 10:44:04 +0200 Subject: [PATCH 3/5] Update CodeMirror pad to work with Netflux --- www/code/main.js | 158 ++++++++++++++++++++++++++++++++++-------- www/common/toolbar.js | 10 ++- 2 files changed, 140 insertions(+), 28 deletions(-) diff --git a/www/code/main.js b/www/code/main.js index 6b9aa65fa..cd3ea4c0c 100644 --- a/www/code/main.js +++ b/www/code/main.js @@ -1,3 +1,4 @@ +require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); define([ '/api/config?cb=' + Math.random().toString(16).substring(2), // '/code/rt_codemirror.js', @@ -5,14 +6,20 @@ define([ '/common/crypto.js', '/common/realtime-input.js', '/common/TextPatcher.js', + '/common/toolbar.js', + 'json.sortify', '/bower_components/jquery/dist/jquery.min.js' -], function (Config, /*RTCode,*/ Messages, Crypto, Realtime, TextPatcher) { +], function (Config, /*RTCode,*/ Messages, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify) { var $ = window.jQuery; var module = window.APP = {}; var ifrw = module.ifrw = $('#pad-iframe')[0].contentWindow; + var stringify = function (obj) { + return JSONSortify(obj); + }; $(function () { - var userName = Crypto.rand64(8); + var userName = Crypto.rand64(8), + toolbar; var key; var channel = ''; @@ -25,15 +32,6 @@ define([ key = hash.slice(32); } - var config = { - //initialState: Messages.codeInitialState, - userName: userName, - websocketURL: Config.websocketURL, - channel: channel, - cryptKey: key, - crypto: Crypto, - }; - var andThen = function (CMeditor) { var $pad = $('#pad-iframe'); var $textarea = $pad.contents().find('#editor1'); @@ -53,6 +51,53 @@ define([ mode: "javascript" }); + var userList = {}; // List of pretty name of all users (mapped with their server ID) + var toolbarList; // List of users still connected to the channel (server IDs) + var addToUserList = function(data) { + for (var attrname in data) { userList[attrname] = data[attrname]; } + if(toolbarList && typeof toolbarList.onChange === "function") { + toolbarList.onChange(userList); + } + }; + + var myData = {}; + var myUserName = ''; // My "pretty name" + var myID; // My server ID + + var setMyID = function(info) { + myID = info.myID || null; + myUserName = myID; + }; + + var createChangeName = function(id, $container) { + var buttonElmt = $container.find('#'+id)[0]; + buttonElmt.addEventListener("click", function() { + var newName = window.prompt("Change your name :", myUserName); + if (newName && newName.trim()) { + var myUserNameTemp = newName.trim(); + if(newName.trim().length > 32) { + myUserNameTemp = myUserNameTemp.substr(0, 32); + } + myUserName = myUserNameTemp; + myData[myID] = { + name: myUserName + }; + addToUserList(myData); + onLocal(); + } + }); + }; + + var config = { + //initialState: Messages.codeInitialState, + userName: userName, + websocketURL: Config.websocketURL, + channel: channel, + cryptKey: key, + crypto: Crypto, + setMyID: setMyID, + }; + // TODO lock editor until chain is synced // then unlock var setEditable = function () { }; @@ -61,47 +106,93 @@ define([ var initializing = true; var onInit = config.onInit = function (info) { + var $bar = $('#pad-iframe')[0].contentWindow.$('#cme_toolbox'); + toolbarList = info.userList; + var config = { + userData: userList, + changeNameID: 'cryptpad-changeName' + }; + toolbar = info.realtime.toolbar = Toolbar.create($bar, info.myID, info.realtime, info.getLag, info.userList, config); + createChangeName('cryptpad-changeName', $bar); window.location.hash = info.channel + key; - var realtime = module.realtime = info.realtime; - module.patchText = TextPatcher.create({ - realtime: realtime, - logging: true, - //initialState: Messages.codeInitialState - }); - if (!hash) { + + /*if (!hash) { editor.setValue(Messages.codeInitialState); module.patchText(Messages.codeInitialState); module.patchText(Messages.codeInitialState); editor.setValue(Messages.codeInitialState); - } + }*/ //$(window).on('hashchange', function() { window.location.reload(); }); }; + var updateUserList = function(shjson) { + // Extract the user list (metadata) from the hyperjson + var hjson = (shjson === "") ? "" : JSON.parse(shjson); + if(hjson && hjson.metadata) { + var userData = hjson.metadata; + // Update the local user data + addToUserList(userData); + } + } + var onReady = config.onReady = function (info) { - console.log("READY!"); + var realtime = module.realtime = info.realtime; + module.patchText = TextPatcher.create({ + realtime: realtime, + logging: true, + //initialState: Messages.codeInitialState + }); var userDoc = module.realtime.getUserDoc(); - editor.setValue(userDoc); + var newDoc = ""; + if(userDoc !== "") { + var hjson = JSON.parse(userDoc); + newDoc = hjson.content; + } + + // Update the user list (metadata) from the hyperjson + //updateUserList(shjson); + + editor.setValue(newDoc); initializing = false; }; var onRemote = config.onRemote = function (info) { if (initializing) { return; } - console.log("REMOTE"); var oldDoc = $textarea.val(); - var userDoc = module.realtime.getUserDoc(); + var shjson = module.realtime.getUserDoc(); - $textarea.val(userDoc); - editor.setValue(userDoc); + // Update the user list (metadata) from the hyperjson + updateUserList(shjson); + var hjson = JSON.parse(shjson); + var remoteDoc = hjson.content; + editor.setValue(remoteDoc); editor.save(); + var op = TextPatcher.diff(oldDoc, remoteDoc); + //Fix cursor here + + var localDoc = $textarea.val(); + var hjson2 = { + content: localDoc, + metadata: userList + }; + + var shjson2 = stringify(hjson2); + + if (shjson2 !== shjson) { + console.error("shjson2 !== shjson"); + module.patchText(shjson2); + } + + // check cursor // apply changes to textarea // replace cursor @@ -109,9 +200,22 @@ define([ var onLocal = config.onLocal = function () { if (initializing) { return; } - console.log("LOCAL"); + editor.save(); - module.patchText(canonicalize($textarea.val())); + var textValue = canonicalize($textarea.val()); + var obj = {content: textValue}; + + // append the userlist to the hyperjson structure + obj.metadata = userList; + + // stringify the json and send it into chainpad + var shjson = stringify(obj); + + module.patchText(shjson); + + if (module.realtime.getUserDoc() !== shjson) { + console.error("realtime.getUserDoc() !== shjson"); + } }; var onAbort = config.onAbort = function (info) { diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 942fb586d..f3adbe1df 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -9,6 +9,7 @@ define([ var USER_LIST_CLS = 'rtwysiwyg-user-list'; /** Id of the button to change my username. */ + var USERNAME_BUTTON_GROUP = 'cryptpad-changeName'; var USERNAME_BUTTON_ID = 'rtwysiwyg-change-username'; /** Id of the div containing the lag info. */ @@ -67,6 +68,13 @@ define([ ' cursor: pointer;', ' color: #000;', '}', + '#' + USERNAME_BUTTON_GROUP + ' {', + ' float: left;', + '}', + '#' + USERNAME_BUTTON_GROUP + ' button {', + ' padding: 0;', + ' margin-right: 5px;', + '}', '.rtwysiwyg-toolbar-leftside div {', ' float: left;', '}', @@ -145,7 +153,7 @@ define([ var createChangeName = function($container, userList, buttonID) { var id = uid(); - userList.innerHTML = 'Change name'; + userList.innerHTML = ''; return $container.find('#'+id)[0]; }; From 492054a93808e5501cb3e591439a4aab1f690df1 Mon Sep 17 00:00:00 2001 From: Yann Flory Date: Wed, 27 Apr 2016 12:17:06 +0200 Subject: [PATCH 4/5] Fix the cursor position when a remote patch is applied --- www/code/main.js | 53 ++++++++++++++++++++++++++++++++++----- www/common/TextPatcher.js | 1 + 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/www/code/main.js b/www/code/main.js index cd3ea4c0c..3e6647b46 100644 --- a/www/code/main.js +++ b/www/code/main.js @@ -8,8 +8,9 @@ define([ '/common/TextPatcher.js', '/common/toolbar.js', 'json.sortify', + '/common/json-ot.js', '/bower_components/jquery/dist/jquery.min.js' -], function (Config, /*RTCode,*/ Messages, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify) { +], function (Config, /*RTCode,*/ Messages, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT) { var $ = window.jQuery; var module = window.APP = {}; var ifrw = module.ifrw = $('#pad-iframe')[0].contentWindow; @@ -96,6 +97,7 @@ define([ cryptKey: key, crypto: Crypto, setMyID: setMyID, + transformFunction: JsonOT.validate }; // TODO lock editor until chain is synced @@ -162,10 +164,37 @@ define([ initializing = false; }; + var cursorToPos = function(cursor, oldText) { + var cLine = cursor.line; + var cCh = cursor.ch; + var pos = 0; + var textLines = oldText.split("\n"); + for (var line = 0; line <= cLine; line++) { + if(line < cLine) { + pos += textLines[line].length+1; + } + else if(line === cLine) { + pos += cCh; + } + } + return pos; + } + + var posToCursor = function(position, newText) { + var cursor = { + line: 0, + ch: 0 + }; + var textLines = newText.substr(0, position).split("\n"); + cursor.line = textLines.length - 1; + cursor.ch = textLines[cursor.line].length; + return cursor; + } + var onRemote = config.onRemote = function (info) { if (initializing) { return; } - var oldDoc = $textarea.val(); + var oldDoc = canonicalize($textarea.val()); var shjson = module.realtime.getUserDoc(); // Update the user list (metadata) from the hyperjson @@ -173,20 +202,32 @@ define([ var hjson = JSON.parse(shjson); var remoteDoc = hjson.content; + + //get old cursor here + var oldCursor = {}; + oldCursor.selectionStart = cursorToPos(editor.getCursor('from'), oldDoc); + oldCursor.selectionEnd = cursorToPos(editor.getCursor('to'), oldDoc); + editor.setValue(remoteDoc); editor.save(); var op = TextPatcher.diff(oldDoc, remoteDoc); - //Fix cursor here + var selects = ['selectionStart', 'selectionEnd'].map(function (attr) { + return TextPatcher.transformCursor(oldCursor[attr], op); + }); + if(selects[0] === selects[1]) { + editor.setCursor(posToCursor(selects[0], remoteDoc)); + } + else { + editor.setSelection(posToCursor(selects[0], remoteDoc), posToCursor(selects[1], remoteDoc)); + } - var localDoc = $textarea.val(); + var localDoc = canonicalize($textarea.val()); var hjson2 = { content: localDoc, metadata: userList }; - var shjson2 = stringify(hjson2); - if (shjson2 !== shjson) { console.error("shjson2 !== shjson"); module.patchText(shjson2); diff --git a/www/common/TextPatcher.js b/www/common/TextPatcher.js index f815fbc97..83bfedc48 100644 --- a/www/common/TextPatcher.js +++ b/www/common/TextPatcher.js @@ -92,6 +92,7 @@ var applyChange = function(ctx, oldval, newval, logging) { }; var transformCursor = function (cursor, op) { + if (!op) { return cursor; } var pos = op.offset; var remove = op.toRemove; var insert = op.toInsert.length; From 619406f868bc1904152041dfd7c6a9b4d777f19c Mon Sep 17 00:00:00 2001 From: Yann Flory Date: Wed, 27 Apr 2016 12:17:39 +0200 Subject: [PATCH 5/5] Send an ACK when someone asks for the history --- NetfluxWebsocketSrv.js | 1 + 1 file changed, 1 insertion(+) diff --git a/NetfluxWebsocketSrv.js b/NetfluxWebsocketSrv.js index 93aac0d0e..2bc4f3f1d 100644 --- a/NetfluxWebsocketSrv.js +++ b/NetfluxWebsocketSrv.js @@ -103,6 +103,7 @@ const handleMessage = function (ctx, user, msg) { let parsed; try { parsed = JSON.parse(json[2]); } catch (err) { console.error(err); return; } if (parsed[0] === 'GET_HISTORY') { + sendMsg(ctx, user, [seq, 'ACK']); getHistory(ctx, parsed[1], function (msg) { sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(msg)]); }, function () {