Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collaborative Editing using WebRTC ( Only 2 peers support ) #1877

Closed
wants to merge 3 commits into from
Closed

Collaborative Editing using WebRTC ( Only 2 peers support ) #1877

wants to merge 3 commits into from

Conversation

abhishekgahlot
Copy link
Member

@abhishekgahlot abhishekgahlot commented Jul 13, 2017

Work continues in #3741!!!

Please note this is incomplete and is currently being updated.

Detailed Description:

  1. We create a grtc constructor inside middleware.

  2. Middleware is used to listen to all the actions being dispatched so that we can send those to P2P data channel. Few states are filtered in middleware that we don't send.

  3. For peer colors, we use a different action which is always dispatched when you select a block shown on the basis of peerID which is unique to each peer.

  4. Locking also happens on the basis of borders. If border for collaboration is visible on one peer side it is locked.

The module used is: https://github.com/abhishekgahlot/gutenberg-rtc
How GRTC works: https://github.com/abhishekgahlot/gutenberg-rtc/blob/master/DESIGN.md

TODO tasks

Blockers

Improvements

@abhishekgahlot abhishekgahlot changed the title Collaborative Editing using WebRTC ( With 2 peers ) Collaborative Editing using WebRTC ( Only 2 peers support ) Jul 13, 2017
@jasmussen
Copy link
Contributor

Nice! Let's tweak the boundaries of a locked block so they look and behave like other blocks. Like this:

mockup

Let's make sure all floats and alignments work, with the tweaked markup. @youknowriad not urgent but if at some point you have time can you look at this PR? Thanks!

@@ -387,6 +387,7 @@ export default class Editable extends Component {
}

