publish: rewrite note components for graph-store

This commit is contained in:
Liam Fitzgerald 2020-09-10 20:10:29 +10:00
parent 393f9fd53c
commit 2160bed3ca
7 changed files with 101 additions and 166 deletions

View File

@ -10,7 +10,6 @@ import ContactsApi from './contacts';
import GroupsApi from './groups'; import GroupsApi from './groups';
import LaunchApi from './launch'; import LaunchApi from './launch';
import LinksApi from './links'; import LinksApi from './links';
import PublishApi from './publish';
import GraphApi from './graph'; import GraphApi from './graph';
import S3Api from './s3'; import S3Api from './s3';
@ -23,7 +22,6 @@ export default class GlobalApi extends BaseApi<StoreState> {
groups = new GroupsApi(this.ship, this.channel, this.store); groups = new GroupsApi(this.ship, this.channel, this.store);
launch = new LaunchApi(this.ship, this.channel, this.store); launch = new LaunchApi(this.ship, this.channel, this.store);
links = new LinksApi(this.ship, this.channel, this.store); links = new LinksApi(this.ship, this.channel, this.store);
publish = new PublishApi(this.ship, this.channel, this.store);
s3 = new S3Api(this.ship, this.channel, this.store); s3 = new S3Api(this.ship, this.channel, this.store);
graph = new GraphApi(this.ship, this.channel, this.store); graph = new GraphApi(this.ship, this.channel, this.store);

View File

@ -9,6 +9,7 @@ import GlobalApi from "~/logic/api/global";
import { Button, Box, Row, Text } from "@tlon/indigo-react"; import { Button, Box, Row, Text } from "@tlon/indigo-react";
import styled from "styled-components"; import styled from "styled-components";
import { Author } from "./Author"; import { Author } from "./Author";
import {GraphNode, TextContent} from "~/types/graph-update";
const ClickBox = styled(Box)` const ClickBox = styled(Box)`
cursor: pointer; cursor: pointer;
@ -17,48 +18,36 @@ const ClickBox = styled(Box)`
interface CommentItemProps { interface CommentItemProps {
pending?: boolean; pending?: boolean;
comment: Comment; comment: GraphNode;
contacts: Contacts; contacts: Contacts;
book: string; book: string;
ship: string; ship: string;
api: GlobalApi; api: GlobalApi;
note: NoteId;
hideNicknames: boolean; hideNicknames: boolean;
hideAvatars: boolean; hideAvatars: boolean;
} }
export function CommentItem(props: CommentItemProps) { export function CommentItem(props: CommentItemProps) {
const { ship, contacts, book, note, api } = props; const { ship, contacts, book, api } = props;
const [editing, setEditing] = useState<boolean>(false); const [editing, setEditing] = useState<boolean>(false);
const commentPath = Object.keys(props.comment)[0]; const commentData = props.comment?.post;
const commentData = props.comment[commentPath]; const comment = commentData.contents[0] as TextContent;
const content = commentData.content.split("\n").map((line, i) => { const content = comment.text.split("\n").map((line, i) => {
return ( return (
<Text className="mb2" key={i}> <Text mb="2" key={i}>
{line} {line}
</Text> </Text>
); );
}); });
const disabled = props.pending || window.ship !== commentData.author.slice(1); const disabled = props.pending || window.ship !== commentData.author;
const onUpdate = async ({ comment }) => {
await api.publish.updateComment(
ship.slice(1),
book,
note,
commentPath,
comment
);
setEditing(false);
};
const onDelete = async () => { const onDelete = async () => {
await api.publish.deleteComment(ship.slice(1), book, note, commentPath); await api.graph.removeNodes(ship, book, [commentData?.index]);
}; };
return ( return (
<Box mb={4} opacity={props.pending ? "60%" : "100%"}> <Box mb={4} opacity={commentData?.pending ? "60%" : "100%"}>
<Row bg="white" my={3}> <Row bg="white" my={3}>
<Author <Author
showImage showImage
@ -68,32 +57,17 @@ export function CommentItem(props: CommentItemProps) {
hideAvatars={props.hideAvatars} hideAvatars={props.hideAvatars}
hideNicknames={props.hideNicknames} hideNicknames={props.hideNicknames}
> >
{!disabled && !editing && ( {!disabled && (
<> <>
<ClickBox color="green" onClick={() => setEditing(true)}>
Edit
</ClickBox>
<ClickBox color="red" onClick={onDelete}> <ClickBox color="red" onClick={onDelete}>
Delete Delete
</ClickBox> </ClickBox>
</> </>
)} )}
{editing && (
<ClickBox onClick={() => setEditing(false)} color="red">
Cancel
</ClickBox>
)}
</Author> </Author>
</Row> </Row>
<Box mb={2}> <Box mb={2}>
{!editing && content} {!editing && content}
{editing && (
<CommentInput
onSubmit={onUpdate}
initial={commentData.content}
label="Update"
/>
)}
</Box> </Box>
</Box> </Box>
); );

View File

@ -8,50 +8,34 @@ import { Contacts } from "~/types/contact-update";
import _ from "lodash"; import _ from "lodash";
import GlobalApi from "~/logic/api/global"; import GlobalApi from "~/logic/api/global";
import { FormikHelpers } from "formik"; import { FormikHelpers } from "formik";
import {GraphNode, Graph} from "~/types/graph-update";
import {createPost} from "~/logic/api/graph";
interface CommentsProps { interface CommentsProps {
comments: Comment[]; comments: Graph;
book: string; book: string;
noteId: NoteId; note: GraphNode;
note: Note;
ship: string; ship: string;
contacts: Contacts; contacts: Contacts;
api: GlobalApi; api: GlobalApi;
numComments: number;
enabled: boolean;
hideAvatars: boolean; hideAvatars: boolean;
hideNicknames: boolean; hideNicknames: boolean;
} }
export function Comments(props: CommentsProps) { export function Comments(props: CommentsProps) {
const { comments, ship, book, note, api, noteId, numComments } = props; const { comments, ship, book, note, api } = props;
const [pending, setPending] = useState<string[]>([]);
useEffect(() => {
_.forEach(comments, (comment: Comment) => {
const { content } = comment[Object.keys(comment)[0]];
setPending((p) => p.filter((p) => p !== content));
});
}, [numComments]);
const onSubmit = async ( const onSubmit = async (
{ comment }, { comment },
actions: FormikHelpers<{ comment: string }> actions: FormikHelpers<{ comment: string }>
) => { ) => {
setPending((p) => [...p, comment]);
const action = {
"new-comment": {
who: ship.slice(1),
book: book,
note: noteId,
body: comment,
},
};
try { try {
await api.publish.publishAction(action); const post = createPost([{ text: comment }], note?.post?.index);
await api.graph.addPost(ship, book, post)
actions.resetForm(); actions.resetForm();
actions.setStatus({ success: null }); actions.setStatus({ success: null });
} catch (e) { } catch (e) {
console.error(e);
actions.setStatus({ error: e.message }); actions.setStatus({ error: e.message });
} }
}; };
@ -59,37 +43,14 @@ export function Comments(props: CommentsProps) {
return ( return (
<Col> <Col>
<CommentInput onSubmit={onSubmit} /> <CommentInput onSubmit={onSubmit} />
{Array.from(pending).map((com, i) => { {Array.from(comments).reverse().map(([idx, comment]) => (
const da = dateToDa(new Date());
const ship = `~${window.ship}`;
const comment = {
[da]: {
author: ship,
content: com,
"date-created": Math.round(new Date().getTime()),
},
} as Comment;
return (
<CommentItem <CommentItem
comment={comment} comment={comment}
key={i} key={idx}
contacts={props.contacts}
ship={ship}
pending={true}
hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars}
/>
);
})}
{props.comments.map((com, i) => (
<CommentItem
comment={com}
key={i}
contacts={props.contacts} contacts={props.contacts}
api={api} api={api}
book={book} book={book}
ship={ship} ship={ship}
note={note["note-id"]}
hideNicknames={props.hideNicknames} hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars} hideAvatars={props.hideAvatars}
/> />

View File

@ -1,22 +1,23 @@
import React, { Component } from "react"; import React from "react";
import { PostFormSchema, PostForm } from "./NoteForm"; import { PostFormSchema, PostForm } from "./NoteForm";
import { Note } from "../../../../types/publish-update";
import { FormikHelpers } from "formik"; import { FormikHelpers } from "formik";
import GlobalApi from "../../../../api/global"; import GlobalApi from "~/logic/api/global";
import { RouteComponentProps } from "react-router-dom"; import { RouteComponentProps } from "react-router-dom";
import { GraphNode, TextContent } from "~/types";
interface EditPostProps { interface EditPostProps {
ship: string; ship: string;
noteId: string; noteId: number;
note: Note; note: GraphNode;
api: GlobalApi; api: GlobalApi;
book: string; book: string;
} }
export function EditPost(props: EditPostProps & RouteComponentProps) { export function EditPost(props: EditPostProps & RouteComponentProps) {
const { note, book, noteId, api, ship, history } = props; const { note, book, noteId, api, ship, history } = props;
const body = note.file.slice(note.file.indexOf(";>") + 2); const [title, file] = note.post.contents as TextContent[];
const body = file.text.slice(file.text.indexOf(";>") + 2);
const initial: PostFormSchema = { const initial: PostFormSchema = {
title: note.title, title: title.text,
body, body,
}; };
@ -25,11 +26,11 @@ export function EditPost(props: EditPostProps & RouteComponentProps) {
actions: FormikHelpers<PostFormSchema> actions: FormikHelpers<PostFormSchema>
) => { ) => {
const { title, body } = values; const { title, body } = values;
const host = ship.slice(1);
try { try {
await api.publish.editNote(host, book, noteId, title, body); // graph-store does not support editing nodes
history.push(`/~publish/notebook/${ship}/${book}/note/${noteId}`); throw new Error("Unsupported");
} catch (e) { } catch (e) {
console.error(e);
actions.setStatus({ error: "Failed to edit notebook" }); actions.setStatus({ error: "Failed to edit notebook" });
} }
}; };
@ -38,7 +39,7 @@ export function EditPost(props: EditPostProps & RouteComponentProps) {
<PostForm <PostForm
initial={initial} initial={initial}
onSubmit={onSubmit} onSubmit={onSubmit}
submitLabel={`Update ${note.title}`} submitLabel={`Update ${title.text}`}
loadingText="Updating..." loadingText="Updating..."
/> />
); );

View File

@ -5,21 +5,15 @@ import { Link, RouteComponentProps } from "react-router-dom";
import { Spinner } from "~/views/components/Spinner"; import { Spinner } from "~/views/components/Spinner";
import { Comments } from "./Comments"; import { Comments } from "./Comments";
import { NoteNavigation } from "./NoteNavigation"; import { NoteNavigation } from "./NoteNavigation";
import {
NoteId,
Note as INote,
Notebook,
} from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
import GlobalApi from "~/logic/api/global"; import GlobalApi from "~/logic/api/global";
import { Author } from "./Author"; import { Author } from "./Author";
import { Contacts, GraphNode, Graph} from "~/types";
interface NoteProps { interface NoteProps {
ship: string; ship: string;
book: string; book: string;
noteId: NoteId; note: GraphNode;
note: INote; notebook: Graph;
notebook: Notebook;
contacts: Contacts; contacts: Contacts;
api: GlobalApi; api: GlobalApi;
hideAvatars: boolean; hideAvatars: boolean;
@ -28,31 +22,27 @@ interface NoteProps {
export function Note(props: NoteProps & RouteComponentProps) { export function Note(props: NoteProps & RouteComponentProps) {
const [deleting, setDeleting] = useState(false); const [deleting, setDeleting] = useState(false);
const { notebook, note, contacts, ship, book, noteId, api } = props; const { contacts, ship, notebook, book, api, note } = props;
useEffect(() => {
api.publish.fetchNote(ship, book, noteId);
}, [ship, book, noteId]);
const baseUrl = `/~publish/notebook/${props.ship}/${props.book}`; const baseUrl = `/~publish/notebook/ship/${props.ship}/${props.book}`;
const deletePost = async () => { const deletePost = async () => {
setDeleting(true); setDeleting(true);
await api.publish.delNote(ship.slice(1), book, noteId); const indices = [note.post.index]
await api.graph.removeNodes(ship, book, indices);
props.history.push(baseUrl); props.history.push(baseUrl);
}; };
const comments = note?.comments || []; const comments = note?.children
const file = note?.file; const file = note?.post?.contents[1]?.text || "";
const newfile = file ? file.slice(file.indexOf(";>") + 2) : ""; const newfile = file ? file.slice(file.indexOf(";>") + 2) : "";
let editPost: JSX.Element | null = null; const noteId = parseInt(note.post.index.split('/')[1], 10);
const editUrl = props.location.pathname + "/edit";
if (`~${window.ship}` === note?.author) { let adminLinks: JSX.Element | null = null;
editPost = ( if (window.ship === note?.post?.author) {
adminLinks = (
<Box display="inline-block"> <Box display="inline-block">
<Link to={editUrl}>
<Text color="green">Edit</Text>
</Link>
<Text <Text
className="dib f9 red2 ml2 pointer" className="dib f9 red2 ml2 pointer"
color="red" color="red"
@ -81,16 +71,16 @@ export function Note(props: NoteProps & RouteComponentProps) {
<Text>{"<- Notebook Index"}</Text> <Text>{"<- Notebook Index"}</Text>
</Link> </Link>
<Col> <Col>
<Text display="block" mb={2}>{note?.title || ""}</Text> <Text display="block" mb={2}>{note?.post?.contents[0]?.text || ""}</Text>
<Box display="flex"> <Box display="flex">
<Author <Author
hideNicknames={props.hideNicknames} hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars} hideAvatars={props.hideAvatars}
ship={note?.author} ship={note?.post?.author}
contacts={contacts} contacts={contacts}
date={note?.["date-created"]} date={note?.post?.["time-sent"]}
/> />
<Text ml={2}>{editPost}</Text> <Text ml={2}>{adminLinks}</Text>
</Box> </Box>
</Col> </Col>
<Box color="black" className="md" style={{ overflowWrap: "break-word" }}> <Box color="black" className="md" style={{ overflowWrap: "break-word" }}>
@ -98,25 +88,20 @@ export function Note(props: NoteProps & RouteComponentProps) {
</Box> </Box>
<NoteNavigation <NoteNavigation
notebook={notebook} notebook={notebook}
prevId={note?.["prev-note"] || undefined} noteId={noteId}
nextId={note?.["next-note"] || undefined}
ship={props.ship} ship={props.ship}
book={props.book} book={props.book}
/> />
{notebook.comments && (
<Comments <Comments
ship={ship} ship={ship}
book={props.book} book={props.book}
noteId={props.noteId}
note={props.note} note={props.note}
comments={comments} comments={comments}
numComments={props.note["num-comments"]}
contacts={props.contacts} contacts={props.contacts}
api={props.api} api={props.api}
hideNicknames={props.hideNicknames} hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars} hideAvatars={props.hideAvatars}
/> />
)}
<Spinner <Spinner
text="Deleting post..." text="Deleting post..."
awaiting={deleting} awaiting={deleting}

View File

@ -2,7 +2,7 @@ import React, { Component } from "react";
import moment from "moment"; import moment from "moment";
import { Box } from "@tlon/indigo-react"; import { Box } from "@tlon/indigo-react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Notebook } from "../../../../types/publish-update"; import {Graph, GraphNode} from "~/types";
function NavigationItem(props: { function NavigationItem(props: {
url: string; url: string;
@ -10,7 +10,7 @@ function NavigationItem(props: {
date: number; date: number;
prev?: boolean; prev?: boolean;
}) { }) {
const date = moment(date).fromNow(); const date = moment(props.date).fromNow();
return ( return (
<Box <Box
justifySelf={props.prev ? "start" : "end"} justifySelf={props.prev ? "start" : "end"}
@ -30,12 +30,19 @@ function NavigationItem(props: {
); );
} }
function getAdjacentId(graph: Graph, child: number, backwards = false): number | null {
const children = Array.from(graph);
const i = children.findIndex(([index]) => index === child);
const target = children[backwards ? i-1 : i+1];
return target?.[0] || null;
}
interface NoteNavigationProps { interface NoteNavigationProps {
book: string; book: string;
nextId?: string; noteId: number;
prevId?: string;
ship: string; ship: string;
notebook: Notebook; notebook: Graph;
} }
export function NoteNavigation(props: NoteNavigationProps) { export function NoteNavigation(props: NoteNavigationProps) {
@ -43,32 +50,39 @@ export function NoteNavigation(props: NoteNavigationProps) {
let prevComponent = <Box />; let prevComponent = <Box />;
let nextUrl = ""; let nextUrl = "";
let prevUrl = ""; let prevUrl = "";
const { nextId, prevId, notebook } = props; const { noteId, notebook } = props;
const next = if(!notebook) {
nextId && nextId in notebook?.notes ? notebook?.notes[nextId] : null; return null;
}
const nextId = getAdjacentId(notebook, noteId);
const prevId = getAdjacentId(notebook, noteId, true);
const next = nextId && notebook.get(nextId);
const prev = prevId && notebook.get(prevId);
const prev =
prevId && prevId in notebook?.notes ? notebook?.notes[prevId] : null;
if (!next && !prev) { if (!next && !prev) {
return null; return null;
} }
if (next) { if (next) {
nextUrl = `/~publish/notebook/${props.ship}/${props.book}/note/${props.nextId}`; nextUrl = `/~publish/notebook/ship/${props.ship}/${props.book}/note/${nextId}`;
const title = next.post.contents[0]?.text;
const date = next.post['time-sent'];
nextComponent = ( nextComponent = (
<NavigationItem <NavigationItem
title={next.title} title={title}
date={next["date-created"]} date={date}
url={nextUrl} url={nextUrl}
/> />
); );
} }
if (prev) { if (prev) {
prevUrl = `/~publish/notebook/${props.ship}/${props.book}/note/${props.prevId}`; prevUrl = `/~publish/notebook/ship/${props.ship}/${props.book}/note/${prevId}`;
const title = prev.post.contents[0]?.text;
const date = prev.post['time-sent'];
prevComponent = ( prevComponent = (
<NavigationItem <NavigationItem
title={prev.title} title={title}
date={prev["date-created"]} date={date}
url={prevUrl} url={prevUrl}
prev prev
/> />

View File

@ -1,27 +1,29 @@
import React from "react"; import React from "react";
import { Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import { NoteId, Note as INote, Notebook } from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
import GlobalApi from "~/logic/api/global"; import GlobalApi from "~/logic/api/global";
import { RouteComponentProps } from "react-router-dom"; import { RouteComponentProps } from "react-router-dom";
import Note from "./Note"; import Note from "./Note";
import { EditPost } from "./EditPost"; import { EditPost } from "./EditPost";
import { GraphNode, Graph, Contacts } from "~/types";
interface NoteRoutesProps { interface NoteRoutesProps {
ship: string; ship: string;
book: string; book: string;
noteId: NoteId; note: GraphNode;
note: INote; noteId: number;
notebook: Notebook; notebook: Graph;
contacts: Contacts; contacts: Contacts;
api: GlobalApi; api: GlobalApi;
hideAvatars: boolean;
hideNicknames: boolean;
} }
export function NoteRoutes(props: NoteRoutesProps & RouteComponentProps) { export function NoteRoutes(props: NoteRoutesProps & RouteComponentProps) {
const { ship, book, noteId } = props; const { ship, book, noteId } = props;
const baseUrl = `/~publish/notebook/${ship}/${book}/note/${noteId}`; const baseUrl = `/~publish/notebook/ship/${ship}/${book}/note/${noteId}`;
const relativePath = (path: string) => `${baseUrl}${path}`; const relativePath = (path: string) => `${baseUrl}${path}`;
return ( return (