publish: cleanup imports

This commit is contained in:
Liam Fitzgerald 2020-08-18 10:57:07 +10:00
parent ad711fa41e
commit 1bcd7f27e6
55 changed files with 227 additions and 2940 deletions

View File

@ -0,0 +1,58 @@
import { useState, useEffect, useMemo, useCallback } from "react";
export function useDropdown<C>(
candidates: C[],
key: (c: C) => string,
searchPred: (query: string, c: C) => boolean
) {
const [options, setOptions] = useState(candidates);
const [selected, setSelected] = useState<C | undefined>();
const search = useCallback(
(s: string) => {
const opts = candidates.filter((c) => searchPred(s, c));
setOptions(opts);
if (selected) {
const idx = opts.findIndex((c) => key(c) === key(selected));
console.log(idx);
if (idx < 0) {
setSelected(undefined);
}
}
},
[candidates, searchPred, key, selected, setOptions, setSelected]
);
const changeSelection = useCallback(
(backward = false) => {
const select = (idx: number) => {
setSelected(options[idx]);
};
if(!selected) { select(0); return false; }
const idx = options.findIndex((c) => key(c) === key(selected));
if (
idx === -1 ||
(options.length - 1 <= idx && !backward)
) {
select(0);
} else if (idx === 0 && backward) {
select(options.length - 1);
} else {
select(idx + (backward ? -1 : 1));
}
return false;
},
[options, setSelected, selected]
);
const next = useCallback(() => changeSelection(), [changeSelection]);
const back = useCallback(() => changeSelection(true), [changeSelection]);
return {
next,
back,
search,
selected,
options,
};
}

View File

@ -0,0 +1,22 @@
import { useState, useCallback } from 'react';
export function useLocalStorageState<T>(key: string, initial: T) {
const [state, _setState] = useState(() => {
const s = localStorage.getItem(key);
if(s) {
return JSON.parse(s) as T;
}
return initial;
});
const setState = useCallback((s: T) => {
_setState(s);
localStorage.setItem(key, JSON.stringify(s));
}, [_setState]);
return [state, setState] as const;
}

View File

@ -0,0 +1,30 @@
import { useMemo, useCallback } from "react";
import { useLocation } from "react-router-dom";
import _ from 'lodash';
export function useQuery() {
const { search } = useLocation();
const query = useMemo(() => new URLSearchParams(search), [search]);
const appendQuery = useCallback(
(q: Record<string, string>) => {
const newQuery = new URLSearchParams(search);
_.forIn(q, (value, key) => {
if (!value) {
newQuery.delete(key);
} else {
newQuery.append(key, value);
}
});
return newQuery.toString();
},
[search]
);
return {
query,
appendQuery,
};
}

View File

@ -0,0 +1,36 @@
import { useState, useEffect, useCallback } from 'react';
export function useWaitForProps<P>(props: P, timeout: number) {
const [resolve, setResolve] = useState<() => void>(() => () => {});
const [ready, setReady] = useState<(p: P) => boolean | undefined>();
useEffect(() => {
if (typeof ready === "function" && ready(props)) {
resolve();
}
}, [props, ready, resolve]);
/**
* Waits until some predicate is true
*
* @param r - Predicate to wait for
* @returns A promise that resolves when `r` returns true, or rejects if the
* waiting times out
*
*/
const waiter = useCallback(
(r: (props: P) => boolean) => {
setReady(() => r);
return new Promise<void>((resolve, reject) => {
setResolve(() => resolve);
setTimeout(() => {
reject(new Error("Timed out"));
}, timeout);
});
},
[setResolve, setReady, timeout]
);
return waiter;
}

View File

@ -1,307 +0,0 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import _ from 'lodash';
import './css/custom.css';
import { Skeleton } from './components/skeleton';
import { NewScreen } from './components/lib/new';
import { JoinScreen } from './components/lib/join';
import { Notebook } from './components/lib/notebook';
import { Note } from './components/lib/note';
import { NewPost } from './components/lib/new-post';
import { EditPost } from './components/lib/edit-post';
export default class PublishApp extends React.Component {
constructor(props) {
super(props);
this.unreadTotal = 0;
}
componentDidMount() {
document.title = 'OS1 - Publish';
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';
this.props.subscription.startApp('publish');
this.props.api.publish.fetchNotebooks();
if (!this.props.sidebarShown) {
this.props.api.local.sidebarToggle();
}
}
componentWillUnmount() {
this.props.subscription.stopApp('publish');
}
render() {
const { props } = this;
const contacts = props.contacts ? props.contacts : {};
const associations = props.associations ? props.associations : { contacts: {} };
const notebooks = props.notebooks ? props.notebooks : {};
const unreadTotal = _.chain(notebooks)
.values()
.map(_.values)
.flatten() // flatten into array of notebooks
.map('num-unread')
.reduce((acc, count) => acc + count, 0)
.value();
if (this.unreadTotal !== unreadTotal) {
document.title = unreadTotal > 0 ? `(${unreadTotal}) OS1 - Publish` : 'OS1 - Publish';
this.unreadTotal = unreadTotal;
}
const { api, groups, sidebarShown, invites } = props;
return (
<Switch>
<Route exact path="/~publish"
render={(props) => {
return (
<Skeleton
popout={false}
active={'sidebar'}
rightPanelHide={true}
sidebarShown={true}
invites={invites}
notebooks={notebooks}
associations={associations}
contacts={contacts}
api={api}
>
<div
className={`h-100 w-100 overflow-x-hidden flex flex-column
bg-white bg-gray0-d dn db-ns`}
>
<div className='pl3 pr3 pt2 dt pb3 w-100 h-100'>
<p className='f9 pt3 gray2 w-100 h-100 dtc v-mid tc'>
Select or create a notebook to begin.
</p>
</div>
</div>
</Skeleton>
);
}}
/>
<Route
exact
path='/~publish/new'
render={props => {
return (
<Skeleton
popout={false}
active={'rightPanel'}
rightPanelHide={false}
sidebarShown={sidebarShown}
invites={invites}
notebooks={notebooks}
associations={associations}
contacts={contacts}
api={api}
>
<NewScreen
associations={associations.contacts}
notebooks={notebooks}
groups={groups}
contacts={contacts}
api={api}
{...props}
/>
</Skeleton>
);
}}
/>
<Route
exact
path='/~publish/join/:ship?/:notebook?'
render={props => {
const ship = props.match.params.ship || '';
const notebook = props.match.params.notebook || '';
return (
<Skeleton
popout={false}
active={'rightPanel'}
rightPanelHide={false}
sidebarShown={sidebarShown}
invites={invites}
notebooks={notebooks}
associations={associations}
contacts={contacts}
api={api}
>
<JoinScreen
notebooks={notebooks}
ship={ship}
notebook={notebook}
api={api}
{...props}
/>
</Skeleton>
);
}}
/>
<Route
exact
path='/~publish/:popout?/notebook/:ship/:notebook/:view?'
render={props => {
const view = props.match.params.view
? props.match.params.view
: 'posts';
const popout = Boolean(props.match.params.popout) || false;
const ship = props.match.params.ship || '';
const notebook = props.match.params.notebook || '';
const path = `${ship}/${notebook}`;
const bookGroupPath =
notebooks?.[ship]?.[notebook]?.['subscribers-group-path'];
const notebookContacts =
bookGroupPath in contacts ? contacts[bookGroupPath] : {};
if (view === 'new') {
return (
<Skeleton
popout={popout}
active={'rightPanel'}
rightPanelHide={false}
sidebarShown={sidebarShown}
invites={invites}
notebooks={notebooks}
associations={associations}
contacts={contacts}
path={path}
api={api}
>
<NewPost
notebooks={notebooks}
ship={ship}
book={notebook}
sidebarShown={sidebarShown}
popout={popout}
api={api}
{...props}
/>
</Skeleton>
);
} else {
return (
<Skeleton
popout={popout}
active={'rightPanel'}
rightPanelHide={false}
sidebarShown={sidebarShown}
invites={invites}
notebooks={notebooks}
associations={associations}
contacts={contacts}
path={path}
api={api}
>
<Notebook
notebooks={notebooks}
view={view}
ship={ship}
book={notebook}
groups={groups}
contacts={contacts}
notebookContacts={notebookContacts}
associations={associations.contacts}
sidebarShown={sidebarShown}
popout={popout}
api={api}
{...props}
/>
</Skeleton>
);
}
}}
/>
<Route
exact
path='/~publish/:popout?/note/:ship/:notebook/:note/:edit?'
render={props => {
const ship = props.match.params.ship || '';
const notebook = props.match.params.notebook || '';
const path = `${ship}/${notebook}`;
const note = props.match.params.note || '';
const popout = Boolean(props.match.params.popout) || false;
const bookGroupPath =
notebooks?.[ship]?.[notebook]?.['subscribers-group-path'];
const notebookContacts = (bookGroupPath in contacts)
? contacts[bookGroupPath] : {};
const edit = Boolean(props.match.params.edit) || false;
if (edit) {
return (
<Skeleton
popout={popout}
active={'rightPanel'}
rightPanelHide={false}
sidebarShown={sidebarShown}
invites={invites}
notebooks={notebooks}
associations={associations}
contacts={contacts}
path={path}
api={api}
>
<EditPost
notebooks={notebooks}
book={notebook}
note={note}
ship={ship}
sidebarShown={sidebarShown}
popout={popout}
api={api}
{...props}
/>
</Skeleton>
);
} else {
return (
<Skeleton
popout={popout}
active={'rightPanel'}
rightPanelHide={false}
sidebarShown={sidebarShown}
invites={invites}
notebooks={notebooks}
associations={associations}
contacts={contacts}
path={path}
api={api}
>
<Note
notebooks={notebooks}
book={notebook}
groups={groups}
contacts={notebookContacts}
ship={ship}
note={note}
sidebarShown={sidebarShown}
popout={popout}
api={api}
{...props}
/>
</Skeleton>
);
}
}}
/>
</Switch>
);
}
}