getContent() {
return this.editor.getContent();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentional ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@youknowriad No it's being worked at, I added this because redux states had react elements which I was not able to serialize. This works fine and output string but we are working on this for proper structure.

@@ -345,66 +348,74 @@ class VisualEditorBlock extends Component {
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */
return (
<div
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to avoid creating a new div here, and reuse the inner div?

export function getPeerData( state, uid ) {
const { peerID, peerName, peerColor, blocksByUid } = state.collaborationMode;

if ( peerColor && peerID && peerName && peerID !== window.grtcProps.peerID && blocksByUid === uid ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit weird to use global variables in a selector? What's the purpose of the window.grtcProps ? Can't we move this to the store maybe?

editor/state.js Outdated
@@ -3,6 +3,7 @@
*/
import optimist from 'redux-optimist';
import { combineReducers, applyMiddleware, createStore } from 'redux';
import ReduxThunk from 'redux-thunk';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need redux thunk? We're using an alternative in Gutenberg https://github.com/aduth/refx

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, where are we using thunks in this code?

@youknowriad
Copy link
Contributor

This PR is not easy to review since I'm lacking a lot of information and knowledge using P2P collaboration.

Some questions:

  • Is the gutenberg-rtc lib a generic rtc communication lib or does it have some specificites related to Gutenberg?
  • Should we move it to the Gutenberg repository too?

@abhishekgahlot
Copy link
Member Author

@youknowriad Thanks for the comments, I will check and fix them. Gutenberg RTC is not a generic library but wrapper written over simple-peer library. I didn't add that to Gutenberg because it does one thing which is maintain P2P data channel (encrypted) with the help of serverside key value store.

@youknowriad
Copy link
Contributor

@abhishekgahlot What about moving the library to a root folder in the gutenberg repository? we have several separate modules already, element, utils, components, date...

@abhishekgahlot
Copy link
Member Author

@youknowriad Sure will do that too. Thanks for notifying.

editor/state.js Outdated
} ) );

const enhancers = [ applyMiddleware( refx( effects ) ) ];
const enhancers = [ applyMiddleware( refx( effects ) ), applyMiddleware( ReduxThunk, grtcMiddleware ) ];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be merged into a single applyMiddleware: applyMiddleware( refx( effects ), grtcMiddleware )

editor/state.js Outdated
@@ -3,6 +3,7 @@
*/
import optimist from 'redux-optimist';
import { combineReducers, applyMiddleware, createStore } from 'redux';
import ReduxThunk from 'redux-thunk';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, where are we using thunks in this code?

'border-bottom': '2px solid ' + peerColor,
},
};
grtcProps.lastPeerData = {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Directly mutating the state, Fix this

/**
* WordPress dependencies
*/
import { __ } from 'i18n';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note with #2172, these need to be updated to prefix the dependencies with @wordpress/. You will need to perform a rebase against the latest version of master and apply your changes:

git fetch origin
git rebase origin/master

@abhishekgahlot
Copy link
Member Author

abhishekgahlot commented Sep 26, 2017

@gziolo These are the few things that have been bottleneck till now.

  • Serialization of data so it can be transferred to other peers.
  • Fix the edge case for 2 peers if one takes a longer time to connect can result in both peers in the unstable state.
  • Ability to queue data so that when another peer/user comes he can see the changes already done by another peer/user.
  • Optimization for XHR Polling.
  • Better UI/UX for users when they are connected.
  • Locking of blocks is done by CSS so navigating by keyboard events is still possible.
  • When collaborator leaves, block remains locked.

grtc.on( 'peerData', onReceivedAction );
// Temporary change for testing, Not using alert. Need UI.
grtc.on( 'peerConnected', function() {
alert( 'Peer connected' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should display global notice instead.

Copy link
Member

@gziolo gziolo Oct 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated, but it still needs some work. Moved to todo list.

@@ -320,6 +327,11 @@ class VisualEditorBlock extends Component {
// Generate a class name for the block's editable form
let { className = getBlockDefaultClassname( block.name ) } = blockType;
className = classnames( className, block.attributes.className );
// Don't show controls when collaboration is enabled.
if ( peerShowStyle ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gziolo @abhishekgahlot wonder if the changes to the visual-block component could be abstracted in a HoC withCollabEditing to try to clean this file cleaner.

Copy link
Member

@gziolo gziolo Oct 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be nice, I will explore this idea. Moved to todo list :)

@@ -83,6 +82,76 @@
transition: 0.2s outline;
}

&.is-collaboration {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class should be more semantic: is-being-edited perhaps.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

opacity: 1;
}

&.collab-red {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These classes should be is-red etc

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

opacity: 0.8;
}

&.collab-null {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to support a null class?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

@@ -16,6 +16,14 @@ import { getBlockTypes } from '@wordpress/blocks';
import { combineUndoableReducers } from './utils/undoable-reducer';

/**
* Collaboration Dependencies
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing spaces here it seems

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

*/
import GRTC from '../grtc/app';

const isMobile = window.innerWidth < 782;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem a good place for this.

Copy link
Member

@gziolo gziolo Sep 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both constants were never used. I removed them.

}

/**
* @param {Object} action is redux action
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use // comments for these.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

*/
import { collaborationMode } from '../../actions';

function CollaborationPanel( { active = false, ...props } ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jasmussen had a nicer idea for placing this in an ellipsis menu. (It's ok for an initial pass, though.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to the todo list.

import { collaborationMode } from '../../actions';

function CollaborationPanel( { active = false, ...props } ) {
const changeMode = () => props.collaborationMode( ! active );
Copy link
Member

@mtias mtias Sep 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe "enableCollaboration" or "toggleCollaborationMode"?

Copy link
Member

@gziolo gziolo Oct 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to unify language we use for this feature. If we would go for collaborative editing then I would go for toggleCollaborativeEditing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

grtc/DESIGN.md Outdated
@@ -0,0 +1,193 @@
# Design
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we call this "architecture" to avoid clashes with design docs on searches, etc?

Copy link
Member

@gziolo gziolo Sep 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. Should we keep grtc in its own package?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's much smaller after encryption got removed. If we would keep it, where should we put it in the repository? Can it remain in the top level or rather should be moved to editor top level folder?

grtc/DESIGN.md Outdated
@@ -0,0 +1,193 @@
# Design
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also @gziolo maybe some of these notes can be added as comments in the code, which is always better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the docs, they may require further iterations. Let's wait until we stabilize API.

*
* @param object $data as request data along with args.
* @return bool else username if logged in.
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions need the version since info.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to todo list.

* @param string base64
* @return bool if its valid
*/
function base64_check( $string ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a rather generic function that might need consideration as part of WP utility functions. cc @pento @mdawaffe

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are the extra sanity checks needed over the strict flag in base64_decode()?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

grtc/crypto.js Outdated
@@ -0,0 +1,32 @@
const forge = require( 'node-forge' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Crypto packages tend to be extremely large for browser consumption. This one is approximately 266kb minified and gzipped. I'd sought similar in Automattic/wp-calypso#17356, though I'm unsure of a good alternative for RSA key generation as you need here. Do you know of other options we could evaluate? I think the size as-is is quite excessive.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can remove the support of encryption and trust the turn servers in the relay. It can be removed completely.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed completely with 4ebad12.

grtc/app.js Outdated
@@ -0,0 +1,471 @@
'use strict';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't need 'use strict'; with Babel (it is added automatically)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to split this file up, perhaps into one-file-per-class?

Copy link
Member

@gziolo gziolo Oct 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use strict removed and classes moved to their own files.

grtc/app.js Outdated
const self = this;
return new Promise( ( resolve, reject ) => {
const data = { peerID: self.peerID, peerName: self.peerName, type: 'initial', signal: self.signalID };
jQuery.post( self.url + '/remove', { [ self.grtcID ]: window.btoa( JSON.stringify( data ) ) }, ( resp ) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jQuery is not defined as a dependency for this script (i.e. incidental that another plugin depends on jQuery, but cannot rely on this). In any case, we might want to consider if we can do without it (maybe fetch, which is already promise based?)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial iteration uses window.fetch. We can update later id we decide on some different approach.

grtc/app.js Outdated
clearSignal() {
const self = this;
return new Promise( ( resolve, reject ) => {
const data = { peerID: self.peerID, peerName: self.peerName, type: 'initial', signal: self.signalID };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and the following line could be made more readable by splitting into separate lines.

Copy link
Member

@gziolo gziolo Oct 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Done 👍

grtc/app.js Outdated
* @return {promise} promise object
*/
clearSignal() {
const self = this;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arrow functions preserve this reference, you don't need to create a separate variable for tracking this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, removed all self occurrences.

grtc/app.js Outdated
* signalID is peer signal used to traverse and connect P2P.
*/
constructor( url, grtcID, peerID, peerName, signalID ) {
const self = this;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With arrow functions, I think this pattern of assigning a self variable should be largely avoidable and arguably discouraged.

Copy link
Member

@gziolo gziolo Oct 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

@gziolo gziolo added [Feature] Blocks Overall functionality of blocks [Status] In Progress Tracking issues with work in progress [Type] Task Issues or PRs that have been broken down into an individual action to take labels Oct 2, 2017
@gziolo
Copy link
Member

gziolo commented Oct 6, 2017

@aduth @mtias I addressed almost all your comments. Those remaining are now on the todo list. I did quite a heavy refactoring, but there are a few things that can be further improved. In particular I would like to change the way we shaped Redux state. If I understood properly it can't properly handle more than 2 peers at the moment. Middleware is still a bit hard to read, so I plan to tackle this one, too.

@abhishekgahlot what did you mean by "Optimization for XHR Polling". Can you elaborate on this one?

@abhishekgahlot
Copy link
Member Author

@gziolo Right now the number of requests that goes to the server in order to check for new peers is done every 5 seconds this can become huge very soon. Maybe something can be done to optimize stop polling when both peers connect?

@codecov
Copy link

codecov bot commented Oct 18, 2017

Codecov Report

Merging #1877 into master will increase coverage by 1.35%.
The diff coverage is 40.65%.

Impacted file tree graph

@@            Coverage Diff            @@
##           master   #1877      +/-   ##
=========================================
+ Coverage   31.64%     33%   +1.35%     
=========================================
  Files         235     208      -27     
  Lines        6658    6039     -619     
  Branches     1196    1074     -122     
=========================================
- Hits         2107    1993     -114     
+ Misses       3824    3409     -415     
+ Partials      727     637      -90
Impacted Files Coverage Δ
editor/selectors.js 95.4% <ø> (+1.01%) ⬆️
editor/reducer.js 89.03% <ø> (-1.03%) ⬇️
editor/modes/visual-editor/block.js 0% <ø> (ø) ⬆️
editor/sidebar/coediting-panel/index.js 0% <0%> (ø)
editor/actions.js 33.33% <0%> (-5.8%) ⬇️
blocks/editable/index.js 10.5% <0%> (-0.53%) ⬇️
editor/sidebar/post-settings/index.js 0% <0%> (ø) ⬆️
editor/store.js 80% <100%> (-3.34%) ⬇️
utils/lang.js 100% <100%> (ø)
editor/middlewares.js 14.89% <14.89%> (ø)
... and 138 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e5e6027...84a1680. Read the comment docs.

@abhishekgahlot abhishekgahlot self-assigned this Nov 2, 2017
@abhishekgahlot abhishekgahlot added this to the Future milestone Nov 2, 2017
@gziolo
Copy link
Member

gziolo commented Nov 30, 2017

Closing in favor of #3741.

@gziolo gziolo closed this Nov 30, 2017
@gziolo gziolo added [Feature] Real-time Collaboration Phase 3 of the Gutenberg roadmap around real-time collaboration [Feature] Extensibility The ability to extend blocks or the editing experience [Type] Technical Prototype Offers a technical exploration into an idea as an example of what's possible and removed [Status] In Progress Tracking issues with work in progress labels Nov 7, 2019
Tug pushed a commit that referenced this pull request Feb 28, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Blocks Overall functionality of blocks [Feature] Extensibility The ability to extend blocks or the editing experience [Feature] Real-time Collaboration Phase 3 of the Gutenberg roadmap around real-time collaboration [Type] Task Issues or PRs that have been broken down into an individual action to take [Type] Technical Prototype Offers a technical exploration into an idea as an example of what's possible
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants