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) {
var discussionList = '{}';
var discussionListJSON = '{}';
var discussionExtractor = /<!--se_discussion_list:([\s\S]+)-->$/.exec(content);
if(discussionExtractor && this.parseDiscussionList(discussionExtractor[1])) {
content = content.substring(0, discussionExtractor.index);
discussionList = discussionExtractor[1];
discussionListJSON = discussionExtractor[1];
}
return {
content: content,
discussionList: discussionList
discussionListJSON: discussionListJSON
};
};
@ -75,7 +75,7 @@ define([
});
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) {
var result = [];
@ -160,7 +160,6 @@ define([
var localTitle = fileDesc.title;
var localDiscussionListJSON = fileDesc.discussionListJSON;
var localDiscussionList = fileDesc.discussionList;
var remoteDiscussionList = JSON.parse(remoteDiscussionListJSON);
// Local/Remote CRCs
var localContentCRC = utils.crc32(localContent);

View File

@ -359,7 +359,7 @@ define([
var $titleContainer;
var marginWidth = 18 * 2 + 25 + 25;
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 buttonsDropdownWidth = 40;
function adjustWindow() {
@ -451,13 +451,11 @@ define([
$("#wmd-code-button").append($('<i class="icon-code">')).appendTo($btnGroupElt);
$("#wmd-image-button").append($('<i class="icon-picture">')).appendTo($btnGroupElt);
$btnGroupElt = $('.wmd-button-group3');
var $openDiscussionElt = $btnGroupElt.find('.button-open-discussion');
$("#wmd-olist-button").append($('<i class="icon-list-numbered">')).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-hr-button").append($('<i class="icon-ellipsis">')).appendTo($btnGroupElt);
$openDiscussionElt.appendTo($btnGroupElt);
$btnGroupElt = $('.wmd-button-group4');
$btnGroupElt = $('.wmd-button-group5');
$("#wmd-undo-button").append($('<i class="icon-reply">')).appendTo($btnGroupElt);
$("#wmd-redo-button").append($('<i class="icon-forward">')).appendTo($btnGroupElt);
};

View File

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

View File

@ -397,20 +397,7 @@ define([
var $removeButton = $(popoverElt.querySelector('.action-remove-discussion'));
if(evt.target.discussionIndex) {
// 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() {
$(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();
var discussion = context.getDiscussion();
delete context.fileDesc.discussionList[discussion.discussionIndex];

View File

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

View File

@ -19,12 +19,15 @@
<li class="wmd-button-group2 btn-group"></li>
</ul>
<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>
</li>
</ul>
<ul class="nav left-buttons">
<li class="wmd-button-group4 btn-group"></li>
<li class="wmd-button-group5 btn-group"></li>
</ul>
<ul class="nav pull-right right-buttons">
<li class="offline-status hide">

View File

@ -9,12 +9,5 @@
<button class="btn btn-primary action-add-comment">Add</button>
</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>
<hr/>

View File

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

View File

@ -30,14 +30,23 @@ define([
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 = {};
syncAttributes.provider = gdriveProvider;
syncAttributes.id = id;
syncAttributes.etag = etag;
syncAttributes.contentCRC = utils.crc32(content);
syncAttributes.titleCRC = utils.crc32(title);
syncAttributes.discussionListCRC = utils.crc32(discussionListJSON);
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;
}
@ -53,12 +62,12 @@ define([
var fileDescList = [];
var fileDesc;
_.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;
var syncLocations = {};
syncLocations[syncAttributes.syncIndex] = syncAttributes;
var parsedContent = gdriveProvider.parseSerializedContent(file.content);
fileDesc = fileMgr.createFile(file.title, parsedContent.content, parsedContent.discussionList, syncLocations);
fileDesc = fileMgr.createFile(file.title, parsedContent.content, parsedContent.discussionListJSON, syncLocations);
fileDescList.push(fileDesc);
});
if(fileDesc !== undefined) {
@ -88,7 +97,7 @@ define([
}, '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');
if(fileId) {
// Check that file is not synchronized with another an existing
@ -107,24 +116,23 @@ define([
callback(error);
return;
}
var syncAttributes = createSyncAttributes(result.id, result.etag, content, title);
var syncAttributes = createSyncAttributes(result.id, result.etag, content, title, discussionListJSON);
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');
googleHelper.createRealtimeFile(parentId, title, accountId, function(error, result) {
if(error) {
callback(error);
return;
}
var syncAttributes = createSyncAttributes(result.id, result.etag, content, title);
var syncAttributes = createSyncAttributes(result.id, result.etag, content, title, discussionListJSON);
callback(undefined, syncAttributes);
});
};
var merge = settings.conflictMode == 'merge';
gdriveProvider.syncUp = function(content, contentCRC, title, titleCRC, discussionList, discussionListCRC, syncAttributes, callback) {
if(
(syncAttributes.contentCRC == contentCRC) && // Content CRC hasn't changed
@ -228,8 +236,9 @@ define([
var parsedContent = gdriveProvider.parseSerializedContent(file.content);
var remoteContent = parsedContent.content;
var remoteTitle = file.title;
var remoteDiscussionList = parsedContent.discussionList;
var remoteCRC = gdriveProvider.syncMerge(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionList);
var remoteDiscussionListJSON = parsedContent.discussionListJSON;
var remoteDiscussionList = JSON.parse(remoteDiscussionListJSON);
var remoteCRC = gdriveProvider.syncMerge(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionList, remoteDiscussionListJSON);
// Update syncAttributes
syncAttributes.etag = file.etag;
@ -237,7 +246,7 @@ define([
// Need to store the whole content for merge
syncAttributes.content = remoteContent;
syncAttributes.title = remoteTitle;
syncAttributes.discussionList = remoteDiscussionList;
syncAttributes.discussionList = remoteDiscussionListJSON;
}
syncAttributes.contentCRC = remoteCRC.contentCRC;
syncAttributes.titleCRC = remoteCRC.titleCRC;
@ -286,9 +295,12 @@ define([
var realtimeContext;
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) {
commentList.push({
realtimeCommentList.push({
author: comment.author,
content: comment.content
});
@ -298,11 +310,20 @@ define([
selectionStart: discussion.selectionStart,
selectionEnd: discussion.selectionEnd,
type: discussion.type,
commentList: commentList
commentList: realtimeCommentList
});
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) {
var discussion = {
discussionIndex: realtimeDiscussion.get('discussionIndex'),
@ -311,13 +332,25 @@ define([
};
var type = realtimeDiscussion.get('type');
type && (discussion.type = type);
var commentList = realtimeDiscussion.get('discussionIndex').asArray();
var commentList = realtimeDiscussion.get('commentList').asArray();
commentList.length && (discussion.commentList = commentList);
return discussion;
}
function mergeDiscussion(localDiscussion, realtimeDiscussion, takeServer) {
if(!takeServer) {
function fromRealtimeDiscussionList(realtimeDiscussionList) {
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('selectionEnd', localDiscussion.selectionEnd);
}
@ -329,62 +362,72 @@ define([
});
}
var realtimeCommentList = realtimeDiscussion.get('commentList');
var localCommentList = localDiscussion.commentList;
function checkLocalComment(comment, index) {
if(!isInDiscussion(comment, realtimeCommentList.asArray())) {
if(takeServer) {
localDiscussion.splice(index, 1);
if(isServerChange) {
localCommentList.splice(index, 1);
commentsChanged = true;
return true;
}
else {
realtimeDiscussion.get('commentList').push(comment);
realtimeCommentList.push(comment);
}
}
}
while(localDiscussion.commentList.some(checkLocalComment)) {}
while(localCommentList.some(checkLocalComment)) {}
function checkRealtimeComment(comment, index) {
if(!isInDiscussion(comment, localDiscussion.commentList)) {
if(!takeServer) {
if(!isInDiscussion(comment, localCommentList)) {
if(!isServerChange) {
realtimeCommentList.remove(index);
return true;
}
else {
localDiscussion.commentList.push(comment);
localCommentList.push(comment);
commentsChanged = true;
}
}
}
while(realtimeCommentList.asArray().some(checkRealtimeComment)) {}
return commentsChanged;
}
function mergeDiscussionList(context, takeServer) {
function mergeDiscussionList(context, isServerChange) {
var commentsChanged = false;
var localDiscussionList = context.fileDesc.discussionList;
_.each(localDiscussionList, function(localDiscussion) {
var realtimeDiscussion = context.discussionList.get(localDiscussion.discussionIndex);
_.values(localDiscussionList).forEach(function(localDiscussion) {
var realtimeDiscussion = context.realtimeDiscussionList.get(localDiscussion.discussionIndex);
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 {
realtimeDiscussion = toRealtimeDiscussion(context, localDiscussion);
context.discussionList.set(localDiscussion.discussionIndex, realtimeDiscussion);
delete localDiscussionList[localDiscussion.discussionIndex];
eventMgr.onDiscussionRemoved(context.fileDesc, localDiscussion);
}
});
context.discussionList.keys().forEach(function(discussionIndex) {
var realtimeDiscussion = context.discussionList.get(discussionIndex);
context.realtimeDiscussionList.keys().forEach(function(discussionIndex) {
var realtimeDiscussion = context.realtimeDiscussionList.get(discussionIndex);
var localDiscussion = localDiscussionList[discussionIndex];
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 {
var discussion = {
discussionIndex: discussionIndex,
selectionStart: realtimeDiscussion.get('selectionStart'),
selectionEnd: realtimeDiscussion.get('selectionEnd'),
commentList: realtimeDiscussion.get('commentList').asArray()
};
localDiscussionList[discussionIndex] = discussion;
eventMgr.onCommentsChanged(context.fileDesc);
context.realtimeDiscussionList.delete(discussionIndex);
}
});
context.fileDesc.discussionList = localDiscussionList; // Write in localStorage
if(commentsChanged) {
eventMgr.onCommentsChanged(context.fileDesc);
}
}
function updateCRCs() {
@ -392,27 +435,84 @@ define([
if(!context) {
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 = {};
context.discussionList.keys().forEach(function(discussionIndex) {
var discussion = fromRealtimeDiscussion(context.discussionList.get(discussionIndex));
context.realtimeDiscussionList.keys().forEach(function(discussionIndex) {
var discussion = fromRealtimeDiscussion(context.realtimeDiscussionList.get(discussionIndex));
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);
}
var onChanges = _.debounce(function() {
var onChange = (function() {
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);
eventMgr.addListener('onContentChanged', onChanges);
eventMgr.addListener('onDiscussionCreated', onChanges);
eventMgr.addListener('onDiscussionRemoved', onChanges);
eventMgr.addListener('onCommentsChanged', onChanges);
return function(fileDesc) {
if(realtimeContext && realtimeContext.fileDesc === fileDesc) {
debouncedOnChange();
}
};
})();
function modelEventListener(evt) {
if(!realtimeContext) {
return;
}
if(evt.isLocal === false) {
realtimeContext.isServerChange = true;
}
onChange(realtimeContext.fileDesc);
}
eventMgr.addListener('onContentChanged', onChange);
eventMgr.addListener('onDiscussionCreated', onChange);
eventMgr.addListener('onDiscussionRemoved', onChange);
eventMgr.addListener('onCommentsChanged', onChange);
// Start realtime synchronization
gdriveProvider.startRealtimeSync = function(fileDesc, syncAttributes) {
@ -421,13 +521,12 @@ define([
syncAttributes: syncAttributes
};
realtimeContext = context;
googleHelper.loadRealtime(syncAttributes.id, fileDesc.content, accountId, function(err, doc) {
googleHelper.loadRealtime(syncAttributes.id, accountId, function(err, doc) {
if(err || !doc) {
return;
}
// If user just switched to another document or file has just been
// reselected
// If user just switched to another document or file has just been reselected
if(context !== realtimeContext) {
return doc.close();
}
@ -436,94 +535,51 @@ define([
context.document = doc;
var model = doc.getModel();
context.model = model;
// Get or create content string
var realtimeString = model.getRoot().get('content');
context.string = realtimeString;
// Saves model content checksum
function updateContentState() {
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();
}
if(!realtimeString) {
// Initial value
realtimeString = model.createString(fileDesc.content);
model.getRoot().set('content', realtimeString);
}
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');
if(!realtimeDiscussionList) {
realtimeDiscussionList = model.createMap();
// Initial value
realtimeDiscussionList = toRealtimeDiscussionList(context);
model.getRoot().set('discussionList', realtimeDiscussionList);
}
context.discussionList = realtimeDiscussionList;
mergeDiscussionList(context, remoteContentChanged === true);
model.endCompoundOperation();
context.realtimeDiscussionList = realtimeDiscussionList;
// Listen to discussion modifications
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
undoExecute = pagedownEditor.uiManager.buttons.undo.execute;
@ -533,28 +589,20 @@ define([
// Set temporary actions for undo/redo buttons
pagedownEditor.uiManager.buttons.undo.execute = function() {
if(model.canUndo) {
// This flag is used to avoid replaying editor's own
// modifications (assuming it's synchronous)
isAceUpToDate = false;
model.undo();
}
};
pagedownEditor.uiManager.buttons.redo.execute = function() {
if(model.canRedo) {
// This flag is used to avoid replaying editor's own
// modifications (assuming it's synchronous)
isAceUpToDate = false;
model.redo();
}
};
// Add event handler for model's UndoRedoStateChanged events
pagedownEditor.uiManager.setUndoRedoButtonStates = function() {
setTimeout(function() {
pagedownEditor.uiManager.setUndoRedoButtonStates = _.debounce(function() {
pagedownEditor.uiManager.setButtonState(pagedownEditor.uiManager.buttons.undo, model.canUndo);
pagedownEditor.uiManager.setButtonState(pagedownEditor.uiManager.buttons.redo, model.canRedo);
}, 50);
};
}, 10);
pagedownEditor.uiManager.setUndoRedoButtonStates();
model.addEventListener(gapi.drive.realtime.EventType.UNDO_REDO_STATE_CHANGED, function() {
pagedownEditor.uiManager.setUndoRedoButtonStates();
@ -611,14 +659,14 @@ define([
};
// Perform AutoSync
gdriveProvider.autosyncFile = function(title, content, config, callback) {
gdriveProvider.autosyncFile = function(title, content, discussionListJSON, config, callback) {
var parentId = config.parentId;
googleHelper.upload(undefined, parentId, title, content, undefined, undefined, accountId, function(error, result) {
if(error) {
callback(error);
return;
}
var syncAttributes = createSyncAttributes(result.id, result.etag, content, title);
var syncAttributes = createSyncAttributes(result.id, result.etag, content, title, discussionListJSON);
callback(undefined, syncAttributes);
});
};

View File

@ -129,7 +129,7 @@
@alert-border-radius: 0;
@label-warning-bg: spin(darken(@logo-yellow, 4%), -6);
@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-border: fade(spin(@logo-yellow, -6), 24%);
@state-danger-text: spin(darken(@logo-orange, 18%), -4);

View File

@ -310,7 +310,7 @@ define([
eventMgr.addListener("onFileCreated", function(fileDesc) {
if(_.size(fileDesc.syncLocations) === 0) {
_.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) {
return;
}
@ -350,7 +350,7 @@ define([
return eventMgr.onError("Real time collaborative document can't be synchronized with multiple locations");
}
// 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) {
return;
}
@ -370,7 +370,7 @@ define([
return;
}
// 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) {
return;
}