mirror of
https://github.com/urbit/shrub.git
synced 2025-01-03 10:02:32 +03:00
publish: update FE for versioned posts
This commit is contained in:
parent
a81c41b942
commit
80f5747cd5
117
pkg/interface/src/logic/lib/publish.ts
Normal file
117
pkg/interface/src/logic/lib/publish.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import { Post, GraphNode, TextContent, Graph, NodeMap } from "~/types";
|
||||
|
||||
const buntPost = (): Post => ({
|
||||
author: '',
|
||||
contents: [],
|
||||
hash: null,
|
||||
index: '',
|
||||
signatures: [],
|
||||
'time-sent': 0
|
||||
});
|
||||
|
||||
export function makeNodeMap(posts: Post[]): Record<string, GraphNode> {
|
||||
let nodes = {};
|
||||
posts.forEach((p) => {
|
||||
nodes[p.index] = { children: { empty: null }, post: p };
|
||||
});
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export function newPost(
|
||||
title: string,
|
||||
body: string
|
||||
): [number, NodeMap] {
|
||||
const now = Date.now();
|
||||
const root: Post = {
|
||||
author: `~${window.ship}`,
|
||||
index: "/" + now,
|
||||
"time-sent": now,
|
||||
contents: [],
|
||||
hash: null,
|
||||
signatures: [],
|
||||
};
|
||||
|
||||
const revContainer: Post = { ...root, index: root.index + "/1" };
|
||||
const commentsContainer = { ...root, index: root.index + "/2" };
|
||||
|
||||
const firstRevision: Post = {
|
||||
...revContainer,
|
||||
index: revContainer.index + "/1",
|
||||
contents: [{ text: title }, { text: body }],
|
||||
};
|
||||
|
||||
const nodes = {
|
||||
[root.index]: {
|
||||
post: root,
|
||||
children: {
|
||||
graph: {
|
||||
1: {
|
||||
post: revContainer,
|
||||
children: {
|
||||
graph: {
|
||||
1: {
|
||||
post: firstRevision,
|
||||
children: { empty: null },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
2: {
|
||||
post: commentsContainer,
|
||||
children: { empty: null },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return [now, nodes];
|
||||
}
|
||||
|
||||
export function editPost(rev: number, noteId: number, title: string, body: string) {
|
||||
const now = Date.now();
|
||||
const newRev: Post = {
|
||||
author: `~${window.ship}`,
|
||||
index: `/${noteId}/1/${rev}`,
|
||||
"time-sent": now,
|
||||
contents: [{ text: title }, { text: body }],
|
||||
hash: null,
|
||||
signatures: [],
|
||||
};
|
||||
const nodes = {
|
||||
[newRev.index]: {
|
||||
post: newRev,
|
||||
children: { empty: null }
|
||||
}
|
||||
};
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export function getLatestRevision(node: GraphNode): [number, string, string, Post] {
|
||||
const revs = node.children.get(1);
|
||||
const empty = [1, "", "", buntPost()] as [number, string, string, Post];
|
||||
if(!revs) {
|
||||
return empty;
|
||||
}
|
||||
const [revNum, rev] = [...revs.children][0];
|
||||
if(!rev) {
|
||||
return empty
|
||||
}
|
||||
const [title, body] = rev.post.contents as TextContent[];
|
||||
return [revNum, title.text, body.text, rev.post];
|
||||
}
|
||||
|
||||
export function getComments(node: GraphNode): GraphNode {
|
||||
const comments = node.children.get(2);
|
||||
if(!comments) {
|
||||
return { post: buntPost(), children: new Map() }
|
||||
}
|
||||
return comments;
|
||||
}
|
||||
|
||||
export function getSnippet(body: string) {
|
||||
const start = body.slice(0, 400);
|
||||
return start === body ? start : `${start}...`;
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import {createPost} from "~/logic/api/graph";
|
||||
import { LocalUpdateRemoteContentPolicy } from "~/types";
|
||||
|
||||
interface CommentsProps {
|
||||
comments: Graph;
|
||||
comments: GraphNode;
|
||||
book: string;
|
||||
note: GraphNode;
|
||||
ship: string;
|
||||
@ -32,7 +32,7 @@ export function Comments(props: CommentsProps) {
|
||||
actions: FormikHelpers<{ comment: string }>
|
||||
) => {
|
||||
try {
|
||||
const post = createPost([{ text: comment }], note?.post?.index);
|
||||
const post = createPost([{ text: comment }], comments?.post?.index);
|
||||
await api.graph.addPost(ship, book, post)
|
||||
actions.resetForm();
|
||||
actions.setStatus({ success: null });
|
||||
@ -45,7 +45,7 @@ export function Comments(props: CommentsProps) {
|
||||
return (
|
||||
<Col>
|
||||
<CommentInput onSubmit={onSubmit} />
|
||||
{Array.from(comments).reverse().map(([idx, comment]) => (
|
||||
{Array.from(comments.children).reverse().map(([idx, comment]) => (
|
||||
<CommentItem
|
||||
comment={comment}
|
||||
key={idx}
|
||||
|
@ -4,6 +4,8 @@ import { FormikHelpers } from "formik";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { GraphNode, TextContent } from "~/types";
|
||||
import { getLatestRevision, editPost } from "~/logic/lib/publish";
|
||||
import {useWaitForProps} from "~/logic/lib/useWaitForProps";
|
||||
interface EditPostProps {
|
||||
ship: string;
|
||||
noteId: number;
|
||||
@ -14,10 +16,11 @@ interface EditPostProps {
|
||||
|
||||
export function EditPost(props: EditPostProps & RouteComponentProps) {
|
||||
const { note, book, noteId, api, ship, history } = props;
|
||||
const [title, file] = note.post.contents as TextContent[];
|
||||
const body = file.text.slice(file.text.indexOf(";>") + 2);
|
||||
const [revNum, title, body] = getLatestRevision(note);
|
||||
|
||||
const waiter = useWaitForProps(props);
|
||||
const initial: PostFormSchema = {
|
||||
title: title.text,
|
||||
title,
|
||||
body,
|
||||
};
|
||||
|
||||
@ -27,8 +30,14 @@ export function EditPost(props: EditPostProps & RouteComponentProps) {
|
||||
) => {
|
||||
const { title, body } = values;
|
||||
try {
|
||||
// graph-store does not support editing nodes
|
||||
throw new Error("Unsupported");
|
||||
const newRev = revNum + 1;
|
||||
const nodes = editPost(newRev, noteId, title, body);
|
||||
await api.graph.addNodes(ship, book, nodes);
|
||||
await waiter(p => {
|
||||
const [rev] = getLatestRevision(note);
|
||||
return rev === newRev;
|
||||
});
|
||||
history.push(`/~publish/notebook/ship/${ship}/${book}/note/${noteId}`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
actions.setStatus({ error: "Failed to edit notebook" });
|
||||
@ -39,7 +48,7 @@ export function EditPost(props: EditPostProps & RouteComponentProps) {
|
||||
<PostForm
|
||||
initial={initial}
|
||||
onSubmit={onSubmit}
|
||||
submitLabel={`Update ${title.text}`}
|
||||
submitLabel={`Update ${title}`}
|
||||
loadingText="Updating..."
|
||||
/>
|
||||
);
|
||||
|
@ -6,6 +6,7 @@ import { Spinner } from "~/views/components/Spinner";
|
||||
import { Comments } from "./Comments";
|
||||
import { NoteNavigation } from "./NoteNavigation";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { getLatestRevision, getComments } from '~/logic/lib/publish';
|
||||
import { Author } from "./Author";
|
||||
import { Contacts, GraphNode, Graph, LocalUpdateRemoteContentPolicy } from "~/types";
|
||||
|
||||
@ -34,18 +35,24 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
props.history.push(baseUrl);
|
||||
};
|
||||
|
||||
const comments = note?.children
|
||||
const file = note?.post?.contents[1]?.text || "";
|
||||
const newfile = file ? file.slice(file.indexOf(";>") + 2) : "";
|
||||
|
||||
const comments = getComments(note);
|
||||
const [revNum, title, body, post] = getLatestRevision(note);
|
||||
|
||||
const noteId = parseInt(note.post.index.split('/')[1], 10);
|
||||
|
||||
let adminLinks: JSX.Element | null = null;
|
||||
if (window.ship === note?.post?.author) {
|
||||
adminLinks = (
|
||||
<Box display="inline-block">
|
||||
<Link to={`${baseUrl}/note/${noteId}/edit`}>
|
||||
<Text
|
||||
color="green"
|
||||
ml={2}
|
||||
>
|
||||
Update
|
||||
</Text>
|
||||
</Link>
|
||||
<Text
|
||||
className="dib f9 red2 ml2 pointer"
|
||||
color="red"
|
||||
ml={2}
|
||||
onClick={deletePost}
|
||||
@ -72,20 +79,20 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
<Text>{"<- Notebook Index"}</Text>
|
||||
</Link>
|
||||
<Col>
|
||||
<Text display="block" mb={2}>{note?.post?.contents[0]?.text || ""}</Text>
|
||||
<Text display="block" mb={2}>{title || ""}</Text>
|
||||
<Box display="flex">
|
||||
<Author
|
||||
hideNicknames={props.hideNicknames}
|
||||
hideAvatars={props.hideAvatars}
|
||||
ship={note?.post?.author}
|
||||
ship={post?.author}
|
||||
contacts={contacts}
|
||||
date={note?.post?.["time-sent"]}
|
||||
date={post?.["time-sent"]}
|
||||
/>
|
||||
<Text ml={2}>{adminLinks}</Text>
|
||||
</Box>
|
||||
</Col>
|
||||
<Box color="black" className="md" style={{ overflowWrap: "break-word" }}>
|
||||
<ReactMarkdown source={newfile} linkTarget={"_blank"} />
|
||||
<ReactMarkdown source={body} linkTarget={"_blank"} />
|
||||
</Box>
|
||||
<NoteNavigation
|
||||
notebook={notebook}
|
||||
|
@ -3,6 +3,7 @@ import moment from "moment";
|
||||
import { Box } from "@tlon/indigo-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {Graph, GraphNode} from "~/types";
|
||||
import {getLatestRevision} from "~/logic/lib/publish";
|
||||
|
||||
function NavigationItem(props: {
|
||||
url: string;
|
||||
@ -36,6 +37,10 @@ function getAdjacentId(graph: Graph, child: number, backwards = false): number |
|
||||
const target = children[backwards ? i-1 : i+1];
|
||||
return target?.[0] || null;
|
||||
}
|
||||
|
||||
function makeNoteUrl(ship: string, book: string, noteId: number) {
|
||||
return `/~publish/notebook/ship/${ship}/${book}/note/${noteId}`;
|
||||
}
|
||||
|
||||
|
||||
interface NoteNavigationProps {
|
||||
@ -48,8 +53,6 @@ interface NoteNavigationProps {
|
||||
export function NoteNavigation(props: NoteNavigationProps) {
|
||||
let nextComponent = <Box />;
|
||||
let prevComponent = <Box />;
|
||||
let nextUrl = "";
|
||||
let prevUrl = "";
|
||||
const { noteId, notebook } = props;
|
||||
if(!notebook) {
|
||||
return null;
|
||||
@ -63,10 +66,10 @@ export function NoteNavigation(props: NoteNavigationProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (next) {
|
||||
nextUrl = `/~publish/notebook/ship/${props.ship}/${props.book}/note/${nextId}`;
|
||||
const title = next.post.contents[0]?.text;
|
||||
const date = next.post['time-sent'];
|
||||
if (next && nextId) {
|
||||
const nextUrl = makeNoteUrl(props.ship, props.book, nextId);
|
||||
const [,title,, post] = getLatestRevision(next);
|
||||
const date = post['time-sent'];
|
||||
nextComponent = (
|
||||
<NavigationItem
|
||||
title={title}
|
||||
@ -75,10 +78,10 @@ export function NoteNavigation(props: NoteNavigationProps) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (prev) {
|
||||
prevUrl = `/~publish/notebook/ship/${props.ship}/${props.book}/note/${prevId}`;
|
||||
const title = prev.post.contents[0]?.text;
|
||||
const date = prev.post['time-sent'];
|
||||
if (prev && prevId) {
|
||||
const prevUrl = makeNoteUrl(props.ship, props.book, prevId);
|
||||
const [,title,, post] = getLatestRevision(prev);
|
||||
const date = post['time-sent'];
|
||||
prevComponent = (
|
||||
<NavigationItem
|
||||
title={title}
|
||||
|
@ -8,6 +8,11 @@ import moment from "moment";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { GraphNode } from "~/types/graph-update";
|
||||
import {
|
||||
getComments,
|
||||
getLatestRevision,
|
||||
getSnippet,
|
||||
} from "~/logic/lib/publish";
|
||||
|
||||
interface NotePreviewProps {
|
||||
host: string;
|
||||
@ -36,7 +41,7 @@ export function NotePreview(props: NotePreviewProps) {
|
||||
name = cite(post?.author);
|
||||
}
|
||||
|
||||
const numComments = node.children.size;
|
||||
const numComments = getComments(node).children.size;
|
||||
const commentDesc =
|
||||
numComments === 0
|
||||
? "No Comments"
|
||||
@ -51,15 +56,19 @@ export function NotePreview(props: NotePreviewProps) {
|
||||
// stubbing pending notification-store
|
||||
const isRead = true;
|
||||
|
||||
const [rev, title, body] = getLatestRevision(node);
|
||||
|
||||
const snippet = getSnippet(body);
|
||||
|
||||
return (
|
||||
<Link to={url}>
|
||||
<Col mb={4}>
|
||||
<WrappedBox mb={1}>{post.contents[0]?.text}</WrappedBox>
|
||||
<WrappedBox mb={1}>{title}</WrappedBox>
|
||||
<WrappedBox mb={1}>
|
||||
<ReactMarkdown
|
||||
unwrapDisallowed
|
||||
allowedTypes={["text", "root", "break", "paragraph"]}
|
||||
source={post.contents[1]?.text}
|
||||
source={snippet}
|
||||
/>
|
||||
</WrappedBox>
|
||||
<Box color="gray" display="flex">
|
||||
@ -74,7 +83,8 @@ export function NotePreview(props: NotePreviewProps) {
|
||||
<Box color={isRead ? "gray" : "green"} mr={3}>
|
||||
{date}
|
||||
</Box>
|
||||
<Box>{commentDesc}</Box>
|
||||
<Box mr={3}>{commentDesc}</Box>
|
||||
<Box>{rev === 1 ? `1 Revision` : `${rev} Revisions`}</Box>
|
||||
</Box>
|
||||
</Col>
|
||||
</Link>
|
||||
|
@ -7,6 +7,7 @@ import { PostForm, PostFormSchema } from "./NoteForm";
|
||||
import {createPost} from "~/logic/api/graph";
|
||||
import {Graph} from "~/types/graph-update";
|
||||
import {Association} from "~/types";
|
||||
import {newPost} from "~/logic/lib/publish";
|
||||
|
||||
interface NewPostProps {
|
||||
api: GlobalApi;
|
||||
@ -27,13 +28,8 @@ export default function NewPost(props: NewPostProps & RouteComponentProps) {
|
||||
) => {
|
||||
const { title, body } = values;
|
||||
try {
|
||||
const post = createPost([{ text: title }, { text: body }])
|
||||
const noteId = parseInt(post.index.split('/')[1], 10);
|
||||
await api.graph.addPost(ship, book, post)
|
||||
await waiter(p => {
|
||||
const { graph } = p;
|
||||
return graph.has(noteId);
|
||||
});
|
||||
const [noteId, nodes] = newPost(title, body)
|
||||
await api.graph.addNodes(ship, book, nodes)
|
||||
history.push(`/~publish/notebook/ship/${ship}/${book}/note/${noteId}`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
Loading…
Reference in New Issue
Block a user