fluent-pad/client/src/app/CollaborativeEditor.tsx

172 lines
5.1 KiB
TypeScript
Raw Normal View History

2021-01-14 13:17:48 +03:00
import * as Automerge from 'automerge';
import DiffMatchPatch from 'diff-match-patch';
2021-01-16 01:38:09 +03:00
import { useEffect, useRef, useState } from 'react';
2021-01-14 13:17:48 +03:00
import { fluentPadServiceId, notifyTextUpdateFnName } from 'src/fluence/constants';
import { useFluenceClient } from './FluenceClientContext';
import * as calls from 'src/fluence/calls';
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-16 01:38:09 +03:00
return JSON.parse(entry);
2021-01-14 13:17:48 +03:00
} 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;
};
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 01:38:09 +03:00
const [text, setText] = useState('');
// const textAreaRef = useRef<HTMLTextAreaElement>(null);
const docSetRef = useRef(new Automerge.DocSet<TextDoc>());
const [amConnection, setAmConnection] = useState<any>();
2021-01-14 13:17:48 +03:00
useEffect(() => {
2021-01-16 01:38:09 +03:00
const doc = Automerge.from({ value: new Automerge.Text() });
docSetRef.current.setDoc('doc', doc);
docSetRef.current.registerHandler((id, doc) => {
if (id === 'doc') {
setText(doc.value.toString());
}
});
const connection = new Automerge.Connection(docSetRef.current, (msg) => {
console.log('on update');
calls.addEntry(client, JSON.stringify(msg));
});
connection.open();
setAmConnection(connection);
2021-01-14 13:17:48 +03:00
const unsub1 = subscribeToEvent(client, fluentPadServiceId, notifyTextUpdateFnName, (args, tetraplets) => {
const [authorPeerId, stateStr, isAuthorized] = args;
if (authorPeerId === client.selfPeerId.toB58String()) {
return;
}
2021-01-14 23:57:24 +03:00
const state = parseState(stateStr);
2021-01-16 01:38:09 +03:00
console.log(state);
if (state) {
connection.receiveMsg(state);
2021-01-14 23:57:24 +03:00
}
2021-01-14 13:17:48 +03:00
});
// don't block
calls.getHistory(client).then((res) => {
2021-01-16 01:38:09 +03:00
for (let e of res) {
try {
const msg = JSON.parse(e.body);
connection.receiveMsg(msg);
} catch (e) {
console.log("history didn't work", e);
}
}
// setText(newDoc);
2021-01-14 13:17:48 +03:00
});
return () => {
unsub1();
};
}, []);
2021-01-16 01:38:09 +03:00
// const amHistory = text
// ? Automerge.getHistory(text).map((x) => {
// return x.snapshot.value;
// })
// : [];
2021-01-14 13:17:48 +03:00
2021-01-16 01:38:09 +03:00
// const textValue = text ? text.value : '';
2021-01-14 13:17:48 +03:00
const handleTextUpdate = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
2021-01-16 01:38:09 +03:00
setText(e.target.value);
2021-01-14 13:17:48 +03:00
2021-01-16 01:38:09 +03:00
let doc = docSetRef.current.getDoc('doc');
if (doc) {
let res = getUpdatedDocFromText(doc, e.target.value);
console.log(res);
docSetRef.current.setDoc('doc', res!);
}
2021-01-14 13:17:48 +03:00
};
return (
<div>
2021-01-16 01:38:09 +03:00
<textarea value={text} onChange={handleTextUpdate} />
<div>
Automerge changes:
<ul>
2021-01-16 01:38:09 +03:00
{/* {amHistory.map((value, index) => (
<li key={index}>{value}</li>
2021-01-16 01:38:09 +03:00
))} */}
</ul>
</div>
2021-01-14 13:17:48 +03:00
</div>
);
};