View File

@ -1,8 +1,8 @@
import React from "react";
import React, {ReactNode} from "react";
import moment from "moment";
import { Sigil } from "../../../../lib/sigil";
import { uxToHex, cite } from "../../../../lib/util";
import { Contacts } from "../../../../types/contact-update";
import { Sigil } from "~/logic/lib/sigil"
import { uxToHex, cite } from "~/logic/lib/util";
import { Contacts } from "~/types/contact-update";
import { Row, Box } from "@tlon/indigo-react";
interface AuthorProps {
@ -10,6 +10,7 @@ interface AuthorProps {
ship: string;
date: number;
showImage?: boolean;
children: ReactNode;
}
export function Author(props: AuthorProps) {
@ -31,7 +32,7 @@ export function Author(props: AuthorProps) {
ship={ship}
size={24}
color={color}
classes="mix-blend-diff"
classes={contact?.color ? '' : "mix-blend-diff"}
/>
)}
</Box>
@ -46,6 +47,7 @@ export function Author(props: AuthorProps) {
<Box ml={2} color="gray">
{dateFmt}
</Box>
{props.children}
</Row>
);
}

View File

@ -1,19 +1,18 @@
import React, { useState } from "react";
import moment from "moment";
import { Sigil } from "../../../../lib/sigil";
import { Sigil } from "~/logic/lib/sigil";
import CommentInput from "./CommentInput";
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 { uxToHex, cite } from "~/logic/lib/util";
import { Comment, NoteId } from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
import GlobalApi from "~/logic/api/global";
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;
padding-left: ${p => p.theme.space[2]}px;
`;
interface CommentItemProps {
@ -64,22 +63,23 @@ export function CommentItem(props: CommentItemProps) {
contacts={contacts}
ship={ship}
date={commentData["date-created"]}
/>
{!disabled && !editing && (
<>
<ClickBox color="green" onClick={() => setEditing(true)}>
Edit
>
{!disabled && !editing && (
<>
<ClickBox color="green" onClick={() => setEditing(true)}>
Edit
</ClickBox>
<ClickBox color="red" onClick={onDelete}>
Delete
</ClickBox>
</>
)}
{editing && (
<ClickBox onClick={() => setEditing(false)} color="red">
Cancel
</ClickBox>
<ClickBox color="red" onClick={onDelete}>
Delete
</ClickBox>
</>
)}
{editing && (
<ClickBox onClick={() => setEditing(false)} color="red">
Cancel
</ClickBox>
)}
)}
</Author>
</Row>
<Box mb={2}>
{!editing && content}

View File

@ -2,11 +2,11 @@ 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 { Comment, Note, NoteId } from "../../../../types/publish-update";
import { Contacts } from "../../../../types/contact-update";
import { dateToDa } from "~/logic/lib/util";
import { Comment, Note, NoteId } from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
import _ from "lodash";
import GlobalApi from "../../../../api/global";
import GlobalApi from "~/logic/api/global";
import { FormikHelpers } from "formik";
interface CommentsProps {
@ -59,9 +59,10 @@ export function Comments(props: CommentsProps) {
<CommentInput onSubmit={onSubmit} />
{Array.from(pending).map((com, i) => {
const da = dateToDa(new Date());
const ship = `~${window.ship}`;
const comment = {
[da]: {
author: `~${window.ship}`,
author: ship,
content: com,
"date-created": Math.round(new Date().getTime()),
},
@ -71,6 +72,7 @@ export function Comments(props: CommentsProps) {
comment={comment}
key={i}
contacts={props.contacts}
ship={ship}
pending={true}
/>
);

View File

@ -1,8 +1,8 @@
import React, { useCallback, useState, useRef, useEffect } from "react";
import { Col, Text, ErrorMessage } from "@tlon/indigo-react";
import { Spinner } from "../../../../components/Spinner";
import { Notebooks } from "../../../../types/publish-update";
import { useWaitForProps } from "../../../../lib/useWaitForProps";
import { Spinner } from "~/views/components/Spinner";
import { Notebooks } from "~/types/publish-update";
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import { RouteComponentProps } from "react-router-dom";
interface JoinScreenProps {

View File

@ -2,16 +2,16 @@ import React, { useState, useEffect } from "react";
import { Box, Text, Col } from "@tlon/indigo-react";
import ReactMarkdown from "react-markdown";
import { Link, RouteComponentProps } from "react-router-dom";
import { Spinner } from "../../../../components/Spinner";
import { Spinner } from "~/views/components/Spinner";
import { Comments } from "./Comments";
import { NoteNavigation } from "./NoteNavigation";
import {
NoteId,
Note as INote,
Notebook,
} from "../../../../types/publish-update";
import { Contacts } from "../../../../types/contact-update";
import GlobalApi from "../../../../api/global";
} from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
import GlobalApi from "~/logic/api/global";
import { Author } from "./Author";
interface NoteProps {
@ -35,7 +35,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
const deletePost = async () => {
setDeleting(true);
await api.publish.delNote(ship, book, noteId);
await api.publish.delNote(ship.slice(1), book, noteId);
props.history.push(baseUrl);
};
@ -73,6 +73,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
maxWidth="500px"
width="100%"
gridRowGap={4}
mx="auto"
>
<Link to={baseUrl}>
<Text>{"<- Notebook Index"}</Text>
@ -100,7 +101,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
/>
{notebook.comments && (
<Comments
ship={props.ship}
ship={ship}
book={props.book}
noteId={props.noteId}
note={props.note}

View File

@ -1,8 +1,8 @@
import React from "react";
import { Col, Box } from "@tlon/indigo-react";
import { cite } from "../../../../lib/util";
import { Note } from "../../../../types/publish-update";
import { Contact } from "../../../../types/contact-update";
import { cite } from "~/logic/lib/util";
import { Note } from "~/types/publish-update";
import { Contact } from "~/types/contact-update";
import ReactMarkdown from "react-markdown";
import moment from "moment";
import { Link } from "react-router-dom";

View File

@ -1,12 +1,12 @@
import React from "react";
import { Route, Switch } from "react-router-dom";
import { NoteId, Note as INote, Notebook } from "../../../../types/publish-update";
import { Contacts } from "../../../../types/contact-update";
import GlobalApi from "../../../../api/global";
import { NoteId, Note as INote, Notebook } from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
import GlobalApi from "~/logic/api/global";
import { RouteComponentProps } from "react-router-dom";
import Note from "./Note";
import EditPost from "./EditPost";
import { EditPost } from "./EditPost";
interface NoteRoutesProps {
ship: string;
@ -26,6 +26,10 @@ export function NoteRoutes(props: NoteRoutesProps & RouteComponentProps) {
const relativePath = (path: string) => `${baseUrl}${path}`;
return (
<Switch>
<Route
path={relativePath("/edit")}
render={(routeProps) => <EditPost {...routeProps} {...props} />}
/>
<Route
path={baseUrl}
exact
@ -33,10 +37,7 @@ export function NoteRoutes(props: NoteRoutesProps & RouteComponentProps) {
return <Note {...routeProps} {...props} />;
}}
/>
<Route
path={relativePath("/edit")}
render={(routeProps) => <EditPost {...routeProps} {...props} />}
/>
</Switch>
);
}

View File

@ -3,7 +3,7 @@ import { Link, RouteComponentProps, Route, Switch } from "react-router-dom";
import { NotebookPosts } from "./NotebookPosts";
import { Subscribers } from "./Subscribers";
import { Settings } from "./Settings";
import { roleForShip } from "../../../../lib/group";
import { roleForShip } from "~/logic/lib/group";
import {
Box,
Button,
@ -15,10 +15,10 @@ import {
TabPanel,
Row,
} from "@tlon/indigo-react";
import { Notebook as INotebook } from "../../../../types/publish-update";
import { Groups } from "../../../../types/group-update";
import { Contacts, Rolodex } from "../../../../types/contact-update";
import GlobalApi from "../../../../api/global";
import { Notebook as INotebook } from "~/types/publish-update";
import { Groups } from "~/types/group-update";
import { Contacts, Rolodex } from "~/types/contact-update";
import GlobalApi from "~/logic/api/global";
import styled from "styled-components";
const TabList = styled(_TabList)`

View File

@ -8,7 +8,7 @@ import { Groups } from "../../../../types/group-update";
import { Contacts, Rolodex } from "../../../../types/contact-update";
import Notebook from "./Notebook";
import NewPost from "./new-post";
import Note from "./Note";
import { NoteRoutes } from './NoteRoutes';
interface NotebookRoutesProps {
api: GlobalApi;
@ -62,7 +62,7 @@ export function NotebookRoutes(
const { noteId } = routeProps.match.params;
const note = notebook?.notes[noteId];
return (
<Note
<NoteRoutes
api={api}
book={book}
ship={ship}

View File

@ -1,10 +1,10 @@
import React, { Component } from "react";
import { Box, Text, Col } from "@tlon/indigo-react";
import { Link } from "react-router-dom";
import SidebarInvite from "../../../../components/SidebarInvite";
import SidebarInvite from "~/views/components/SidebarInvite";
import { Welcome } from "./Welcome";
import { GroupItem } from "./GroupItem";
import { alphabetiseAssociations } from "../../../../lib/util";
import { alphabetiseAssociations } from "~/logic/lib/util";
export function Sidebar(props: any) {
const sidebarInvites = !(props.invites && props.invites["/publish"])

View File

@ -1,6 +1,6 @@
import React from "react";
import { Box } from "@tlon/indigo-react";
import { useLocalStorageState } from "../../../../lib/useLocalStorageState";
import { useLocalStorageState } from "~/logic/lib/useLocalStorageState";
export function Welcome(props: Parameters<typeof Box>[0]) {
const [wasWelcomed, setWasWelcomed] = useLocalStorageState(

View File

@ -1,29 +0,0 @@
import React from 'react';
const CommentInput = React.forwardRef((props, ref) => (
<textarea
{...props}
ref={ref}
id="comment"
name="comment"
placeholder="Leave a comment here"
className={
'f9 db border-box w-100 ba b--gray3 pt2 ph2 br1 ' +
'b--gray2-d mb2 focus-b--black focus-b--white-d white-d bg-gray0-d'
}
aria-describedby="comment-desc"
style={{ height: '4rem', resize: 'vertical' }}
onKeyDown={(e) => {
if (
(e.getModifierState('Control') || event.metaKey) &&
e.key === 'Enter'
) {
props.onSubmit();
}
}}
></textarea>
));
CommentInput.displayName = 'commentInput';
export default CommentInput;

View File

@ -1,155 +0,0 @@
import React, { Component } from 'react';
import moment from 'moment';
import { Sigil } from '~/logic/lib/sigil';
import CommentInput from './comment-input';
import { uxToHex, cite } from '~/logic/lib/util';
export class CommentItem extends Component {
constructor(props) {
super(props);
this.state = {
commentBody: ''
};
this.commentChange = this.commentChange.bind(this);
this.commentEdit = this.commentEdit.bind(this);
moment.updateLocale('en', {
relativeTime: {
past: function(input) {
return input === 'just now'
? input
: input + ' ago';
},
s : 'just now',
future : 'in %s',
m : '1m',
mm : '%dm',
h : '1h',
hh : '%dh',
d : '1d',
dd : '%dd',
M : '1 month',
MM : '%d months',
y : '1 year',
yy : '%d years'
}
});
}
commentEdit() {
const commentPath = Object.keys(this.props.comment)[0];
const commentBody = this.props.comment[commentPath].content;
this.setState({ commentBody });
this.props.onEdit();
}
focusTextArea(text) {
text && text.focus();
}
commentChange(e) {
this.setState({
commentBody: e.target.value
});
}
onUpdate() {
this.props.onUpdate(this.state.commentBody);
}
render() {
const pending = this.props.pending ? 'o-60' : '';
const commentData = this.props.comment[Object.keys(this.props.comment)[0]];
const content = commentData.content.split('\n').map((line, i) => {
return (
<p className="mb2" key={i}>{line}</p>
);
});
const date = moment(commentData['date-created']).fromNow();
const contact = commentData.author.substr(1) in this.props.contacts
? this.props.contacts[commentData.author.substr(1)] : false;
let name = commentData.author;
let color = '#000000';
let classes = 'mix-blend-diff';
let avatar = 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 { editing } = this.props;
const disabled = this.props.pending
|| window.ship !== commentData.author.slice(1);
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>
{ !editing && !disabled && (
<>
<div onClick={this.commentEdit.bind(this)} className="green2 pointer ml2 f9 pt1">
Edit
</div>
<div onClick={this.props.onDelete} className="red2 pointer ml2 f9 pt1">
Delete
</div>
</>
) }
</div>
<div className="f8 lh-solid mb2">
{ !editing && content }
{ editing && (
<CommentInput style={{ resize:'vertical' }}
ref={(el) => {
this.focusTextArea(el);
}}
onChange={this.commentChange}
value={this.state.commentBody}
onSubmit={this.onUpdate.bind(this)}
>
</CommentInput>
)}
</div>
{ editing && (
<div className="flex">
<div onClick={this.onUpdate.bind(this)} className="br1 green2 pointer f9 pt1 b--green2 ba pa2 dib">
Submit
</div>
<div onClick={this.props.onEditCancel} className="br1 black white-d pointer f9 b--gray2 ba pa2 dib ml2">
Cancel
</div>
</div>
)}
</div>
);
}
}
export default CommentItem;

View File

@ -1,200 +0,0 @@
import React, { Component } from 'react';
import { CommentItem } from './comment-item';
import CommentInput from './comment-input';
import { dateToDa } from '~/logic/lib/util';
import { Spinner } from '~/views/components/Spinner';
export class Comments extends Component {
constructor(props) {
super(props);
this.state = {
commentBody: '',
pending: new Set(),
awaiting: null,
editing: null
};
this.commentSubmit = this.commentSubmit.bind(this);
this.commentChange = this.commentChange.bind(this);
}
componentDidUpdate(prevProps) {
const previousComments = prevProps.comments[0] || {};
const currentComments = this.props.comments[0] || {};
const previous = Object.keys(previousComments) || [];
const current = Object.keys(currentComments) || [];
if ((prevProps.comments && this.props.comments) &&
(previous !== current)) {
const pendingSet = this.state.pending;
Object.keys(currentComments).map((com) => {
const obj = currentComments[com];
for (const each of pendingSet.values()) {
if (obj.content === each['new-comment'].body) {
pendingSet.delete(each);
this.setState({ pending: pendingSet });
}
}
});
}
}
commentSubmit(evt) {
const comment = {
'new-comment': {
who: this.props.ship.slice(1),
book: this.props.book,
note: this.props.note,
body: this.state.commentBody
}
};
const pendingState = this.state.pending;
pendingState.add(comment);
this.setState({ pending: pendingState });
this.textArea.value = '';
this.setState({ commentBody: '', awaiting: 'new' });
const submit = this.props.api.publish.publishAction(comment);
submit.then(() => {
this.setState({ awaiting: null });
});
}
commentChange(evt) {
this.setState({
commentBody: evt.target.value
});
}
commentEdit(idx) {
this.setState({ editing: idx });
}
commentEditCancel() {
this.setState({ editing: null });
}
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 });
});
}
render() {
if (!this.props.enabled) {
return null;
}
const { editing } = this.state;
const pendingArray = Array.from(this.state.pending).map((com, i) => {
const da = dateToDa(new Date());
const comment = {
[da]: {
author: `~${window.ship}`,
content: com['new-comment'].body,
'date-created': Math.round(new Date().getTime())
}
};
return (
<CommentItem
comment={comment}
key={i}
contacts={this.props.contacts}
pending={true}
/>
);
});
const commentArray = this.props.comments.map((com, i) => {
return (
<CommentItem
comment={com}
key={i}
contacts={this.props.contacts}
onUpdate={u => this.commentUpdate(i, u)}
onDelete={() => this.commentDelete(i)}
onEdit={() => this.commentEdit(i)}
onEditCancel={this.commentEditCancel.bind(this)}
editing={i === editing}
disabled={Boolean(this.state.awaiting) || editing}
/>
);
});
const disableComment = ((this.state.commentBody === '') || (Boolean(this.state.awaiting)));
const commentClass = (disableComment)
? 'bg-transparent f9 pa2 br1 ba b--gray2 gray2'
: 'bg-transparent f9 pa2 br1 ba b--gray2 black white-d pointer';
const spinnerText =
this.state.awaiting === 'new'
? 'Posting commment...'
: this.state.awaiting === 'edit'
? 'Updating comment...'
: 'Deleting comment...';
return (
<div>
<div className="mv8 relative">
<div>
<CommentInput style={{ resize:'vertical' }}
ref={(el) => {
this.textArea = el;
}}
onChange={this.commentChange}
value={this.state.commentBody}
disabled={Boolean(this.state.editing)}
onSubmit={this.commentSubmit}
>
</CommentInput>
</div>
<button disabled={disableComment}
onClick={this.commentSubmit}
className={commentClass}
>
Add comment
</button>
<Spinner text={spinnerText} awaiting={this.state.awaiting} classes="absolute bottom-0 right-0 pb2" />
</div>
{pendingArray}
{commentArray}
</div>
);
}
}
export default Comments;

View File

@ -1,89 +0,0 @@
import React, { Component } from 'react';
export class Dropdown extends Component {
constructor(props) {
super(props);
this.toggleDropdown = this.toggleDropdown.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
this.collapseAndDispatch = this.collapseAndDispatch.bind(this);
this.state = {
open: false
};
}
componentDidMount() {
document.addEventListener('mousedown', this.handleClickOutside);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClickOutside);
}
handleClickOutside(evt) {
if (this.optsList && !this.optsList.contains(evt.target) &&
this.optsButton && !this.optsButton.contains(evt.target)) {
this.setState({ open: false });
}
}
toggleDropdown() {
this.setState({ open: !this.state.open });
}
collapseAndDispatch(action) {
this.setState({ open: false }, action);
}
render() {
const alignment = (this.props.align)
? this.props.align : 'right';
const display = (this.state.open)
? 'block' : 'none';
const optionsClass = (this.state.open)
? 'open' : 'closed';
let leftAlign = '';
let rightAlign = '0';
if (alignment === 'left') {
leftAlign = '0';
rightAlign = '';
}
const optionsList = this.props.options.map((val, i) => {
return (
<button key={i} className={val.cls}
onClick={() => this.collapseAndDispatch(val.action)}
>
{val.txt}
</button>
);
});
return (
<div className={'options relative dib pr3 pointer ' + optionsClass}
ref={(el) => {
this.optsButton = el;
}}
onClick={this.toggleDropdown}
>
<button className="bg-transparent white-d pointer mb1 br2 pa2 pr4">
{this.props.buttonText}
</button>
<div className="absolute flex flex-column pv2 ba b--gray4 br2 z-1 bg-white bg-gray0-d"
ref={(el) => {
this.optsList = el;
}}
style={{ left: leftAlign, right: rightAlign, width:this.props.width, display: display }}
>
{optionsList}
</div>
</div>
);
}
}
export default Dropdown;

View File

@ -1,145 +0,0 @@
import React, { Component } from 'react';
import { SidebarSwitcher } from '~/views/components/SidebarSwitch';
import { Spinner } from '~/views/components/Spinner';
import { Link } from 'react-router-dom';
import { Controlled as CodeMirror } from 'react-codemirror2';
import { dateToDa } from '~/logic/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>
);
}
}
export default EditPost;

View File

@ -1,45 +0,0 @@
import React, { Component } from 'react';
import { Box, Text } from "@tlon/indigo-react";
import { NotebookItem } from './NotebookItem';
export class GroupItem extends Component {
render() {
const { props } = this;
const association = props.association ? props.association : {};
let title = association['app-path'] ? association['app-path'] : 'Unmanaged Notebooks';
if (association.metadata && association.metadata.title) {
title = association.metadata.title !== ''
? association.metadata.title : title;
}
const groupedBooks = props.groupedBooks ? props.groupedBooks : [];
const first = (props.index === 0) ? 'pt1' : 'pt6';
const notebookItems = groupedBooks.map((each, i) => {
const unreads = props.notebooks[each]['num-unread'] || 0;
let title = each.substr(1);
if (props.notebooks[each].title) {
title = (props.notebooks[each].title !== '')
? props.notebooks[each].title : title;
}
return (
<NotebookItem
key={i}
unreadCount={unreads}
title={title}
path={each}
selected={(props.path === each)}
/>
);
});
return (
<Box className={first}>
<Box fontSize={0} px={3} fontWeight="700" pb={2} color="lightGray">{title}</Box>
{notebookItems}
</Box>
);
}
}
export default GroupItem;

View File

@ -1,176 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { Spinner } from '~/views/components/Spinner';
import urbitOb from 'urbit-ob';
export class JoinScreen extends Component {
constructor(props) {
super(props);
this.state = {
book: '',
error: false,
awaiting: null,
disable: false
};
this.bookChange = this.bookChange.bind(this);
}
componentDidMount() {
this.componentDidUpdate();
}
componentDidUpdate(prevProps) {
if ((this.props.ship) && (this.props.notebook)) {
const incomingBook = `${this.props.ship}/${this.props.notebook}`;
if (this.props.api && (prevProps?.api !== this.props.api)) {
this.setState({ book: incomingBook }, () => {
this.onClickJoin();
});
}
}
// redirect to notebook when we have it
if (this.props.notebooks) {
if (this.state.awaiting) {
const book = this.state.awaiting.split('/');
const ship = book[0];
const notebook = book[1];
if ((ship in this.props.notebooks) &&
(notebook in this.props.notebooks[ship])) {
this.setState({ disable: false, book: '/' });
this.props.history.push(`/~publish/notebook/${ship}/${notebook}`);
}
}
}
}
notebooksInclude(text, notebookObj) {
let verdict = false;
let keyPair = [];
// validate that it's a worthwhile thing to check
// certainly a unit would be nice here
if (text.indexOf('/') === -1) {
return verdict;
} else {
keyPair = text.split('/');
};
// check both levels of object
if (keyPair[0] in notebookObj) {
if (keyPair[1] in notebookObj[keyPair[0]]) {
verdict = true;
}
}
return verdict;
}
onClickJoin() {
const { props, state } = this;
const text = state.book;
let book = text.split('/');
const ship = book[0];
book.splice(0, 1);
book = '/' + book.join('/');
if (this.notebooksInclude(state.book, props.notebooks)) {
const href = `/~publish/notebook/${ship}${book}`;
return props.history.push(href);
}
if (book.length < 2 || !urbitOb.isValidPatp(ship)) {
this.setState({
error: true
});
return;
}
const actionData = {
subscribe: {
who: ship.replace('~',''),
book: /\/?(.*)/.exec(book)[1]
}
};
// TODO: askHistory setting
this.setState({ disable: true });
this.props.api.publish.publishAction(actionData).catch((err) => {
console.log(err);
}).then(() => {
this.setState({ awaiting: text });
});
}
bookChange(event) {
this.setState({
book: event.target.value
});
}
render() {
const { state } = this;
let joinClasses = 'db f9 green2 ba pa2 b--green2 bg-gray0-d pointer';
if ((state.disable) || (!state.book) || (state.book === '/')) {
joinClasses = 'db f9 gray2 ba pa2 b--gray3 bg-gray0-d';
}
let errElem = (<span />);
if (state.error) {
errElem = (
<span className="f9 inter red2 db">
Notebook must have a valid name.
</span>
);
}
return (
<div className={'h-100 w-100 pt4 overflow-x-hidden flex flex-column ' +
'bg-gray0-d white-d pa3'}
>
<div
className="w-100 dn-m dn-l dn-xl inter pt1 pb6 f8"
>
<Link to="/~publish/">{'⟵ All Notebooks'}</Link>
</div>
<h2 className="mb3 f8">Subscribe to an Existing Notebook</h2>
<div className="w-100">
<p className="f8 lh-copy mt3 db">Enter a <span className="mono">~ship/notebook-name</span></p>
<p className="f9 gray2 mb4">Notebook names use lowercase, hyphens, and slashes.</p>
<textarea
ref={ (e) => {
this.textarea = e;
} }
className={'f7 mono ba bg-gray0-d white-d pa3 mb2 db ' +
'focus-b--black focus-b--white-d b--gray3 b--gray2-d nowrap '}
placeholder="~zod/dream-journal"
spellCheck="false"
rows={1}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.onClickJoin();
}
}}
style={{
resize: 'none'
}}
onChange={this.bookChange}
value={this.state.book}
/>
{errElem}
<br />
<button
disabled={(this.state.disable) || (!state.book) || (state.book === '/')}
onClick={this.onClickJoin.bind(this)}
className={joinClasses}
>Join Notebook</button>
<Spinner awaiting={this.state.disable} classes="mt4" text="Joining notebook..." />
</div>
</div>
);
}
}
export default JoinScreen;

View File

