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 () {
diff --git a/www/code/main.js b/www/code/main.js
index 26cd0b1e9..3e6647b46 100644
--- a/www/code/main.js
+++ b/www/code/main.js
@@ -1,42 +1,43 @@
+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',
+// '/code/rt_codemirror.js',
'/common/messages.js',
'/common/crypto.js',
'/common/realtime-input.js',
'/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) {
+], function (Config, /*RTCode,*/ Messages, Crypto, Realtime, TextPatcher, Toolbar, JSONSortify, JsonOT) {
var $ = window.jQuery;
- var ifrw = $('#pad-iframe')[0].contentWindow;
- var module = {};
+ 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 = '';
+ 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 = {
- 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');
- var editor = CMeditor.fromTextArea($textarea[0], {
+ var editor = module.editor = CMeditor.fromTextArea($textarea[0], {
lineNumbers: true,
lineWrapping: true,
autoCloseBrackets: true,
@@ -50,47 +51,218 @@ define([
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
mode: "javascript"
});
- editor.setValue(Messages.codeInitialState);
+
+ 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,
+ transformFunction: JsonOT.validate
+ };
// 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) {
+ 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 = info.realtime;
+
+
+
+ /*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) {
+ var realtime = module.realtime = info.realtime;
module.patchText = TextPatcher.create({
realtime: realtime,
logging: true,
+ //initialState: Messages.codeInitialState
});
- $(window).on('hashchange', function() {
- window.location.reload();
- });
- };
- var onReady = config.onReady = function (info) {
- console.log("READY!");
-
+ var userDoc = module.realtime.getUserDoc();
+
+ 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 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 = canonicalize($textarea.val());
+ var shjson = module.realtime.getUserDoc();
+
+ // Update the user list (metadata) from the hyperjson
+ updateUserList(shjson);
+
+ 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);
+ 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 = canonicalize($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
};
var onLocal = config.onLocal = function () {
+ if (initializing) { return; }
+
editor.save();
- //rtw.onEvent();
+ 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) {
// TODO alert the user
// inform of network disconnect
+ window.alert("Network Connection Lost!");
};
var realtime = module.realtime = Realtime.start(config);
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;
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];
};