diff --git a/smuggler-api/src/api.ts b/smuggler-api/src/api.ts index bbc19f79..ec2754ba 100644 --- a/smuggler-api/src/api.ts +++ b/smuggler-api/src/api.ts @@ -154,7 +154,7 @@ async function getNode({ const node = new TNode( nid, ntype, - NodeTextData.fromJson(text), + text as NodeTextData, moment(res.headers[kHeaderCreatedAt]), moment(res.headers[kHeaderLastModified]), meta, @@ -174,7 +174,7 @@ async function updateNode({ text: NodeTextData cancelToken: CancelToken }) { - const value = { text: text.toJson() } + const value = { text } const headers = { [kHeaderContentType]: Mime.JSON, } @@ -247,7 +247,7 @@ async function getNodesSlice({ index_text = null, meta = null, } = item - const textObj = NodeTextData.fromJson(text) + const textObj = text as NodeTextData const extattrsObj = extattrs ? NodeExtattrs.fromJson(extattrs) : null return new TNode( nid, diff --git a/smuggler-api/src/types.ts b/smuggler-api/src/types.ts index 0aa56037..0e4c993a 100644 --- a/smuggler-api/src/types.ts +++ b/smuggler-api/src/types.ts @@ -24,50 +24,12 @@ export function makeEmptySlate(): SlateText { } // see smuggler/src/types.rs -export class NodeTextData { - slate: SlateText - +export type NodeTextData = { + slate: SlateText | undefined // Deprecated - draft: Optional - + draft: any | undefined // Deprecated - chunks: Optional - - constructor(slate: SlateText) { - this.slate = slate - } - - static fromJson({ - slate, - draft, - chunks, - }: { - slate?: SlateText - draft?: object - chunks?: any - }): NodeTextData { - if (slate == null) { - slate = makeEmptySlate() - } - return new NodeTextData(slate) - } - - toJson(): { slate: SlateText } { - const { slate } = this - return { slate } - } - - getText(): SlateText { - const { slate } = this - if (slate) { - return slate - } - return makeEmptySlate() - } - - updateText(slate: SlateText): NodeTextData { - return new NodeTextData(slate) - } + chunks: any | undefined } export enum NodeType { diff --git a/truthsayer/package.json b/truthsayer/package.json index e52e317f..5d1865e8 100644 --- a/truthsayer/package.json +++ b/truthsayer/package.json @@ -52,6 +52,7 @@ "ts-node": "^9.1.1", "typescript": "4.1.3", "universal-cookie": "^4.0", + "use-async-effect": "^2.2.3", "uuid": "^3.3" }, "scripts": { diff --git a/truthsayer/src/card/ChainActionBar.tsx b/truthsayer/src/card/ChainActionBar.tsx index d67ef493..0c8f04ac 100644 --- a/truthsayer/src/card/ChainActionBar.tsx +++ b/truthsayer/src/card/ChainActionBar.tsx @@ -53,11 +53,8 @@ async function cloneNode({ if (!node) { return null } - const doc = makeACopy( - TDoc.fromNodeTextData(node.getText()), - node.getNid(), - isBlank || false - ) + let doc = await TDoc.fromNodeTextData(node.getText()) + doc = doc.makeACopy(node.getNid(), isBlank || false) return await smuggler.node.create({ doc: doc.toNodeTextData(), cancelToken, diff --git a/truthsayer/src/card/FullCard.tsx b/truthsayer/src/card/FullCard.tsx index e25c812f..848997d6 100644 --- a/truthsayer/src/card/FullCard.tsx +++ b/truthsayer/src/card/FullCard.tsx @@ -6,6 +6,7 @@ import { WideCard } from './WideCard' import { DocEditor } from '../doc/DocEditor' import { ImageNode } from '../doc/image/ImageNode' import { SlateText } from '../doc/types' +import { TDoc } from '../doc/doc_util' import { Loader } from '../lib/loader' @@ -18,8 +19,8 @@ export function FullCard({ node, addRef, stickyEdges, saveNode }) { editor = } else { const saveText = (text: SlateText) => { - const newText = node.getText().updateText(text) - saveNode(newText) + const doc = new TDoc(text) + saveNode(doc.toNodeTextData()) } editor = ( diff --git a/truthsayer/src/card/FullCardFootbar.js b/truthsayer/src/card/FullCardFootbar.js index 0fb2fec4..69952391 100644 --- a/truthsayer/src/card/FullCardFootbar.js +++ b/truthsayer/src/card/FullCardFootbar.js @@ -59,28 +59,30 @@ class PrivateFullCardFootbarImpl extends React.Component { handleCopyMarkdown = () => { const toaster = this.props.context.toaster - const md = this.props.getMarkdown() - navigator.clipboard.writeText(md).then( - () => { - /* clipboard successfully set */ - toaster.push({ - title: 'Copied', - message: 'Note copied to clipboard as markdown', - }) - }, - () => { - /* clipboard write failed */ - toaster.push({ - title: 'Error', - message: 'Write to system clipboard failed', - }) - } - ) + this.props.getMarkdown().then((md) => { + navigator.clipboard.writeText(md).then( + () => { + /* clipboard successfully set */ + toaster.push({ + title: 'Copied', + message: 'Note copied to clipboard as markdown', + }) + }, + () => { + /* clipboard write failed */ + toaster.push({ + title: 'Error', + message: 'Write to system clipboard failed', + }) + } + ) + }) } handleDownloadMarkdown = () => { - const md = this.props.getMarkdown() - downloadAsFile(`${this.props.nid}.txt`, md) + this.props.getMarkdown().then((md) => { + downloadAsFile(`${this.props.nid}.txt`, md) + }) } handleArchiveDoc = () => { @@ -238,8 +240,8 @@ export function FullCardFootbar({ children, node, ...rest }) { if (node && node.meta) { const { nid, meta } = node if (node.isOwnedBy(account)) { - const getMarkdown = () => { - return docAsMarkdown(node) + const getMarkdown = async () => { + return await docAsMarkdown(node) } return ( { - const [value, setValue] = useState([]) + const [value, setValue] = useState([]) const [showJinn, setShowJinn] = useState(false) const nid = node.nid - useEffect(() => { - let isSubscribed = true - setValue(node.getText().getText()) - return () => (isSubscribed = false) - }, [nid]) + useAsyncEffect( + async (isMounted) => { + const doc = await TDoc.fromNodeTextData(node.getText()) + if (isMounted()) { + setValue(doc.slate) + } + }, + [nid] + ) const renderElement = useCallback( (props) => , [nid] @@ -133,12 +140,15 @@ export const DocEditor = ({ className, node, saveText }) => { export const ReadOnlyDoc = ({ className, node }) => { const [value, setValue] = useState([]) - useEffect(() => { - let isSubscribed = true - const slateDoc = node.getText().getText() - setValue(slateDoc) - return () => (isSubscribed = false) - }, [node]) + useAsyncEffect( + async (isMounted) => { + const doc = await TDoc.fromNodeTextData(node.getText()) + if (isMounted()) { + setValue(doc.slate) + } + }, + [node] + ) const renderElement = useCallback( (props) => , [node] diff --git a/truthsayer/src/doc/doc_util.test.ts b/truthsayer/src/doc/doc_util.test.ts index 1d58f48d..6fcfec9c 100644 --- a/truthsayer/src/doc/doc_util.test.ts +++ b/truthsayer/src/doc/doc_util.test.ts @@ -3,10 +3,9 @@ import { render } from '@testing-library/react' import { exctractDocTitle, getPlainText, - getDocSlate, makeParagraph, makeLeaf, - TDoc, + makeDoc, } from './doc_util' import { makeChunk } from './chunk_util.jsx' import { markdownToDraft } from '../markdown/conv' @@ -16,32 +15,38 @@ import lodash from 'lodash' test('exctractDocTitle - raw string', async () => { const text = 'RmdBzaGUgdHJpZWQgdG8gd2FzaCBvZm' - const doc = await getDocSlate(text) - const title = exctractDocTitle(doc) + const doc = await makeDoc({ plain: text }) + const title = exctractDocTitle(doc.slate) expect(title).toStrictEqual(text) }) test('exctractDocTitle - empty object', () => { - const slate = [makeParagraph([makeLeaf()])] + const slate = [makeParagraph([makeLeaf('')])] const title = exctractDocTitle(slate) expect(title).toStrictEqual('Some page' + '\u2026') }) test('exctractDocTitle - multiple chunks', async () => { const text = 'RmdB zaGUgdHJpZWQgd G8gd2FzaCBvZm' - const doc: TDoc = { + const doc = await makeDoc({ chunks: [makeChunk(text), makeChunk('asdf'), , makeChunk('123')], - } - const title = exctractDocTitle(await getDocSlate(doc)) + }) + const title = exctractDocTitle(doc.slate) expect(title).toStrictEqual(text) }) test('getPlainText - chunks', () => { const text = 'RmdB zaGUgdHJpZWQgd G8gd2FzaCBvZm' - const doc: TDoc = { - chunks: [makeChunk(text), makeChunk('asdf'), , makeChunk('123')], - } - const texts = getPlainText(doc) + const texts = getPlainText({ + chunks: [ + makeChunk(text) as any, + makeChunk('asdf') as any, + , + makeChunk('123') as any, + ], + slate: undefined, + draft: undefined, + }) expect(texts).toStrictEqual([text, 'asdf', '123']) }) @@ -61,10 +66,11 @@ __Trees were swaying__ [](@1618686400/YYYY-MMMM-DD-dddd) -----` - const doc: TDoc = { + const texts = getPlainText({ draft: markdownToDraft(source), - } - const texts = getPlainText(doc) + slate: undefined, + chunks: undefined, + }) expect(texts).toContain('Header 1') expect(texts).toContain('Header 2') expect(texts).toContain('Schools') @@ -105,10 +111,11 @@ __Trees were swaying__ [](@1618686400/YYYY-MMMM-DD-dddd) -----` - const doc: TDoc = { + const texts = getPlainText({ slate: await markdownToSlate(source), - } - const texts = getPlainText(doc) + draft: undefined, + chunks: undefined, + }) expect(texts).toContain('Header 1') expect(texts).toContain('Header 2') expect(texts).toContain( diff --git a/truthsayer/src/doc/doc_util.ts b/truthsayer/src/doc/doc_util.ts index f9281942..0054407c 100644 --- a/truthsayer/src/doc/doc_util.ts +++ b/truthsayer/src/doc/doc_util.ts @@ -33,29 +33,55 @@ export class TDoc { } toNodeTextData(): NodeTextData { - return new NodeTextData(this.slate) + const { slate } = this + return { slate, draft: null, chunks: null } } - static fromNodeTextData(nodeTextData: NodeTextData): TDoc { - return new TDoc(nodeTextData.getText() as SlateText) + static async fromNodeTextData({ + slate, + draft, + chunks, + }: NodeTextData): Promise { + if (slate) { + return await makeDoc({ slate: slate as SlateText, draft, chunks }) + } + return await makeDoc({ slate: undefined, draft, chunks }) } static makeEmpty(): TDoc { return new TDoc(makeEmptySlate()) } + + makeACopy(nid: string, isBlankCopy: boolean): TDoc { + let { slate } = this + const title = exctractDocTitle(slate) + let label + if (isBlankCopy) { + slate = blankSlate(slate) + label = `Blank copy of "${title}"` + } else { + slate = lodash.cloneDeep(slate) + label = `Copy of "${title}"` + } + slate.push(makeThematicBreak(), makeParagraph([makeNodeLink(label, nid)])) + return new TDoc(slate) + } } -export function exctractDocTitle(slate: SlateText): string { - const title = slate.reduce((acc, item) => { - if (!acc && (isHeaderSlateBlock(item) || isTextSlateBlock(item))) { - const [text, _] = getSlateDescendantAsPlainText(item) - const title = _truncateTitle(text) - if (title) { - return title +export function exctractDocTitle(slate?: SlateText): string { + let title + if (slate) { + title = slate.reduce((acc, item) => { + if (!acc && (isHeaderSlateBlock(item) || isTextSlateBlock(item))) { + const [text, _] = getSlateDescendantAsPlainText(item) + const title = _truncateTitle(text) + if (title) { + return title + } } - } - return acc - }, null) + return acc + }, null) + } return title || 'Some page\u2026' } @@ -92,21 +118,6 @@ function blankSlate(slate: SlateText): SlateText { }) } -export function makeACopy(doc: TDoc, nid: string, isBlankCopy: boolean): TDoc { - let slate = doc.slate - const title = exctractDocTitle(slate) - let label - if (isBlankCopy) { - slate = blankSlate(slate) - label = `Blank copy of "${title}"` - } else { - slate = lodash.cloneDeep(doc.slate) - label = `Copy of "${title}"` - } - slate.push(makeThematicBreak(), makeParagraph([makeNodeLink(label, nid)])) - return new TDoc(slate) -} - // Deprecated export function extractDocAsMarkdown(doc: TDoc): string { if (lodash.isString(doc)) { @@ -134,28 +145,27 @@ export async function enforceTopHeader(doc: TDoc): TDoc { } export async function makeDoc({ + slate, chunks, draft, - slate, - mdText, + plain, }: { + slate?: SlateText chunks?: TChunk[] draft?: TDraftDoc - slate?: SlateText - mdText?: string + plain?: string }): Promise { if (slate) { return new TDoc(slate) } if (chunks) { const slate = await markdownToSlate( - extractDocAsMarkdown( - chunks - .reduce((acc, current) => { - return `${acc}\n\n${current.source}` - }, '') - .trim() - ) + chunks + .map((chunk: any) => { + return (chunk as TChunk).source + }) + .join('\n\n') + .trim() ) return new TDoc(slate) } @@ -163,8 +173,8 @@ export async function makeDoc({ const slate = await markdownToSlate(draftToMarkdown(draft)) return new TDoc(slate) } - if (mdText) { - const slate = await markdownToSlate(mdText) + if (plain) { + const slate = await markdownToSlate(plain) return new TDoc(slate) } return new TDoc(makeEmptySlate()) @@ -194,51 +204,13 @@ export async function getDocDraft(doc: TDoc): TDraftDoc { return await makeDoc({}) } -export async function getDocSlate(doc: TDoc | string): Promise { - let slate - if (lodash.isString(doc)) { - slate = await markdownToSlate(doc) - } else { - doc = doc || {} - slate = doc.slate - if (!lodash.isArray(slate)) { - const { chunks, draft } = doc - if (chunks) { - const source = chunks.reduce((acc, curr) => { - if (isTextChunk(curr)) { - return `${acc}\n\n${curr.source}` - } - return acc - }, '') - slate = await markdownToSlate(source) - } else if (draft) { - // Oh, this is a cheeky approach, but we don't have time - slate = await markdownToSlate(draftToMarkdown(draft)) - } else { - slate = [] - } - } - } - return enforceMinimalSlate(slate) -} - -export async function exctractDoc(doc: TDoc | string): TDoc { - const slate = await getDocSlate(doc) - return await makeDoc({ slate }) -} - -export function getPlainText(doc: TDoc): string[] { - if (lodash.isString(doc)) { - return [doc] - } - doc = doc || {} - const { chunks, draft, slate } = doc +export function getPlainText({ slate, draft, chunks }: NodeTextData): string[] { if (slate) { - return getSlateAsPlainText(slate) + return getSlateAsPlainText(slate as SlateText) } else if (chunks) { return chunks - .map((item) => item.source) - .filter((source) => lodash.isString(source) && source.length > 0) + .map((item: any) => (item as TChunk).source) + .filter((source: any) => lodash.isString(source) && source.length > 0) } else if (draft) { return getDraftAsTextChunks(draft) } @@ -312,15 +284,16 @@ function getDraftAsTextChunks(draft: TDraftDoc): string[] { return lodash.concat(texts, entities) } -export function docAsMarkdown(node: TNode): string { +export async function docAsMarkdown(node: TNode): Promise { let md = '' if (node.isImage()) { const source = node.getBlobSource() md = md.concat(`![](${source})`) } - const text = node.getText().getText() + const text = node.getText() if (text) { - md = md.concat(slateToMarkdown(text)) + const doc = await TDoc.fromNodeTextData(text) + md = md.concat(slateToMarkdown(doc.slate)) } return md } diff --git a/truthsayer/src/doc/editor/plugins/jinn.tsx b/truthsayer/src/doc/editor/plugins/jinn.tsx index fcda3f51..c8b1534d 100644 --- a/truthsayer/src/doc/editor/plugins/jinn.tsx +++ b/truthsayer/src/doc/editor/plugins/jinn.tsx @@ -1,31 +1,20 @@ import React from 'react' import { - Editable, - ReactEditor, - Slate, - useReadOnly, - useSlateStatic, - withReact, -} from 'slate-react' -import { - Descendant, Editor, Element as SlateElement, Point, Range, Transforms, - createEditor, } from 'slate' -import { debug } from '../../../util/log' import { Optional } from '../../../util/types' -import { Modal, Form, ListGroup } from 'react-bootstrap' +import { Modal, Form } from 'react-bootstrap' import { SearchGrid } from '../../../grid/SearchGrid' -import { makeNodeLink, exctractDocTitle, getDocSlate } from '../../doc_util' +import { makeNodeLink, exctractDocTitle } from '../../doc_util' import { dateTimeJinnSearch } from './jinn-datetime' @@ -117,8 +106,8 @@ class JinnModal extends React.Component { onNodeCardClick = (node) => { const nid = node.getNid() - const text = node.getData().getText() - const title = exctractDocTitle(text) + const text = node.getText() + const title = exctractDocTitle(text.slate) const element = makeNodeLink(title, nid) this.insertElement(element) } diff --git a/truthsayer/src/grid/search/search.ts b/truthsayer/src/grid/search/search.ts index a10b5bc7..bfacb4a0 100644 --- a/truthsayer/src/grid/search/search.ts +++ b/truthsayer/src/grid/search/search.ts @@ -1,5 +1,5 @@ import { TNode } from 'smuggler-api' -import { getSlateAsPlainText } from '../../doc/doc_util' +import { getPlainText } from '../../doc/doc_util' import { Optional } from './../../util/types' import { debug } from './../../util/log' @@ -34,7 +34,7 @@ export function searchNodeFor( // Empty search fall back to show everything return node } - const blocks = getSlateAsPlainText(node.getText().getText()) + const blocks = getPlainText(node.getText()) const matchedIndex = blocks.findIndex((text) => { const ret = text.search(pattern) >= 0 return ret diff --git a/truthsayer/src/upload/UploadLocalFile.ts b/truthsayer/src/upload/UploadLocalFile.ts index c4386a53..40d38553 100644 --- a/truthsayer/src/upload/UploadLocalFile.ts +++ b/truthsayer/src/upload/UploadLocalFile.ts @@ -1,7 +1,7 @@ import { smuggler, CancelToken } from 'smuggler-api' import { debug } from '../util/log' -import { exctractDoc } from '../doc/doc_util' +import { makeDoc } from '../doc/doc_util' import { MimeType } from '../util/Mime' import { Optional } from '../util/types' @@ -63,7 +63,7 @@ function uploadLocalTextFile( Math.round((file.size * 100) / 1024) * 100 }KiB\`)*\n` const text = event.target.result + appendix - exctractDoc(text).then((doc) => { + makeDoc({ plain: text }).then((doc) => { smuggler.node .create({ doc, from_nid, to_nid, cancelToken }) .then((node) => { diff --git a/yarn.lock b/yarn.lock index 172b5631..b114921c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12527,6 +12527,11 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-async-effect@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/use-async-effect/-/use-async-effect-2.2.3.tgz#752cc065b7d002c177e754cd0624a166018341bc" + integrity sha512-l/FSqFyqPwhpDJseKyJ9/FZi3PBat2LOcspAqFJznm6H8pgWs67WIjMDr1s3WFBd6cS9zvQrySX0HyHfZi/Upg== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"