diff --git a/css/default.css b/css/default.css index f648e252..bf47a172 100644 --- a/css/default.css +++ b/css/default.css @@ -480,7 +480,7 @@ div.dropdown-menu textarea { .icon-gdrive.realtime { width: 18px; - background-position: -180px 0; + background-position: -162px 0; } .icon-dropbox { diff --git a/img/icons.png b/img/icons.png index a656096d..84b27f79 100644 Binary files a/img/icons.png and b/img/icons.png differ diff --git a/js/classes/AsyncTask.js b/js/classes/AsyncTask.js index 56c14e49..bc1d1531 100644 --- a/js/classes/AsyncTask.js +++ b/js/classes/AsyncTask.js @@ -127,6 +127,12 @@ define([ // Run the next task in the queue if any and no other running function runTask() { + + // Wait for user first interaction before running first task + if(core.isUserReal === false) { + return + } + // Use defer to avoid stack overflow _.defer(function() { diff --git a/js/core.js b/js/core.js index d5d2b1b2..567bb60a 100644 --- a/js/core.js +++ b/js/core.js @@ -26,12 +26,12 @@ define([ }; // Used to detect user activity - var userReal = false; + core.isUserReal = false; var userActive = false; var windowUnique = true; var userLastActivity = 0; function setUserActive() { - userReal = true; + core.isUserReal = true; userActive = true; userLastActivity = utils.currentTime; } @@ -46,7 +46,7 @@ define([ // Used to only have 1 window of the application in the same browser var windowId = undefined; function checkWindowUnique() { - if(userReal === false || windowUnique === false) { + if(core.isUserReal === false || windowUnique === false) { return; } if(windowId === undefined) { diff --git a/js/helpers/googleHelper.js b/js/helpers/googleHelper.js index 4c870941..7febb083 100644 --- a/js/helpers/googleHelper.js +++ b/js/helpers/googleHelper.js @@ -85,6 +85,13 @@ define([ task.chain(localAuthenticate); }); } + googleHelper.forceAuthenticate = function() { + authenticated = false; + var task = new AsyncTask(); + connect(task); + authenticate(task); + task.enqueue(); + }; googleHelper.upload = function(fileId, parentId, title, content, etag, callback) { var result = undefined; @@ -458,7 +465,7 @@ define([ task.enqueue(); }; - googleHelper.loadRealtime = function(fileId, content, callback) { + googleHelper.loadRealtime = function(fileId, content, callback, errorCallback) { var doc = undefined; var task = new AsyncTask(); connect(task); @@ -473,10 +480,8 @@ define([ var string = model.createString(content); model.getRoot().set('content', string); }, function(err) { - // handleErrors - handleError({ - code: err.type - }, task); + errorCallback(err); + task.error(new Error(err.message)); }); }); task.onSuccess(function() { @@ -503,7 +508,7 @@ define([ task.retry(new Error(errorMsg)); return; } - else if(error.code === 401 || error.code === 403) { + else if(error.code === 401 || error.code === 403 || error.code == "token_refresh_required") { authenticated = false; errorMsg = "Access to Google account is not authorized."; task.retry(new Error(errorMsg), 1); diff --git a/js/providers/gdriveProvider.js b/js/providers/gdriveProvider.js index eb422296..d85f6f80 100644 --- a/js/providers/gdriveProvider.js +++ b/js/providers/gdriveProvider.js @@ -89,7 +89,7 @@ define([ callback(undefined, syncAttributes); }); }; - + gdriveProvider.exportRealtimeFile = function(event, title, content, callback) { var parentId = utils.getInputTextValue("#input-sync-export-gdrive-parentid"); googleHelper.createRealtimeFile(parentId, title, function(error, result) { @@ -266,33 +266,38 @@ define([ extensionMgr.addHookCallback("onEditorConfigure", function(editorParam) { editor = editorParam; }); - + // Start realtime synchronization var realtimeDocument = undefined; var realtimeBinding = undefined; var undoExecute = undefined; var redoExecute = undefined; - gdriveProvider.startRealtimeSync = function(localTitle, localContent, syncAttributes, callback) { - logger.log("Starting Google Drive realtime synchronization"); - googleHelper.loadRealtime(syncAttributes.id, localContent, function(err, doc) { + gdriveProvider.startRealtimeSync = function(fileDesc, syncAttributes) { + googleHelper.loadRealtime(syncAttributes.id, fileDesc.content, function(err, doc) { if(err || !doc) { - callback(err); return; } + + // If user just switched to another document + if(fileMgr.currentFile !== fileDesc) { + doc.close(); + return; + } + + logger.log("Starting Google Drive realtime synchronization"); realtimeDocument = doc; var model = realtimeDocument.getModel(); var string = model.getRoot().get('content'); - + // Saves model content checksum function updateContentState() { syncAttributes.contentCRC = utils.crc32(string.getText()); utils.storeAttributes(syncAttributes); } - + var debouncedRefreshPreview = _.debounce(editor.refreshPreview, 100); // Called when a modification has been detected function contentChangeListener(e) { - console.log(e); // If modification comes down from a collaborator if(e.isLocal === false) { logger.log("Google Drive realtime document updated from server"); @@ -304,15 +309,15 @@ define([ string.addEventListener(gapi.drive.realtime.EventType.TEXT_INSERTED, contentChangeListener); string.addEventListener(gapi.drive.realtime.EventType.TEXT_DELETED, contentChangeListener); realtimeDocument.addEventListener(gapi.drive.realtime.EventType.DOCUMENT_SAVE_STATE_CHANGED, function(e) { - console.log(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 = string.getText(); var remoteContentCRC = utils.crc32(remoteContent); @@ -321,25 +326,25 @@ define([ if(fileContentChanged === true && localContentChanged === true) { if(remoteContentChanged === true) { // Conflict detected - fileMgr.createFile(localTitle + " (backup)", localContent); - extensionMgr.onMessage('Conflict detected on "' + localTitle + '". A backup has been created locally.'); + fileMgr.createFile(fileDesc.title + " (backup)", localContent); + extensionMgr.onMessage('Conflict detected on "' + fileDesc.title + '". A backup has been created locally.'); } else { // Add local modifications if no collaborators change string.setText(localContent); } } - + // Binds model with textarea realtimeBinding = gapi.drive.realtime.databinding.bindString(string, $("#wmd-input")[0]); - + // Update content state according to collaborators changes if(remoteContentChanged === true) { logger.log("Google Drive realtime document updated from server"); updateContentState(); debouncedRefreshPreview(); } - + // Save undo/redo buttons actions undoExecute = editor.uiManager.buttons.undo.execute; redoExecute = editor.uiManager.buttons.redo.execute; @@ -351,18 +356,30 @@ define([ editor.uiManager.buttons.redo.execute = function() { model.canRedo && model.redo(); }; - + // Add event handler for model's UndoRedoStateChanged events function setUndoRedoState() { editor.uiManager.setButtonState(editor.uiManager.buttons.undo, model.canUndo); editor.uiManager.setButtonState(editor.uiManager.buttons.redo, model.canRedo); } - model.addEventListener( - gapi.drive.realtime.EventType.UNDO_REDO_STATE_CHANGED, - setUndoRedoState); + model.addEventListener(gapi.drive.realtime.EventType.UNDO_REDO_STATE_CHANGED, setUndoRedoState); setUndoRedoState(); - callback(); + }, function(err) { + console.error(err); + if(err.type == "token_refresh_required") { + googleHelper.forceAuthenticate(); + } + else if(err.type == "not_found") { + extensionMgr.onError('"' + fileDesc.title + '" has been removed from Google Drive.'); + fileDesc.removeSyncLocation(syncAttributes); + extensionMgr.onSyncRemoved(fileDesc, syncAttributes); + gdriveProvider.stopRealtimeSync(); + } + else if(err.isFatal) { + extensionMgr.onError('An error has forced real time synchronization to stop.'); + gdriveProvider.stopRealtimeSync(); + } }); }; @@ -377,7 +394,7 @@ define([ realtimeDocument.close(); realtimeDocument = undefined; } - + // Set back original undo/redo actions editor.uiManager.buttons.undo.execute = undoExecute; editor.uiManager.buttons.redo.execute = redoExecute; diff --git a/js/synchronizer.js b/js/synchronizer.js index 64f1fa65..2d14abad 100644 --- a/js/synchronizer.js +++ b/js/synchronizer.js @@ -240,10 +240,7 @@ define([ // 2. we are online function tryStartRealtimeSync() { if(realtimeFileDesc !== undefined && isOnline === true) { - core.lockUI(true); - realtimeSyncAttributes.provider.startRealtimeSync(realtimeFileDesc.title, realtimeFileDesc.content, realtimeSyncAttributes, function() { - core.lockUI(false); - }); + realtimeSyncAttributes.provider.startRealtimeSync(realtimeFileDesc, realtimeSyncAttributes); } } @@ -319,7 +316,7 @@ define([ }); } else { - if(_.size(fileDesc.syncLocations) > 0 && _.first(_.values(obj)).isRealtime) { + if(_.size(fileDesc.syncLocations) > 0 && _.first(_.values(fileDesc.syncLocations)).isRealtime) { extensionMgr.onError("Realtime collaboration document can't be synchronized with multiple locations"); return; } @@ -344,7 +341,7 @@ define([ // Provider's manual export button $(".action-sync-manual-" + provider.providerId).click(function(event) { var fileDesc = fileMgr.currentFile; - if(_.size(fileDesc.syncLocations) > 0 && _.first(_.values(obj)).isRealtime) { + if(_.size(fileDesc.syncLocations) > 0 && _.first(_.values(fileDesc.syncLocations)).isRealtime) { extensionMgr.onError("Realtime collaboration document can't be synchronized with multiple locations"); return; } diff --git a/js/utils.js b/js/utils.js index d13b4276..faa854a5 100644 --- a/js/utils.js +++ b/js/utils.js @@ -137,6 +137,7 @@ define([ // Reset input control in all modals utils.resetModalInputs = function() { $(".modal input[type=text]:not([disabled]), .modal input[type=password], .modal textarea").val(""); + $(".modal input[type=checkbox]").prop("checked", false); }; // Basic trim function