@ -1,196 +0,0 @@
import React, { Component } from 'react';
import { SidebarSwitcher } from '~/views/components/SidebarSwitch';
import { Spinner } from '~/views/components/Spinner';
import { Link } from 'react-router-dom';
import { Controlled as CodeMirror } from 'react-codemirror2';
import { dateToDa, stringToSymbol } from '~/logic/lib/util';
import 'codemirror/mode/markdown/markdown';
export class NewPost extends Component {
constructor(props) {
super(props);
this.state = {
body: '',
title: '',
submit: false,
awaiting: null,
disabled: false
};
this.postSubmit = this.postSubmit.bind(this);
this.titleChange = this.titleChange.bind(this);
this.bodyChange = this.bodyChange.bind(this);
}
postSubmit() {
const { state } = this;
// perf testing:
/*let closure = () => {
let x = 0;
for (var i = 0; i < 5; i++) {
x++;
let rand = Math.floor(Math.random() * 1000);
const newNote = {
'new-note': {
who: this.props.ship.slice(1),
book: this.props.book,
note: stringToSymbol(this.state.title + '-' + Date.now() + '-' + rand),
title: 'asdf-' + rand + '-' + Date.now(),
body: 'asdf-' + Date.now()
}
};
this.props.api.publishAction(newNote);
}
setTimeout(closure, 3000);
};
setTimeout(closure, 2000);*/
if (state.submit && !state.disabled) {
const newNote = {
'new-note': {
who: this.props.ship.slice(1),
book: this.props.book,
note: stringToSymbol(this.state.title),
title: this.state.title,
body: this.state.body
}
};
this.setState({ disabled: true });
this.props.api.publish.publishAction(newNote).then(() => {
this.setState({ awaiting: newNote['new-note'].note });
}).catch((err) => {
if (err.includes('note already exists')) {
const timestamp = Math.floor(Date.now() / 1000);
newNote['new-note'].note += '-' + timestamp;
this.setState({ awaiting: newNote['new-note'].note });
this.props.api.publish.publishAction(newNote);
} else {
this.setState({ disabled: false, awaiting: null });
}
});
}
}
componentDidMount() {
this.componentDidUpdate();
}
componentDidUpdate(prevProps) {
if (prevProps && prevProps.api !== this.props.api) {
this.props.api.publish.fetchNotebook(this.props.ship, this.props.book);
}
const notebook = this.props.notebooks[this.props.ship][this.props.book];
if (notebook.notes[this.state.awaiting]) {
this.setState({ disabled: false, awaiting: null });
const popout = (this.props.popout) ? 'popout/' : '';
const redirect =
`/~publish/${popout}note/${this.props.ship}/${this.props.book}/${this.state.awaiting}`;
this.props.history.push(redirect);
}
}
titleChange(evt) {
const submit = !(evt.target.value === '' || this.state.body === '');
this.setState({ title: evt.target.value, submit: submit });
}
bodyChange(editor, data, value) {
const submit = !(value === '' || this.state.title === '');
this.setState({ body: value, submit: submit });
}
render() {
const { props, state } = this;
const notebook = props.notebooks[props.ship][props.book];
const options = {
mode: 'markdown',
theme: 'tlon',
lineNumbers: false,
lineWrapping: true,
scrollbarStyle: null,
cursorHeight: 0.85
};
const date = dateToDa(new Date()).slice(1, -10);
const submitStyle = ((!state.disabled && state.submit) && (state.awaiting === null))
? { color: '#2AA779', cursor: 'pointer' }
: { color: '#B1B2B3', cursor: 'auto' };
const hrefIndex = props.location.pathname.indexOf('/notebook/');
const publishsubStr = props.location.pathname.substr(hrefIndex);
const popoutHref = `/~publish/popout${publishsubStr}`;
const hiddenOnPopout = (props.popout)
? '' : 'dib-m dib-l dib-xl';
const newIndex = props.location.pathname.indexOf('/new');
const backHref = props.location.pathname.slice(0, newIndex);
return (
<div className='f9 h-100 relative publish'>
<div className='w-100 dn-m dn-l dn-xl inter pt4 pb4 f9 pl4'>
<Link to={backHref}>{'<- Back'}</Link>
</div>
<SidebarSwitcher
popout={props.popout}
sidebarShown={props.sidebarShown}
api={this.props.api}
classes="absolute top-1 pl4"
/>
<div className='w-100 tl pv4 flex justify-center'>
<button
className={'bg-transparent v-mid w-100 w-90-l w-80-m mw6 tl h1 pl4'}
disabled={(!state.submit && state.disabled) || (state.awaiting !== null)}
style={submitStyle}
onClick={this.postSubmit}
>
Publish To {notebook.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='pa4'>
<input
autoFocus
type='text'
className='bg-transparent white-d w-100 pb2'
onChange={this.titleChange}
placeholder='New Post'
/>
<div className='gray2'>{date}</div>
</div>
<div className='NewPost'>
<CodeMirror
value={state.body}
options={options}
onBeforeChange={(e, d, v) => this.bodyChange(e, d, v)}
onChange={(editor, data, value) => {}}
/>
<Spinner
text='Creating post...'
awaiting={this.state.disabled}
classes='absolute bottom-1 right-1 ba b--gray1-d pa2'
/>
</div>
</div>
</div>
);
}
}
export default NewPost;

View File

@ -1,9 +1,9 @@
import React from "react";
import { stringToSymbol } from "../../../../lib/util";
import { stringToSymbol } from "~/logic/lib/util";
import { FormikHelpers } from "formik";
import GlobalApi from "../../../../api/global";
import { useWaitForProps } from "../../../../lib/useWaitForProps";
import { Notebook } from "../../../../types/publish-update";
import GlobalApi from "~/logic/api/global";
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import { Notebook } from "~/types/publish-update";
import { RouteComponentProps } from "react-router-dom";
import { PostForm, PostFormSchema } from "./NoteForm";

View File

@ -1,198 +0,0 @@
import React, { Component } from 'react';
import { InviteSearch } from '~/views/components/InviteSearch';
import { Spinner } from '~/views/components/Spinner';
import { Link } from 'react-router-dom';
import { stringToSymbol } from '~/logic/lib/util';
export class NewScreen extends Component {
constructor(props) {
super(props);
this.state = {
idName: '',
description: '',
invites: {
groups: [],
ships: []
},
disabled: false,
createGroup: false,
awaiting: false
};
this.idChange = this.idChange.bind(this);
this.descriptionChange = this.descriptionChange.bind(this);
this.setInvite = this.setInvite.bind(this);
}
componentDidUpdate(prevProps) {
const { props, state } = this;
if (props.notebooks && (('~' + window.ship) in props.notebooks)) {
if (state.awaiting in props.notebooks['~' + window.ship]) {
const notebook = `/~${window.ship}/${state.awaiting}`;
props.history.push('/~publish/notebook' + notebook);
}
}
}
idChange(event) {
this.setState({
idName: event.target.value
});
}
descriptionChange(event) {
this.setState({
description: event.target.value
});
}
setInvite(value) {
this.setState({ invites: value });
}
onClickCreate() {
const { props, state } = this;
const bookId = stringToSymbol(state.idName);
let groupInfo = null;
if (state.invites.groups.length > 0) {
groupInfo = {
'group-path': state.invites.groups[0],
'invitees': [],
'use-preexisting': true,
'make-managed': false
};
} else if (this.state.createGroup) {
groupInfo = {
'group-path': `/ship/~${window.ship}/${bookId}`,
'invitees': state.invites.ships,
'use-preexisting': false,
'make-managed': true
};
} else {
groupInfo = {
'group-path': `/ship/~${window.ship}/${bookId}`,
'invitees': state.invites.ships,
'use-preexisting': false,
'make-managed': false
};
}
const action = {
'new-book': {
book: bookId,
title: state.idName,
about: state.description,
coms: true,
group: groupInfo
}
};
this.setState({ awaiting: bookId, disabled: true }, () => {
props.api.publish.publishAction(action).then(() => {
});
});
}
render() {
let createClasses = 'pointer db f9 green2 bg-gray0-d ba pv3 ph4 mv7 b--green2';
if (!this.state.idName || this.state.disabled) {
createClasses = 'db f9 gray2 ba bg-gray0-d pa2 pv3 ph4 mv7 b--gray3';
}
let idErrElem = <span />;
if (this.state.idError) {
idErrElem = (
<span className="f9 inter red2 db pt2">
Notebook must have a valid name.
</span>
);
}
return (
<div
className={
'h-100 w-100 mw6 pa3 pt4 overflow-x-hidden flex flex-column white-d'
}
>
<div className="w-100 dn-m dn-l dn-xl inter pt1 pb6 f8">
<Link to="/~publish/">{'⟵ All Notebooks'}</Link>
</div>
<h2 className="mb3 f8">New Notebook</h2>
<div className="w-100">
<p className="f8 mt3 lh-copy db">Name</p>
<p className="f9 gray2 db mb2 pt1">
Provide a name for your notebook
</p>
<textarea
className={
'f7 ba bg-gray0-d white-d pa3 db w-100 ' +
'focus-b--black focus-b--white-d b--gray3 b--gray2-d'
}
placeholder="eg. My Journal"
rows={1}
style={{
resize: 'none'
}}
onChange={this.idChange}
value={this.state.idName}
/>
{idErrElem}
<p className="f8 mt4 lh-copy db">
Description
<span className="gray3 ml1">(Optional)</span>
</p>
<p className="f9 gray2 db mb2 pt1">
What&apos;s your notebook about?
</p>
<textarea
className={
'f7 ba bg-gray0-d white-d pa3 db w-100 ' +
'focus-b--black focus-b--white-d b--gray3 b--gray2-d'
}
placeholder="Notebook description"
rows={1}
style={{
resize: 'none'
}}
onChange={this.descriptionChange}
value={this.state.description}
/>
<div className="mt4 db relative">
<p className="f8">
Invite
<span className="gray3"> (Optional)</span>
</p>
<Link className="green2 absolute right-0 bottom-0 f9" to="/~groups/new">Create Group</Link>
<p className="f9 gray2 db mv1 pb4">
Selected ships or group will be invited to read your notebook. Additional writers can be added from the &apos;subscribers&apos; panel.
</p>
</div>
<InviteSearch
associations={this.props.associations}
groupResults={true}
shipResults={true}
groups={this.props.groups}
contacts={this.props.contacts}
invites={this.state.invites}
setInvite={this.setInvite}
/>
<button
disabled={this.state.disabled}
onClick={this.onClickCreate.bind(this)}
className={createClasses}
>
Create Notebook
</button>
<Spinner
awaiting={this.state.awaiting}
classes="mt3"
text="Creating notebook..."
/>
</div>
</div>
);
}
}
export default NewScreen;

View File

@ -2,15 +2,15 @@ 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 { AsyncButton } from "../../../../components/AsyncButton";
import { FormError } from "../../../../components/FormError";
import GlobalApi from "~/logic/api/global";
import { AsyncButton } from "~/views/components/AsyncButton";
import { FormError } from "~/views/components/FormError";
import { RouteComponentProps } from "react-router-dom";
import { stringToSymbol } from "../../../../lib/util";
import GroupSearch from "../../../../components/GroupSearch";
import { Associations } from "../../../../types/metadata-update";
import { useWaitForProps } from "../../../../lib/useWaitForProps";
import { Notebooks } from "../../../../types/publish-update";
import { stringToSymbol } from "~/logic/lib/util";
import GroupSearch from "~/views/components/GroupSearch";
import { Associations } from "~/types/metadata-update";
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import { Notebooks } from "~/types/publish-update";
interface FormSchema {
name: string;

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 pt4">
{prevComponent}
{nextComponent}
</div>
);
}
}
export default NoteNavigation;

View File

@ -1,269 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { SidebarSwitcher } from '~/views/components/SidebarSwitch';
import { Spinner } from '~/views/components/Spinner';
import { Comments } from './comments';
import { NoteNavigation } from './note-navigation';
import moment from 'moment';
import ReactMarkdown from 'react-markdown';
import { cite } from '~/logic/lib/util';
export class Note extends Component {
constructor(props) {
super(props);
this.state = {
deleting: false,
sentRead: false
};
moment.updateLocale('en', {
relativeTime: {
past: function(input) {
return input === 'just now'
? input
: input + ' ago';
},
s : 'just now',
future : 'in %s',
m : '1m',
mm : '%dm',
h : '1h',
hh : '%dh',
d : '1d',
dd : '%dd',
M : '1 month',
MM : '%d months',
y : '1 year',
yy : '%d years'
}
});
this.scrollElement = React.createRef();
this.onScroll = this.onScroll.bind(this);
this.deletePost = this.deletePost.bind(this);
}
componentDidMount() {
this.componentDidUpdate();
this.onScroll();
}
componentDidUpdate(prevProps) {
const { props, state } = this;
if ((prevProps && prevProps.api !== props.api) || props.api) {
if (!(props.notebooks[props.ship]?.[props.book]?.notes?.[props.note]?.file)) {
props.api.publish.fetchNote(props.ship, props.book, props.note);
}
if (prevProps && prevProps.note !== props.note) {
this.setState({ sentRead: false });
}
if (!state.sentRead &&
props.notebooks?.[props.ship]?.[props.book]?.notes?.[props.note] &&
!props.notebooks[props.ship][props.book].notes[props.note].read) {
const readAction = {
read: {
who: props.ship.slice(1),
book: props.book,
note: props.note
}
};
this.setState({ sentRead: true }, () => {
props.api.publish.publishAction(readAction);
});
}
}
}
onScroll() {
const notebook = this.props.notebooks?.[this.props.ship]?.[this.props.book];
const note = notebook?.notes?.[this.props.note];
if (!note?.comments) {
return;
}
const scrollTop = this.scrollElement.scrollTop;
const clientHeight = this.scrollElement.clientHeight;
const scrollHeight = this.scrollElement.scrollHeight;
let atBottom = false;
if (scrollHeight - scrollTop - clientHeight < 40) {
atBottom = true;
}
const loadedComments = note.comments.length;
const allComments = note['num-comments'];
const fullyLoaded = (loadedComments === allComments);
if (atBottom && !fullyLoaded) {
this.props.api.publish.fetchCommentsPage(this.props.ship,
this.props.book, this.props.note, loadedComments, 30);
}
}
deletePost() {
const { props } = this;
const deleteAction = {
'del-note': {
who: this.props.ship.slice(1),
book: this.props.book,
note: this.props.note
}
};
const popout = (props.popout) ? 'popout/' : '';
const baseUrl = `/~publish/${popout}notebook/${props.ship}/${props.book}`;
this.setState({ deleting: true });
this.props.api.publish.publishAction(deleteAction)
.then(() => {
props.history.push(baseUrl);
});
}
render() {
const { props } = this;
const notebook = props.notebooks?.[props.ship]?.[props.book] || {};
const comments = notebook?.notes?.[props.note]?.comments || false;
const title = notebook?.notes?.[props.note]?.title || '';
const author = notebook?.notes?.[props.note]?.author || '';
const file = notebook?.notes?.[props.note]?.file || '';
const date = moment(notebook.notes?.[props.note]?.['date-created']).fromNow() || 0;
const contact = author.substr(1) in props.contacts
? props.contacts[author.substr(1)] : false;
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 = notebook?.notes?.[props.note]?.['prev-note'] || null;
const nextId = notebook?.notes?.[props.note]?.['next-note'] || null;
const prevDate = moment(notebook?.notes?.[prevId]?.['date-created']).fromNow() || 0;
const nextDate = 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;
const editUrl = props.location.pathname + '/edit';
if (`~${window.ship}` === author) {
editPost = <div className="dib">
<Link className="green2 f9" to={editUrl}>Edit</Link>
<p className="dib f9 red2 ml2 pointer"
onClick={(() => this.deletePost())}
>Delete</p>
</div>;
}
const popout = (props.popout) ? 'popout/' : '';
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 baseUrl = `/~publish/${popout}notebook/${props.ship}/${props.book}`;
return (
<div
className='h-100 overflow-y-scroll'
onScroll={this.onScroll}
ref={(el) => {
this.scrollElement = el;
}}
>
<SidebarSwitcher
popout={props.popout}
sidebarShown={props.sidebarShown}
api={this.props.api}
classes="absolute top-1 pl4"
/>
<div className='h-100 flex flex-column items-center pa4'>
<div className='w-100 flex justify-center pb6'>
<Link className='f9 w-100 w-90-m w-90-l mw6 tl' to={baseUrl}>
{'<- Notebook index'}
</Link>
<Link
to={popoutHref}
className={'dn absolute right-1 top-1 ' + hiddenOnPopout}
target='_blank'
>
<img src='/~landscape/img/popout.png' height={16} width={16} />
</Link>
</div>
<div className='w-100 mw6'>
<div className='flex flex-column'>
<div className='f9 mb1' style={{ overflowWrap: 'break-word' }}>
{title}
</div>
<div className='flex mb6'>
<div
className={
'di f9 gray2 mr2 ' + (contact.nickname ? null : 'mono')
}
title={author}
style={{ lineHeight: 1.6 }}
>
{name}
</div>
<div className='di' style={{ lineHeight: 1 }}>
<span className='f9 gray2 dib'>{date}</span>
<span className='ml2 dib'>{editPost}</span>
</div>
</div>
</div>
<div className='md' style={{ overflowWrap: 'break-word' }}>
<ReactMarkdown source={newfile} linkTarget={'_blank'} />
</div>
<NoteNavigation
popout={props.popout}
prev={prev}
next={next}
ship={props.ship}
book={props.book}
/>
<Comments
enabled={notebook.comments}
ship={props.ship}
book={props.book}
note={props.note}
comments={comments}
contacts={props.contacts}
api={this.props.api}
/>
<Spinner
text='Deleting post...'
awaiting={this.state.deleting}
classes='absolute bottom-1 right-1 ba b--gray1-d pa2'
/>
</div>
</div>
</div>
);
}
}
export default Note;

View File

@ -1,28 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
export class NotebookItem extends Component {
render() {
const { props } = this;
const selectedClass = (props.selected) ? 'bg-gray5 bg-gray1-d c-default' : 'pointer hover-bg-gray5 hover-bg-gray1-d';
const unread = (props.unreadCount > 0)
? <p className="dib f9 fr"><span className="dib white bg-gray3 bg-gray2-d fw6 br1" style={{ padding: '1px 5px' }}>
{props.unreadCount}
</span></p> : <span />;
return (
<Link
to={'/~publish/notebook/' + props.path}
>
<div className={'w-100 v-mid f9 ph5 pv1 ' + selectedClass}>
<p className="dib f9">{props.title}</p>
{unread}
</div>
</Link>
);
}
}
export default NotebookItem;

View File

@ -1,103 +0,0 @@
import React, { Component } from 'react';
import moment from 'moment';
import { Link } from 'react-router-dom';
import ReactMarkdown from 'react-markdown';
import { cite } from '~/logic/lib/util';
export class NotebookPosts extends Component {
constructor(props) {
super(props);
moment.updateLocale('en', {
relativeTime: {
past: function(input) {
return input === 'just now'
? input
: input + ' ago';
},
s : 'just now',
future : 'in %s',
m : '1m',
mm : '%dm',
h : '1h',
hh : '%dh',
d : '1d',
dd : '%dd',
M : '1 month',
MM : '%d months',
y : '1 year',
yy : '%d years'
}
});
}
render() {
const { props } = this;
const notes = [];
for (let i=0; i<props.list.length; i++) {
const noteId = props.list[i];
const note = props.notes[noteId];
if (!note) {
break;
}
const contact = note.author.substr(1) in props.contacts
? props.contacts[note.author.substr(1)] : false;
let name = note.author;
if (contact) {
name = (contact.nickname.length > 0)
? contact.nickname : note.author;
}
if (name === note.author) {
name = cite(note.author);
}
let comment = 'No Comments';
if (note['num-comments'] == 1) {
comment = '1 Comment';
} else if (note['num-comments'] > 1) {
comment = `${note['num-comments']} Comments`;
}
const date = moment(note['date-created']).fromNow();
const popout = (props.popout) ? 'popout/' : '';
const url = `/~publish/${popout}note/${props.host}/${props.notebookName}/${noteId}`;
notes.push(
<Link key={i} to={url}>
<div className="mv6">
<div className="mb1"
style={{ overflowWrap: 'break-word' }}
>
{note.title}
</div>
<p className="mb1"
style={{ overflowWrap: 'break-word' }}
>
<ReactMarkdown
unwrapDisallowed
allowedTypes={['text', 'root', 'break', 'paragraph']}
source={note.snippet}
/>
</p>
<div className="flex">
<div className={(contact.nickname ? null : 'mono') +
' gray2 mr3'}
title={note.author}
>{name}</div>
<div className={((note.read) ? "gray2 " : "green2 ") + "mr3"}>{date}</div>
<div className="gray2">{comment}</div>
</div>
</div>
</Link>
);
}
return (
<div className="flex-col">
{notes}
</div>
);
}
}
export default NotebookPosts;

View File

