2021-01-14 13:17:48 +03:00
|
|
|
import * as Automerge from 'automerge';
|
|
|
|
import DiffMatchPatch from 'diff-match-patch';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
import { fluentPadServiceId, notifyTextUpdateFnName } from 'src/fluence/constants';
|
|
|
|
import { useFluenceClient } from './FluenceClientContext';
|
|
|
|
import * as calls from 'src/fluence/calls';
|
2021-01-16 00:48:57 +03:00
|
|
|
import { FluenceClient, subscribeToEvent } from '@fluencelabs/fluence';
|
|
|
|
import _ from 'lodash';
|
2021-01-14 13:17:48 +03:00
|
|
|
|
|
|
|
interface TextDoc {
|
|
|
|
value: Automerge.Text;
|
|
|
|
}
|
|
|
|
|
|
|
|
const dmp = new DiffMatchPatch();
|
|
|
|
|
|
|
|
const getUpdatedDocFromText = (oldDoc: TextDoc | null, newText: string) => {
|
|
|
|
const prevText = oldDoc ? oldDoc.value.toString() : '';
|
|
|
|
const diff = dmp.diff_main(prevText, newText);
|
|
|
|
dmp.diff_cleanupSemantic(diff);
|
|
|
|
const patches = dmp.patch_make(prevText, diff);
|
|
|
|
|
|
|
|
const newDoc = Automerge.change(oldDoc, (doc) => {
|
|
|
|
patches.forEach((patch) => {
|
|
|
|
let idx = patch.start1;
|
|
|
|
patch.diffs.forEach(([operation, changeText]) => {
|
|
|
|
switch (operation) {
|
|
|
|
case 1: // Insertion
|
|
|
|
doc.value.insertAt!(idx, ...changeText.split(''));
|
|
|
|
break;
|
|
|
|
case 0: // No Change
|
|
|
|
idx += changeText.length;
|
|
|
|
break;
|
|
|
|
case -1: // Deletion
|
|
|
|
for (let i = 0; i < changeText.length; i++) {
|
|
|
|
doc.value.deleteAt!(idx);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
return newDoc;
|
|
|
|
};
|
|
|
|
|
2021-01-14 23:57:24 +03:00
|
|
|
const parseState = (entry: string) => {
|
2021-01-14 13:17:48 +03:00
|
|
|
try {
|
2021-01-14 23:57:24 +03:00
|
|
|
const obj = JSON.parse(entry);
|
2021-01-14 13:17:48 +03:00
|
|
|
if (obj.fluentPadState) {
|
|
|
|
return Automerge.load(obj.fluentPadState) as TextDoc;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
} catch (e) {
|
2021-01-14 23:57:24 +03:00
|
|
|
console.log('couldnt parse state format: ' + entry);
|
2021-01-14 13:17:48 +03:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-01-14 19:47:26 +03:00
|
|
|
const applyStates = (startingDoc: TextDoc | null, entries: calls.Entry[]) => {
|
2021-01-14 13:17:48 +03:00
|
|
|
let res = startingDoc;
|
2021-01-14 19:47:26 +03:00
|
|
|
for (let entry of entries) {
|
2021-01-14 23:57:24 +03:00
|
|
|
const state = parseState(entry.body) as TextDoc;
|
2021-01-14 13:17:48 +03:00
|
|
|
if (state) {
|
|
|
|
if (!res) {
|
|
|
|
res = state;
|
|
|
|
} else {
|
|
|
|
res = Automerge.merge(res, state);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-14 23:57:24 +03:00
|
|
|
if (res === null) {
|
|
|
|
res = Automerge.from({
|
|
|
|
value: new Automerge.Text(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-01-14 13:17:48 +03:00
|
|
|
return res;
|
|
|
|
};
|
|
|
|
|
2021-01-16 00:48:57 +03:00
|
|
|
const broadcastUpdates = _.debounce(async (client: FluenceClient, doc: TextDoc) => {
|
|
|
|
const entry = {
|
|
|
|
fluentPadState: Automerge.save(doc),
|
|
|
|
};
|
|
|
|
const entryStr = JSON.stringify(entry);
|
|
|
|
|
|
|
|
await calls.addEntry(client, entryStr);
|
|
|
|
}, 200);
|
|
|
|
|
2021-01-14 13:17:48 +03:00
|
|
|
export const CollaborativeEditor = () => {
|
|
|
|
const client = useFluenceClient()!;
|
2021-01-16 00:48:57 +03:00
|
|
|
const [text, setText] = useState<TextDoc | null>(Automerge.from({ value: new Automerge.Text() }));
|
2021-01-14 13:17:48 +03:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const unsub1 = subscribeToEvent(client, fluentPadServiceId, notifyTextUpdateFnName, (args, tetraplets) => {
|
2021-01-16 00:48:57 +03:00
|
|
|
const [authorPeerId, stateStr, isAuthorized] = args;
|
|
|
|
if (authorPeerId === client.selfPeerId.toB58String()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-01-14 23:57:24 +03:00
|
|
|
const state = parseState(stateStr);
|
|
|
|
if (state && text) {
|
|
|
|
const newDoc = Automerge.merge(text, state);
|
|
|
|
setText(newDoc);
|
|
|
|
}
|
2021-01-14 13:17:48 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
// don't block
|
|
|
|
calls.getHistory(client).then((res) => {
|
|
|
|
const newDoc = applyStates(text, res);
|
|
|
|
setText(newDoc);
|
|
|
|
});
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
unsub1();
|
|
|
|
};
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const amHistory = text
|
|
|
|
? Automerge.getHistory(text).map((x) => {
|
|
|
|
return x.snapshot.value;
|
|
|
|
})
|
|
|
|
: [];
|
|
|
|
|
|
|
|
const textValue = text ? text.value.toString() : '';
|
|
|
|
|
|
|
|
const handleTextUpdate = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
|
|
const newDoc = getUpdatedDocFromText(text, e.target.value)!;
|
|
|
|
setText(newDoc);
|
|
|
|
|
|
|
|
// don't block
|
2021-01-16 00:48:57 +03:00
|
|
|
setImmediate(broadcastUpdates, client, newDoc);
|
2021-01-14 13:17:48 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<textarea value={textValue} disabled={!text} onChange={handleTextUpdate} />
|
2021-01-16 00:48:57 +03:00
|
|
|
<div>
|
|
|
|
Automerge changes:
|
|
|
|
<ul>
|
|
|
|
{amHistory.map((value, index) => (
|
|
|
|
<li key={index}>{value}</li>
|
|
|
|
))}
|
|
|
|
</ul>
|
|
|
|
</div>
|
2021-01-14 13:17:48 +03:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|