Collab Module - Message Author

I implemented the Collab module and now I would like to have a way to show which user wrote what on the canvas. Has anybody already implemented something like this ? Could you share some thoughts on how ? Another thing I would like to do is to show the user all new things which were created/edited since the last time he saw the canvas.

Are you talking about how to display the information (I think decorations would be the best approach) or how to find it (the information is in the step history, but it can be a bit tricky to extract it)?

Mainly how to store it and find it. ProseMirror knows the clientID for each step, which is different from my app’s userID, so I suppose the first thing would be to somehow store the userID information for each node?

Not for each node – nodes aren’t the appropriate boundaries for this, since they can contain content inserted by different users. You’ll want to store the steps with additional metainformation (such as timestamps and user), and then build up a data structure that associates ranges in the document with a given user. The change tracking example does something similar (though it tracks ranges per commit, rather than per user).

1 Like

I’ll try that, thanks!

I was able to implement a plugin that shows who wrote what in the canvas based on the commit tracking plugin and now I am trying to make it work with the Collab plugin. First thing I have to do is to have the Central Authority store the data structure that associates ranges in the document with a given user alongside the canonical document version. The problem is that the tracking plugin needs the transform but the CA receives only the steps. I saw it is possible to create a Transform from a list of steps by using the Transform.step function. Should I replace the code in the CA that does this:

this._proseMirrorDoc = Step.fromJSON(canvasSchema, step).apply(this._proseMirrorDoc).doc;

with something like this?

const state = EditorState.create({
      doc: doc,
      plugins: prosemirrorSetup({schema: canvasSchema}).concat( [collab({version: docVersion}), tracking({trackMap: trackMap})] )
});
let transform = new Transform(doc);
stepsFromClient.forEach(step => transform = transform.step(step))
const newState = state.apply(transform);

// store data below.
const doc = newState.doc;
const tracingMap = newState.track$;
const docVersion = newState.collab$.version;

Is that the right approach? I am going to implement it now and will post if it worked.

You probably don’t need an editor state in your collab server. What makes you think you need a transform or a state?

mainly the fact that the tracking plugin uses stuff from the Transform class such as Transform.mapping.

You can construct mappings yourself, without a transform instance, but even if you use a transform (which might be convenient), there’s no reason to create an editor state instance.

You are right. I extracted the tracking function from the tracking plugin and now I use it on the server to build the TrackState data structure without needing a plugin and therefore without needing an EditorState. I also realized the only thing I need from the trackingPlugin is the blameMap and the updateBlameMap function.

I am still struggling to make the tracking plugin work perfectly with the collab module. I changed the trackingFunction (which is used both in the server and inside the plugin) to receive a list of steps, each one of the steps will also have the userId of the user who executed the step.

When the user types something I add the logged user id to the steps:

dispatchTransaction: transaction => {
  this.addUserIdToLocalSteps(transaction)
  this.dispatchTransaction(transaction);
},
....
 private addUserIdToLocalSteps(transaction){
    transaction.steps.forEach(step => step.userId = this.authService.getLoggedUser().id);
 }

When I receive new steps from the server I parse them and add the userId:

private getNewVersionFromServer(){
...
  this.canvasBackendConnector 
      .getStepsSince(getVersion(this.localCanvasState))
      .then(canvasSteps => this.parseStepsAndAddUserId(canvasSteps))
      .then(stepsAndClientIDs => this.receiveRemoteSteps(stepsAndClientIDs))
...
}

...

private parseStepsAndAddUserId(canvasSteps: CanvasStep[]){
    const proseMirrorSteps = [];
    const clientIDs = [];

    canvasSteps.forEach(canvasStep => {
        const parsedStep = Step.fromJSON(canvasSchema, canvasStep.proseMirrorStep);
        parsedStep.userId = canvasStep.userId;
        proseMirrorSteps.push(parsedStep);
        clientIDs.push(canvasStep.clientID);
    });
   
    return {proseMirrorSteps, clientIDs};     
}

...

private receiveRemoteSteps(stepsAndClientIDs){
    const transaction = receiveTransaction(this.localCanvasState, stepsAndClientIDs.proseMirrorSteps, stepsAndClientIDs.clientIDs);
    this.serverTransaction$.next(transaction);
}

Everything works perfectly until there is a conflict. When the conflict happens receiveTramsaction will rebase the steps and when that happens the ‘userId’ information will be lost and the tracking plugin will not work.

I was thinking that only local steps get rebased so only local steps can lose the ‘userId’. If that is the case I could loop through the steps of the transaction returned by receiveTransaction and add the loggedUserId to the ones which don’t have an ‘userId’.

Is that assumption correct ?

Don’t add extra information to the steps, at least not while they are still being handled by the editor. The transaction is probably the proper place to store the user id, and when you call sendableSteps you can look at the origins field of the result to figure out which transaction each step originated from, so that you can send the user ids along to the server.

I had a similar problem when I tried to add the userId to the transaction and not the steps. The problem is that when I call receiveTransaction and there are conflicts the transaction returned will have the remote steps(which belong to one user) and the rebased local steps(which belong to a different user). When this happens the tracking plugin breaks because it treats all steps within the transaction as if they were from the same user.

I already store the steps with the corresponding clientID and userId and all the collaboration code is working well. The problem is making the tracking plugin code work with the collaboration module.

PS: just noticed my changes to make the tracking plugin use the steps instead of the transform broke the plugin :confused:.

If my assumption that during receiveTransaction all steps which lose their userId (because they were rebased) are local steps and if I can make the tracking plugin code work using just the steps I think I would have a working implementation of the collab and tracking plugin.

Summarizing:

  1. Is it right to assume only local steps are rebased?
  2. Can you comment on how to make the tracking plugin work using only the Steps, without a Transform ? I tried something like this:

I replace this

applyTransform(transform) {
   updateBlameMap(this.blameMap, transform, userId)
}
function updateBlameMap(map, transform, id) {
  let result = [], mapping = transform.mapping
...
}

with this:

applyTransform(transform) {
   transform.steps.reduce((blameMap, currStep) => {
           const stepMapping = new Mapping;
           stepMapping.appendMap(currStep.getMap());
           return updateBlameMap(blameMap, stepMapping, currStep.userId)
    }, blameMap);
}
function updateBlameMap(map, mapping, id) {
  let result = []

But it didn’t work.

Ok, I was able to make the tracking plugin code work based only on steps instead of transactions and it seems like my assumption about only local steps being rebased is correct.