screenshare2.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. ScreenShare = (function() {
  2. function encode(cells) {
  3. const codec = new mxCodec();
  4. const encoded = codec.encode(cells);
  5. return mxUtils.getXml(encoded);
  6. };
  7. return class {
  8. decode(xmlString) {
  9. const parsedXml = mxUtils.parseXml(xmlString).documentElement;
  10. const codec = new mxCodec();
  11. codec.lookup = id => this.graph.model.cells[id];
  12. return codec.decode(parsedXml);
  13. }
  14. constructor(client, peers, graph, undoManager, confirm, alert) {
  15. this.graph = graph;
  16. this.undoManager = undoManager;
  17. this.sharingWith = null;
  18. this.confirm = confirm;
  19. this.alert = alert;
  20. const otherPeerEndScreenshare = peer => {
  21. if (this.sharingWith === peer) {
  22. this.alert(`Peer ${shortUUID(peer)} left. You are alone again.`);
  23. this.sharingWith = null;
  24. }
  25. }
  26. peers.on('leave', otherPeerEndScreenshare);
  27. const share = (what, data) => {
  28. if (this.sharingWith) {
  29. this.p2p.send(this.sharingWith, what, data,
  30. err => { if (err) console.log("ignoring err:", err) });
  31. }
  32. }
  33. let listenerEnabled = true;
  34. this.undoManager.addListener(null, (source, eventObj) => {
  35. if (listenerEnabled) {
  36. if (eventObj.properties.edit) {
  37. const {changes, redone, undone, significant} = eventObj.properties.edit;
  38. share("undoEvent", {
  39. encodedChanges: changes.map(c => encode(c)),
  40. redone,
  41. undone,
  42. significant,
  43. });
  44. }
  45. }
  46. });
  47. this.graph.selectionModel.addListener(mxEvent.CHANGE, (source, eventObj) => {
  48. if (listenerEnabled) {
  49. const {added, removed} = eventObj.properties;
  50. share("selectionEvent", {
  51. addedIds: removed ? removed.map(cell => cell.id) : [],
  52. removedIds: added ? added.map(cell => cell.id) : [],
  53. });
  54. }
  55. });
  56. // Locking
  57. const locked = {}; // map cell id => mxCellHighlight
  58. const lockCell = cell => {
  59. const highlight = locked[cell.id];
  60. if (!highlight) {
  61. const highlight = new mxCellHighlight(this.graph, "#7700ff", 6);
  62. highlight.highlight(this.graph.view.getState(cell));
  63. locked[cell.id] = highlight;
  64. }
  65. };
  66. const unlockCell = cell => {
  67. const highlight = locked[cell.id];
  68. if (highlight) {
  69. highlight.destroy();
  70. delete locked[cell.id]
  71. }
  72. }
  73. // Locking part #1: Intercepting mxGraph.fireMouseEvent
  74. const oldFireMouseEvent = this.graph.fireMouseEvent;
  75. this.graph.fireMouseEvent = function(evtName, me, sender) {
  76. if (me.state && locked[me.state.cell.id]) {
  77. // clicked shape is locked
  78. return;
  79. }
  80. oldFireMouseEvent.apply(this, arguments);
  81. }
  82. // Locking part #2: Ignore double clicks on locked cells
  83. const oldDblClick = this.graph.dblClick;
  84. this.graph.dblClick = function(evt, cell) {
  85. if (cell && locked[cell.id]) {
  86. // clicked shape is locked
  87. return;
  88. }
  89. oldDblClick.apply(this, arguments);
  90. }
  91. // Locking part #3: Protect locked cells from ever being selected
  92. const oldMxSelectionChange = mxSelectionChange; // override constructor :)
  93. mxSelectionChange = function(selectionModel, added, removed) {
  94. oldMxSelectionChange.apply(this, arguments);
  95. if (this.added) {
  96. this.added = this.added.filter(cell => !locked[cell.id]);
  97. }
  98. }
  99. mxSelectionChange.prototype = oldMxSelectionChange.prototype;
  100. // mxGraphHandler overrides to get previews of moving shapes
  101. // These overrides wrap the original implementations and additionally send messages to the "screensharee".
  102. // The screensharee uses this messages to draw previews at his side.
  103. // Begin of move
  104. const oldStart = this.graph.graphHandler.start;
  105. this.graph.graphHandler.start = function(cell, x, y, cells) {
  106. oldStart.apply(this, arguments);
  107. // cells that will be moved on our side
  108. cells = this.graph.graphHandler.getCells(cell);
  109. share("graphHandlerStart", {
  110. cellIds: cells.map(cell => cell.id),
  111. x, y
  112. });
  113. };
  114. // Redraw operation, caused by mouseMove event, during move
  115. const oldUpdateLivePreview = this.graph.graphHandler.updateLivePreview;
  116. this.graph.graphHandler.updateLivePreview = function(dx, dy) {
  117. oldUpdateLivePreview.apply(this, arguments);
  118. share("graphHandlerUpdateLivePreview", {dx, dy});
  119. }
  120. // End of move
  121. const oldReset = this.graph.graphHandler.reset;
  122. this.graph.graphHandler.reset = function() {
  123. oldReset.apply(this, arguments);
  124. share("graphHandlerReset", null); // no data
  125. };
  126. //// VERTEX HANDLER OVERRIDES - a broken attempt at previewing resizing shapes ....
  127. // const oldVertexStart = mxVertexHandler.prototype.start;
  128. // mxVertexHandler.prototype.start = function(x, y, index) {
  129. // console.log("begin resize", this, x, y , index);
  130. // oldVertexStart.apply(this, arguments);
  131. // shareFunctionCall("vertexHandlerStart", {
  132. // cellId: this.state.cell.id,
  133. // x, y,
  134. // index, // number (0-7) of resize handle pressed
  135. // });
  136. // }
  137. // const oldVertexReset = mxVertexHandler.prototype.reset;
  138. // mxVertexHandler.prototype.reset = function() {
  139. // console.log("reset resize");
  140. // oldVertexReset.apply(this, arguments);
  141. // shareFunctionCall("vertexHandlerReset", {
  142. // cellId: this.state.cell.id,
  143. // });
  144. // }
  145. // const oldVertexUpdateLivePreview = mxVertexHandler.prototype.updateLivePreview;
  146. // mxVertexHandler.prototype.updateLivePreview = function(me) {
  147. // console.log("update resize preview", me);
  148. // oldVertexUpdateLivePreview.apply(this, arguments);
  149. // shareFunctionCall("vertexHandlerUpdateLivePreview", {
  150. // cellId: this.state.cell.id,
  151. // bounds: {
  152. // x: this.bounds.x,
  153. // y: this.bounds.y,
  154. // width: this.bounds.width,
  155. // height: this.bounds.height,
  156. // },
  157. // });
  158. // }
  159. // Handler for incoming requests from other peers
  160. this.p2p = new PeerToPeer(client, {
  161. // Handlers for received mxGraphHandler messages that we sent above.
  162. // mxGraphHandler (moving cells)
  163. "graphHandlerStart": (from, {cellIds, x, y}, reply) => {
  164. if (this.sharingWith === from) {
  165. // the mxGraphHandler will determine the cells to move based on the current selection
  166. // a hack within a hack - we temporarily override 'getCells':
  167. const oldGetCells = this.graph.graphHandler.getCells;
  168. this.graph.graphHandler.getCells = function(initialCell) {
  169. return cellIds.map(id => this.graph.model.cells[id]);
  170. }
  171. oldStart.apply(this.graph.graphHandler, [
  172. null, // 'cells' - this argument isn't important since we overrided getCells
  173. x, y,
  174. null,
  175. ]);
  176. this.graph.graphHandler.checkPreview(); // force some stuff to happen
  177. this.graph.graphHandler.getCells = oldGetCells; // restore override
  178. }
  179. reply();
  180. },
  181. "graphHandlerUpdateLivePreview": (from, {dx,dy}, reply) => {
  182. if (this.sharingWith === from) {
  183. oldUpdateLivePreview.apply(this.graph.graphHandler, [dx, dy]);
  184. }
  185. reply();
  186. },
  187. "graphHandlerReset": (from, _, reply) => {
  188. if (this.sharingWith === from) {
  189. oldReset.apply(this.graph.graphHandler, []);
  190. }
  191. reply();
  192. },
  193. "undoEvent": (from, {encodedChanges, undone, redone, significant}, reply) => {
  194. if (this.sharingWith === from) {
  195. try {
  196. listenerEnabled = false;
  197. // Undoable Edit happened at other peer
  198. const changes = encodedChanges.map(encoded => {
  199. const change = this.decode(encoded);
  200. change.model = this.graph.model;
  201. return change
  202. });
  203. if (undone) {
  204. this.undoManager.undo();
  205. }
  206. else if (redone) {
  207. this.undoManager.redo();
  208. }
  209. else {
  210. // Probably not necessary to wrap in update-transaction, but won't do harm:
  211. this.graph.model.beginUpdate();
  212. changes.forEach(change => this.graph.model.execute(change));
  213. this.graph.model.endUpdate();
  214. }
  215. }
  216. finally {
  217. listenerEnabled = true;
  218. reply(); // acknowledge
  219. }
  220. }
  221. },
  222. "selectionEvent": (from, {addedIds, removedIds}, reply) => {
  223. if (this.sharingWith === from) {
  224. try {
  225. listenerEnabled = false;
  226. // Selection changed at other peer - lock selected cells
  227. const removed = removedIds.map(id => this.graph.model.cells[id]);
  228. const added = addedIds.map(id => this.graph.model.cells[id]);
  229. removed.forEach(unlockCell);
  230. added.forEach(lockCell);
  231. }
  232. finally {
  233. listenerEnabled = true;
  234. reply(); // acknowledge
  235. }
  236. }
  237. },
  238. // Received Screen Share request
  239. "init_screenshare": (from, {graphSerialized, selectedCellIds}, reply) => {
  240. const yes = () => {
  241. const doc = mxUtils.parseXml(graphSerialized);
  242. const codec = new mxCodec(doc);
  243. codec.decode(doc.documentElement, this.graph.model);
  244. selectedCellIds.forEach(id => lockCell(this.graph.model.cells[id]));
  245. this.sharingWith = from;
  246. reply(); // acknowledge
  247. this.alert("You are now <b>screen sharing</b> with " + shortUUID(from));
  248. };
  249. const no = () => {
  250. reply("denied")
  251. };
  252. this.confirm(`Peer ${shortUUID(from)} wants to <b>screen share</b>.<br />Your diagram will be erased and replaced by his/hers.<br /><br />Accept?`, yes, no);
  253. },
  254. "end_screenshare": (from, data, reply) => {
  255. reply();
  256. otherPeerEndScreenshare(from);
  257. }
  258. });
  259. }
  260. initshare(peer) {
  261. const doIt = () => {
  262. const graphSerialized = encode(this.graph.model);
  263. const selectedCellIds = this.graph.getSelectionCells().map(cell => cell.id);
  264. this.p2p.send(peer, "init_screenshare", {
  265. graphSerialized,
  266. selectedCellIds,
  267. }, (err, data) => {
  268. if (err) {
  269. if (err === "denied") {
  270. this.alert(`Peer ${peer} <b>denied</b> your sharing request :(`);
  271. } else {
  272. this.alert("Error sending screenshare request: " + err);
  273. }
  274. }
  275. else {
  276. this.alert("Accepted: You are now <b>screen sharing</b> with " + shortUUID(peer));
  277. this.sharingWith = peer;
  278. }
  279. });
  280. this.alert("Request sent. Awaiting response.")
  281. }
  282. if (this.sharingWith && this.sharingWith !== peer) {
  283. // first, end earlier screenshare
  284. const yes = () => {
  285. this.p2p.send(this.sharingWith, "end_screenshare", null, (err, data) => {
  286. // don't care about response
  287. doIt();
  288. })
  289. };
  290. const no = () => {
  291. // do nothing
  292. }
  293. this.confirm(`To screenshare with peer ${shortUUID(peer)}, you <b>first have to end your screenshare</b> with peer ${shortUUID(this.sharingWith)}.<br /><br />OK?`,
  294. yes, no);
  295. } else {
  296. doIt();
  297. }
  298. }
  299. }
  300. })();