/*! * VisualEditor collab extensions. * * @copyright See AUTHORS.txt */ /* global Peer */ ve.collab = {}; /** * Recursively serialize objects into plain data. * * Non-plain objects must have a .serialize or .toJSON method. * * @param {Object|Array} value The value to serialize * @return {Object|Array} The serialized version */ ve.collab.serialize = function ( value ) { if ( Array.isArray( value ) ) { return value.map( ( item ) => ve.collab.serialize( item ) ); } else if ( value === null || typeof value !== 'object' ) { return value; } else if ( value.constructor === Object ) { const serialized = {}; for ( const property in value ) { serialized[ property ] = ve.collab.serialize( value[ property ] ); } return serialized; } else if ( typeof value.serialize === 'function' ) { return ve.collab.serialize( value.serialize() ); } else if ( typeof value.toJSON === 'function' ) { return ve.collab.serialize( value.toJSON() ); } throw new Error( 'Cannot serialize ' + value ); }; ve.collab.newPeer = function () { // To use the public PeerJS server: return new Peer(); // To use a local PeerJS server: // return new Peer( undefined, { host: 'localhost', port: 9000, path: '/myapp' } ); // To use a ve.FakePeer (for debugging): // return new ve.FakePeer(); }; ve.collab.initPeerServer = function ( userName ) { const surface = ve.init.target.surface, completeHistory = surface.model.documentModel.completeHistory; ve.collab.peerServer = new ve.dm.CollabTransportServer( completeHistory.getLength() ); if ( completeHistory.getLength() > 0 ) { completeHistory.transactions[ 0 ].authorId = 1; } ve.collab.peerServer.protocolServer.rebaseServer.updateDocState( // The doc name is arbitrary since the in-browser server only serves one doc 've-collab-doc', 1, ve.dm.Change.static.deserialize( completeHistory.serialize(), true ), completeHistory, {} ); ve.collab.peerServer.peer = ve.collab.newPeer(); ve.collab.peerServer.peer.on( 'open', ( id ) => { /* eslint-disable-next-line no-console */ console.log( 'Open. Now in another browser window, do:\nve.collab.initPeerClient( \'' + id + '\' );' ); ve.collab.initPeerClient( id, true, userName ); } ); ve.collab.peerServer.peer.on( 'connection', ( conn ) => { ve.collab.peerServer.onConnection( conn ); } ); }; ve.collab.initPeerClient = function ( serverId, isMain, userName ) { const surface = ve.init.target.surface, completeHistory = surface.model.documentModel.completeHistory, peerClient = ve.collab.newPeer(); if ( completeHistory.getLength() > 0 ) { completeHistory.transactions[ 0 ].authorId = 1; } // HACK: Disable redo command until supported (T185706) ve.ui.commandRegistry.unregister( 'redo' ); if ( !isMain ) { ve.ui.commandRegistry.unregister( 'showSave' ); } ve.init.target.constructor.static.toolbarGroups = ve.copy( ve.init.target.constructor.static.toolbarGroups ); ve.init.target.constructor.static.toolbarGroups.unshift( { name: 'authorList', include: [ 'authorList' ], align: 'after' } ); peerClient.on( 'open', ( /* id */ ) => { const conn = peerClient.connect( serverId ); // On old js-BinaryPack (before https://github.com/peers/js-binarypack/pull/10 ), // you need JSON serialization, else it crashes on Unicode code points over U+FFFF // var conn = peerClient.connect( serverId, { serialization: 'json' } ); conn.on( 'open', () => { surface.model.createSynchronizer( 'foo', { peerConnection: conn } ); surface.model.synchronizer.commitLength = completeHistory.getLength(); surface.model.synchronizer.sentLength = completeHistory.getLength(); surface.model.synchronizer.once( 'initDoc', ( error ) => { if ( error ) { OO.ui.alert( // eslint-disable-next-line no-jquery/no-append-html $( '
' ).append( ve.htmlMsg( 'visualeditor-rebase-corrupted-document-error', $( '
' ).text( error.stack ) )
),
{ title: ve.msg( 'visualeditor-rebase-corrupted-document-title' ), size: 'large' }
);
return;
}
const toolbar = ve.init.target.getToolbar();
toolbar.setup(
ve.init.target.constructor.static.toolbarGroups,
ve.init.target.surface
);
toolbar.onWindowResize();
if ( !isMain ) {
// eslint-disable-next-line no-jquery/no-global-selector
$( '.ve-ui-toolbar-saveButton' ).css( 'text-decoration', 'line-through' );
}
} );
ve.collab.connectModelSynchronizer();
surface.model.synchronizer.changeAuthor( { name: userName } );
} );
} );
};
ve.collab.connectModelSynchronizer = function () {
const ceSurface = ve.init.target.surface.view;
ceSurface.model.synchronizer.connect( ceSurface, {
authorSelect: 'onSynchronizerAuthorUpdate',
authorChange: 'onSynchronizerAuthorUpdate',
authorDisconnect: 'onSynchronizerAuthorDisconnect',
wrongDoc: 'onSynchronizerWrongDoc',
pause: 'onSynchronizerPause'
} );
};
ve.collab.join = function () {
const serverId = new URLSearchParams( location.search ).get( 'collabSession' );
if ( serverId ) {
// Valid session URL
ve.collab.start( serverId );
}
};
/**
* Top-level function to host or join a collab session
*
* @param {string} [serverId] Id of session to join; undefined means host a new session
*/
ve.collab.start = function ( serverId ) {
if ( serverId ) {
// Join an existing session
ve.init.target.surface.dialogs.openWindow( 'joinCollabDialog' ).closing.then( ( data ) => {
if ( !( data && data.action === 'accept' ) ) {
return;
}
ve.collab.initPeerClient( serverId, false, data.userName );
} );
return;
}
// Else host a new session
ve.init.target.surface.dialogs.openWindow( 'hostCollabDialog' );
};