Realtime sync fixes

This commit is contained in:
benweet 2014-04-08 00:19:47 +01:00
parent bfc93ad920
commit 6d8ed95352
11 changed files with 237 additions and 204 deletions

View File

@ -50,15 +50,15 @@ define([
}; };
Provider.prototype.parseSerializedContent = function(content) { Provider.prototype.parseSerializedContent = function(content) {
var discussionList = '{}'; var discussionListJSON = '{}';
var discussionExtractor = /<!--se_discussion_list:([\s\S]+)-->$/.exec(content); var discussionExtractor = /<!--se_discussion_list:([\s\S]+)-->$/.exec(content);
if(discussionExtractor && this.parseDiscussionList(discussionExtractor[1])) { if(discussionExtractor && this.parseDiscussionList(discussionExtractor[1])) {
content = content.substring(0, discussionExtractor.index); content = content.substring(0, discussionExtractor.index);
discussionList = discussionExtractor[1]; discussionListJSON = discussionExtractor[1];
} }
return { return {
content: content, content: content,
discussionList: discussionList discussionListJSON: discussionListJSON
}; };
}; };
@ -75,7 +75,7 @@ define([
}); });
var merge = settings.conflictMode == 'merge'; var merge = settings.conflictMode == 'merge';
Provider.prototype.syncMerge = function(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionListJSON) { Provider.prototype.syncMerge = function(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionList, remoteDiscussionListJSON) {
function cleanupDiffs(diffs) { function cleanupDiffs(diffs) {
var result = []; var result = [];
@ -160,7 +160,6 @@ define([
var localTitle = fileDesc.title; var localTitle = fileDesc.title;
var localDiscussionListJSON = fileDesc.discussionListJSON; var localDiscussionListJSON = fileDesc.discussionListJSON;
var localDiscussionList = fileDesc.discussionList; var localDiscussionList = fileDesc.discussionList;
var remoteDiscussionList = JSON.parse(remoteDiscussionListJSON);
// Local/Remote CRCs // Local/Remote CRCs
var localContentCRC = utils.crc32(localContent); var localContentCRC = utils.crc32(localContent);

View File

@ -359,7 +359,7 @@ define([
var $titleContainer; var $titleContainer;
var marginWidth = 18 * 2 + 25 + 25; var marginWidth = 18 * 2 + 25 + 25;
var titleWidth = 18 + 348; var titleWidth = 18 + 348;
var leftButtonsWidth = 18 * 4 + 80 + 160 + 200 + 80; var leftButtonsWidth = 18 * 5 + 80 + 160 + 160 + 40 + 80;
var rightButtonsWidth = 18 + 80; var rightButtonsWidth = 18 + 80;
var buttonsDropdownWidth = 40; var buttonsDropdownWidth = 40;
function adjustWindow() { function adjustWindow() {
@ -451,13 +451,11 @@ define([
$("#wmd-code-button").append($('<i class="icon-code">')).appendTo($btnGroupElt); $("#wmd-code-button").append($('<i class="icon-code">')).appendTo($btnGroupElt);
$("#wmd-image-button").append($('<i class="icon-picture">')).appendTo($btnGroupElt); $("#wmd-image-button").append($('<i class="icon-picture">')).appendTo($btnGroupElt);
$btnGroupElt = $('.wmd-button-group3'); $btnGroupElt = $('.wmd-button-group3');
var $openDiscussionElt = $btnGroupElt.find('.button-open-discussion');
$("#wmd-olist-button").append($('<i class="icon-list-numbered">')).appendTo($btnGroupElt); $("#wmd-olist-button").append($('<i class="icon-list-numbered">')).appendTo($btnGroupElt);
$("#wmd-ulist-button").append($('<i class="icon-list-bullet">')).appendTo($btnGroupElt); $("#wmd-ulist-button").append($('<i class="icon-list-bullet">')).appendTo($btnGroupElt);
$("#wmd-heading-button").append($('<i class="icon-text-height">')).appendTo($btnGroupElt); $("#wmd-heading-button").append($('<i class="icon-text-height">')).appendTo($btnGroupElt);
$("#wmd-hr-button").append($('<i class="icon-ellipsis">')).appendTo($btnGroupElt); $("#wmd-hr-button").append($('<i class="icon-ellipsis">')).appendTo($btnGroupElt);
$openDiscussionElt.appendTo($btnGroupElt); $btnGroupElt = $('.wmd-button-group5');
$btnGroupElt = $('.wmd-button-group4');
$("#wmd-undo-button").append($('<i class="icon-reply">')).appendTo($btnGroupElt); $("#wmd-undo-button").append($('<i class="icon-reply">')).appendTo($btnGroupElt);
$("#wmd-redo-button").append($('<i class="icon-forward">')).appendTo($btnGroupElt); $("#wmd-redo-button").append($('<i class="icon-forward">')).appendTo($btnGroupElt);
}; };

View File

@ -332,6 +332,7 @@ define([
range.deleteContents(); range.deleteContents();
range.insertNode(document.createTextNode(replacement)); range.insertNode(document.createTextNode(replacement));
} }
editor.setValue = setValue;
function setValueNoWatch(value) { function setValueNoWatch(value) {
setValue(value); setValue(value);

View File

@ -397,20 +397,7 @@ define([
var $removeButton = $(popoverElt.querySelector('.action-remove-discussion')); var $removeButton = $(popoverElt.querySelector('.action-remove-discussion'));
if(evt.target.discussionIndex) { if(evt.target.discussionIndex) {
// If it's an existing discussion // If it's an existing discussion
var $removeCancelButton = $(popoverElt.querySelectorAll('.action-remove-discussion-cancel'));
var $removeConfirmButton = $(popoverElt.querySelectorAll('.action-remove-discussion-confirm'));
$removeButton.click(function() { $removeButton.click(function() {
$(popoverElt.querySelector('.new-comment-block')).addClass('hide');
$(popoverElt.querySelector('.remove-discussion-confirm')).removeClass('hide');
popoverElt.querySelector('.scrollport').scrollTop = 9999999;
});
$removeCancelButton.click(function() {
$(popoverElt.querySelector('.new-comment-block')).removeClass('hide');
$(popoverElt.querySelector('.remove-discussion-confirm')).addClass('hide');
popoverElt.querySelector('.scrollport').scrollTop = 9999999;
context.$contentInputElt.focus();
});
$removeConfirmButton.click(function() {
closeCurrentPopover(); closeCurrentPopover();
var discussion = context.getDiscussion(); var discussion = context.getDiscussion();
delete context.fileDesc.discussionList[discussion.discussionIndex]; delete context.fileDesc.discussionList[discussion.discussionIndex];

View File

@ -587,7 +587,7 @@ define([
task.enqueue(); task.enqueue();
}; };
googleHelper.loadRealtime = function(fileId, content, accountId, callback, errorCallback) { googleHelper.loadRealtime = function(fileId, accountId, callback, errorCallback) {
var doc; var doc;
var task = new AsyncTask(); var task = new AsyncTask();
connect(task); connect(task);
@ -599,11 +599,7 @@ define([
// onFileLoaded // onFileLoaded
doc = result; doc = result;
task.chain(); task.chain();
}, function(model) { }, undefined, function(err) {
// initializeModel
var string = model.createString(content);
model.getRoot().set('content', string);
}, function(err) {
errorCallback(err); errorCallback(err);
task.error(new Error(err.message)); task.error(new Error(err.message));
}); });

View File

@ -19,12 +19,15 @@
<li class="wmd-button-group2 btn-group"></li> <li class="wmd-button-group2 btn-group"></li>
</ul> </ul>
<ul class="nav left-buttons"> <ul class="nav left-buttons">
<li class="wmd-button-group3 btn-group"> <li class="wmd-button-group3 btn-group"></li>
</ul>
<ul class="nav left-buttons">
<li class="wmd-button-group4 btn-group">
<a class="btn btn-success button-open-discussion"><i class="icon-comment-alt"></i></a> <a class="btn btn-success button-open-discussion"><i class="icon-comment-alt"></i></a>
</li> </li>
</ul> </ul>
<ul class="nav left-buttons"> <ul class="nav left-buttons">
<li class="wmd-button-group4 btn-group"></li> <li class="wmd-button-group5 btn-group"></li>
</ul> </ul>
<ul class="nav pull-right right-buttons"> <ul class="nav pull-right right-buttons">
<li class="offline-status hide"> <li class="offline-status hide">

View File

@ -9,12 +9,5 @@
<button class="btn btn-primary action-add-comment">Add</button> <button class="btn btn-primary action-add-comment">Add</button>
</div> </div>
</div> </div>
<div class="remove-discussion-confirm hide">
<blockquote>Remove this discussion, really?</blockquote>
<div class="form-group text-right">
<button class="btn btn-default action-remove-discussion-cancel">No</button>
<button class="btn btn-primary action-remove-discussion-confirm">Yes</button>
</div>
</div>
</div> </div>
<hr/> <hr/>

View File

@ -32,13 +32,21 @@ define([
return "sync." + PROVIDER_DROPBOX + "." + encodeURIComponent(path.toLowerCase()); return "sync." + PROVIDER_DROPBOX + "." + encodeURIComponent(path.toLowerCase());
} }
function createSyncAttributes(path, versionTag, content) { var merge = settings.conflictMode == 'merge';
function createSyncAttributes(path, versionTag, content, discussionListJSON) {
discussionListJSON = discussionListJSON || '{}';
var syncAttributes = {}; var syncAttributes = {};
syncAttributes.provider = dropboxProvider; syncAttributes.provider = dropboxProvider;
syncAttributes.path = path; syncAttributes.path = path;
syncAttributes.version = versionTag; syncAttributes.version = versionTag;
syncAttributes.contentCRC = utils.crc32(content); syncAttributes.contentCRC = utils.crc32(content);
syncAttributes.discussionListCRC = utils.crc32(discussionListJSON);
syncAttributes.syncIndex = createSyncIndex(path); syncAttributes.syncIndex = createSyncIndex(path);
if(merge === true) {
// Need to store the whole content for merge
syncAttributes.content = content;
syncAttributes.discussionList = discussionListJSON;
}
return syncAttributes; return syncAttributes;
} }
@ -53,11 +61,11 @@ define([
} }
var fileDescList = []; var fileDescList = [];
_.each(result, function(file) { _.each(result, function(file) {
var syncAttributes = createSyncAttributes(file.path, file.versionTag, file.content); var parsedContent = dropboxProvider.parseSerializedContent(file.content);
var syncAttributes = createSyncAttributes(file.path, file.versionTag, parsedContent.content, parsedContent.discussionListJSON);
var syncLocations = {}; var syncLocations = {};
syncLocations[syncAttributes.syncIndex] = syncAttributes; syncLocations[syncAttributes.syncIndex] = syncAttributes;
var parsedContent = dropboxProvider.parseSerializedContent(file.content); var fileDesc = fileMgr.createFile(file.name, parsedContent.content, parsedContent.discussionListJSON, syncLocations);
var fileDesc = fileMgr.createFile(file.name, parsedContent.content, parsedContent.discussionList, syncLocations);
fileMgr.selectFile(fileDesc); fileMgr.selectFile(fileDesc);
fileDescList.push(fileDesc); fileDescList.push(fileDesc);
}); });
@ -86,7 +94,7 @@ define([
}); });
}; };
dropboxProvider.exportFile = function(event, title, content, callback) { dropboxProvider.exportFile = function(event, title, content, discussionListJSON, callback) {
var path = utils.getInputTextValue("#input-sync-export-dropbox-path", event); var path = utils.getInputTextValue("#input-sync-export-dropbox-path", event);
path = checkPath(path); path = checkPath(path);
if(path === undefined) { if(path === undefined) {
@ -104,12 +112,11 @@ define([
if(error) { if(error) {
return callback(error); return callback(error);
} }
var syncAttributes = createSyncAttributes(result.path, result.versionTag, content); var syncAttributes = createSyncAttributes(result.path, result.versionTag, content, discussionListJSON);
callback(undefined, syncAttributes); callback(undefined, syncAttributes);
}); });
}; };
var merge = settings.conflictMode == 'merge';
dropboxProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, syncAttributes, callback) { dropboxProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, syncAttributes, callback) {
if( if(
(syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed (syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed
@ -188,8 +195,9 @@ define([
var file = change.stat; var file = change.stat;
var parsedContent = dropboxProvider.parseSerializedContent(file.content); var parsedContent = dropboxProvider.parseSerializedContent(file.content);
var remoteContent = parsedContent.content; var remoteContent = parsedContent.content;
var remoteDiscussionList = parsedContent.discussionList; var remoteDiscussionListJSON = parsedContent.discussionListJSON;
var remoteCRC = dropboxProvider.syncMerge(fileDesc, syncAttributes, remoteContent, fileDesc.title, remoteDiscussionList); var remoteDiscussionList = JSON.parse(remoteDiscussionListJSON);
var remoteCRC = dropboxProvider.syncMerge(fileDesc, syncAttributes, remoteContent, fileDesc.title, remoteDiscussionList, remoteDiscussionListJSON);
// Update syncAttributes // Update syncAttributes
syncAttributes.version = file.versionTag; syncAttributes.version = file.versionTag;
if(merge === true) { if(merge === true) {

View File

@ -30,14 +30,23 @@ define([
return "sync." + providerId + "." + id; return "sync." + providerId + "." + id;
} }
function createSyncAttributes(id, etag, content, title) { var merge = settings.conflictMode == 'merge';
function createSyncAttributes(id, etag, content, title, discussionListJSON) {
discussionListJSON = discussionListJSON || '{}';
var syncAttributes = {}; var syncAttributes = {};
syncAttributes.provider = gdriveProvider; syncAttributes.provider = gdriveProvider;
syncAttributes.id = id; syncAttributes.id = id;
syncAttributes.etag = etag; syncAttributes.etag = etag;
syncAttributes.contentCRC = utils.crc32(content); syncAttributes.contentCRC = utils.crc32(content);
syncAttributes.titleCRC = utils.crc32(title); syncAttributes.titleCRC = utils.crc32(title);
syncAttributes.discussionListCRC = utils.crc32(discussionListJSON);
syncAttributes.syncIndex = createSyncIndex(id); syncAttributes.syncIndex = createSyncIndex(id);
if(merge === true) {
// Need to store the whole content for merge
syncAttributes.content = content;
syncAttributes.title = title;
syncAttributes.discussionList = discussionListJSON;
}
return syncAttributes; return syncAttributes;
} }
@ -53,12 +62,12 @@ define([
var fileDescList = []; var fileDescList = [];
var fileDesc; var fileDesc;
_.each(result, function(file) { _.each(result, function(file) {
var syncAttributes = createSyncAttributes(file.id, file.etag, file.content, file.title); var parsedContent = gdriveProvider.parseSerializedContent(file.content);
var syncAttributes = createSyncAttributes(file.id, file.etag, parsedContent.content, file.title, parsedContent.discussionListJSON);
syncAttributes.isRealtime = file.isRealtime; syncAttributes.isRealtime = file.isRealtime;
var syncLocations = {}; var syncLocations = {};
syncLocations[syncAttributes.syncIndex] = syncAttributes; syncLocations[syncAttributes.syncIndex] = syncAttributes;
var parsedContent = gdriveProvider.parseSerializedContent(file.content); fileDesc = fileMgr.createFile(file.title, parsedContent.content, parsedContent.discussionListJSON, syncLocations);
fileDesc = fileMgr.createFile(file.title, parsedContent.content, parsedContent.discussionList, syncLocations);
fileDescList.push(fileDesc); fileDescList.push(fileDesc);
}); });
if(fileDesc !== undefined) { if(fileDesc !== undefined) {
@ -88,7 +97,7 @@ define([
}, 'doc', accountId); }, 'doc', accountId);
}; };
gdriveProvider.exportFile = function(event, title, content, callback) { gdriveProvider.exportFile = function(event, title, content, discussionListJSON, callback) {
var fileId = utils.getInputTextValue('#input-sync-export-' + providerId + '-fileid'); var fileId = utils.getInputTextValue('#input-sync-export-' + providerId + '-fileid');
if(fileId) { if(fileId) {
// Check that file is not synchronized with another an existing // Check that file is not synchronized with another an existing
@ -107,24 +116,23 @@ define([
callback(error); callback(error);
return; return;
} }
var syncAttributes = createSyncAttributes(result.id, result.etag, content, title); var syncAttributes = createSyncAttributes(result.id, result.etag, content, title, discussionListJSON);
callback(undefined, syncAttributes); callback(undefined, syncAttributes);
}); });
}; };
gdriveProvider.exportRealtimeFile = function(event, title, content, callback) { gdriveProvider.exportRealtimeFile = function(event, title, content, discussionListJSON, callback) {
var parentId = utils.getInputTextValue('#input-sync-export-' + providerId + '-parentid'); var parentId = utils.getInputTextValue('#input-sync-export-' + providerId + '-parentid');
googleHelper.createRealtimeFile(parentId, title, accountId, function(error, result) { googleHelper.createRealtimeFile(parentId, title, accountId, function(error, result) {
if(error) { if(error) {
callback(error); callback(error);
return; return;
} }
var syncAttributes = createSyncAttributes(result.id, result.etag, content, title); var syncAttributes = createSyncAttributes(result.id, result.etag, content, title, discussionListJSON);
callback(undefined, syncAttributes); callback(undefined, syncAttributes);
}); });
}; };
var merge = settings.conflictMode == 'merge';
gdriveProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, syncAttributes, callback) { gdriveProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, syncAttributes, callback) {
if( if(
(syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed (syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed
@ -228,8 +236,9 @@ define([
var parsedContent = gdriveProvider.parseSerializedContent(file.content); var parsedContent = gdriveProvider.parseSerializedContent(file.content);
var remoteContent = parsedContent.content; var remoteContent = parsedContent.content;
var remoteTitle = file.title; var remoteTitle = file.title;
var remoteDiscussionList = parsedContent.discussionList; var remoteDiscussionListJSON = parsedContent.discussionListJSON;
var remoteCRC = gdriveProvider.syncMerge(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionList); var remoteDiscussionList = JSON.parse(remoteDiscussionListJSON);
var remoteCRC = gdriveProvider.syncMerge(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionList, remoteDiscussionListJSON);
// Update syncAttributes // Update syncAttributes
syncAttributes.etag = file.etag; syncAttributes.etag = file.etag;
@ -237,7 +246,7 @@ define([
// Need to store the whole content for merge // Need to store the whole content for merge
syncAttributes.content = remoteContent; syncAttributes.content = remoteContent;
syncAttributes.title = remoteTitle; syncAttributes.title = remoteTitle;
syncAttributes.discussionList = remoteDiscussionList; syncAttributes.discussionList = remoteDiscussionListJSON;
} }
syncAttributes.contentCRC = remoteCRC.contentCRC; syncAttributes.contentCRC = remoteCRC.contentCRC;
syncAttributes.titleCRC = remoteCRC.titleCRC; syncAttributes.titleCRC = remoteCRC.titleCRC;
@ -286,9 +295,12 @@ define([
var realtimeContext; var realtimeContext;
function toRealtimeDiscussion(context, discussion) { function toRealtimeDiscussion(context, discussion) {
var commentList = context.model.createList(); var realtimeCommentList = context.model.createList();
realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_ADDED, modelEventListener);
realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_REMOVED, modelEventListener);
realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_SET, modelEventListener);
discussion.commentList && discussion.commentList.forEach(function(comment) { discussion.commentList && discussion.commentList.forEach(function(comment) {
commentList.push({ realtimeCommentList.push({
author: comment.author, author: comment.author,
content: comment.content content: comment.content
}); });
@ -298,11 +310,20 @@ define([
selectionStart: discussion.selectionStart, selectionStart: discussion.selectionStart,
selectionEnd: discussion.selectionEnd, selectionEnd: discussion.selectionEnd,
type: discussion.type, type: discussion.type,
commentList: commentList commentList: realtimeCommentList
}); });
return realtimeDiscussion; return realtimeDiscussion;
} }
function toRealtimeDiscussionList(context) {
var realtimeDiscussionList = context.model.createMap();
_.each(context.fileDesc.discussionList, function(localDiscussion) {
var realtimeDiscussion = toRealtimeDiscussion(context, localDiscussion);
realtimeDiscussionList.set(localDiscussion.discussionIndex, realtimeDiscussion);
});
return realtimeDiscussionList;
}
function fromRealtimeDiscussion(realtimeDiscussion) { function fromRealtimeDiscussion(realtimeDiscussion) {
var discussion = { var discussion = {
discussionIndex: realtimeDiscussion.get('discussionIndex'), discussionIndex: realtimeDiscussion.get('discussionIndex'),
@ -311,13 +332,25 @@ define([
}; };
var type = realtimeDiscussion.get('type'); var type = realtimeDiscussion.get('type');
type && (discussion.type = type); type && (discussion.type = type);
var commentList = realtimeDiscussion.get('discussionIndex').asArray(); var commentList = realtimeDiscussion.get('commentList').asArray();
commentList.length && (discussion.commentList = commentList); commentList.length && (discussion.commentList = commentList);
return discussion; return discussion;
} }
function mergeDiscussion(localDiscussion, realtimeDiscussion, takeServer) { function fromRealtimeDiscussionList(realtimeDiscussionList) {
if(!takeServer) { var localDiscussionList = {};
realtimeDiscussionList.keys().forEach(function(discussionIndex) {
var realtimeDiscussion = realtimeDiscussionList.get(discussionIndex);
var discussion = fromRealtimeDiscussion(realtimeDiscussion);
localDiscussionList[discussionIndex] = discussion;
});
return localDiscussionList;
}
function mergeDiscussion(localDiscussion, realtimeDiscussion, isServerChange) {
var commentsChanged = false;
// We only pay attention to local selection modifications
if(!isServerChange) {
realtimeDiscussion.set('selectionStart', localDiscussion.selectionStart); realtimeDiscussion.set('selectionStart', localDiscussion.selectionStart);
realtimeDiscussion.set('selectionEnd', localDiscussion.selectionEnd); realtimeDiscussion.set('selectionEnd', localDiscussion.selectionEnd);
} }
@ -329,62 +362,72 @@ define([
}); });
} }
var realtimeCommentList = realtimeDiscussion.get('commentList'); var realtimeCommentList = realtimeDiscussion.get('commentList');
var localCommentList = localDiscussion.commentList;
function checkLocalComment(comment, index) { function checkLocalComment(comment, index) {
if(!isInDiscussion(comment, realtimeCommentList.asArray())) { if(!isInDiscussion(comment, realtimeCommentList.asArray())) {
if(takeServer) { if(isServerChange) {
localDiscussion.splice(index, 1); localCommentList.splice(index, 1);
commentsChanged = true;
return true; return true;
} }
else { else {
realtimeDiscussion.get('commentList').push(comment); realtimeCommentList.push(comment);
} }
} }
} }
while(localDiscussion.commentList.some(checkLocalComment)) {} while(localCommentList.some(checkLocalComment)) {}
function checkRealtimeComment(comment, index) { function checkRealtimeComment(comment, index) {
if(!isInDiscussion(comment, localDiscussion.commentList)) { if(!isInDiscussion(comment, localCommentList)) {
if(!takeServer) { if(!isServerChange) {
realtimeCommentList.remove(index); realtimeCommentList.remove(index);
return true; return true;
} }
else { else {
localDiscussion.commentList.push(comment); localCommentList.push(comment);
commentsChanged = true;
} }
} }
} }
while(realtimeCommentList.asArray().some(checkRealtimeComment)) {} while(realtimeCommentList.asArray().some(checkRealtimeComment)) {}
return commentsChanged;
} }
function mergeDiscussionList(context, takeServer) { function mergeDiscussionList(context, isServerChange) {
var commentsChanged = false;
var localDiscussionList = context.fileDesc.discussionList; var localDiscussionList = context.fileDesc.discussionList;
_.each(localDiscussionList, function(localDiscussion) { _.values(localDiscussionList).forEach(function(localDiscussion) {
var realtimeDiscussion = context.discussionList.get(localDiscussion.discussionIndex); var realtimeDiscussion = context.realtimeDiscussionList.get(localDiscussion.discussionIndex);
if(realtimeDiscussion) { if(realtimeDiscussion) {
mergeDiscussion(localDiscussion, realtimeDiscussion, takeServer); commentsChanged |= mergeDiscussion(localDiscussion, realtimeDiscussion, isServerChange);
}
else if(!isServerChange) {
realtimeDiscussion = toRealtimeDiscussion(context, localDiscussion);
context.realtimeDiscussionList.set(localDiscussion.discussionIndex, realtimeDiscussion);
} }
else { else {
realtimeDiscussion = toRealtimeDiscussion(context, localDiscussion); delete localDiscussionList[localDiscussion.discussionIndex];
context.discussionList.set(localDiscussion.discussionIndex, realtimeDiscussion); eventMgr.onDiscussionRemoved(context.fileDesc, localDiscussion);
} }
}); });
context.discussionList.keys().forEach(function(discussionIndex) { context.realtimeDiscussionList.keys().forEach(function(discussionIndex) {
var realtimeDiscussion = context.discussionList.get(discussionIndex); var realtimeDiscussion = context.realtimeDiscussionList.get(discussionIndex);
var localDiscussion = localDiscussionList[discussionIndex]; var localDiscussion = localDiscussionList[discussionIndex];
if(localDiscussion) { if(localDiscussion) {
mergeDiscussion(localDiscussion, realtimeDiscussion, takeServer); commentsChanged |= mergeDiscussion(localDiscussion, realtimeDiscussion, isServerChange);
}
else if(isServerChange) {
var discussion = fromRealtimeDiscussion(realtimeDiscussion);
localDiscussionList[discussionIndex] = discussion;
eventMgr.onDiscussionCreated(context.fileDesc, discussion);
} }
else { else {
var discussion = { context.realtimeDiscussionList.delete(discussionIndex);
discussionIndex: discussionIndex,
selectionStart: realtimeDiscussion.get('selectionStart'),
selectionEnd: realtimeDiscussion.get('selectionEnd'),
commentList: realtimeDiscussion.get('commentList').asArray()
};
localDiscussionList[discussionIndex] = discussion;
eventMgr.onCommentsChanged(context.fileDesc);
} }
}); });
context.fileDesc.discussionList = localDiscussionList; // Write in localStorage context.fileDesc.discussionList = localDiscussionList; // Write in localStorage
if(commentsChanged) {
eventMgr.onCommentsChanged(context.fileDesc);
}
} }
function updateCRCs() { function updateCRCs() {
@ -392,27 +435,84 @@ define([
if(!context) { if(!context) {
return; return;
} }
context.syncAttributes.contentCRC = utils.crc32(context.string.getText()); var syncAttributes = context.syncAttributes;
var content = context.realtimeString.getText();
syncAttributes.contentCRC = utils.crc32(content);
var discussionList = {}; var discussionList = {};
context.discussionList.keys().forEach(function(discussionIndex) { context.realtimeDiscussionList.keys().forEach(function(discussionIndex) {
var discussion = fromRealtimeDiscussion(context.discussionList.get(discussionIndex)); var discussion = fromRealtimeDiscussion(context.realtimeDiscussionList.get(discussionIndex));
discussionList[discussion.discussionIndex] = discussion; discussionList[discussion.discussionIndex] = discussion;
}); });
context.syncAttributes.discussionListCRC = utils.crc32(JSON.stringify(discussionList)); var discussionListJSON = JSON.stringify(discussionList);
syncAttributes.discussionListCRC = utils.crc32(discussionListJSON);
if(merge === true) {
// Need to store the whole content for merge
syncAttributes.content = content;
syncAttributes.discussionList = discussionListJSON;
}
utils.storeAttributes(context.syncAttributes); utils.storeAttributes(context.syncAttributes);
} }
var onChanges = _.debounce(function() {
var context = realtimeContext; var onChange = (function() {
if(!context) { var debouncedOnChange = _.debounce(function() {
var context = realtimeContext;
if(!context) {
return;
}
if(context.isServerChange) {
logger.log('Realtime syncing remote changes');
}
else {
// Model is supposed to be updated on local modifications
context.model.beginCompoundOperation();
logger.log('Realtime syncing local changes');
}
// Check content modifications
var localContent = context.fileDesc.content;
var remoteContent = context.realtimeString.getText();
var contentChanged = localContent != remoteContent;
if(contentChanged) {
if(context.isServerChange) {
editor.setValue(remoteContent);
}
else {
context.realtimeString.setText(localContent);
}
}
// Check discussion modifications
mergeDiscussionList(context, context.isServerChange);
// For local changes, CRCs are updated on "save success" event
if(context.isServerChange) {
updateCRCs();
}
else {
context.model.endCompoundOperation();
}
context.isServerChange = false;
}, 0);
return function(fileDesc) {
if(realtimeContext && realtimeContext.fileDesc === fileDesc) {
debouncedOnChange();
}
};
})();
function modelEventListener(evt) {
if(!realtimeContext) {
return; return;
} }
context.isServerChange = false; if(evt.isLocal === false) {
}, 0); realtimeContext.isServerChange = true;
}
eventMgr.addListener('onContentChanged', onChanges); onChange(realtimeContext.fileDesc);
eventMgr.addListener('onDiscussionCreated', onChanges); }
eventMgr.addListener('onDiscussionRemoved', onChanges); eventMgr.addListener('onContentChanged', onChange);
eventMgr.addListener('onCommentsChanged', onChanges); eventMgr.addListener('onDiscussionCreated', onChange);
eventMgr.addListener('onDiscussionRemoved', onChange);
eventMgr.addListener('onCommentsChanged', onChange);
// Start realtime synchronization // Start realtime synchronization
gdriveProvider.startRealtimeSync = function(fileDesc, syncAttributes) { gdriveProvider.startRealtimeSync = function(fileDesc, syncAttributes) {
@ -421,13 +521,12 @@ define([
syncAttributes: syncAttributes syncAttributes: syncAttributes
}; };
realtimeContext = context; realtimeContext = context;
googleHelper.loadRealtime(syncAttributes.id, fileDesc.content, accountId, function(err, doc) { googleHelper.loadRealtime(syncAttributes.id, accountId, function(err, doc) {
if(err || !doc) { if(err || !doc) {
return; return;
} }
// If user just switched to another document or file has just been // If user just switched to another document or file has just been reselected
// reselected
if(context !== realtimeContext) { if(context !== realtimeContext) {
return doc.close(); return doc.close();
} }
@ -436,94 +535,51 @@ define([
context.document = doc; context.document = doc;
var model = doc.getModel(); var model = doc.getModel();
context.model = model; context.model = model;
// Get or create content string
var realtimeString = model.getRoot().get('content'); var realtimeString = model.getRoot().get('content');
context.string = realtimeString; if(!realtimeString) {
// Initial value
// Saves model content checksum realtimeString = model.createString(fileDesc.content);
function updateContentState() { model.getRoot().set('content', realtimeString);
syncAttributes.contentCRC = utils.crc32(realtimeString.getText());
utils.storeAttributes(syncAttributes);
}
var debouncedRefreshPreview = _.debounce(pagedownEditor.refreshPreview, 100);
// Listen to insert text events
realtimeString.addEventListener(gapi.drive.realtime.EventType.TEXT_INSERTED, function(evt) {
if(aceEditor !== undefined && (isAceUpToDate === false || e.isLocal === false)) {
// Update ACE editor
var position = aceEditor.session.doc.indexToPosition(e.index);
aceEditor.session.insert(position, e.text);
isAceUpToDate = true;
}
// If modifications come down from a collaborator
if(e.isLocal === false) {
logger.log("Google Drive realtime document updated from server");
updateContentState();
aceEditor === undefined && debouncedRefreshPreview();
}
});
// Listen to delete text events
realtimeString.addEventListener(gapi.drive.realtime.EventType.TEXT_DELETED, function(e) {
if(aceEditor !== undefined && (isAceUpToDate === false || e.isLocal === false)) {
// Update ACE editor
var range = (function(posStart, posEnd) {
return new Range(posStart.row, posStart.column, posEnd.row, posEnd.column);
})(aceEditor.session.doc.indexToPosition(e.index), aceEditor.session.doc.indexToPosition(e.index + e.text.length));
aceEditor.session.remove(range);
isAceUpToDate = true;
}
// If modifications come down from a collaborator
if(e.isLocal === false) {
logger.log("Google Drive realtime document updated from server");
updateContentState();
aceEditor === undefined && debouncedRefreshPreview();
}
});
doc.addEventListener(gapi.drive.realtime.EventType.DOCUMENT_SAVE_STATE_CHANGED, function(e) {
// Save success event
if(e.isPending === false && e.isSaving === false) {
logger.log("Google Drive realtime document successfully saved on server");
updateContentState();
}
});
// Try to merge offline modifications
var localContent = fileDesc.content;
var localContentChanged = syncAttributes.contentCRC != utils.crc32(localContent);
var remoteContent = realtimeString.getText();
var remoteContentCRC = utils.crc32(remoteContent);
var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC;
var fileContentChanged = localContent != remoteContent;
model.beginCompoundOperation('Open and merge');
if(fileContentChanged === true && localContentChanged === true) {
if(remoteContentChanged === true) {
// Conflict detected
fileMgr.createFile(fileDesc.title + " (backup)", localContent);
eventMgr.onMessage('Conflict detected on "' + fileDesc.title + '". A backup has been created locally.');
}
else {
// Add local modifications if no collaborators change
realtimeString.setText(localContent);
}
}
// Update content state according to collaborators changes
if(remoteContentChanged === true) {
logger.log("Google Drive realtime document updated from server");
aceEditor !== undefined && aceEditor.setValue(remoteContent, -1);
updateContentState();
aceEditor === undefined && debouncedRefreshPreview();
} }
context.realtimeString = realtimeString;
// Listen to content modifications
realtimeString.addEventListener(gapi.drive.realtime.EventType.TEXT_INSERTED, modelEventListener);
realtimeString.addEventListener(gapi.drive.realtime.EventType.TEXT_DELETED, modelEventListener);
// Get or create discussion map
var realtimeDiscussionList = model.getRoot().get('discussionList'); var realtimeDiscussionList = model.getRoot().get('discussionList');
if(!realtimeDiscussionList) { if(!realtimeDiscussionList) {
realtimeDiscussionList = model.createMap(); // Initial value
realtimeDiscussionList = toRealtimeDiscussionList(context);
model.getRoot().set('discussionList', realtimeDiscussionList); model.getRoot().set('discussionList', realtimeDiscussionList);
} }
context.discussionList = realtimeDiscussionList; context.realtimeDiscussionList = realtimeDiscussionList;
mergeDiscussionList(context, remoteContentChanged === true); // Listen to discussion modifications
model.endCompoundOperation(); realtimeDiscussionList.addEventListener(gapi.drive.realtime.EventType.VALUE_CHANGED, modelEventListener);
realtimeDiscussionList.keys().forEach(function(discussionIndex) {
var realtimeDiscussion = context.realtimeDiscussionList.get(discussionIndex);
var realtimeCommentList = realtimeDiscussion.get('commentList');
// Listen to comment modifications in every discussion
realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_ADDED, modelEventListener);
realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_REMOVED, modelEventListener);
realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_SET, modelEventListener);
});
// Also listen to "save success" event
doc.addEventListener(gapi.drive.realtime.EventType.DOCUMENT_SAVE_STATE_CHANGED, function(e) {
if(e.isPending === false && e.isSaving === false) {
updateCRCs();
}
});
// Merge offline modifications
var remoteContent = realtimeString.getText();
var remoteTitle = fileDesc.title; // Not synchronized, so make sure no changes will be detected
var remoteDiscussionList = fromRealtimeDiscussionList(realtimeDiscussionList);
var remoteDiscussionListJSON = JSON.stringify(remoteDiscussionList);
gdriveProvider.syncMerge(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionList, remoteDiscussionListJSON);
// Save undo/redo buttons default actions // Save undo/redo buttons default actions
undoExecute = pagedownEditor.uiManager.buttons.undo.execute; undoExecute = pagedownEditor.uiManager.buttons.undo.execute;
@ -533,28 +589,20 @@ define([
// Set temporary actions for undo/redo buttons // Set temporary actions for undo/redo buttons
pagedownEditor.uiManager.buttons.undo.execute = function() { pagedownEditor.uiManager.buttons.undo.execute = function() {
if(model.canUndo) { if(model.canUndo) {
// This flag is used to avoid replaying editor's own
// modifications (assuming it's synchronous)
isAceUpToDate = false;
model.undo(); model.undo();
} }
}; };
pagedownEditor.uiManager.buttons.redo.execute = function() { pagedownEditor.uiManager.buttons.redo.execute = function() {
if(model.canRedo) { if(model.canRedo) {
// This flag is used to avoid replaying editor's own
// modifications (assuming it's synchronous)
isAceUpToDate = false;
model.redo(); model.redo();
} }
}; };
// Add event handler for model's UndoRedoStateChanged events // Add event handler for model's UndoRedoStateChanged events
pagedownEditor.uiManager.setUndoRedoButtonStates = function() { pagedownEditor.uiManager.setUndoRedoButtonStates = _.debounce(function() {
setTimeout(function() { pagedownEditor.uiManager.setButtonState(pagedownEditor.uiManager.buttons.undo, model.canUndo);
pagedownEditor.uiManager.setButtonState(pagedownEditor.uiManager.buttons.undo, model.canUndo); pagedownEditor.uiManager.setButtonState(pagedownEditor.uiManager.buttons.redo, model.canRedo);
pagedownEditor.uiManager.setButtonState(pagedownEditor.uiManager.buttons.redo, model.canRedo); }, 10);
}, 50);
};
pagedownEditor.uiManager.setUndoRedoButtonStates(); pagedownEditor.uiManager.setUndoRedoButtonStates();
model.addEventListener(gapi.drive.realtime.EventType.UNDO_REDO_STATE_CHANGED, function() { model.addEventListener(gapi.drive.realtime.EventType.UNDO_REDO_STATE_CHANGED, function() {
pagedownEditor.uiManager.setUndoRedoButtonStates(); pagedownEditor.uiManager.setUndoRedoButtonStates();
@ -611,14 +659,14 @@ define([
}; };
// Perform AutoSync // Perform AutoSync
gdriveProvider.autosyncFile = function(title, content, config, callback) { gdriveProvider.autosyncFile = function(title, content, discussionListJSON, config, callback) {
var parentId = config.parentId; var parentId = config.parentId;
googleHelper.upload(undefined, parentId, title, content, undefined, undefined, accountId, function(error, result) { googleHelper.upload(undefined, parentId, title, content, undefined, undefined, accountId, function(error, result) {
if(error) { if(error) {
callback(error); callback(error);
return; return;
} }
var syncAttributes = createSyncAttributes(result.id, result.etag, content, title); var syncAttributes = createSyncAttributes(result.id, result.etag, content, title, discussionListJSON);
callback(undefined, syncAttributes); callback(undefined, syncAttributes);
}); });
}; };

View File

@ -129,7 +129,7 @@
@alert-border-radius: 0; @alert-border-radius: 0;
@label-warning-bg: spin(darken(@logo-yellow, 4%), -6); @label-warning-bg: spin(darken(@logo-yellow, 4%), -6);
@label-danger-bg: spin(darken(@logo-orange, 4%), -4); @label-danger-bg: spin(darken(@logo-orange, 4%), -4);
@state-warning-text: spin(darken(@logo-yellow, 15%), -6); @state-warning-text: spin(darken(@logo-yellow, 14%), -6);
@state-warning-bg: fade(spin(@logo-yellow, -6), 12%); @state-warning-bg: fade(spin(@logo-yellow, -6), 12%);
@state-warning-border: fade(spin(@logo-yellow, -6), 24%); @state-warning-border: fade(spin(@logo-yellow, -6), 24%);
@state-danger-text: spin(darken(@logo-orange, 18%), -4); @state-danger-text: spin(darken(@logo-orange, 18%), -4);

View File

@ -310,7 +310,7 @@ define([
eventMgr.addListener("onFileCreated", function(fileDesc) { eventMgr.addListener("onFileCreated", function(fileDesc) {
if(_.size(fileDesc.syncLocations) === 0) { if(_.size(fileDesc.syncLocations) === 0) {
_.each(providerMap, function(provider) { _.each(providerMap, function(provider) {
provider.autosyncConfig.enabled && provider.autosyncFile(fileDesc.title, fileDesc.content, provider.autosyncConfig, function(error, syncAttributes) { provider.autosyncConfig.enabled && provider.autosyncFile(fileDesc.title, fileDesc.content, fileDesc.discussionListJSON, provider.autosyncConfig, function(error, syncAttributes) {
if(error) { if(error) {
return; return;
} }
@ -350,7 +350,7 @@ define([
return eventMgr.onError("Real time collaborative document can't be synchronized with multiple locations"); return eventMgr.onError("Real time collaborative document can't be synchronized with multiple locations");
} }
// Perform the provider's real time export // Perform the provider's real time export
provider.exportRealtimeFile(event, fileDesc.title, fileDesc.content, function(error, syncAttributes) { provider.exportRealtimeFile(event, fileDesc.title, fileDesc.content, fileDesc.discussionListJSON, function(error, syncAttributes) {
if(error) { if(error) {
return; return;
} }
@ -370,7 +370,7 @@ define([
return; return;
} }
// Perform the provider's standard export // Perform the provider's standard export
provider.exportFile(event, fileDesc.title, fileDesc.content, function(error, syncAttributes) { provider.exportFile(event, fileDesc.title, fileDesc.content, fileDesc.discussionListJSON, function(error, syncAttributes) {
if(error) { if(error) {
return; return;
} }