@ -1,275 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { SidebarSwitcher } from '~/views/components/SidebarSwitch';
import { NotebookPosts } from './notebook-posts';
import { Subscribers } from './subscribers';
import { Settings } from './settings';
import { cite } from '~/logic/lib/util';
import { roleForShip } from '~/logic/lib/group';
export class Notebook extends Component {
constructor(props) {
super(props);
this.onScroll = this.onScroll.bind(this);
this.unsubscribe = this.unsubscribe.bind(this);
}
onScroll() {
const notebook = this.props.notebooks[this.props.ship][this.props.book];
const scrollTop = this.scrollElement.scrollTop;
const clientHeight = this.scrollElement.clientHeight;
const scrollHeight = this.scrollElement.scrollHeight;
let atBottom = false;
if (scrollHeight - scrollTop - clientHeight < 40) {
atBottom = true;
}
if (!notebook.notes && this.props.api) {
this.props.api.publish.fetchNotebook(this.props.ship, this.props.book);
return;
}
const loadedNotes = Object.keys(notebook?.notes).length || 0;
const allNotes = notebook?.['notes-by-date'].length || 0;
const fullyLoaded = (loadedNotes === allNotes);
if (atBottom && !fullyLoaded) {
this.props.api.publish.fetchNotesPage(this.props.ship, this.props.book, loadedNotes, 30);
}
}
componentDidUpdate(prevProps) {
const { props } = this;
if ((prevProps && (prevProps.api !== props.api)) || props.api) {
const notebook = props.notebooks?.[props.ship]?.[props.book];
if (!notebook?.subscribers) {
props.api.publish.fetchNotebook(props.ship, props.book);
}
}
}
componentDidMount() {
this.componentDidUpdate();
const notebook = this.props.notebooks?.[this.props.ship]?.[this.props.book];
if (notebook?.notes) {
this.onScroll();
}
}
unsubscribe() {
const action = {
unsubscribe: {
who: this.props.ship.slice(1),
book: this.props.book
}
};
this.props.api.publish.publishAction(action);
this.props.history.push('/~publish');
}
render() {
const { props } = this;
// popout logic
const hrefIndex = props.location.pathname.indexOf('/notebook/');
const publishsubStr = props.location.pathname.substr(hrefIndex);
const popoutHref = `/~publish/popout${publishsubStr}`;
const hiddenOnPopout = props.popout ? '' : 'dib-m dib-l dib-xl';
const notebook = props.notebooks?.[props.ship]?.[props.book];
const tabStyles = {
posts: 'bb b--gray4 b--gray2-d gray2 pv4 ph2',
about: 'bb b--gray4 b--gray2-d gray2 pv4 ph2',
subscribers: 'bb b--gray4 b--gray2-d gray2 pv4 ph2',
settings: 'bb b--gray4 b--gray2-d pr2 gray2 pv4 ph2'
};
tabStyles[props.view] = 'bb b--black b--white-d black white-d pv4 ph2';
let inner = null;
switch (props.view) {
case 'posts': {
const notesList = notebook?.['notes-by-date'] || [];
const notes = notebook?.notes || null;
inner = <NotebookPosts notes={notes}
popout={props.popout}
list={notesList}
host={props.ship}
notebookName={props.book}
contacts={props.notebookContacts}
/>;
break;
}
case 'about':
inner = <p className="f8 lh-solid">{notebook?.about}</p>;
break;
case 'subscribers':
inner = <Subscribers
host={this.props.ship}
book={this.props.book}
notebook={notebook}
contacts={this.props.contacts}
associations={this.props.associations}
groups={this.props.groups}
api={this.props.api}
/>;
break;
case 'settings':
inner = <Settings
host={this.props.ship}
book={this.props.book}
notebook={notebook}
groups={this.props.groups}
contacts={this.props.contacts}
associations={this.props.associations}
history={this.props.history}
api={this.props.api}
/>;
break;
default:
break;
}
// displaying nicknames, sigil colors for contacts
const contact = props.ship.substr(1) in props.notebookContacts
? props.notebookContacts[props.ship.substr(1)] : false;
let name = props.ship;
if (contact) {
name = (contact.nickname.length > 0)
? contact.nickname : props.ship;
}
if (name === props.ship) {
name = cite(props.ship);
}
const popout = (props.popout) ? 'popout/' : '';
const base = `/~publish/${popout}notebook/${props.ship}/${props.book}`;
const about = base + '/about';
const subs = base + '/subscribers';
const settings = base + '/settings';
const newUrl = base + '/new';
let newPost = null;
if (notebook?.['writers-group-path'] in props.groups) {
const group = props.groups[notebook?.['writers-group-path']];
const writers = group.tags?.publish?.[`writers-${props.book}`] || new Set();
if (props.ship === `~${window.ship}` || writers.has(ship)) {
newPost = (
<Link
to={newUrl}
className='NotebookButton bg-light-green green2 pa2'
>
New Post
</Link>
);
}
}
const unsub = (window.ship === props.ship.slice(1))
? null
: <button onClick={this.unsubscribe}
className="NotebookButton bg-white bg-gray0-d black white-d ba b--black b--gray2-d ml3 ph1"
>
Unsubscribe
</button>;
const group = props.groups[notebook?.['writers-group-path']];
const role = group ? roleForShip(group, window.ship) : undefined;
const subsComponent = (this.props.ship.slice(1) === window.ship) || (role === 'admin')
? (<Link to={subs} className={tabStyles.subscribers}>
Subscribers
</Link>)
: null
const settingsComponent = (this.props.ship.slice(1) !== window.ship)
? null
: <Link to={settings} className={tabStyles.settings}>
Settings
</Link>;
return (
<div
className='overflow-y-scroll h-100'
style={{ paddingLeft: 16, paddingRight: 16 }}
onScroll={this.onScroll}
ref={(el) => {
this.scrollElement = el;
}}
>
<div className='w-100 dn-m dn-l dn-xl inter pt4 pb6 f9'>
<Link to='/~publish'>{'<- All Notebooks'}</Link>
</div>
<div style={{ paddingTop: 11 }}>
<SidebarSwitcher
popout={props.popout}
sidebarShown={props.sidebarShown}
api={this.props.api}
classes="absolute top-1"
/>
</div>
<div
className='center mw6 f9 h-100'
style={{ paddingLeft: 16, paddingRight: 16 }}
>
<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 className='h-100 pt0 pt8-m pt8-l pt8-xl no-scrollbar'>
<div className='flex justify-between' style={{ marginBottom: 32 }}>
<div className='flex-col'>
<div className='mb1'>{notebook?.title}</div>
<span>
<span className='gray3 mr1'>by</span>
<span
className={contact.nickname ? null : 'mono'}
title={props.ship}
>
{name}
</span>
</span>
</div>
<div className='flex'>
{newPost}
{unsub}
</div>
</div>
<div className='flex' style={{ marginBottom: 24 }}>
<Link to={base} className={tabStyles.posts}>
All Posts
</Link>
<Link to={about} className={tabStyles.about}>
About
</Link>
{subsComponent}
{settingsComponent}
<div
className='bb b--gray4 b--gray2-d gray2 pv4 ph2'
style={{ flexGrow: 1 }}
></div>
</div>
<div
style={{ height: 'calc(100% - 188px)' }}
className='f9 lh-solid'
>
{inner}
</div>
</div>
</div>
</div>
);
}
}
export default Notebook;

View File

@ -1,289 +0,0 @@
import React, { Component } from 'react';
import { Spinner } from '~/views/components/Spinner';
import { InviteSearch } from '~/views/components/InviteSearch';
import Toggle from '~/views/components/toggle';
export class Settings extends Component {
constructor(props) {
super(props);
this.state = {
title: '',
description: '',
comments: false,
disabled: false,
type: 'Editing',
targetGroup: null,
inclusive: false
};
this.deleteNotebook = this.deleteNotebook.bind(this);
this.changeTitle = this.changeTitle.bind(this);
this.changeDescription = this.changeDescription.bind(this);
this.changeComments = this.changeComments.bind(this);
this.changeTargetGroup = this.changeTargetGroup.bind(this);
this.changeInclusive = this.changeInclusive.bind(this);
}
componentDidMount() {
const { props } = this;
if (props.notebook) {
this.setState({
title: props.notebook.title,
description: props.notebook.about,
comments: props.notebook.comments
});
}
}
componentDidUpdate(prevProps) {
const { props } = this;
if (prevProps !== props) {
if (props.notebook) {
if (prevProps.notebook && prevProps.notebook !== props.notebook) {
if (prevProps.notebook.title !== props.notebook.title) {
this.setState({ title: props.notebook.title });
}
if (prevProps.notebook.about !== props.notebook.about) {
this.setState({ description: props.notebook.about });
}
if (prevProps.notebook.comments !== props.notebook.comments) {
this.setState({ comments: props.notebook.comments });
}
}
}
}
}
changeTitle(event) {
this.setState({ title: event.target.value });
}
changeDescription(event) {
this.setState({ description: event.target.value });
}
changeComments() {
this.setState({ comments: !this.state.comments, disabled: true }, (() => {
this.props.api.publish.publishAction({
'edit-book': {
book: this.props.book,
title: this.props.notebook.title,
about: this.props.notebook.about,
coms: this.state.comments,
group: null
}
}).then(() => {
this.setState({ disabled: false });
});
}));
}
deleteNotebook() {
const action = {
'del-book': {
book: this.props.book
}
};
this.setState({ disabled: true, type: 'Deleting' });
this.props.api.publish.publishAction(action).then(() => {
this.props.history.push('/~publish');
});
}
changeTargetGroup(target) {
if (target.groups.length === 1) {
this.setState({ targetGroup: target.groups[0] });
} else {
this.setState({ targetGroup: null });
}
}
changeInclusive(event) {
this.setState({ inclusive: Boolean(event.target.checked) });
}
groupifyNotebook() {
const { props, state } = this;
this.setState({
disabled: true,
type: 'Converting'
}, (() => {
this.props.api.publish.publishAction({
groupify: {
book: props.book,
target: state.targetGroup,
inclusive: state.inclusive
}
}).then(() => this.setState({ disabled: false }));
}));
}
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>
);
}
}
renderHeader(title, subtitle) {
return (
<>
<p className="f9 mt6 lh-copy">{title}</p>
<p className="f9 gray2 db mb4">{subtitle}</p>
</>
);
}
render() {
if (this.props.host.slice(1) === window.ship) {
return (
<div className="flex-column">
{this.renderGroupify()}
{this.renderHeader(
'Delete Notebook',
'Permanently delete this notebook. (All current members will no longer see this notebook)')}
<button
className="bg-transparent b--red2 red2 pointer dib f9 ba pa2"
onClick={this.deleteNotebook}
>
Delete this notebook
</button>
{this.renderHeader('Rename', 'Change the name of this notebook')}
<div className="relative w-100 flex" style={{ maxWidth: '29rem' }}>
<input
className={
'f8 ba b--gray3 b--gray2-d bg-gray0-d white-d ' +
'focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3'
}
value={this.state.title}
onChange={this.changeTitle}
disabled={this.state.disabled}
onBlur={() => {
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 });
});
}}
/>
</div>
{this.renderHeader("Change description", "Change the description of this notebook")}
<div className="relative w-100 flex" style={{ maxWidth: '29rem' }}>
<input
className={
'f8 ba b--gray3 b--gray2-d bg-gray0-d white-d ' +
'focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3'
}
value={this.state.description}
onChange={this.changeDescription}
onBlur={() => {
this.setState({ disabled: true });
this.props.api.publish
.publishAction({
'edit-book': {
book: this.props.book,
title: this.props.notebook.title,
about: this.state.description,
coms: this.props.notebook.comments,
group: null
}
})
.then(() => {
this.setState({ disabled: false });
});
}}
/>
</div>
<div className="mv6">
<Toggle
boolean={this.state.comments}
change={this.changeComments}
/>
<span className="dib f9 white-d inter ml3">Comments</span>
<p className="f9 gray2 pt1" style={{ paddingLeft: 40 }}>
Subscribers may comment when enabled
</p>
</div>
<Spinner
awaiting={this.state.disabled}
classes="absolute right-1 bottom-1 pa2 ba b--black b--gray0-d white-d"
text={`${this.state.type} notebook...`}
/>
</div>
);
} else {
return '';
}
}
}
export default Settings;

