diff --git a/public/res/classes/Provider.js b/public/res/classes/Provider.js index 890a7f29..bd667fe9 100644 --- a/public/res/classes/Provider.js +++ b/public/res/classes/Provider.js @@ -374,7 +374,7 @@ define([ if(conflictList.length) { eventMgr.onMessage('"' + remoteTitle + '" has conflicts that you have to review.'); } - }), this); + }, this)); } // Return remote CRCs diff --git a/public/res/extensions/comments.js b/public/res/extensions/comments.js index 5625a02a..4e86e455 100644 --- a/public/res/extensions/comments.js +++ b/public/res/extensions/comments.js @@ -198,7 +198,7 @@ define([ // Refresh conversation if popover is open var context = currentContext; if(context.discussionIndex) { - context.popoverElt.querySelector('.discussion-comment-list').innerHTML = getDiscussionComments(); + context.getPopoverElt().querySelector('.discussion-comment-list').innerHTML = getDiscussionComments(); } try { cssApplier.undoToRange(context.rangyRange); diff --git a/public/res/extensions/dialogManageSynchronization.js b/public/res/extensions/dialogManageSynchronization.js index 1e5eaa04..7523108c 100644 --- a/public/res/extensions/dialogManageSynchronization.js +++ b/public/res/extensions/dialogManageSynchronization.js @@ -11,7 +11,7 @@ define([ dialogManageSynchronization.onEventMgrCreated = function(eventMgrParameter) { eventMgr = eventMgrParameter; }; - + var synchronizer; dialogManageSynchronization.onSynchronizerCreated = function(synchronizerParameter) { synchronizer = synchronizerParameter; @@ -34,7 +34,7 @@ define([ $msgSyncListElt.addClass("hide"); $msgNoSyncElt.removeClass("hide"); } - + var syncListHtml = _.reduce(fileDesc.syncLocations, function(result, syncAttributes) { return result + _.template(dialogManageSynchronizationLocationHTML, { syncAttributes: syncAttributes, @@ -42,12 +42,11 @@ define([ }); }, ''); syncListElt.innerHTML = syncListHtml; - + _.each(syncListElt.querySelectorAll('.remove-button'), function(removeButtonElt) { var $removeButtonElt = $(removeButtonElt); var syncAttributes = fileDesc.syncLocations[$removeButtonElt.data('syncIndex')]; $removeButtonElt.click(function() { - synchronizer.tryStopRealtimeSync(); fileDesc.removeSyncLocation(syncAttributes); eventMgr.onSyncRemoved(fileDesc, syncAttributes); }); @@ -71,4 +70,4 @@ define([ return dialogManageSynchronization; -}); \ No newline at end of file +}); diff --git a/public/res/helpers/googleHelper.js b/public/res/helpers/googleHelper.js index 5d337b03..18d29408 100644 --- a/public/res/helpers/googleHelper.js +++ b/public/res/helpers/googleHelper.js @@ -562,6 +562,30 @@ define([ dataType: file.isRealtime ? 'json' : 'text', timeout: constants.AJAX_TIMEOUT }).done(function(data) { + if(file.isRealtime) { + data = data.data.value; + data = { + content: data.content.value, + discussionList: (function() { + var discussionList = {}; + data.discussionList && _.each(data.discussionList.value, function(discussionObject) { + var discussion = { + discussionIndex: discussionObject.value.discussionIndex.json, + selectionStart: discussionObject.value.selectionStart.json, + selectionEnd: discussionObject.value.selectionEnd.json, + }; + var type = (discussionObject.value.type || {}).json; + type && (discussion.type = type); + var commentList = (discussionObject.value.commentList || {}).value || []; + commentList.length && (discussion.commentList = commentList.map(function(commentObject) { + return commentObject.json; + })); + discussionList[discussion.discussionIndex] = discussion; + }); + return discussionList; + })() + }; + } file.content = data; objects.shift(); task.chain(recursiveDownloadContent); diff --git a/public/res/providers/gdriveProviderBuilder.js b/public/res/providers/gdriveProviderBuilder.js index 4d9718e6..e9612124 100644 --- a/public/res/providers/gdriveProviderBuilder.js +++ b/public/res/providers/gdriveProviderBuilder.js @@ -290,12 +290,32 @@ define([ // Realtime closure var realtimeContext; (function() { + var inCompoundOperation = false; + function modelChangeWrapper(cb) { + realtimeContext.isChanging = true; + try { cb(); } + finally { + if(inCompoundOperation) { + try { realtimeContext.model.endCompoundOperation(); } + catch(e) {} + inCompoundOperation = false; + } + realtimeContext.isChanging = false; + } + } + function compound(cb) { + if(!inCompoundOperation) { + realtimeContext.model.beginCompoundOperation(); + inCompoundOperation = true; + } + cb(); + } function toRealtimeDiscussion(context, discussion) { 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); + realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_ADDED, onModelChange); + realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_REMOVED, onModelChange); + realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_SET, onModelChange); discussion.commentList && discussion.commentList.forEach(function(comment) { realtimeCommentList.push({ author: comment.author, @@ -322,6 +342,10 @@ define([ } function fromRealtimeDiscussion(realtimeDiscussion) { + var realtimeCommentList = realtimeDiscussion.get('commentList'); + realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_ADDED, onModelChange); + realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_REMOVED, onModelChange); + realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_SET, onModelChange); var discussion = { discussionIndex: realtimeDiscussion.get('discussionIndex'), selectionStart: realtimeDiscussion.get('selectionStart'), @@ -329,7 +353,7 @@ define([ }; var type = realtimeDiscussion.get('type'); type && (discussion.type = type); - var commentList = realtimeDiscussion.get('commentList').asArray(); + var commentList = realtimeCommentList.asArray(); commentList.length && (discussion.commentList = commentList); return discussion; } @@ -344,12 +368,14 @@ define([ return localDiscussionList; } - function mergeDiscussion(localDiscussion, realtimeDiscussion, isServerChange) { + function mergeDiscussion(localDiscussion, realtimeDiscussion, isModelChange) { var commentsChanged = false; - // We only pay attention to local selection modifications - if(!isServerChange) { - realtimeDiscussion.set('selectionStart', localDiscussion.selectionStart); - realtimeDiscussion.set('selectionEnd', localDiscussion.selectionEnd); + // We don't pay attention to model selection modifications + if(!isModelChange) { + compound(function() { + realtimeDiscussion.set('selectionStart', localDiscussion.selectionStart); + realtimeDiscussion.set('selectionEnd', localDiscussion.selectionEnd); + }); } function isInDiscussion(comment, commentList) { return commentList.some(function(commentInDiscussion) { @@ -362,21 +388,25 @@ define([ var localCommentList = localDiscussion.commentList; function checkLocalComment(comment, index) { if(!isInDiscussion(comment, realtimeCommentList.asArray())) { - if(isServerChange) { + if(isModelChange) { localCommentList.splice(index, 1); commentsChanged = true; return true; } else { - realtimeCommentList.push(comment); + compound(function() { + realtimeCommentList.push(comment); + }); } } } while(localCommentList.some(checkLocalComment)) {} function checkRealtimeComment(comment, index) { if(!isInDiscussion(comment, localCommentList)) { - if(!isServerChange) { - realtimeCommentList.remove(index); + if(!isModelChange) { + compound(function() { + realtimeCommentList.remove(index); + }); return true; } else { @@ -389,17 +419,19 @@ define([ return commentsChanged; } - function mergeDiscussionList(context, isServerChange) { + function mergeDiscussionList(context, isModelChange) { var commentsChanged = false; var localDiscussionList = context.fileDesc.discussionList; _.values(localDiscussionList).forEach(function(localDiscussion) { var realtimeDiscussion = context.realtimeDiscussionList.get(localDiscussion.discussionIndex); if(realtimeDiscussion) { - commentsChanged |= mergeDiscussion(localDiscussion, realtimeDiscussion, isServerChange); + commentsChanged |= mergeDiscussion(localDiscussion, realtimeDiscussion, isModelChange); } - else if(!isServerChange) { - realtimeDiscussion = toRealtimeDiscussion(context, localDiscussion); - context.realtimeDiscussionList.set(localDiscussion.discussionIndex, realtimeDiscussion); + else if(!isModelChange) { + compound(function() { + realtimeDiscussion = toRealtimeDiscussion(context, localDiscussion); + context.realtimeDiscussionList.set(localDiscussion.discussionIndex, realtimeDiscussion); + }); } else { delete localDiscussionList[localDiscussion.discussionIndex]; @@ -410,15 +442,17 @@ define([ var realtimeDiscussion = context.realtimeDiscussionList.get(discussionIndex); var localDiscussion = localDiscussionList[discussionIndex]; if(localDiscussion) { - commentsChanged |= mergeDiscussion(localDiscussion, realtimeDiscussion, isServerChange); + commentsChanged |= mergeDiscussion(localDiscussion, realtimeDiscussion, isModelChange); } - else if(isServerChange) { + else if(isModelChange) { var discussion = fromRealtimeDiscussion(realtimeDiscussion); localDiscussionList[discussionIndex] = discussion; eventMgr.onDiscussionCreated(context.fileDesc, discussion); } else { - context.realtimeDiscussionList.delete(discussionIndex); + compound(function() { + context.realtimeDiscussionList.delete(discussionIndex); + }); } }); context.fileDesc.discussionList = localDiscussionList; // Write in localStorage @@ -451,59 +485,55 @@ define([ } var onChange = (function() { - var debouncedOnChange = _.debounce(function() { + var debouncedOnChange = utils.debounce(function() { var context = realtimeContext; if(!context) { return; } - if(context.isServerChange) { - logger.log('Realtime syncing remote changes'); + if(context.isModelChange) { + logger.log('Realtime syncing model 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); + modelChangeWrapper(function() { + // Check content modifications + var localContent = context.fileDesc.content; + var remoteContent = context.realtimeString.getText(); + var contentChanged = localContent != remoteContent; + if(contentChanged) { + if(context.isModelChange) { + editor.setValue(remoteContent); + } + else { + compound(function() { + context.realtimeString.setText(localContent); + }); + } } - else { - context.realtimeString.setText(localContent); + + // Check discussion modifications + mergeDiscussionList(context, context.isModelChange); + + // For local changes, CRCs are updated on "save success" event + if(context.isModelChange) { + updateStatus(); } - } - - // Check discussion modifications - mergeDiscussionList(context, context.isServerChange); - - - // For local changes, CRCs are updated on "save success" event - if(context.isServerChange) { - updateStatus(); - } - else { - context.model.endCompoundOperation(); - } - context.isServerChange = false; - }, 0); + context.isModelChange = false; + }); + }); return function(fileDesc) { if(realtimeContext && realtimeContext.fileDesc === fileDesc) { debouncedOnChange(); } }; })(); - function modelEventListener(evt) { - if(!realtimeContext) { + function onModelChange() { + if(!realtimeContext || realtimeContext.isChanging) { return; } - if(evt.isLocal === false) { - realtimeContext.isServerChange = true; - } + realtimeContext.isModelChange = true; onChange(realtimeContext.fileDesc); } eventMgr.addListener('onContentChanged', onChange); @@ -513,6 +543,9 @@ define([ // Start realtime synchronization gdriveProvider.startRealtimeSync = function(fileDesc, syncAttributes) { + if(realtimeContext !== undefined) { + return; + } var context = { fileDesc: fileDesc, syncAttributes: syncAttributes @@ -520,6 +553,9 @@ define([ realtimeContext = context; googleHelper.loadRealtime(syncAttributes.id, accountId, function(err, doc) { if(err || !doc) { + if(context === realtimeContext) { + realtimeContext = undefined; + } return; } @@ -542,8 +578,8 @@ define([ } 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); + realtimeString.addEventListener(gapi.drive.realtime.EventType.TEXT_INSERTED, onModelChange); + realtimeString.addEventListener(gapi.drive.realtime.EventType.TEXT_DELETED, onModelChange); // Get or create discussion map var realtimeDiscussionList = model.getRoot().get('discussionList'); @@ -554,14 +590,14 @@ define([ } context.realtimeDiscussionList = realtimeDiscussionList; // Listen to discussion modifications - realtimeDiscussionList.addEventListener(gapi.drive.realtime.EventType.VALUE_CHANGED, modelEventListener); + realtimeDiscussionList.addEventListener(gapi.drive.realtime.EventType.VALUE_CHANGED, onModelChange); 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); + realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_ADDED, onModelChange); + realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_REMOVED, onModelChange); + realtimeCommentList.addEventListener(gapi.drive.realtime.EventType.VALUES_SET, onModelChange); }); // Also listen to "save success" event @@ -577,6 +613,7 @@ define([ var remoteDiscussionList = fromRealtimeDiscussionList(realtimeDiscussionList); var remoteDiscussionListJSON = JSON.stringify(remoteDiscussionList); gdriveProvider.syncMerge(fileDesc, syncAttributes, remoteContent, remoteTitle, remoteDiscussionList, remoteDiscussionListJSON); + onChange(context.fileDesc); // Save undo/redo buttons default actions undoExecute = pagedownEditor.uiManager.buttons.undo.execute; @@ -584,8 +621,8 @@ define([ setUndoRedoButtonStates = pagedownEditor.uiManager.setUndoRedoButtonStates; // Set temporary actions for undo/redo buttons - pagedownEditor.uiManager.buttons.undo.execute = model.undo; - pagedownEditor.uiManager.buttons.redo.execute = model.redo; + pagedownEditor.uiManager.buttons.undo.execute = _.bind(model.undo, model); + pagedownEditor.uiManager.buttons.redo.execute = _.bind(model.redo, model); // Add event handler for model's UndoRedoStateChanged events pagedownEditor.uiManager.setUndoRedoButtonStates = _.debounce(function() { @@ -609,7 +646,7 @@ define([ gdriveProvider.stopRealtimeSync(); } else if(err.isFatal) { - eventMgr.onError('Real time synchronization is temporarily unavailable.'); + // Retry will be attempted shortly... gdriveProvider.stopRealtimeSync(); } }); @@ -619,6 +656,8 @@ define([ gdriveProvider.stopRealtimeSync = function() { logger.log("Stopping Google Drive realtime synchronization"); if(realtimeContext !== undefined) { + try { realtimeContext.model.endCompoundOperation(); } + catch(e) {} realtimeContext.document && realtimeContext.document.close(); realtimeContext = undefined; } diff --git a/public/res/publisher.js b/public/res/publisher.js index c98f5aad..556a44d3 100644 --- a/public/res/publisher.js +++ b/public/res/publisher.js @@ -63,8 +63,8 @@ define([ // Clean fields from deleted files in local storage Object.keys(storage).forEach(function(key) { - var match = key.match(/(publish\.\S+?)\.\S+/); - if(match && !publishIndexMap.hasOwnProperty(match[1])) { + var match = key.match(/publish\.\S+/); + if(match && !publishIndexMap.hasOwnProperty(match[0])) { storage.removeItem(key); } }); diff --git a/public/res/synchronizer.js b/public/res/synchronizer.js index 98301abb..f95c070c 100644 --- a/public/res/synchronizer.js +++ b/public/res/synchronizer.js @@ -52,8 +52,8 @@ define([ // Clean fields from deleted files in local storage Object.keys(storage).forEach(function(key) { - var match = key.match(/(sync\.\S+?)\.\S+/); - if(match && !syncIndexMap.hasOwnProperty(match[1])) { + var match = key.match(/sync\.\S+/); + if(match && !syncIndexMap.hasOwnProperty(match[0])) { storage.removeItem(key); } }); @@ -241,28 +241,16 @@ define([ * Realtime synchronization **************************************************************************/ - var realtimeFileDesc; - var realtimeSyncAttributes; var isOnline = true; - // Determines if open file has real time sync location and tries to start - // real time sync - function onFileOpen(fileDesc) { - realtimeFileDesc = _.some(fileDesc.syncLocations, function(syncAttributes) { - realtimeSyncAttributes = syncAttributes; - return syncAttributes.isRealtime; - }) ? fileDesc : undefined; - tryStartRealtimeSync(); - } - // Tries to start/stop real time sync on online/offline event function onOfflineChanged(isOfflineParam) { if(isOfflineParam === false) { isOnline = true; - tryStartRealtimeSync(); + startRealtimeSync(); } else { - synchronizer.tryStopRealtimeSync(); + stopRealtimeSync(); isOnline = false; } } @@ -270,24 +258,36 @@ define([ // Starts real time synchronization if: // 1. current file has real time sync location // 2. we are online - function tryStartRealtimeSync() { - if(realtimeFileDesc !== undefined && isOnline === true) { - realtimeSyncAttributes.provider.startRealtimeSync(realtimeFileDesc, realtimeSyncAttributes); - } + function startRealtimeSync() { + var fileDesc = fileMgr.currentFile; + _.each(fileDesc.syncLocations, function(syncAttributes) { + syncAttributes.isRealtime && syncAttributes.provider.startRealtimeSync(fileDesc, syncAttributes); + }); } // Stops previously started synchronization if any - synchronizer.tryStopRealtimeSync = function() { - if(realtimeFileDesc !== undefined && isOnline === true) { - realtimeSyncAttributes.provider.stopRealtimeSync(); - } - }; + function stopRealtimeSync() { + _.each(providerMap, function(provider) { + provider.stopRealtimeSync && provider.stopRealtimeSync(); + }); + } // Triggers realtime synchronization from eventMgr events if(window.viewerMode === false) { - eventMgr.addListener("onFileOpen", onFileOpen); - eventMgr.addListener("onFileClosed", synchronizer.tryStopRealtimeSync); + // On file open, try to start realtime sync + eventMgr.addListener("onFileOpen", startRealtimeSync); + // On new sync location, try to start realtime sync + eventMgr.addListener("onSyncExportSuccess", startRealtimeSync); + // On file close, stop any active realtime synchronization + eventMgr.addListener("onFileClosed", stopRealtimeSync); + // Start/stop realtime sync depending on network status eventMgr.addListener("onOfflineChanged", onOfflineChanged); + // Try to start realtime sync every 15 sec in case of error + eventMgr.addListener("onPeriodicRun", _.throttle(startRealtimeSync, 15000)); + // Stop realtime sync if synchronized location is removed + eventMgr.addListener("onSyncRemoved", function(fileDesc, syncAttributes) { + fileDesc === fileMgr.currentFile && syncAttributes.isRealtime && syncAttributes.provider.stopRealtimeSync(); + }); } /*************************************************************************** @@ -368,11 +368,6 @@ define([ syncAttributes.isRealtime = true; fileDesc.addSyncLocation(syncAttributes); eventMgr.onSyncExportSuccess(fileDesc, syncAttributes); - - // Start the real time sync - realtimeFileDesc = fileDesc; - realtimeSyncAttributes = syncAttributes; - tryStartRealtimeSync(); }); } else { diff --git a/public/res/utils.js b/public/res/utils.js index 945a65ba..037c3da6 100644 --- a/public/res/utils.js +++ b/public/res/utils.js @@ -13,7 +13,7 @@ define([ // Faster than setTimeout (see http://dbaron.org/log/20100309-faster-timeouts) utils.defer = (function() { var timeouts = []; - var messageName = "delay"; + var messageName = "deferMsg"; window.addEventListener("message", function(evt) { if(evt.source == window && evt.data == messageName) { evt.stopPropagation();