kopia lustrzana https://github.com/c9/core
930 wiersze
35 KiB
JavaScript
930 wiersze
35 KiB
JavaScript
![]() |
define(function(require, exports, module) {
|
|||
|
"use strict";
|
|||
|
|
|||
|
main.consumes = [
|
|||
|
"Panel", "tabManager", "fs", "metadata", "ui", "apf", "settings",
|
|||
|
"preferences", "ace", "util", "collab.connect", "collab.workspace",
|
|||
|
"timeslider", "OTDocument", "notification.bubble", "dialog.error", "dialog.alert",
|
|||
|
"collab.util", "error_handler", "layout", "menus", "installer", "c9"
|
|||
|
];
|
|||
|
main.provides = ["collab"];
|
|||
|
return main;
|
|||
|
|
|||
|
function main(options, imports, register) {
|
|||
|
var Panel = imports.Panel;
|
|||
|
var tabManager = imports.tabManager;
|
|||
|
var fs = imports.fs;
|
|||
|
var c9 = imports.c9;
|
|||
|
var metadata = imports.metadata;
|
|||
|
var installer = imports.installer;
|
|||
|
var ui = imports.ui;
|
|||
|
var apf = imports.apf;
|
|||
|
var ace = imports.ace;
|
|||
|
var util = imports.util;
|
|||
|
var collabUtil = imports["collab.util"];
|
|||
|
var settings = imports.settings;
|
|||
|
var prefs = imports.preferences;
|
|||
|
var connect = imports["collab.connect"];
|
|||
|
var workspace = imports["collab.workspace"];
|
|||
|
var bubble = imports["notification.bubble"];
|
|||
|
var timeslider = imports.timeslider;
|
|||
|
var OTDocument = imports.OTDocument;
|
|||
|
var showAlert = imports["dialog.alert"].show;
|
|||
|
var showError = imports["dialog.error"].show;
|
|||
|
var errorHandler = imports.error_handler;
|
|||
|
var layout = imports.layout;
|
|||
|
var menus = imports.menus;
|
|||
|
|
|||
|
var css = require("text!./collab.css");
|
|||
|
var staticPrefix = options.staticPrefix;
|
|||
|
|
|||
|
var plugin = new Panel("Ajax.org", main.consumes, {
|
|||
|
index: 45,
|
|||
|
width: 250,
|
|||
|
caption: "Collaborate",
|
|||
|
buttonCSSClass: "collab",
|
|||
|
panelCSSClass: "collab-bar",
|
|||
|
minWidth: 130,
|
|||
|
where: "right"
|
|||
|
});
|
|||
|
var emit = plugin.getEmitter();
|
|||
|
|
|||
|
// open collab documents
|
|||
|
var documents = {};
|
|||
|
var openFallbackTimeouts = {};
|
|||
|
var saveFallbackTimeouts = {};
|
|||
|
var usersLeaving = {};
|
|||
|
var failedSaveAttempts = 0;
|
|||
|
var OPEN_FILESYSTEM_FALLBACK_TIMEOUT = 6000;
|
|||
|
var SAVE_FILESYSTEM_FALLBACK_TIMEOUT = 30000;
|
|||
|
var SAVE_FILESYSTEM_FALLBACK_TIMEOUT_REPEATED = 15000;
|
|||
|
|
|||
|
// Check that all the dependencies are installed
|
|||
|
installer.createSession("c9.ide.collab", require("./install"));
|
|||
|
|
|||
|
var loaded = false;
|
|||
|
function load() {
|
|||
|
if (loaded) return;
|
|||
|
loaded = true;
|
|||
|
|
|||
|
connect.on("message", onMessage);
|
|||
|
connect.on("connecting", onConnecting);
|
|||
|
connect.on("connect", onConnectMsg);
|
|||
|
connect.on("disconnect", onDisconnect);
|
|||
|
|
|||
|
metadata.on("beforeReadFile", beforeReadFile, plugin);
|
|||
|
fs.on("afterReadFile", afterReadFile, plugin);
|
|||
|
fs.on("beforeWriteFile", beforeWriteFile, plugin);
|
|||
|
|
|||
|
ace.on("initAceSession", function(e) {
|
|||
|
var doc = e.doc;
|
|||
|
var path = doc.tab.path;
|
|||
|
var otDoc = documents[path];
|
|||
|
if (otDoc && !otDoc.session)
|
|||
|
otDoc.setSession(doc.getSession().session);
|
|||
|
});
|
|||
|
|
|||
|
tabManager.on("focusSync", function(e) {
|
|||
|
var tab = e.tab;
|
|||
|
var otDoc = documents[tab.path];
|
|||
|
if (otDoc && !otDoc.session) {
|
|||
|
var doc = tab.document;
|
|||
|
var docSession = doc.getSession();
|
|||
|
docSession && otDoc.setSession(docSession.session);
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
tabManager.on("focus", function(e) {
|
|||
|
var tab = e.tab;
|
|||
|
if (tab && tab.editor) {
|
|||
|
var id = getTabId(tab);
|
|||
|
id && connect.send("MESSAGE", {
|
|||
|
action: "focus",
|
|||
|
clientId: workspace.myClientId,
|
|||
|
userId: workspace.myUserId,
|
|||
|
tabId: id
|
|||
|
});
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
plugin.on("userMessage", function(e) {
|
|||
|
if (e.action == "focus") {
|
|||
|
workspace.updateOpenDocs(e, "activate");
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
ui.insertCss(css, staticPrefix, plugin);
|
|||
|
|
|||
|
window.addEventListener("unload", function() {
|
|||
|
leaveAll();
|
|||
|
}, false);
|
|||
|
|
|||
|
tabManager.on("open", function(e) {
|
|||
|
var tab = e.tab;
|
|||
|
tab.on("setPath", function(e) {
|
|||
|
onSetPath(tab, e.oldpath, e.path);
|
|||
|
});
|
|||
|
});
|
|||
|
|
|||
|
tabManager.on("tabDestroy", function(e) {
|
|||
|
leaveDocument(e.tab.path);
|
|||
|
}, plugin);
|
|||
|
|
|||
|
// Author layer settings
|
|||
|
var showAuthorInfoKey = "user/collab/@show-author-info";
|
|||
|
prefs.add({
|
|||
|
"General": {
|
|||
|
"Collaboration": {
|
|||
|
"Show Authorship Info": {
|
|||
|
type: "checkbox",
|
|||
|
position: 8000,
|
|||
|
path: showAuthorInfoKey
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}, plugin);
|
|||
|
|
|||
|
settings.on("read", function () {
|
|||
|
settings.setDefaults("user/collab", [["show-author-info", true]]);
|
|||
|
refreshActiveDocuments();
|
|||
|
}, plugin);
|
|||
|
|
|||
|
settings.on("user/collab", function () {
|
|||
|
refreshActiveDocuments();
|
|||
|
}, plugin);
|
|||
|
|
|||
|
workspace.on("sync", scheduleUpdateUserBadges);
|
|||
|
scheduleUpdateUserBadges();
|
|||
|
}
|
|||
|
|
|||
|
var drawn = false;
|
|||
|
function draw(options) {
|
|||
|
if (drawn) return;
|
|||
|
drawn = true;
|
|||
|
|
|||
|
var bar = options.aml;
|
|||
|
var html = bar.$int;
|
|||
|
emit.sticky("drawPanels", { html: html, aml: bar });
|
|||
|
}
|
|||
|
|
|||
|
function onDisconnect() {
|
|||
|
for (var docId in documents) {
|
|||
|
var doc = documents[docId];
|
|||
|
doc.disconnect();
|
|||
|
}
|
|||
|
// bubbleNotification("Collab disconnected");
|
|||
|
emit("disconnect");
|
|||
|
}
|
|||
|
|
|||
|
function onConnecting () {
|
|||
|
// bubbleNotification("Collab connecting");
|
|||
|
}
|
|||
|
|
|||
|
function onConnectMsg(msg) {
|
|||
|
workspace.syncWorkspace(msg.data);
|
|||
|
|
|||
|
for (var docId in documents)
|
|||
|
documents[docId].load();
|
|||
|
|
|||
|
// bubbleNotification(msg.err || "Collab connected");
|
|||
|
emit("connect");
|
|||
|
}
|
|||
|
|
|||
|
function onMessage(msg) {
|
|||
|
var data = msg.data || {};
|
|||
|
var user = data && data.userId && workspace.getUser(data.userId);
|
|||
|
var type = msg.type;
|
|||
|
var docId = data.docId;
|
|||
|
if (docId && "/~".indexOf(docId[0]) === -1)
|
|||
|
docId = data.docId = "/" + docId;
|
|||
|
var doc = documents[docId];
|
|||
|
|
|||
|
if (!connect.connected && type !== "CONNECT")
|
|||
|
return console.warn("[OT] Not connected - ignoring:", msg);
|
|||
|
|
|||
|
if (data.clientId && data.clientId === workspace.myOldClientId)
|
|||
|
return console.warn("[OT] Skipping my own 'away' disconnection notifications");
|
|||
|
|
|||
|
switch (type) {
|
|||
|
case "CHAT_MESSAGE":
|
|||
|
data.increment = true;
|
|||
|
emit("chatMessage", data);
|
|||
|
break;
|
|||
|
case "USER_JOIN":
|
|||
|
user = data.user;
|
|||
|
if (!user)
|
|||
|
break;
|
|||
|
workspace.joinClient(user, data.clientId);
|
|||
|
notifyUserOnline(user);
|
|||
|
break;
|
|||
|
case "USER_LEAVE":
|
|||
|
workspace.leaveClient(data.userId, data.clientId);
|
|||
|
notifyUserOffline(user);
|
|||
|
break;
|
|||
|
case "LEAVE_DOC":
|
|||
|
workspace.updateOpenDocs(data, "leave");
|
|||
|
doc && doc.clientLeave(data.clientId);
|
|||
|
break;
|
|||
|
case "JOIN_DOC":
|
|||
|
workspace.updateOpenDocs(data, "join");
|
|||
|
if (workspace.myClientId !== data.clientId)
|
|||
|
return;
|
|||
|
if (!doc)
|
|||
|
return console.warn("[OT] Received msg for file that is not open - docId:", docId, "open docs:", Object.keys(documents));
|
|||
|
doc.joinData(data);
|
|||
|
break;
|
|||
|
case "RESOLVE_CONFLICT":
|
|||
|
emit("resolveConflict", { path: docId });
|
|||
|
break;
|
|||
|
case "LARGE_DOC":
|
|||
|
doc && doc.leave();
|
|||
|
doc && reportLargeDocument(doc, !msg.data.response);
|
|||
|
delete documents[docId];
|
|||
|
break;
|
|||
|
case "DOC_CHANGED_ON_DISK":
|
|||
|
reportDocChangedOnDisk(docId);
|
|||
|
break;
|
|||
|
case "DOC_HAS_PENDING_CHANGES":
|
|||
|
reportDocHasPendingChanges(docId);
|
|||
|
break;
|
|||
|
|
|||
|
case "USER_STATE":
|
|||
|
workspace.updateUserState(data.userId, data.state);
|
|||
|
break;
|
|||
|
case "CLEAR_CHAT":
|
|||
|
emit("chatClear", data);
|
|||
|
break;
|
|||
|
case "MESSAGE":
|
|||
|
if (emit("userMessage", data) !== false)
|
|||
|
handleUserMessage(data);
|
|||
|
break;
|
|||
|
case "ERROR":
|
|||
|
errorHandler.log(
|
|||
|
data.err || new Error("Collab error"),
|
|||
|
util.extend({}, { users: workspace.users, userId: workspace.myUserId, clientId: workspace.myClientId }, data)
|
|||
|
);
|
|||
|
break;
|
|||
|
case "POST_PROCESSOR_ERROR":
|
|||
|
emit("postProcessorError", data);
|
|||
|
return;
|
|||
|
case "RESET_DB":
|
|||
|
showAlert("Uh oh!",
|
|||
|
"Workspace issue encountered",
|
|||
|
"Your workspace encountered an issue, but don’t worry, we’ve resolved it. " +
|
|||
|
"Your data is still intact, however your file revision history may have been lost. " +
|
|||
|
"Give us just a moment to complete the recovery so you can get back to your project. ",
|
|||
|
function() {
|
|||
|
setTimeout(function() {
|
|||
|
window.location.reload();
|
|||
|
}, 1000);
|
|||
|
}
|
|||
|
);
|
|||
|
break;
|
|||
|
default:
|
|||
|
if (!doc)
|
|||
|
return console.warn("[OT] Received msg for file that is not open", docId, msg);
|
|||
|
if (doc.loaded)
|
|||
|
doc.handleMessage(msg);
|
|||
|
else
|
|||
|
console.warn("[OT] Doc ", docId, " not yet inited - MSG:", msg);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function notifyUserOffline(user) {
|
|||
|
clearTimeout(usersLeaving[user.fullname]);
|
|||
|
usersLeaving[user.fullname] = setTimeout(function() {
|
|||
|
if (!user.online)
|
|||
|
bubbleNotification("went offline", user);
|
|||
|
delete usersLeaving[user.fullname];
|
|||
|
}, 4000);
|
|||
|
}
|
|||
|
|
|||
|
function notifyUserOnline(user) {
|
|||
|
if (usersLeaving[user.fullname]) {
|
|||
|
// User left for like 4 seconds, don't notify
|
|||
|
clearTimeout(usersLeaving[user.fullname]);
|
|||
|
return;
|
|||
|
}
|
|||
|
if (user.online <= 1)
|
|||
|
bubbleNotification("came online", user);
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Join a document and report progress and on-load contents
|
|||
|
* @param {String} docId
|
|||
|
* @param {Document} doc
|
|||
|
* @param {Function} [progress]
|
|||
|
*/
|
|||
|
function joinDocument(docId, doc, progress) {
|
|||
|
console.log("[OT] Join", docId);
|
|||
|
var docSession = doc.getSession();
|
|||
|
var aceSession = docSession && docSession.session;
|
|||
|
if (!aceSession)
|
|||
|
console.warn("[OT] Ace session not ready for:", docId, "- will setSession when ready!");
|
|||
|
|
|||
|
var otDoc = documents[docId] || new OTDocument(docId, doc);
|
|||
|
|
|||
|
if (aceSession)
|
|||
|
otDoc.setSession(aceSession);
|
|||
|
|
|||
|
if (progress)
|
|||
|
setupProgressCallback(otDoc, progress);
|
|||
|
|
|||
|
if (documents[docId])
|
|||
|
return console.warn("[OT] Document previously joined -", docId,
|
|||
|
"STATE: loading:", otDoc.loading, "loaded:", otDoc.loaded, "inited:", otDoc.inited);
|
|||
|
|
|||
|
documents[docId] = otDoc;
|
|||
|
|
|||
|
// test late join - document syncing - best effort
|
|||
|
if (connect.connected)
|
|||
|
otDoc.load();
|
|||
|
|
|||
|
return otDoc;
|
|||
|
}
|
|||
|
|
|||
|
function setupProgressCallback(otDoc, progress) {
|
|||
|
otDoc.on("joinProgress", function(e) {
|
|||
|
progress && progress(e.loaded, e.total, e.complete);
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
function leaveDocument(docId) {
|
|||
|
if (!docId || !documents[docId] || !connect.connected)
|
|||
|
return;
|
|||
|
console.log("[OT] Leave", docId);
|
|||
|
var doc = documents[docId];
|
|||
|
doc.leave(); // will also dispose
|
|||
|
delete documents[docId];
|
|||
|
}
|
|||
|
|
|||
|
function reportLargeDocument(doc, forceReadonly) {
|
|||
|
var docId = doc.id;
|
|||
|
delete documents[doc.id];
|
|||
|
if (workspace.isAdmin && !forceReadonly) {
|
|||
|
if (workspace.onlineCount === 1)
|
|||
|
return console.log("File is very large, collaborative editing disabled: " + docId);
|
|||
|
return showError("File is very large, collaborative editing disabled: " + docId, 5000);
|
|||
|
}
|
|||
|
showError("File is very large. Collaborative editing disabled: " + docId, 5000);
|
|||
|
var tab = tabManager.findTab(docId);
|
|||
|
if (!tab || !tab.editor)
|
|||
|
return;
|
|||
|
tab.classList.add("error");
|
|||
|
if (doc.readonly)
|
|||
|
return;
|
|||
|
doc.readonly = true;
|
|||
|
}
|
|||
|
|
|||
|
function reportDocChangedOnDisk(path) {
|
|||
|
emit("change", {
|
|||
|
path: path,
|
|||
|
type: "change",
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
function reportDocHasPendingChanges(path) {
|
|||
|
var tab = tabManager.findTab(path);
|
|||
|
if (tab) {
|
|||
|
setTimeout(function() {
|
|||
|
// Make the tab show as unsaved
|
|||
|
tab.document.undoManager.bookmark(-2);
|
|||
|
}, 50);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function saveDocument(docId, fallbackFn, fallbackArgs, callback) {
|
|||
|
var doc = documents[docId];
|
|||
|
var joinError;
|
|||
|
clearTimeout(saveFallbackTimeouts[docId]);
|
|||
|
|
|||
|
saveFallbackTimeouts[docId] = setTimeout(function() {
|
|||
|
console.warn("[OT] collab saveFallbackTimeout while trying to save file", docId, "- trying fallback approach instead");
|
|||
|
fsSaveFallback();
|
|||
|
doc.off("saved", onSaved);
|
|||
|
failedSaveAttempts++;
|
|||
|
emit("saveFallbackStart", { path: docId });
|
|||
|
}, SAVE_FILESYSTEM_FALLBACK_TIMEOUT * Math.pow(2, -Math.min(failedSaveAttempts, 5)));
|
|||
|
|
|||
|
function onSaved(e) {
|
|||
|
doc.off("saved", onSaved);
|
|||
|
clearTimeout(saveFallbackTimeouts[docId]);
|
|||
|
if (e.err) {
|
|||
|
if ((e.code == "ETIMEOUT" || e.code == "EMISMATCH") && fallbackFn) {
|
|||
|
// The vfs socket is probably dead ot stale
|
|||
|
console.warn("[OT] collab error:", e.code, "trying to save file", docId, "- trying fallback approach instead");
|
|||
|
return fsSaveFallback({ code: e.code, err: e.err });
|
|||
|
} else {
|
|||
|
sendSaveError({ code: e.code, err: e.err }, "Collab saving failed on unexpected error");
|
|||
|
}
|
|||
|
} else {
|
|||
|
failedSaveAttempts = 0;
|
|||
|
}
|
|||
|
callback(e.err);
|
|||
|
}
|
|||
|
|
|||
|
function fsSaveFallback(attempt) {
|
|||
|
var message = doc && doc.loaded
|
|||
|
? "Warning: using fallback saving on loaded document"
|
|||
|
: "Warning: using fallback saving on unloaded document";
|
|||
|
if (doc && doc.saveStateDebugging) {
|
|||
|
message = "Warning: using fallback saving due to save timeout";
|
|||
|
}
|
|||
|
|
|||
|
console.warn(message, attempt);
|
|||
|
|
|||
|
fallbackFn.apply(null, fallbackArgs);
|
|||
|
}
|
|||
|
|
|||
|
function sendSaveError(attempt, message) {
|
|||
|
errorHandler.reportError(new Error(message), {
|
|||
|
docId: docId,
|
|||
|
loading: doc && doc.loading,
|
|||
|
loaded: doc && doc.loaded,
|
|||
|
inited: doc && doc.inited,
|
|||
|
rejoinReason: doc && doc.rejoinReason,
|
|||
|
state: doc && doc.state,
|
|||
|
stateWhenSaveCalled: doc && doc.stateWhenSaveCalled,
|
|||
|
saveStateDebugging: doc && doc.saveStateDebugging,
|
|||
|
joinError: joinError,
|
|||
|
connected: connect.connected,
|
|||
|
attempt: attempt,
|
|||
|
failedSaveAttempts: failedSaveAttempts
|
|||
|
}, ["collab"]);
|
|||
|
}
|
|||
|
|
|||
|
if (!doc.loaded || !connect.connected) {
|
|||
|
if (connect.connected && !doc.loaded && !doc.loading) {
|
|||
|
// broken state we are not joined and not trying to join
|
|||
|
clearTimeout(saveFallbackTimeouts[docId]);
|
|||
|
fsSaveFallback("document not joined and not trying to join");
|
|||
|
return;
|
|||
|
}
|
|||
|
doc.once("joined", function(e) {
|
|||
|
joinError = e && e.err;
|
|||
|
if (e && !e.err)
|
|||
|
doCollabSave();
|
|||
|
});
|
|||
|
}
|
|||
|
else {
|
|||
|
doCollabSave();
|
|||
|
}
|
|||
|
|
|||
|
function doCollabSave() {
|
|||
|
doc.on("saved", onSaved);
|
|||
|
doc.save();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function leaveAll() {
|
|||
|
Object.keys(documents).forEach(function(docId) {
|
|||
|
leaveDocument(docId);
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
function refreshActiveDocuments() {
|
|||
|
for (var docId in documents) {
|
|||
|
var doc = documents[docId];
|
|||
|
var tab = doc.original.tab;
|
|||
|
if (tab.pane.activeTab === tab && doc.inited)
|
|||
|
doc.authorLayer.refresh();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/*
|
|||
|
function getDocumentsWithinPath(path) {
|
|||
|
var docIds = Object.keys(documents);
|
|||
|
var docs = [];
|
|||
|
for (var i = 0, l = docIds.length; i < l; ++i) {
|
|||
|
var doc = documents[docIds[i]];
|
|||
|
if (doc.id.indexOf(path) === 0)
|
|||
|
docs.push(doc);
|
|||
|
}
|
|||
|
return docs;
|
|||
|
};
|
|||
|
*/
|
|||
|
|
|||
|
/**
|
|||
|
* Start a Collab session with each metadata read.
|
|||
|
*
|
|||
|
* e.path
|
|||
|
* e.tab
|
|||
|
* e.callback
|
|||
|
* e.progress
|
|||
|
*/
|
|||
|
function beforeReadFile(e) {
|
|||
|
var path = e.path;
|
|||
|
var progress = e.progress;
|
|||
|
var callback = e.callback;
|
|||
|
if (!path || e.tab.editorType != "ace")
|
|||
|
return;
|
|||
|
var otDoc = documents[path];
|
|||
|
if (!otDoc)
|
|||
|
otDoc = documents[path] = joinDocument(path, e.tab.document, progress);
|
|||
|
else
|
|||
|
setupProgressCallback(otDoc, progress);
|
|||
|
|
|||
|
otDoc.on("joined", onJoined);
|
|||
|
otDoc.on("largeDocument", reportLargeDocument.bind(null, otDoc));
|
|||
|
otDoc.on("joinProgress", startWatchDog);
|
|||
|
otDoc.on("beforeSave", function(e) {
|
|||
|
emit("beforeSave", e);
|
|||
|
});
|
|||
|
|
|||
|
// Load using XHR while collab not connected
|
|||
|
if (!connect.connected) {
|
|||
|
// Someone else listening to beforeReadFile
|
|||
|
// will have to call our callback
|
|||
|
callback = null;
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
startWatchDog();
|
|||
|
|
|||
|
var fallbackXhrAbort;
|
|||
|
return {
|
|||
|
abort: function() {
|
|||
|
if (fallbackXhrAbort)
|
|||
|
fallbackXhrAbort();
|
|||
|
else
|
|||
|
console.log("TODO: [OT] abort joining a document");
|
|||
|
}
|
|||
|
};
|
|||
|
|
|||
|
function startWatchDog() {
|
|||
|
clearTimeout(openFallbackTimeouts[path]);
|
|||
|
openFallbackTimeouts[path] = setTimeout(function() {
|
|||
|
console.warn("[OT] JOIN_DOC timed out:", path, "- fallback to filesystem, but don't abort");
|
|||
|
fsOpenFallback();
|
|||
|
otDoc.off("joined", onJoined);
|
|||
|
}, OPEN_FILESYSTEM_FALLBACK_TIMEOUT);
|
|||
|
}
|
|||
|
|
|||
|
function onJoined(e) {
|
|||
|
otDoc.off("joined", onJoined);
|
|||
|
clearTimeout(openFallbackTimeouts[path]);
|
|||
|
openFallbackTimeouts[path] = null;
|
|||
|
if (e.err) {
|
|||
|
if (e.err.code != "ENOENT" && e.err.code != "ELARGE")
|
|||
|
console.warn("[OT] JOIN_DOC failed:", path, "- fallback to filesystem");
|
|||
|
return fsOpenFallback();
|
|||
|
}
|
|||
|
console.log("[OT] Joined", otDoc.id);
|
|||
|
callback && callback(e.err, e.contents, e.metadata);
|
|||
|
}
|
|||
|
|
|||
|
function fsOpenFallback() {
|
|||
|
var xhr = fs.readFileWithMetadata(path, "utf8", callback || function() {}, progress) || {};
|
|||
|
fallbackXhrAbort = ((xhr && xhr.abort) || function() {}).bind(xhr);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Normalize tab contents after file read.
|
|||
|
*/
|
|||
|
function afterReadFile(e) {
|
|||
|
var path = e.path;
|
|||
|
var tab = tabManager.findTab(path);
|
|||
|
var doc = documents[path];
|
|||
|
if (!tab || !doc || doc.loaded)
|
|||
|
return;
|
|||
|
var httpLoadedValue = tab.document.value;
|
|||
|
var normHttpValue = collabUtil.normalizeTextLT(httpLoadedValue);
|
|||
|
if (httpLoadedValue !== normHttpValue)
|
|||
|
tab.document.value = normHttpValue;
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Save using collab server for OT-enabled documents.
|
|||
|
* Overrides the normal file writing behavior.
|
|||
|
*/
|
|||
|
function beforeWriteFile(e) {
|
|||
|
var path = e.path;
|
|||
|
var tab = tabManager.findTab(path);
|
|||
|
var doc = documents[path];
|
|||
|
|
|||
|
// Fall back to default writeFile if not applicable
|
|||
|
if (!doc || !tab)
|
|||
|
return;
|
|||
|
if (timeslider.visible)
|
|||
|
return false;
|
|||
|
|
|||
|
// Override default writeFile
|
|||
|
var args = e.args.slice();
|
|||
|
var progress = args.pop();
|
|||
|
var callback = args.pop();
|
|||
|
var defaultWriteFile = e.fn;
|
|||
|
saveDocument(path, defaultWriteFile, e.args, callback);
|
|||
|
return false;
|
|||
|
}
|
|||
|
|
|||
|
function onSetPath(tab, oldpath, path) {
|
|||
|
console.log("[OT] detected rename/save as from", oldpath, "to", path);
|
|||
|
leaveDocument(oldpath);
|
|||
|
joinDocument(path, tab.document);
|
|||
|
// TODO this is flaky, there should be rename command in the server
|
|||
|
documents[path].once("joined", function(e) {
|
|||
|
if (e.err) {
|
|||
|
leaveDocument(oldpath);
|
|||
|
setTimeout(joinDocument(path, tab.document), SAVE_FILESYSTEM_FALLBACK_TIMEOUT_REPEATED);
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
function bubbleNotification(msg, user) {
|
|||
|
if (!user)
|
|||
|
return bubble.popup(msg);
|
|||
|
|
|||
|
var chatName = apf.escapeXML(user.fullname);
|
|||
|
var md5Email = user.md5Email;
|
|||
|
var defaultImgUrl = encodeURIComponent("https://www.aiga.org/uploadedImages/AIGA/Content/About_AIGA/Become_a_member/generic_avatar_300.gif");
|
|||
|
console.log("Collab:", user.fullname, msg);
|
|||
|
bubble.popup('<img width=26 height=26 class="gravatar-image" src="https://secure.gravatar.com/avatar/' +
|
|||
|
md5Email + '?s=26&d=' + defaultImgUrl + '" /><span>' +
|
|||
|
chatName + '<span class="notification_sub">' + msg + '</span></span>');
|
|||
|
}
|
|||
|
|
|||
|
/***** sync tabs *****/
|
|||
|
function getTabState(tabId) {
|
|||
|
var tab;
|
|||
|
if (tabId) {
|
|||
|
tabManager.getTabs().some(function(t) {
|
|||
|
if (getTabId(t) == tabId) {
|
|||
|
return (tab = t);
|
|||
|
}
|
|||
|
});
|
|||
|
} else {
|
|||
|
tab = tabManager.focussedTab;
|
|||
|
}
|
|||
|
if (!tab) return;
|
|||
|
var state = tab.getState();
|
|||
|
var doc = state.document;
|
|||
|
if (doc) {
|
|||
|
doc.value = doc.undoManager = doc.meta = undefined;
|
|||
|
if (doc.ace) {
|
|||
|
// doc.ace.folds = doc.ace.options = undefined;
|
|||
|
doc.ace = { selection: doc.ace.selection };
|
|||
|
}
|
|||
|
}
|
|||
|
state.className = undefined;
|
|||
|
return state;
|
|||
|
}
|
|||
|
|
|||
|
function getTabId(tab) {
|
|||
|
if (!tab || !tab.document)
|
|||
|
return;
|
|||
|
var meta = tab.document.meta;
|
|||
|
if (meta.preview || meta.newfile)
|
|||
|
return;
|
|||
|
if (tab.editorType == "terminal")
|
|||
|
return "terminal:" + (tab.document.getState().terminal || {}).id;
|
|||
|
if (tab.editorType == "output") {
|
|||
|
var state = tab.document.getState().output;
|
|||
|
var config = state.config || {};
|
|||
|
return "run-config:" + (config.name || state.id);
|
|||
|
}
|
|||
|
if (tab.editorType == "preview")
|
|||
|
return tab.name;
|
|||
|
if (tab.path)
|
|||
|
return tab.path;
|
|||
|
}
|
|||
|
|
|||
|
var lastJump;
|
|||
|
function revealUser(clientId, tabId) {
|
|||
|
if (clientId == workspace.myClientId) {
|
|||
|
handleUserMessage({
|
|||
|
action: "open",
|
|||
|
target: clientId,
|
|||
|
tabState: lastJump
|
|||
|
});
|
|||
|
} else {
|
|||
|
connect.send("MESSAGE", {
|
|||
|
source: workspace.myClientId,
|
|||
|
target: clientId,
|
|||
|
action: "getTab",
|
|||
|
tabId: tabId
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function listOpenFiles(clientId) {
|
|||
|
connect.send("MESSAGE", {
|
|||
|
source: workspace.myClientId,
|
|||
|
target: clientId,
|
|||
|
action: "listOpenFiles"
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
function handleUserMessage(data) {
|
|||
|
if (data.action == "getTab") {
|
|||
|
if (data.target == workspace.myClientId) {
|
|||
|
connect.send("MESSAGE", {
|
|||
|
source: workspace.myClientId,
|
|||
|
target: data.source,
|
|||
|
action: "open",
|
|||
|
tabState: getTabState(data.tabId),
|
|||
|
});
|
|||
|
}
|
|||
|
} else if (data.action == "open") {
|
|||
|
if (data.target == workspace.myClientId) {
|
|||
|
if (data.tabState) {
|
|||
|
var tabState = getTabState() || {};
|
|||
|
if (shouldUpdateLastJump(lastJump, tabState, data.tabState))
|
|||
|
lastJump = tabState;
|
|||
|
data.tabState.focus = true;
|
|||
|
if (data.tabState.document)
|
|||
|
delete data.tabState.document.filter;
|
|||
|
tabManager.open(data.tabState);
|
|||
|
}
|
|||
|
}
|
|||
|
} else if (data.action == "listOpenFiles") {
|
|||
|
if (data.fileList) {
|
|||
|
workspace.updateOpenDocs({
|
|||
|
clientId: data.source,
|
|||
|
userId: data.userId,
|
|||
|
fileList: data.fileList
|
|||
|
}, "set");
|
|||
|
} else if (data.target == workspace.myClientId) {
|
|||
|
var openFiles = tabManager.getTabs().map(function(tab) {
|
|||
|
return getTabId(tab);
|
|||
|
}).filter(Boolean);
|
|||
|
var active = openFiles.indexOf(getTabId(tabManager.focussedTab));
|
|||
|
connect.send("MESSAGE", {
|
|||
|
userId: workspace.myUserId,
|
|||
|
source: workspace.myClientId,
|
|||
|
target: data.source,
|
|||
|
action: "listOpenFiles",
|
|||
|
fileList: {
|
|||
|
active: active,
|
|||
|
documents: openFiles
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function shouldUpdateLastJump(prevState, state, newState) {
|
|||
|
function getSelection(s) {
|
|||
|
return s && s.document && s.document.ace && s.document.ace.selection;
|
|||
|
}
|
|||
|
if (!prevState) return true;
|
|||
|
if (!newState.name) return false;
|
|||
|
var prevSel = JSON.stringify(getSelection(prevState));
|
|||
|
var sel = JSON.stringify(getSelection(state));
|
|||
|
var newSel = JSON.stringify(getSelection(newState));
|
|||
|
if (prevState.name == state.name && newSel == sel) {
|
|||
|
return false;
|
|||
|
}
|
|||
|
if (state.name == newState.name && newSel == sel)
|
|||
|
return false;
|
|||
|
return true;
|
|||
|
}
|
|||
|
|
|||
|
function scheduleUpdateUserBadges() {
|
|||
|
if (scheduleUpdateUserBadges.timer) return;
|
|||
|
scheduleUpdateUserBadges.timer = setTimeout(function() {
|
|||
|
scheduleUpdateUserBadges.timer = null;
|
|||
|
updateUserBadges();
|
|||
|
}, 10);
|
|||
|
}
|
|||
|
|
|||
|
function updateUserBadges() {
|
|||
|
var users = workspace.users;
|
|||
|
var myId = workspace.myUserId;
|
|||
|
Object.keys(users).forEach(function(id) {
|
|||
|
if (id == myId) return;
|
|||
|
var user = users[id];
|
|||
|
if (!user.online)
|
|||
|
return menus.remove("user_" + id);
|
|||
|
if (menus.getMenuId("user_" + id))
|
|||
|
return;
|
|||
|
addButton(id, user.fullname, user.md5Email);
|
|||
|
});
|
|||
|
|
|||
|
function addButton(uid, name, md5Email) {
|
|||
|
menus.remove("user_" + uid);
|
|||
|
var parent = layout.getElement("barExtras");
|
|||
|
|
|||
|
// Create Menu
|
|||
|
var mnuUser = new ui.menu();
|
|||
|
plugin.addElement(mnuUser);
|
|||
|
|
|||
|
// Add named button
|
|||
|
var icon = util.getGravatarUrl(md5Email, 32, "");
|
|||
|
menus.addItemByPath("user_" + uid + "/", mnuUser, 110000, plugin);
|
|||
|
|
|||
|
// Add sub menu items
|
|||
|
var c = 500;
|
|||
|
menus.addItemByPath("user_" + uid + "/Open Active File", new ui.item({
|
|||
|
onclick: function() {
|
|||
|
var user = workspace.users[uid];
|
|||
|
revealUser(user.clients[0]);
|
|||
|
}
|
|||
|
}), c += 100, plugin);
|
|||
|
|
|||
|
var button = menus.get("user_" + uid).item;
|
|||
|
button.setAttribute("class", "btnName");
|
|||
|
button.setAttribute("icon", icon);
|
|||
|
button.setAttribute("iconsize", "16px 16px");
|
|||
|
button.setAttribute("tooltip", name);
|
|||
|
if (options.showFullNameInMenuBar) {
|
|||
|
button.setAttribute("caption", name);
|
|||
|
}
|
|||
|
if (button.$ext)
|
|||
|
button.$ext.style.color = collabUtil.formatColor(workspace.colorPool[uid]);
|
|||
|
ui.insertByIndex(parent, button, 550, plugin);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
/***** Lifecycle *****/
|
|||
|
plugin.on("newListener", function(event, listener) {
|
|||
|
if (event == "connect" && connect.connected)
|
|||
|
listener();
|
|||
|
});
|
|||
|
plugin.on("load", function() {
|
|||
|
load();
|
|||
|
});
|
|||
|
plugin.on("draw", function(e) {
|
|||
|
draw(e);
|
|||
|
});
|
|||
|
plugin.on("unload", function() {
|
|||
|
loaded = false;
|
|||
|
drawn = true;
|
|||
|
});
|
|||
|
|
|||
|
/**
|
|||
|
* @singleton
|
|||
|
*/
|
|||
|
plugin.freezePublicAPI({
|
|||
|
_events: [
|
|||
|
/**
|
|||
|
* Fires when the collab panel is first drawn to enable sub-collab-panels to listen and render correctly
|
|||
|
* @event drawPanels
|
|||
|
* @param {Object} e
|
|||
|
* @param {HTMLElement} e.html the html element to build collan panels on top of
|
|||
|
* @param {AMLElement} e.aml the apf element to build collan panels on top of
|
|||
|
*/
|
|||
|
"drawPanels",
|
|||
|
/**
|
|||
|
* Fires when the collab is connected and the collab workspace is synced
|
|||
|
* @event connect
|
|||
|
*/
|
|||
|
"connect",
|
|||
|
/**
|
|||
|
* Fires when a chat message arrives (the chat plugin should listen to it to get chat messages)
|
|||
|
* @event chatMessage
|
|||
|
* @param {Object} e
|
|||
|
* @param {String} e.userId the chat message author user id
|
|||
|
* @param {String} e.text the chat text to diaplay
|
|||
|
* @param {Boolean} e.increment should the chat counter be incremented (not yet implemented)
|
|||
|
*/
|
|||
|
"chatMessage",
|
|||
|
|
|||
|
"beforeSave",
|
|||
|
|
|||
|
"message"
|
|||
|
],
|
|||
|
/**
|
|||
|
* Get a clone of open collab documents
|
|||
|
* @property {Object} documents
|
|||
|
*/
|
|||
|
get documents() { return util.cloneObject(documents); },
|
|||
|
/**
|
|||
|
* Specifies whether the collab is connected or not
|
|||
|
* @property {Boolean} connected
|
|||
|
*/
|
|||
|
get connected() { return connect.connected; },
|
|||
|
/**
|
|||
|
* Specifies whether the collab debug is enabled or not
|
|||
|
* @property {Boolean} debug
|
|||
|
*/
|
|||
|
get debug() { return connect.debug; },
|
|||
|
/**
|
|||
|
* Get the open collab document with path
|
|||
|
* @param {String} path the file path of the document
|
|||
|
* @return {OTDocument} the collab document open with this path
|
|||
|
*/
|
|||
|
getDocument: function (path) { return documents[path]; },
|
|||
|
/**
|
|||
|
* Send a message to the collab server
|
|||
|
* @param {String} type the type of the message
|
|||
|
* @param {Object} message the message body to send
|
|||
|
*/
|
|||
|
send: function(type, message) { connect.send(type, message); },
|
|||
|
/**
|
|||
|
* @ignore
|
|||
|
*/
|
|||
|
revealUser: revealUser,
|
|||
|
listOpenFiles: listOpenFiles,
|
|||
|
leaveDocument: leaveDocument,
|
|||
|
joinDocument: joinDocument,
|
|||
|
|
|||
|
});
|
|||
|
|
|||
|
register(null, {
|
|||
|
"collab": plugin
|
|||
|
});
|
|||
|
}
|
|||
|
});
|