publish: finish rewrite

This commit is contained in:
Liam Fitzgerald 2020-08-18 10:03:39 +10:00
parent 135d9ceb20
commit 5570c076fd
24 changed files with 650 additions and 877 deletions

View File

@ -1,6 +1,6 @@
import BaseApi from './base';
import { PublishResponse } from '../types/publish-response';
import { PatpNoSig } from '../types/noun';
import { PatpNoSig, Path } from '../types/noun';
import { BookId, NoteId } from '../types/publish-update';
export default class PublishApi extends BaseApi {
@ -80,5 +80,80 @@ export default class PublishApi extends BaseApi {
publishAction(act: any) {
return this.action('publish', 'publish-action', act);
}
newBook(bookId: string, title: string, description: string, group: Path) {
return this.publishAction({
"new-book": {
book: bookId,
title: title,
about: description,
coms: true,
group: {
'group-path': group,
invitees: [],
'use-preexisting': true,
'make-managed': true
}
}
});
}
newNote(who: PatpNoSig, book: string, note: string, title: string, body: string) {
return this.publishAction({
'new-note': {
who,
book,
note,
title,
body
}
});
}
editNote(who: PatpNoSig, book: string, note: string, title: string, body: string) {
return this.publishAction({
'edit-note': {
who,
book,
note,
title,
body
}
});
}
delNote(who: PatpNoSig, book: string, note: string) {
return this.publishAction({
'del-note': {
who,
book,
note
}
});
}
updateComment(who: PatpNoSig, book: string, note: string, comment: Path, body: string) {
return this.publishAction({
'edit-comment': {
who,
book,
note,
comment,
body
}
});
}
deleteComment(who: PatpNoSig, book: string, note: string, comment: Path ) {
return this.publishAction({
"del-comment": {
who,
book,
note,
comment
},
});
}
}

View File

@ -127,7 +127,7 @@ export class LinkSubmit extends Component {
</button>
<Spinner awaiting={this.state.disabled} classes="mt3 absolute right-0" text="Posting to collection..." />
</div>
);
) ;
}
}

View File