View File

@ -1,127 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import SidebarInvite from '~/views/components/SidebarInvite';
import { Welcome } from './welcome';
import { GroupItem } from './group-item';
import { alphabetiseAssociations } from '~/logic/lib/util';
export class Sidebar extends Component {
render() {
const { props } = this;
const activeClasses = (props.active === 'sidebar') ? ' ' : 'dn-s ';
let hiddenClasses = true;
if (props.popout) {
hiddenClasses = false;
} else {
hiddenClasses = props.sidebarShown;
};
const sidebarInvites = !(props.invites && props.invites['/publish'])
? null
: Object.keys(props.invites['/publish'])
.map((uid) => {
return (
<SidebarInvite
key={uid}
invite={props.invites['/publish'][uid]}
onAccept={() => props.api.invite.accept('/publish', uid)}
onDecline={() => props.api.invite.decline('/publish', uid)}
/>
);
});
const associations =
(props.associations && 'contacts' in props.associations)
? alphabetiseAssociations(props.associations.contacts) : {};
const notebooks = {};
Object.keys(props.notebooks).map((host) => {
Object.keys(props.notebooks[host]).map((notebook) => {
const title = `${host}/${notebook}`;
notebooks[title] = props.notebooks[host][notebook];
});
});
const groupedNotebooks = {};
Object.keys(notebooks).map((book) => {
const path = notebooks[book]['subscribers-group-path']
? notebooks[book]['subscribers-group-path'] : book;
if (path in associations) {
if (groupedNotebooks[path]) {
const array = groupedNotebooks[path];
array.push(book);
groupedNotebooks[path] = array;
} else {
groupedNotebooks[path] = [book];
}
} else {
if (groupedNotebooks['/~/']) {
const array = groupedNotebooks['/~/'];
array.push(book);
groupedNotebooks['/~/'] = array;
} else {
groupedNotebooks['/~/'] = [book];
};
}
});
const groupedItems = Object.keys(associations)
.map((each, i) => {
const books = groupedNotebooks[each] || [];
if (books.length === 0)
return;
if (groupedNotebooks['/~/'] &&
groupedNotebooks['/~/'].length !== 0) {
i = i + 1;
}
return (
<GroupItem
key={i}
index={i}
association={associations[each]}
groupedBooks={books}
notebooks={notebooks}
path={props.path}
/>
);
});
if (groupedNotebooks['/~/'] &&
groupedNotebooks['/~/'].length !== 0) {
groupedItems.unshift(
<GroupItem
key={'/~/'}
index={0}
association={'/~/'}
groupedBooks={groupedNotebooks['/~/']}
notebooks={notebooks}
path={props.path}
/>
);
}
return (
<div
className={
'bn br-m br-l br-xl b--gray4 b--gray1-d lh-copy h-100 ' +
'flex-shrink-0 pt3 pt0-m pt0-l pt0-xl relative ' +
'overflow-y-hidden ' + activeClasses +
(hiddenClasses ? 'flex-basis-100-s flex-basis-250-ns' : 'dn')
}
>
<div className="w-100 f9">
<Link to="/~publish/new" className="green2 pa4 f9 dib">
New Notebook
</Link>
</div>
<div className="overflow-y-auto pb1"
style={{ height: 'calc(100% - 82px)' }}
>
<Welcome notebooks={props.notebooks} />
{sidebarInvites}
{groupedItems}
</div>
</div>
);
}
}
export default Sidebar;

View File

@ -1,85 +0,0 @@
import React, { Component } from 'react';
import { GroupView } from '~/views/components/Group';
import { resourceFromPath } from '~/logic/lib/group';
export class Subscribers extends Component {
constructor(props) {
super(props);
this.redirect = this.redirect.bind(this);
this.addUser = this.addUser.bind(this);
this.removeUser = this.removeUser.bind(this);
this.addAll = this.addAll.bind(this);
}
addUser(who, path) {
this.props.api.groups.add(path, [who]);
}
removeUser(who, path) {
this.props.api.groups.remove(path, [who]);
}
redirect(url) {
window.location.href = url;
}
addAll() {
const path = this.props.notebook['writers-group-path'];
const group = path ? this.props.groups[path] : null;
const resource = resourceFromPath(path);
this.props.api.groups.addTag(
resource,
{ app: 'publish', tag: `writers-${this.props.book}` },
[...group.members].map(m => `~${m}`)
);
}
render() {
const path = this.props.notebook['writers-group-path'];
const group = path ? this.props.groups[path] : null;
const tags = [
{
description: 'Writer',
tag: `writers-${this.props.book}`,
addDescription: 'Make Writer',
app: 'publish',
},
];
const appTags = [
{
app: 'publish',
tag: `writers-${this.props.book}`,
desc: `Writer`,
addDesc: 'Allow user to write to this notebook'
},
];
return (
<div>
<button
onClick={this.addAll}
className={'dib f9 black gray4-d bg-gray0-d ba pa2 mb4 b--black b--gray1-d pointer'}
>
Add all members as writers
</button>
<GroupView
permissions
resourcePath={path}
group={group}
tags={tags}
appTags={appTags}
contacts={this.props.contacts}
groups={this.props.groups}
associations={this.props.associations}
api={this.props.api}
/>
</div>
);
}
}
export default Subscribers;

View File

@ -1,41 +0,0 @@
import React, { Component } from 'react';
export class Welcome extends Component {
constructor() {
super();
this.state = {
show: true
};
this.disableWelcome = this.disableWelcome.bind(this);
}
disableWelcome() {
this.setState({ show: false });
localStorage.setItem('urbit-publish:wasWelcomed', JSON.stringify(true));
}
render() {
let wasWelcomed = localStorage.getItem('urbit-publish:wasWelcomed');
if (wasWelcomed === null) {
localStorage.setItem('urbit-publish:wasWelcomed', JSON.stringify(false));
return wasWelcomed = false;
} else {
wasWelcomed = JSON.parse(wasWelcomed);
}
const notebooks = this.props.notebooks ? this.props.notebooks : {};
return ((!wasWelcomed && this.state.show) && (notebooks.length !== 0)) ? (
<div className="ma4 pa2 white-d bg-welcome-green bg-gray1-d">
<p className="f8 lh-copy">Notebooks are for longer-form writing and discussion. Each Notebook is a collection of Markdown-formatted notes with optional comments.</p>
<p className="f8 pt2 dib pointer bb"
onClick={(() => this.disableWelcome())}
>
Close this
</p>
</div>
) : <div />;
}
}
export default Welcome;

View File

@ -1,49 +0,0 @@
import React, { Component } from 'react';
import { Sidebar } from './lib/sidebar';
import ErrorBoundary from '~/views/components/ErrorBoundary';
export class Skeleton extends Component {
render() {
const { props } = this;
const rightPanelHide = props.rightPanelHide
? 'dn-s' : '';
const popout = props.popout
? props.popout : false;
const popoutWindow = (popout)
? '' : 'ph4-m ph4-l ph4-xl pb4-m pb4-l pb4-xl';
const popoutBorder = (popout)
? '': 'ba-m ba-l ba-xl b--gray4 b--gray1-d br1';
return (
<div className={'w-100 h-100 ' + popoutWindow}>
<div className={'cf w-100 h-100 flex ' + popoutBorder}>
<Sidebar
popout={popout}
sidebarShown={props.sidebarShown}
active={props.active}
notebooks={props.notebooks}
contacts={props.contacts}
path={props.path}
invites={props.invites}
associations={props.associations}
api={this.props.api}
/>
<div className={'h-100 w-100 relative white-d flex-auto ' + rightPanelHide} style={{
flexGrow: 1
}}
>
<ErrorBoundary>
{props.children}
</ErrorBoundary>
</div>
</div>
</div>
);
}
}
export default Skeleton;

View File

@ -14,7 +14,7 @@ import {
ErrorMessage,
InputCaption,
} from "@tlon/indigo-react";
import { useDropdown } from "../lib/useDropdown";
import { useDropdown } from "~/logic/lib/useDropdown";
import styled from "styled-components";
import { space, color, layout, border } from "styled-system";