mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-14 17:41:33 +03:00
publish: finish rewrite
This commit is contained in:
parent
135d9ceb20
commit
5570c076fd
@ -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
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
) ;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
/>
|
||||
|
51
pkg/interface/src/apps/publish/components/lib/Author.tsx
Normal file
51
pkg/interface/src/apps/publish/components/lib/Author.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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..."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
59
pkg/interface/src/apps/publish/components/lib/NoteForm.tsx
Normal file
59
pkg/interface/src/apps/publish/components/lib/NoteForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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}`;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
@ -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>
|
||||
|
@ -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 ? (
|
||||
|
13
pkg/interface/src/components/FormError.tsx
Normal file
13
pkg/interface/src/components/FormError.tsx
Normal 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>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user