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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import React, { Component } from "react";
import moment from "moment";
import { Box } from "@tlon/indigo-react";
import { Link } from "react-router-dom";
import { Notebook } from "../../../../types/publish-update";
import {Graph, GraphNode} from "~/types";
function NavigationItem(props: {
url: string;
@ -10,7 +10,7 @@ function NavigationItem(props: {
date: number;
prev?: boolean;
}) {
const date = moment(date).fromNow();
const date = moment(props.date).fromNow();
return (
<Box
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 {
book: string;
nextId?: string;
prevId?: string;
noteId: number;
ship: string;
notebook: Notebook;
notebook: Graph;
}
export function NoteNavigation(props: NoteNavigationProps) {
@ -43,32 +50,39 @@ export function NoteNavigation(props: NoteNavigationProps) {
let prevComponent = <Box />;
let nextUrl = "";
let prevUrl = "";
const { nextId, prevId, notebook } = props;
const next =
nextId && nextId in notebook?.notes ? notebook?.notes[nextId] : null;
const { noteId, notebook } = props;
if(!notebook) {
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) {
return null;
}
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 = (
<NavigationItem
title={next.title}
date={next["date-created"]}
title={title}
date={date}
url={nextUrl}
/>
);
}
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 = (
<NavigationItem
title={prev.title}
date={prev["date-created"]}
title={title}
date={date}
url={prevUrl}
prev
/>

View File

@ -1,27 +1,29 @@
import React from "react";
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 { RouteComponentProps } from "react-router-dom";
import Note from "./Note";
import { EditPost } from "./EditPost";
import { GraphNode, Graph, Contacts } from "~/types";
interface NoteRoutesProps {
ship: string;
book: string;
noteId: NoteId;
note: INote;
notebook: Notebook;
note: GraphNode;
noteId: number;
notebook: Graph;
contacts: Contacts;
api: GlobalApi;
hideAvatars: boolean;
hideNicknames: boolean;
}
export function NoteRoutes(props: NoteRoutesProps & RouteComponentProps) {
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}`;
return (