@ -77,6 +77,7 @@ export default function PublishApp(props: PublishAppProps) {
return (
<Route
path={[
"/~publish/notebook/:ship/:notebook/note/:noteId",
"/~publish/notebook/:ship/:notebook/*",
"/~publish/notebook/:ship/:notebook",
"/~publish",
@ -139,32 +140,27 @@ export default function PublishApp(props: PublishAppProps) {
? props.match.params.view
: "posts";
const popout = Boolean(props.match.params.popout) || false;
const ship = props.match.params.ship || "";
const book = props.match.params.notebook || "";
const bookGroupPath =
notebooks?.[ship]?.[notebook]?.["subscribers-group-path"];
notebooks?.[ship]?.[book]?.["subscribers-group-path"];
const notebookContacts =
bookGroupPath in contacts ? contacts[bookGroupPath] : {};
const notebook = notebooks[ship][book];
return (
<NotebookRoutes
notebook={notebook}
view={view}
ship={ship}
book={book}
groups={groups}
contacts={contacts}
notebookContacts={notebookContacts}
associations={associations.contacts}
sidebarShown={sidebarShown}
popout={popout}
api={api}
{...props}
/>

View File

@ -0,0 +1,51 @@
import React from "react";
import moment from "moment";
import { Sigil } from "../../../../lib/sigil";
import { uxToHex, cite } from "../../../../lib/util";
import { Contacts } from "../../../../types/contact-update";
import { Row, Box } from "@tlon/indigo-react";
interface AuthorProps {
contacts: Contacts;
ship: string;
date: number;
showImage?: boolean;
}
export function Author(props: AuthorProps) {
const { contacts, ship, date, showImage } = props;
const noSig = ship.slice(1);
const contact = noSig in contacts ? contacts[noSig] : null;
const color = contact?.color ? `#${uxToHex(contact?.color)}` : "#000000";
const name = contact?.nickname || cite(ship);
const dateFmt = moment(date).fromNow();
return (
<Row alignItems="center" width="auto">
{showImage && (
<Box>
{contact?.avatar ? (
<img src={contact?.avatar} height={24} width={24} className="dib" />
) : (
<Sigil
ship={ship}
size={24}
color={color}
classes="mix-blend-diff"
/>
)}
</Box>
)}
<Box
ml={showImage ? 2 : 0}
color="gray"
fontFamily={contact?.nickname ? "sans" : "mono"}
>
{name}
</Box>
<Box ml={2} color="gray">
{dateFmt}
</Box>
</Row>
);
}

View File

@ -1,9 +1,8 @@
import React from "react";
import * as Yup from "yup";
import GlobalApi from "../../../../api/global";
import { Formik, FormikHelpers, Form } from "formik";
import { AsyncButton } from '../../../../components/AsyncButton';
import { TextArea, Button } from "@tlon/indigo-react";
import { TextArea } from "@tlon/indigo-react";
interface FormSchema {
comment: string;

View File

@ -1,4 +1,4 @@
import React, { Component, useState, useCallback } from "react";
import React, { useState } from "react";
import moment from "moment";
import { Sigil } from "../../../../lib/sigil";
import CommentInput from "./CommentInput";
@ -6,7 +6,15 @@ import { uxToHex, cite } from "../../../../lib/util";
import { Comment, NoteId } from "../../../../types/publish-update";
import { Contacts } from "../../../../types/contact-update";
import GlobalApi from "../../../../api/global";
import { Button } from "@tlon/indigo-react";
import { Button, Box, Row, Text } from "@tlon/indigo-react";
import styled from "styled-components";
import { Author } from "./Author";
const ClickBox = styled(Box)`
cursor: pointer;
margin-left: ${(p) => p.theme.space[2]}px;
padding-top: ${(p) => p.theme.space[1]}px;
`;
interface CommentItemProps {
pending?: boolean;
@ -19,103 +27,61 @@ interface CommentItemProps {
}
export function CommentItem(props: CommentItemProps) {
const { ship, contacts, book, note, api } = props;
const [editing, setEditing] = useState<boolean>(false);
const pending = props.pending ? "o-60" : "";
const commentPath = Object.keys(props.comment)[0];
const commentData = props.comment[commentPath];
const content = commentData.content.split("\n").map((line, i) => {
return (
<p className="mb2" key={i}>
<Text className="mb2" key={i}>
{line}
</p>
</Text>
);
});
const date = moment(commentData["date-created"]).fromNow();
const contact =
commentData.author.substr(1) in props.contacts
? props.contacts[commentData.author.substr(1)]
: null;
let name = commentData.author;
let color = "#000000";
let classes = "mix-blend-diff";
let avatar: string | null = null;
if (contact) {
name = contact.nickname.length > 0 ? contact.nickname : commentData.author;
color = `#${uxToHex(contact.color)}`;
classes = "";
avatar = contact.avatar;
}
const img =
avatar !== null ? (
<img src={avatar} height={24} width={24} className="dib" />
) : (
<Sigil
ship={commentData.author}
size={24}
color={color}
classes={classes}
/>
);
if (name === commentData.author) {
name = cite(commentData.author);
}
const disabled = props.pending || window.ship !== commentData.author.slice(1);
const onUpdate = useCallback(
async ({ comment }) => {
const action = {
"edit-comment": {
who: props.ship.slice(1),
book: props.book,
note: props.note,
body: comment,
comment: commentPath,
},
};
await props.api.publish.publishAction(action);
const onUpdate = async ({ comment }) => {
await api.publish.updateComment(
ship.slice(1),
book,
note,
commentPath,
comment
);
setEditing(false);
};
setEditing(false);
},
[props.api, props.ship, props.note, props.book, commentPath, setEditing]
);
const onDelete = async () => {
await api.publish.deleteComment(ship.slice(1), book, note, commentPath);
};
return (
<div className={"mb8 " + pending}>
<div className="flex mv3 bg-white bg-gray0-d">
{img}
<div
className={"f9 mh2 pt1 " + (contact?.nickname ? null : "mono")}
title={commentData.author}
>
{name}
</div>
<div className="f9 gray3 pt1">{date}</div>
<Box mb={4} opacity={props.pending ? "60%" : "100%"}>
<Row bg="white" my={3}>
<Author
showImage
contacts={contacts}
ship={ship}
date={commentData["date-created"]}
/>
{!disabled && !editing && (
<>
<div
onClick={() => setEditing(true)}
className="green2 pointer ml2 f9 pt1"
>
<ClickBox color="green" onClick={() => setEditing(true)}>
Edit
</div>
<div className="red2 pointer ml2 f9 pt1">Delete</div>
</ClickBox>
<ClickBox color="red" onClick={onDelete}>
Delete
</ClickBox>
</>
)}
{editing && (
<div
onClick={() => setEditing(false)}
className="red2 pointer ml2 f9 pt1"
>
<ClickBox onClick={() => setEditing(false)} color="red">
Cancel
</div>
</ClickBox>
)}
</div>
<div className="f8 lh-solid mb2">
</Row>
<Box mb={2}>
{!editing && content}
{editing && (
<CommentInput
@ -124,8 +90,8 @@ export function CommentItem(props: CommentItemProps) {
label="Update"
/>
)}
</div>
</div>
</Box>
</Box>
);
}

View File

@ -1,59 +1,14 @@
import React, { useState, useEffect, useCallback } from "react";
import { Col } from "@tlon/indigo-react";
import { CommentItem } from "./CommentItem";
import CommentInput from "./CommentInput";
import { dateToDa } from "../../../../lib/util";
import { Spinner } from "../../../../components/Spinner";
import { Comment, Note, NoteId } from "../../../../types/publish-update";
import { Contacts } from "../../../../types/contact-update";
import _ from "lodash";
import GlobalApi from "../../../../api/global";
import { FormikHelpers } from "formik";
/**
*
commentUpdate(idx, body) {
const path = Object.keys(this.props.comments[idx])[0];
const comment = {
'edit-comment': {
who: this.props.ship.slice(1),
book: this.props.book,
note: this.props.note,
body: body,
comment: path
}
};
this.setState({ awaiting: 'edit' });
this.props.api.publish
.publishAction(comment)
.then(() => {
this.setState({ awaiting: null, editing: null });
});
}
commentDelete(idx) {
const path = Object.keys(this.props.comments[idx])[0];
const comment = {
'del-comment': {
who: this.props.ship.slice(1),
book: this.props.book,
note: this.props.note,
comment: path
}
};
this.setState({ awaiting: { kind: 'del', what: idx } });
this.props.api.publish
.publishAction(comment)
.then(() => {
this.setState({ awaiting: null });
});
}
*/
interface CommentsProps {
comments: Comment[];
book: string;
@ -62,20 +17,20 @@ interface CommentsProps {
ship: string;
contacts: Contacts;
api: GlobalApi;
numComments: number;
enabled: boolean;
}
export function Comments(props: CommentsProps) {
const { comments, ship, book, note, api, noteId } = props;
const { comments, ship, book, note, api, noteId, numComments } = props;
const [pending, setPending] = useState<string[]>([]);
const [awaiting, setAwaiting] = useState<string | null>(null);
useEffect(() => {
_.forEach(comments, (comment: Comment) => {
const { content } = comment[Object.keys(comment)[0]];
setPending((p) => p.filter((p) => p === content));
setPending((p) => p.filter((p) => p !== content));
});
}, [comments.length]);
}, [numComments]);
const onSubmit = async (
{ comment },
@ -99,58 +54,39 @@ export function Comments(props: CommentsProps) {
}
};
if (!props.enabled) {
return null;
}
const pendingArray = Array.from(pending).map((com, i) => {
const da = dateToDa(new Date());
const comment = {
[da]: {
author: `~${window.ship}`,
content: com,
"date-created": Math.round(new Date().getTime()),
},
};
return (
<CommentItem
comment={comment}
key={i}
contacts={props.contacts}
pending={true}
/>
);
});
const commentArray = props.comments.map((com, i) => {
return (
<CommentItem
comment={com}
key={i}
contacts={props.contacts}
api={api}
book={book}
ship={ship}
note={note}
/>
);
});
const spinnerText =
awaiting === "new"
? "Posting commment..."
: awaiting === "edit"
? "Updating comment..."
: "Deleting comment...";
return (
<div>
<div className="mv8 relative">
<CommentInput onSubmit={onSubmit} />
</div>
{pendingArray}
{commentArray}
</div>
<Col>
<CommentInput onSubmit={onSubmit} />
{Array.from(pending).map((com, i) => {
const da = dateToDa(new Date());
const comment = {
[da]: {
author: `~${window.ship}`,
content: com,
"date-created": Math.round(new Date().getTime()),
},
} as Comment;
return (
<CommentItem
comment={comment}
key={i}
contacts={props.contacts}
pending={true}
/>
);
})}
{props.comments.map((com, i) => (
<CommentItem
comment={com}
key={i}
contacts={props.contacts}
api={api}
book={book}
ship={ship}
note={note["note-id"]}
/>
))}
</Col>
);
}

View File

@ -1,145 +1,45 @@
import React, { Component } from 'react';
import { SidebarSwitcher } from '../../../../components/SidebarSwitch';
import { Spinner } from '../../../../components/Spinner';
import { Link } from 'react-router-dom';
import { Controlled as CodeMirror } from 'react-codemirror2';
import { dateToDa } from '../../../../lib/util';
import 'codemirror/mode/markdown/markdown';
export class EditPost extends Component {
constructor(props) {
super(props);
this.state = {
body: '',
submit: false,
awaiting: false
};
this.postSubmit = this.postSubmit.bind(this);
this.bodyChange = this.bodyChange.bind(this);
}
componentDidMount() {
this.componentDidUpdate();
}
componentDidUpdate(prevProps) {
const { props, state } = this;
const contents = props.notebooks[props.ship]?.[props.book]?.notes?.[props.note]?.file;
if (prevProps && prevProps.api !== props.api) {
if (!contents) {
props.api?.fetchNote(props.ship, props.book, props.note);
}
}
if (contents && state.body === '') {
const notebook = props.notebooks[props.ship][props.book];
const note = notebook.notes[props.note];
const file = note.file;
const body = file.slice(file.indexOf(';>') + 3);
this.setState({ body: body });
}
}
postSubmit() {
const { props, state } = this;
const notebook = props.notebooks[props.ship][props.book];
const note = notebook.notes[props.note];
const title = note.title;
const editNote = {
'edit-note': {
who: props.ship.slice(1),
book: props.book,
note: props.note,
title: title,
body: state.body
}
};
this.setState({ awaiting: true });
this.props.api.publish.publishAction(editNote).then(() => {
const editIndex = props.location.pathname.indexOf('/edit');
const noteHref = props.location.pathname.slice(0, editIndex);
this.setState({ awaiting: false });
props.history.push(noteHref);
});
}
bodyChange(editor, data, value) {
const submit = !(value === '');
this.setState({ body: value, submit: submit });
}
render() {
const { props, state } = this;
const notebook = props.notebooks[props.ship][props.book];
const note = notebook.notes[props.note];
const title = note.title;
let date = dateToDa(new Date(note['date-created']));
date = date.slice(1, -10);
const submitStyle = (state.submit)
? { color: '#2AA779', cursor: 'pointer' }
: { color: '#B1B2B3', cursor: 'auto' };
const hrefIndex = props.location.pathname.indexOf('/note/');
const publishsubStr = props.location.pathname.substr(hrefIndex);
const popoutHref = `/~publish/popout${publishsubStr}`;
const hiddenOnPopout = (props.popout)
? '' : 'dib-m dib-l dib-xl';
const options = {
mode: 'markdown',
theme: 'tlon',
lineNumbers: false,
lineWrapping: true,
scrollbarStyle: null,
cursorHeight: 0.85
};
return (
<div className="f9 h-100 relative publish">
<div className="w-100 tl pv4 flex justify-center">
<SidebarSwitcher
sidebarShown={props.sidebarShown}
popout={props.popout}
api={this.props.api}
/>
<button
className="v-mid bg-transparent w-100 w-80-m w-90-l mw6 tl h1 pl4"
disabled={!state.submit}
style={submitStyle}
onClick={this.postSubmit}
>
Save "{title}"
</button>
<Link
className={'dn absolute right-1 top-1 ' + hiddenOnPopout}
to={popoutHref}
target="_blank"
>
<img src="/~landscape/img/popout.png"
height={16}
width={16}
/>
</Link>
</div>
<div className="mw6 center">
<div className="pl4">
<div className="gray2">{date}</div>
</div>
<div className="EditPost">
<CodeMirror
value={state.body}
options={options}
onBeforeChange={(e, d, v) => this.bodyChange(e, d, v)}
onChange={(editor, data, value) => {}}
/>
<Spinner text="Editing post..." awaiting={this.state.awaiting} classes="absolute bottom-1 right-1 ba b--gray1-d pa2" />
</div>
</div>
</div>
);
}
import React, { Component } from "react";
import { PostFormSchema, PostForm } from "./NoteForm";
import { Note } from "../../../../types/publish-update";
import { FormikHelpers } from "formik";
import GlobalApi from "../../../../api/global";
import { RouteComponentProps } from "react-router-dom";
interface EditPostProps {
ship: string;
noteId: string;
note: Note;
api: GlobalApi;
book: string;
}
export default EditPost;
export function EditPost(props: EditPostProps & RouteComponentProps) {
const { note, book, noteId, api, ship, history } = props;
const body = note.file.slice(note.file.indexOf(";>") + 2);
const initial: PostFormSchema = {
title: note.title,
body,
};
const onSubmit = async (
values: PostFormSchema,
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}`);
} catch (e) {
actions.setStatus({ error: "Failed to edit notebook" });
}
};
return (
<PostForm
initial={initial}
onSubmit={onSubmit}
submitLabel={`Update ${note.title}`}
loadingText="Updating..."
/>
);
}

View File

@ -1,17 +1,18 @@
import React, { Component, useState, useEffect } from "react";
import React, { useState, useEffect } from "react";
import { Box, Text, Col } from "@tlon/indigo-react";
import ReactMarkdown from 'react-markdown';
import ReactMarkdown from "react-markdown";
import { Link, RouteComponentProps } from "react-router-dom";
import { SidebarSwitcher } from "../../../../components/SidebarSwitch";
import { Spinner } from "../../../../components/Spinner";
import { Comments } from "./Comments";
import { NoteNavigation } from "./NoteNavigation";
import moment from "moment";
import { cite } from "../../../../lib/util";
import { PublishContent } from "./PublishContent";
import { NoteId, Note as INote, Notebook } from "../../../../types/publish-update";
import {
NoteId,
Note as INote,
Notebook,
} from "../../../../types/publish-update";
import { Contacts } from "../../../../types/contact-update";
import GlobalApi from "../../../../api/global";
import { Author } from "./Author";
interface NoteProps {
ship: string;
@ -25,10 +26,7 @@ interface NoteProps {
export function Note(props: NoteProps & RouteComponentProps) {
const [deleting, setDeleting] = useState(false);
const { notebook, note, contacts, ship, book, noteId, api } = props;
// fetch note and read actions
//
useEffect(() => {
api.publish.fetchNote(ship, book, noteId);
}, [ship, book, noteId]);
@ -37,81 +35,32 @@ export function Note(props: NoteProps & RouteComponentProps) {
const deletePost = async () => {
setDeleting(true);
const deleteAction = {
"del-note": {
who: props.ship.slice(1),
book: props.book,
note: props.noteId,
},
};
await api.publish.publishAction(deleteAction);
await api.publish.delNote(ship, book, noteId);
props.history.push(baseUrl);
};
const comments = note?.comments || false;
const title = note?.title || "";
const author = note?.author || "";
const file = note?.file || "";
const date = moment(note?.["date-created"]).fromNow() || 0;
const comments = note?.comments || [];
const file = note?.file;
const newfile = file ? file.slice(file.indexOf(";>") + 2) : "";
const contact =
author.substr(1) in props.contacts
? props.contacts[author.substr(1)]
: null;
let name = author;
if (contact) {
name = contact.nickname.length > 0 ? contact.nickname : author;
}
if (name === author) {
name = cite(author);
}
if (!file) {
return null;
}
const newfile = file.slice(file.indexOf(";>") + 2);
const prevId = note?.["prev-note"] || null;
const nextId = note?.["next-note"] || null;
const prevDate =
prevId ? moment(notebook?.notes?.[prevId]?.["date-created"]).fromNow() : 0;
const nextDate =
nextId ? moment(notebook?.notes?.[nextId]?.["date-created"]).fromNow() : 0;
const prev =
prevId === null
? null
: {
id: prevId,
title: notebook?.notes?.[prevId]?.title,
date: prevDate,
};
const next =
nextId === null
? null
: {
id: nextId,
title: notebook?.notes?.[nextId]?.title,
date: nextDate,
};
let editPost = null;
let editPost: JSX.Element | null = null;
const editUrl = props.location.pathname + "/edit";
if (`~${window.ship}` === author) {
if (`~${window.ship}` === note?.author) {
editPost = (
<div className="dib">
<Link className="green2 f9" to={editUrl}>
Edit
<Box display="inline-block">
<Link to={editUrl}>
<Text color="green">Edit</Text>
</Link>
<p
<Text
className="dib f9 red2 ml2 pointer"
onClick={() => deletePost()}
color="red"
ml={2}
onClick={deletePost}
css={{ cursor: "pointer" }}
>
Delete
</p>
</div>
</Text>
</Box>
);
}
@ -120,7 +69,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
my={3}
display="grid"
gridTemplateColumns="1fr"
gridAutoRows="auto"
gridAutoRows="min-content"
maxWidth="500px"
width="100%"
gridRowGap={4}
@ -129,41 +78,38 @@ export function Note(props: NoteProps & RouteComponentProps) {
<Text>{"<- Notebook Index"}</Text>
</Link>
<Col>
<Box mb={2}>{title}</Box>
<Box mb={2}>{note?.title || ""}</Box>
<Box display="flex">
<Text
color="gray"
mr={2}
fontFamily={contact?.nickname ? "sans" : "mono"}
>
{name}
</Text>
<Text className="di" style={{ lineHeight: 1 }}>
<Text color="gray">{date}</Text>
<Text ml={2}>{editPost}</Text>
</Text>
<Author
ship={ship}
contacts={contacts}
date={note?.["date-created"]}
/>
<Text ml={2}>{editPost}</Text>
</Box>
</Col>
<Box className="md" style={{ overflowWrap: "break-word" }}>
<ReactMarkdown source={newfile} linkTarget={"_blank"} />
</Box>
<NoteNavigation
prev={prev}
next={next}
notebook={notebook}
prevId={note?.["prev-note"] || undefined}
nextId={note?.["next-note"] || undefined}
ship={props.ship}
book={props.book}
/>
<Comments
enabled={notebook.comments}
ship={props.ship}
book={props.book}
noteId={props.noteId}
note={props.note}
comments={comments}
numComments={props.note?.["num-comments"]}
contacts={props.contacts}
api={props.api}
/>
{notebook.comments && (
<Comments
ship={props.ship}
book={props.book}
noteId={props.noteId}
note={props.note}
comments={comments}
numComments={props.note["num-comments"]}
contacts={props.contacts}
api={props.api}
/>
)}
<Spinner
text="Deleting post..."
awaiting={deleting}

View File

@ -0,0 +1,59 @@
import React from "react";
import * as Yup from "yup";
import { Box, Input } from "@tlon/indigo-react";
import { AsyncButton } from "../../../../components/AsyncButton";
import { Formik, Form, FormikHelpers } from "formik";
import { MarkdownField } from "./MarkdownField";
interface PostFormProps {
initial: PostFormSchema;
onSubmit: (
values: PostFormSchema,
actions: FormikHelpers<PostFormSchema>
) => Promise<any>;
submitLabel: string;
loadingText: string;
}
const formSchema = Yup.object({
title: Yup.string().required("Title cannot be blank"),
body: Yup.string().required("Post cannot be blank"),
});
export interface PostFormSchema {
title: string;
body: string;
}
export function PostForm(props: PostFormProps) {
const { initial, onSubmit, submitLabel, loadingText } = props;
return (
<Box
width="100%"
p={[2, 4]}
display="grid"
justifyItems="start"
gridTemplateRows={["64px 64px 1fr", "64px 1fr"]}
gridTemplateColumns={["1fr", "1fr 1fr"]}
gridColumnGap={2}
gridRowGap={2}
>
<Formik
validationSchema={formSchema}
initialValues={initial}
onSubmit={onSubmit}
>
<Form style={{ display: "contents" }}>
<Input width="100%" placeholder="Post Title" id="title" />
<Box mt={1} justifySelf="end">
<AsyncButton loadingText={loadingText} type="submit">
{submitLabel}
</AsyncButton>
</Box>
<MarkdownField gridColumn="1/3" id="body" />
</Form>
</Formik>
</Box>
);
}

View File

@ -1,14 +1,16 @@
import React, { Component } from "react";
import { Col, Box } from "@tlon/indigo-react";
import moment from "moment";
import { Box } from "@tlon/indigo-react";
import { Link } from "react-router-dom";
import {Note} from "../../../../types/publish-update";
import { Notebook } from "../../../../types/publish-update";
function NavigationItem(props: {
url: string;
title: string;
date: string;
date: number;
prev?: boolean;
}) {
const date = moment(date).fromNow();
return (
<Box
justifySelf={props.prev ? "start" : "end"}
@ -22,43 +24,54 @@ function NavigationItem(props: {
{props.prev ? "Previous" : "Next"}
</Box>
<Box mb={1}>{props.title}</Box>
<Box color="gray">{props.date}</Box>
<Box color="gray">{date}</Box>
</Link>
</Box>
);
}
interface NoteNavigationProps {
book: string;
next: any;
prev: any;
nextId?: string;
prevId?: string;
ship: string;
notebook: Notebook;
}
export function NoteNavigation(props: NoteNavigationProps) {
let nextComponent = <div />;
let prevComponent = <div />;
let nextComponent = <Box />;
let prevComponent = <Box />;
let nextUrl = "";
let prevUrl = "";
const { next, prev } = props;
const { nextId, prevId, notebook } = props;
const next =
nextId && nextId in notebook?.notes ? notebook?.notes[nextId] : null;
if(!next && !prev) {
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.next.id}`;
nextUrl = `/~publish/notebook/${props.ship}/${props.book}/note/${props.nextId}`;
nextComponent = (
<NavigationItem title={next.title} date={next.date} url={nextUrl} />
<NavigationItem
title={next.title}
date={next["date-created"]}
url={nextUrl}
/>
);
}
if (prev) {
prevUrl = `/~publish/notebook/${props.ship}/${props.book}/note/${props.prev.id}`;
prevUrl = `/~publish/notebook/${props.ship}/${props.book}/note/${props.prevId}`;
prevComponent = (
<NavigationItem title={prev.title} date={prev.date} url={prevUrl} prev />
<NavigationItem
title={prev.title}
date={prev["date-created"]}
url={prevUrl}
prev
/>
);
}

View File

@ -19,7 +19,7 @@ interface NoteRoutesProps {
}
export function NoteRoutes(props: NoteRoutesProps & RouteComponentProps) {
const { ship, book, api, notebook, notebookContacts, noteId } = props;
const { ship, book, noteId } = props;
const baseUrl = `/~publish/notebook/${ship}/${book}/note/${noteId}`;

View File

@ -1,12 +1,9 @@
import React, { Component, useEffect } from "react";
import React from "react";
import { Link, RouteComponentProps, Route, Switch } from "react-router-dom";
import { SidebarSwitcher } from "../../../../components/SidebarSwitch";
import { NotebookPosts } from "./NotebookPosts";
import { Subscribers } from "./subscribers";
import { Subscribers } from "./Subscribers";
import { Settings } from "./Settings";
import { cite } from "../../../../lib/util";
import { roleForShip } from "../../../../lib/group";
import { PublishContent } from "./PublishContent";
import {
Box,
Button,
@ -45,16 +42,9 @@ interface NotebookProps {
export function Notebook(props: NotebookProps & RouteComponentProps) {
const { api, ship, book, notebook, notebookContacts, groups } = props;
useEffect(() => {
api.publish.fetchNotesPage(ship, book, 1, 50);
api.publish.fetchNotebook(ship, book);
}, [ship, book, api]);
const contact = notebookContacts[ship];
const group = groups[notebook?.["writers-group-path"]];
const role = group ? roleForShip(group, window.ship) : undefined;
const isOwn = `~${window.ship}` === ship;
const isAdmin = role === "admin" || isOwn;
@ -67,14 +57,17 @@ export function Notebook(props: NotebookProps & RouteComponentProps) {
return (
<Box
pt={4}
mx="auto"
display="grid"
gridAutoRows="min-content"
gridTemplateColumns="1fr 1fr"
width="100%"
gridTemplateColumns={["1fr", "1fr 1fr"]}
maxWidth="500px"
gridRowGap={6}
gridRowGap={[4, 6]}
gridColumnGap={3}
>
<Box display={["block", "none"]} gridColumn={"1/3"}>
<Link to="/~publish">{"<- All Notebooks"}</Link>
</Box>
<Box>
{notebook?.title}
<br />
@ -83,7 +76,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps) {
{contact?.nickname || ship}
</Text>
</Box>
<Row justifyContent="flex-end">
<Row justifyContent={["flex-start", "flex-end"]}>
{isWriter && (
<Link to={`/~publish/notebook/${ship}/${book}/new`}>
<Button primary border>
@ -92,12 +85,12 @@ export function Notebook(props: NotebookProps & RouteComponentProps) {
</Link>
)}
{!isOwn && (
<Button ml={2} error border>
<Button ml={isWriter ? 2 : 0} error border>
Unsubscribe
</Button>
)}
</Row>
<Box gridColumn="1/4">
<Box gridColumn={["1/2", "1/3"]}>
<Tabs>
<TabList>
<Tab>All Posts</Tab>

View File

@ -1,5 +1,5 @@
import React from "react";
import { RouteComponentProps, Link, Route } from "react-router-dom";
import React, { useEffect } from "react";
import { RouteComponentProps, Link, Route, Switch } from "react-router-dom";
import { Box, Text } from "@tlon/indigo-react";
import GlobalApi from "../../../../api/global";
import { PublishContent } from "./PublishContent";
@ -26,11 +26,17 @@ export function NotebookRoutes(
) {
const { ship, book, api, notebook, notebookContacts } = props;
useEffect(() => {
api.publish.fetchNotesPage(ship, book, 1, 50);
api.publish.fetchNotebook(ship, book);
}, [ship, book]);
const baseUrl = `/~publish/notebook/${ship}/${book}`;
const relativePath = (path: string) => `${baseUrl}${path}`;
return (
<PublishContent sidebarShown api={api}>
<Switch>
<Route
path={baseUrl}
exact
@ -69,6 +75,6 @@ export function NotebookRoutes(
);
}}
/>
</PublishContent>
</Switch>
);
}

View File

@ -11,8 +11,9 @@ interface PublishContentProps {
api: GlobalApi;
}
export const PublishContent = (props: PublishContentProps) => {
const { children, sidebarShown, api } = props;
export const PublishContent = forwardRef((props: PublishContentProps) => {
const { children, sidebarShown, api, onScroll } = props;
const { query } = useQuery();
const popout = !!query.get("popout");

View File

@ -1,8 +1,6 @@
import React, { Component, useEffect } from "react";
import { Spinner } from "../../../../components/Spinner";
import React, { useEffect } from "react";
import { AsyncButton } from "../../../../components/AsyncButton";
import { InviteSearch } from "../../../../components/InviteSearch";
import Toggle from "../../../../components/toggle";
import * as Yup from "yup";
import {
Box,
Input,
@ -11,100 +9,13 @@ import {
InputLabel,
InputCaption,
Button,
Center,
} from "@tlon/indigo-react";
import { Formik, Form, useFormikContext, FormikHelpers } from "formik";
import GlobalApi from "../../../../api/global";
import { Notebook } from "../../../../types/publish-update";
import { Contacts } from "../../../../types/contact-update";
/*
*
*
renderGroupify() {
const { props, state } = this;
const owner = (props.host.slice(1) === window.ship);
const ownedUnmanaged =
owner &&
!props.contacts[props.notebook?.['writers-group-path']];
if (!ownedUnmanaged) {
return null;
} else {
// don't give the option to make inclusive if we don't own the target
// group
const targetOwned = (state.targetGroup)
? Boolean(state.targetGroup.includes(`/~${window.ship}/`))
: false;
let inclusiveToggle = <div />;
if (targetOwned) {
inclusiveToggle = (
<div className="mt4">
<Toggle
boolean={state.inclusive}
change={this.changeInclusive}
/>
<span className="dib f9 white-d inter ml3">
Add all members to group
</span>
<p className="f9 gray2 pt1" style={{ paddingLeft: 40 }}>
Add notebook members to the group if they aren't in it yet
</p>
</div>
);
}
return (
<div>
<div className={'w-100 fl mt3 mb3'} style={{ maxWidth: '29rem' }}>
{this.renderHeader(
'Convert Notebook',
'Convert this notebook into a group with associated chat, or select a group to add this notebook to.')}
<InviteSearch
groups={props.groups}
contacts={props.contacts}
associations={props.associations}
groupResults={true}
shipResults={false}
invites={{
groups: state.targetGroup ? [state.targetGroup] : [],
ships: []
}}
setInvite={this.changeTargetGroup}
/>
{inclusiveToggle}
<button
onClick={this.groupifyNotebook.bind(this)}
className={'dib f9 black gray4-d bg-gray0-d ba pa2 mt4 b--black b--gray1-d pointer'}
disabled={this.state.disabled}
>
{state.targetGroup ? 'Add to group' : 'Convert to group'}
</button>
</div>
</div>
);
{this.renderHeader(
'Delete Notebook',
'Permanently delete this notebook. (All current members will no longer see this notebook)')}
{() => {
this.setState({ disabled: true });
this.props.api.publish
.publishAction({
'edit-book': {
book: this.props.book,
title: this.state.title,
about: this.props.notebook.about,
coms: this.props.notebook.comments,
group: null
}
})
.then(() => {
this.setState({ disabled: false });
});
}}
*/
import { FormError } from "../../../../components/FormError";
interface SettingsProps {
host: string;
@ -120,6 +31,12 @@ interface FormSchema {
comments: boolean;
}
const formSchema = Yup.object({
name: Yup.string().required("Notebook must have a name"),
description: Yup.string(),
comments: Yup.boolean(),
});
const ResetOnPropsChange = (props: { init: FormSchema; book: string }) => {
const { resetForm } = useFormikContext<FormSchema>();
useEffect(() => {
@ -134,64 +51,77 @@ export function Settings(props: SettingsProps) {
const initialValues: FormSchema = {
name: notebook?.title,
description: notebook?.about,
comments: true,
comments: notebook?.comments,
};
const onSubmit = async (
values: FormSchema,
actions: FormikHelpers<FormSchema>
) => {
await api.publish.publishAction({
"edit-book": {
book,
title: values.name,
about: values.description,
coms: values.comments,
group: null,
},
});
api.publish.fetchNotebook(host, book)
try {
await api.publish.publishAction({
"edit-book": {
book,
title: values.name,
about: values.description,
coms: values.comments,
group: null,
},
});
api.publish.fetchNotebook(host, book);
actions.setStatus({ success: null });
} catch (e) {
console.log(e);
actions.setStatus({ error: e.message });
}
};
if (props.host.slice(1) !== window.ship) {
return null;
}
return (
<Formik initialValues={initialValues} onSubmit={onSubmit}>
<Form>
<Box maxWidth="300px" mb={4} gridTemplateColumns="1fr" gridAutoRows="auto" display="grid">
<Col mb={4}>
<InputLabel>Delete Notebook</InputLabel>
<InputCaption>
Permanently delete this notebook. (All current members will no
longer see this notebook
</InputCaption>
<Button mt={1} border error>
Delete this notebook
</Button>
</Col>
<Input
id="name"
label="Rename"
caption="Change the name of this notebook"
/>
<Input
id="description"
label="Change description"
caption="Change the description of this notebook"
/>
<Checkbox
id="comments"
label="Comments"
caption="Subscribers may comment when enabled"
/>
<ResetOnPropsChange init={initialValues} book={book} />
<AsyncButton loadingText="Updating.." border>
Save
</AsyncButton>
</Box>
</Form>
<Formik
validationSchema={formSchema}
initialValues={initialValues}
onSubmit={onSubmit}
>
<Form>
<Box
maxWidth="300px"
mb={4}
gridTemplateColumns="1fr"
gridAutoRows="auto"
display="grid"
>
<Col mb={4}>
<InputLabel>Delete Notebook</InputLabel>
<InputCaption>
Permanently delete this notebook. (All current members will no
longer see this notebook
</InputCaption>
<Button mt={1} border error>
Delete this notebook
</Button>
</Col>
<Input
id="name"
label="Rename"
caption="Change the name of this notebook"
/>
<Input
id="description"
label="Change description"
caption="Change the description of this notebook"
/>
<Checkbox
id="comments"
label="Comments"
caption="Subscribers may comment when enabled"
/>
<ResetOnPropsChange init={initialValues} book={book} />
<AsyncButton loadingText="Updating.." border>
Save
</AsyncButton>
<FormError message="Failed to update settings" />
</Box>
</Form>
</Formik>
);
}

View File

@ -105,6 +105,8 @@ export function Sidebar(props: any) {
);
}
const display = props.path ? ['none', 'block'] : 'block';
return (
<Col
borderRight={[0, 1]}
@ -112,6 +114,7 @@ export function Sidebar(props: any) {
height="100%"
pt={[3, 0]}
overflowY="auto"
display={display}
maxWidth={["none", "250px"]}
>
<Box>

View File

@ -1,31 +1,11 @@
import React, { useCallback } from "react";
import * as Yup from "yup";
import {
MarkdownEditor as _MarkdownEditor,
Box,
Button,
Input,
ErrorMessage,
} from "@tlon/indigo-react";
import { dateToDa, stringToSymbol } from "../../../../lib/util";
import { AsyncButton } from '../../../../components/AsyncButton';
import { Formik, Form, FormikHelpers } from "formik";
import React from "react";
import { stringToSymbol } from "../../../../lib/util";
import { FormikHelpers } from "formik";
import GlobalApi from "../../../../api/global";
import { useWaitForProps } from "../../../../lib/useWaitForProps";
import { Notebooks, Notebook } from "../../../../types/publish-update";
import { MarkdownField } from './MarkdownField';
import { Notebook } from "../../../../types/publish-update";
import { RouteComponentProps } from "react-router-dom";
interface FormSchema {
title: string;
body: string;
}
const formSchema = Yup.object({
title: Yup.string().required("Title cannot be blank"),
body: Yup.string().required("Post cannot be blank"),
});
import { PostForm, PostFormSchema } from "./NoteForm";
interface NewPostProps {
api: GlobalApi;
@ -34,92 +14,52 @@ interface NewPostProps {
notebook: Notebook;
}
function NewPost(props: NewPostProps & RouteComponentProps) {
export default function NewPost(props: NewPostProps & RouteComponentProps) {
const { api, book, notebook, ship, history } = props;
const waiter = useWaitForProps(props, 20000);
const onSubmit = useCallback(
async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
const noteId = stringToSymbol(values.title);
const newNote = {
"new-note": {
who: window.ship,
book: book,
note: noteId,
title: values.title,
body: values.body,
},
};
const onSubmit = async (
values: PostFormSchema,
actions: FormikHelpers<PostFormSchema>
) => {
let noteId = stringToSymbol(values.title);
const { title, body } = values;
const host = ship.slice(1);
try {
try {
try {
await api.publish.publishAction(newNote);
} catch (e) {
if (e.includes("note already exists")) {
const timestamp = Math.floor(Date.now() / 1000);
newNote["new-note"].note += "-" + timestamp;
api.publish.publishAction(newNote);
} else {
throw e;
}
}
await waiter((p) => {
return !!p?.notebook?.notes[newNote["new-note"].note];
});
history.push(
`/~publish/notebook/${ship}/${book}/note/${newNote["new-note"].note}`
);
await api.publish.newNote(host, book, noteId, title, body);
} catch (e) {
console.error(e);
actions.setStatus({ error: "Posting note failed" });
if (e.includes("note already exists")) {
const timestamp = Math.floor(Date.now() / 1000);
noteId = `${noteId}-${timestamp}`;
await api.publish.newNote(host, book, noteId, title, body);
} else {
throw e;
}
}
},
[api, waiter, book]
);
await waiter((p) => {
return !!p?.notebook?.notes[noteId];
});
history.push(`/~publish/notebook/${ship}/${book}/note/${noteId}`);
} catch (e) {
console.error(e);
actions.setStatus({ error: "Posting note failed" });
}
};
const initialValues: FormSchema = {
const initialValues: PostFormSchema = {
title: "",
body: "",
};
const date = dateToDa(new Date()).slice(1, -10);
return (
<Box
width="100%"
px={4}
pb={4}
display="grid"
justifyItems="start"
gridTemplateRows="64px 1fr"
gridTemplateColumns="1fr 1fr"
gridRowGap={2}
>
<Formik
validationSchema={formSchema}
initialValues={initialValues}
onSubmit={onSubmit}
>
{({ isSubmitting, isValid }) => (
<Form style={{ display: "contents" }}>
<Input width="100%" placeholder="Post Title" id="title" />
<Box mt={1} justifySelf="end">
<AsyncButton
loadingText="Posting..."
disabled={!isValid}
type="submit"
>
Publish To {notebook.title}
</AsyncButton>
</Box>
<MarkdownField gridColumn="1/3" id="body" />
</Form>
)}
</Formik>
</Box>
<PostForm
initial={initialValues}
onSubmit={onSubmit}
submitLabel={`Publish to ${notebook?.title}`}
loadingText="Posting..."
/>
);
}
export default NewPost;

View File

@ -1,20 +1,11 @@
import React, { Component, useCallback } from "react";
import {
Box,
Input,
Text,
Button,
Col,
ErrorMessage,
} from "@tlon/indigo-react";
import React, { useCallback } from "react";
import { Box, Input, Col } from "@tlon/indigo-react";
import { Formik, Form } from "formik";
import * as Yup from "yup";
import GlobalApi from "../../../../api/global";
import DropdownSearch, {
InviteSearch,
} from "../../../../components/InviteSearch";
import { AsyncButton } from "../../../../components/AsyncButton";
import { Link, RouteComponentProps } from "react-router-dom";
import { FormError } from "../../../../components/FormError";
import { RouteComponentProps } from "react-router-dom";
import { stringToSymbol } from "../../../../lib/util";
import GroupSearch from "../../../../components/GroupSearch";
import { Associations } from "../../../../types/metadata-update";
@ -40,42 +31,23 @@ interface NewScreenProps {
}
export function NewScreen(props: NewScreenProps & RouteComponentProps) {
const { api, history } = props;
const { history } = props;
const waiter = useWaitForProps(props, 5000);
const onSubmit = useCallback(
async (values: FormSchema, actions) => {
const bookId = stringToSymbol(values.name);
const groupInfo = {
"group-path": values.group,
invitees: [],
"use-preexisting": true,
"make-managed": true,
};
const action = {
"new-book": {
book: bookId,
title: values.name,
about: values.description,
coms: true,
group: groupInfo,
},
};
try {
await props.api.publish.publishAction(action);
await waiter((p) => !!p?.notebooks?.[`~${window.ship}`]?.[bookId]);
actions.setSubmitting(false);
actions.setStatus({ success: null });
history.push(`/~publish/notebook/~${window.ship}/${bookId}`);
} catch (e) {
console.error(e);
actions.setSubmitting(false);
actions.setStatus({ error: "Notebook creation failed" });
}
},
[api]
);
const onSubmit = async (values: FormSchema, actions) => {
const bookId = stringToSymbol(values.name);
try {
const { name, description, group } = values;
await props.api.publish.newBook(bookId, name, description, group);
await waiter((p) => !!p?.notebooks?.[`~${window.ship}`]?.[bookId]);
actions.setStatus({ success: null });
history.push(`/~publish/notebook/~${window.ship}/${bookId}`);
} catch (e) {
console.error(e);
actions.setStatus({ error: "Notebook creation failed" });
}
};
return (
<Col p={3}>
<Box mb={4}>New Notebook</Box>
@ -84,44 +56,40 @@ export function NewScreen(props: NewScreenProps & RouteComponentProps) {
initialValues={{ name: "", description: "", group: "" }}
onSubmit={onSubmit}
>
{({ isSubmitting, status }) => (
<Form>
<Box
display="grid"
gridTemplateRows="auto"
gridRowGap={2}
gridTemplateColumns="300px"
>
<Input
id="name"
label="Name"
caption="Provide a name for your notebook"
placeholder="eg. My Journal"
/>
<Input
id="description"
label="Description"
caption="What's your notebook about?"
placeholder="Notebook description"
/>
<GroupSearch
id="group"
label="Group"
caption="What group is the notebook for?"
associations={props.associations}
/>
<Form>
<Box
display="grid"
gridTemplateRows="auto"
gridRowGap={2}
gridTemplateColumns="300px"
>
<Input
id="name"
label="Name"
caption="Provide a name for your notebook"
placeholder="eg. My Journal"
/>
<Input
id="description"
label="Description"
caption="What's your notebook about?"
placeholder="Notebook description"
/>
<GroupSearch
id="group"
label="Group"
caption="What group is the notebook for?"
associations={props.associations}
/>
<Box justifySelf="start">
<AsyncButton loadingText="Creating..." type="submit" border>
Create Notebook
</AsyncButton>
</Box>
{status && status.error && (
<ErrorMessage>{status.error}</ErrorMessage>
)}
<Box justifySelf="start">
<AsyncButton loadingText="Creating..." type="submit" border>
Create Notebook
</AsyncButton>
</Box>
</Form>
)}
<FormError message="Notebook Creation failed" />
</Box>
</Form>
</Formik>
</Col>
);

View File

@ -1,59 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
export class NoteNavigation extends Component {
constructor(props) {
super(props);
}
render() {
let nextComponent = null;
let prevComponent = null;
let nextUrl = '';
let prevUrl = '';
const popout = (this.props.popout) ? 'popout/' : '';
if (this.props.next && this.props.prev) {
nextUrl = `/~publish/${popout}note/${this.props.ship}/${this.props.book}/${this.props.next.id}`;
prevUrl = `/~publish/${popout}note/${this.props.ship}/${this.props.book}/${this.props.prev.id}`;
nextComponent =
<Link to={nextUrl} className="di flex-column flex-auto tr w-100 pv6 bt bb b--gray3">
<div className="f9 gray2 mb2">Next</div>
<div className="f9 mb1 truncate">{this.props.next.title}</div>
<div className="f9 gray2">{this.props.next.date}</div>
</Link>;
prevComponent =
<Link to={prevUrl} className="di flex-column flex-auto w-100 pv6 bt br bb b--gray3">
<div className="f9 gray2 mb2">Previous</div>
<div className="f9 mb1 truncate">{this.props.prev.title}</div>
<div className="f9 gray2">{this.props.prev.date}</div>
</Link>;
} else if (this.props.prev) {
prevUrl = `/~publish/${popout}note/${this.props.ship}/${this.props.book}/${this.props.prev.id}`;
prevComponent =
<Link to={prevUrl} className="di flex-column flex-auto w-100 pv6 bt bb b--gray3">
<div className="f9 gray2 mb2">Previous</div>
<div className="f9 mb1 truncate">{this.props.prev.title}</div>
<div className="f9 gray2">{this.props.prev.date}</div>
</Link>;
} else if (this.props.next) {
nextUrl = `/~publish/${popout}note/${this.props.ship}/${this.props.book}/${this.props.next.id}`;
nextComponent =
<Link to={nextUrl} className="di flex-column flex-auto tr w-100 pv6 bt bb b--gray3">
<div className="f9 gray2 mb2">Next</div>
<div className="f9 mb1 truncate">{this.props.next.title}</div>
<div className="f9 gray2">{this.props.next.date}</div>
</Link>;
}
return (
<div className="flex">
{prevComponent}
{nextComponent}
</div>
);
}
}
export default NoteNavigation;

View File

@ -1,9 +1,8 @@
import React, { useMemo } from "react";
import { Box } from "@tlon/indigo-react";
import React, { useRef, SyntheticEvent } from "react";
import { Box, Center } from "@tlon/indigo-react";
import { Sidebar } from "./lib/Sidebar";
import ErrorBoundary from "../../../components/ErrorBoundary";
import { Notebooks } from "../../../types/publish-update";
import { Path } from "../../../types/noun";
import { SelectedGroup } from "../../../types/local-update";
import { Rolodex } from "../../../types/contact-update";
import { Invites } from "../../../types/invite-update";
@ -11,11 +10,12 @@ import GlobalApi from "../../../api/global";
import { Associations } from "../../../types/metadata-update";
import { RouteComponentProps } from "react-router-dom";
type SkeletonProps = RouteComponentProps<{ ship: string; notebook: string }> & {
sidebarShown: boolean;
popout: boolean;
type SkeletonProps = RouteComponentProps<{
ship?: string;
notebook?: string;
noteId?: string;
}> & {
notebooks: Notebooks;
active: "sidebar" | "rightPanel";
invites: Invites;
associations: Associations;
selectedGroups: SelectedGroup[];
@ -25,28 +25,61 @@ type SkeletonProps = RouteComponentProps<{ ship: string; notebook: string }> & {
};
export function Skeleton(props: SkeletonProps) {
const popout = props.popout ? props.popout : false;
const { api, notebooks } = props;
const { ship, notebook, noteId } = props.match.params;
const popoutWindow = popout ? 0 : [0, 3];
const path =
(ship &&
notebook &&
`${props.match.params.ship}/${props.match.params.notebook}`) ||
undefined;
const panelDisplay = props.active === "sidebar" ? ["none", "block"] : "block";
const path = `${props.match.params.ship}/${props.match.params.notebook}`;
const onScroll = (e: SyntheticEvent<HTMLDivElement>) => {
const { scrollHeight, scrollTop, clientHeight } = e.target;
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
if (noteId && notebook && ship) {
const note = notebooks?.[ship]?.[notebook]?.notes?.[noteId];
if (!note || !note.comments) {
return;
}
const loadedComments = note.comments?.length;
const fullyLoaded = note["num-comments"] === loadedComments;
if (distanceFromBottom < 40) {
if (!fullyLoaded) {
api.publish.fetchCommentsPage(
ship,
notebook,
noteId,
loadedComments,
30
);
}
if (!note.read) {
api.publish.publishAction({
read: {
who: ship.slice(1),
book: notebook,
note: noteId,
},
});
}
}
} else if (notebook && ship) {
}
};
const panelDisplay = !path ? ["none", "block"] : "block";
return (
<Box height="100%" width="100%" px={popoutWindow} pb={popoutWindow}>
<Box height="100%" width="100%" px={[0, 3]} pb={[0, 3]}>
<Box
display="flex"
border={popout ? 0 : 1}
borderColor="washedGray"
border={[0, 1]}
borderColor={["washedGray", "washedGray"]}
borderRadius={1}
width="100%"
height="100%"
>
<Sidebar
popout={popout}
sidebarShown={props.sidebarShown}
active={props.active}
notebooks={props.notebooks}
contacts={props.contacts}
path={path}
@ -60,6 +93,10 @@ export function Skeleton(props: SkeletonProps) {
width="100%"
height="100%"
position="relative"
px={[3, 4]}
fontSize={0}
overflowY="scroll"
onScroll={onScroll}
>
<ErrorBoundary>{props.children}</ErrorBoundary>
</Box>

View File

@ -35,7 +35,7 @@ export function AsyncButton({
}, [status]);
return (
<Button border disabled={!isValid || isSubmitting} type="submit" {...rest}>
<Button border disabled={!isValid} type="submit" {...rest}>
{isSubmitting ? (
<Spinner awaiting text={loadingText} />
) : success === true ? (

View File

@ -0,0 +1,13 @@
import React from "react";
import { useFormikContext } from "formik";
import { ErrorMessage } from "@tlon/indigo-react";
export function FormError(props: { message: string }) {
const { status } = useFormikContext();
let s = status || {};
return (
<ErrorMessage>{"error" in s ? props.message : null}</ErrorMessage>
);
}