Merge pull request #4018 from urbit/mp/publish/writers

publish: surface writers in new channel creation
This commit is contained in:
matildepark 2020-11-25 00:24:11 -05:00 committed by GitHub
commit e7154c0c53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 208 additions and 165 deletions

View File

@ -1,15 +1,11 @@
import React, { PureComponent } from "react";
import { Link, RouteComponentProps, Route, Switch } from "react-router-dom";
import { NotebookPosts } from "./NotebookPosts";
import { roleForShip } from "~/logic/lib/group";
import { Box, Button, Text, Row, Col } from "@tlon/indigo-react";
import { Groups } from "~/types/group-update";
import { Contacts, Rolodex } from "~/types/contact-update";
import GlobalApi from "~/logic/api/global";
import styled from "styled-components";
import { Associations, Graph, Association } from "~/types";
import { deSig } from "~/logic/lib/util";
import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton";
import React from 'react';
import { Link, RouteComponentProps } from 'react-router-dom';
import { NotebookPosts } from './NotebookPosts';
import { Box, Button, Text, Row, Col } from '@tlon/indigo-react';
import { Groups } from '~/types/group-update';
import { Contacts, Rolodex } from '~/types/contact-update';
import GlobalApi from '~/logic/api/global';
import { Associations, Graph, Association } from '~/types';
interface NotebookProps {
api: GlobalApi;
@ -24,7 +20,6 @@ interface NotebookProps {
hideNicknames: boolean;
baseUrl: string;
rootUrl: string;
associations: Associations;
}
interface NotebookState {
@ -40,21 +35,23 @@ export function Notebook(props: NotebookProps & RouteComponentProps) {
groups,
hideNicknames,
association,
graph,
graph
} = props;
const { metadata } = association;
const group = groups[association?.["group-path"]];
if (!group) return null; // Waitin on groups to populate
const group = groups[association?.['group-path']];
if (!group) {
return null; // Waiting on groups to populate
};
const relativePath = (p: string) => props.baseUrl + p;
const contact = notebookContacts?.[ship];
const role = group ? roleForShip(group, window.ship) : undefined;
const isOwn = `~${window.ship}` === ship;
let isWriter = true;
const isWriter =
isOwn || group.tags?.publish?.[`writers-${book}`]?.has(window.ship);
if (group.tags?.publish?.[`writers-${book}`]) {
isWriter = isOwn || group.tags?.publish?.[`writers-${book}`]?.has(window.ship);
}
const showNickname = contact?.nickname && !hideNicknames;
@ -62,16 +59,15 @@ export function Notebook(props: NotebookProps & RouteComponentProps) {
<Col gapY="4" pt={4} mx="auto" px={3} maxWidth="768px">
<Row justifyContent="space-between">
<Box>
<Text> {metadata?.title}</Text>
<br />
<Text display='block'>{metadata?.title}</Text>
<Text color="lightGray">by </Text>
<Text fontFamily={showNickname ? "sans" : "mono"}>
<Text fontFamily={showNickname ? 'sans' : 'mono'}>
{showNickname ? contact?.nickname : ship}
</Text>
</Box>
{isWriter && (
<Link to={relativePath("/new")}>
<Button primary style={{ cursor: "pointer" }}>
<Link to={relativePath('/new')}>
<Button primary style={{ cursor: 'pointer' }}>
New Post
</Button>
</Link>
@ -82,7 +78,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps) {
graph={graph}
host={ship}
book={book}
contacts={!!notebookContacts ? notebookContacts : {}}
contacts={notebookContacts ? notebookContacts : {}}
hideNicknames={hideNicknames}
baseUrl={props.baseUrl}
/>

View File

@ -4,6 +4,7 @@ import { ShipSearch } from '~/views/components/ShipSearch';
import { Formik, Form, FormikHelpers } from 'formik';
import { resourceFromPath } from '~/logic/lib/group';
import { AsyncButton } from '~/views/components/AsyncButton';
import { cite } from '~/logic/lib/util';
export class Writers extends Component {
render() {
@ -27,6 +28,7 @@ export class Writers extends Component {
actions.setStatus({ error: e.message });
}
};
const writers = Array.from(groups?.[association?.['group-path']]?.tags.publish?.[`writers-${name}`] || new Set()).map(e => cite(`~${e}`)).join(', ');
return (
<Box maxWidth='512px'>
@ -49,6 +51,10 @@ export class Writers extends Component {
</AsyncButton>
</Form>
</Formik>
{writers.length > 0 && <>
<Text display='block' mt='2'>Current writers:</Text>
<Text mt='2' display='block' mono>{writers}</Text>
</>}
</Box>
);
}

View File

@ -1,37 +1,42 @@
import React, { useCallback } from 'react';
import React from 'react';
import {
Box,
ManagedTextInputField as Input,
Col,
ManagedRadioButtonField as Radio,
Text,
} from "@tlon/indigo-react";
import { Formik, Form } from "formik";
import * as Yup from "yup";
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, parentPath } from "~/logic/lib/util";
import GroupSearch from "~/views/components/GroupSearch";
import { Associations } from "~/types/metadata-update";
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import { Groups } from "~/types/group-update";
import { ShipSearch } from "~/views/components/ShipSearch";
import { Rolodex, Workspace } from "~/types";
Icon,
Row
} from '@tlon/indigo-react';
import { Formik, Form } from 'formik';
import * as Yup from 'yup';
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, parentPath } from '~/logic/lib/util';
import { resourceFromPath } from '~/logic/lib/group';
import { Associations } from '~/types/metadata-update';
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
import { Groups } from '~/types/group-update';
import { ShipSearch } from '~/views/components/ShipSearch';
import { Rolodex, Workspace } from '~/types';
interface FormSchema {
name: string;
description: string;
ships: string[];
moduleType: "chat" | "publish" | "link";
moduleType: 'chat' | 'publish' | 'link';
writers: string[];
}
const formSchema = Yup.object({
const formSchema = (group, groups) => Yup.object({
name: Yup.string().required('Channel must have a name'),
description: Yup.string(),
ships: Yup.array(Yup.string()),
moduleType: Yup.string().required('Must choose channel type')
moduleType: Yup.string().required('Must choose channel type'),
writers: Yup.array(Yup.string().test('ingroup', 'Writers must be in group',
value => groups?.[group]?.members?.has(value)))
});
interface NewChannelProps {
@ -45,136 +50,172 @@ interface NewChannelProps {
}
export function NewChannel(props: NewChannelProps & RouteComponentProps) {
const { history, api, group, workspace } = props;
const { history, api, group, workspace, groups } = props;
const waiter = useWaitForProps(props, 5000);
const onSubmit = async (values: FormSchema, actions) => {
const resId: string = stringToSymbol(values.name)
+ ((workspace?.type !== 'home') ? `-${Math.floor(Math.random() * 10000)}`
: '');
+ ((workspace?.type !== 'home') ? `-${Math.floor(Math.random() * 10000)}`
: '');
try {
const { name, description, moduleType, ships } = values;
const { name, description, moduleType, ships, writers } = values;
switch (moduleType) {
case 'chat':
const appPath = `/~${window.ship}/${resId}`;
const groupPath = group || `/ship${appPath}`;
const appPath = `/~${window.ship}/${resId}`;
const groupPath = group || `/ship${appPath}`;
await api.chat.create(
name,
description,
appPath,
groupPath,
{ invite: { pending: ships.map(s => `~${s}`) } },
ships.map(s => `~${s}`),
true,
false
await api.chat.create(
name,
description,
appPath,
groupPath,
{ invite: { pending: ships.map(s => `~${s}`) } },
ships.map(s => `~${s}`),
true,
false
);
break;
case "publish":
case "link":
if (group) {
await api.graph.createManagedGraph(
resId,
name,
description,
group,
moduleType
);
} else {
await api.graph.createUnmanagedGraph(
resId,
name,
description,
{ invite: { pending: ships.map((s) => `~${s}`) } },
moduleType
);
}
break;
default:
console.log('fallthrough');
}
case 'publish':
if (writers.length > 0) {
const resource = resourceFromPath(group);
await api.groups.addTag(
resource,
{ app: 'publish', tag: `writers-${resId}` },
writers.map(s => `~${s}`)
);
}
case 'link':
if (group) {
await api.graph.createManagedGraph(
resId,
name,
description,
group,
moduleType
);
} else {
await api.graph.createUnmanagedGraph(
resId,
name,
description,
{ invite: { pending: ships.map(s => `~${s}`) } },
moduleType
);
}
break;
default:
console.log('fallthrough');
}
if (!group) {
await waiter(p => Boolean(p?.groups?.[`/ship/~${window.ship}/${resId}`]));
}
if (moduleType === 'chat') {
await waiter(p => Boolean(p?.chatSynced?.[`/~${window.ship}/${resId}`]));
}
actions.setStatus({ success: null });
const resourceUrl = parentPath(location.pathname);
history.push(
`${resourceUrl}/resource/${moduleType}` +
`${moduleType !== 'chat' ? '/ship' : ''}/~${window.ship}/${resId}`
);
} catch (e) {
console.error(e);
actions.setStatus({ error: 'Channel creation failed' });
}
};
return (
<Col overflowY="auto" p={3}>
<Box pb='3' display={['block', 'none']} onClick={() => history.push(props.baseUrl)}>
{'<- Back'}
</Box>
<Box fontWeight="bold" mb={4} color="black">
New Channel
</Box>
<Formik
validationSchema={formSchema}
initialValues={{
moduleType: 'chat',
name: '',
description: '',
group: '',
ships: []
}}
onSubmit={onSubmit}
>
<Form>
<Col
maxWidth="348px"
gapY="4"
>
<Col gapY="2">
<Box color="black" mb={2}>Channel Type</Box>
<Radio label="Chat" id="chat" name="moduleType" />
<Radio label="Notebook" id="publish" name="moduleType" />
<Radio label="Collection" id="link" name="moduleType" />
</Col>
<Input
id="name"
label="Name"
caption="Provide a name for your channel"
placeholder="eg. My Channel"
/>
<Input
id="description"
label="Description"
caption="What's your channel about?"
placeholder="Channel description"
/>
{(workspace?.type === 'home') &&
<ShipSearch
groups={props.groups}
contacts={props.contacts}
id="ships"
label="Invitees"
/>}
<Box justifySelf="start">
<AsyncButton
primary
loadingText="Creating..."
type="submit"
border
>
Create Channel
</AsyncButton>
</Box>
<FormError message="Channel creation failed" />
</Col>
</Form>
</Formik>
</Col>
);
}
if (!group) {
await waiter(p => Boolean(p?.groups?.[`/ship/~${window.ship}/${resId}`]));
}
if (moduleType === 'chat') {
await waiter(p => Boolean(p?.chatSynced?.[`/~${window.ship}/${resId}`]));
}
actions.setStatus({ success: null });
const resourceUrl = parentPath(location.pathname);
history.push(
`${resourceUrl}/resource/${moduleType}` +
`${moduleType !== 'chat' ? '/ship' : ''}/~${window.ship}/${resId}`
);
} catch (e) {
console.error(e);
actions.setStatus({ error: 'Channel creation failed' });
}
};
return (
<Col overflowY="auto" p={3}>
<Box pb='3' display={['block', 'none']} onClick={() => history.push(props.baseUrl)}>
{'<- Back'}
</Box>
<Box fontWeight="bold" mb={4} color="black">
New Channel
</Box>
<Formik
validationSchema={formSchema(group, groups)}
initialValues={{
moduleType: 'chat',
name: '',
description: '',
group: '',
ships: [],
writers: []
}}
onSubmit={onSubmit}
>
{ ({ errors, values }) => <Form>
<Col
maxWidth="348px"
gapY="4"
>
<Col gapY="2">
<Box color="black" mb={2}>Channel Type</Box>
<Radio label="Chat" id="chat" name="moduleType" />
<Radio label="Notebook" id="publish" name="moduleType" />
<Radio label="Collection" id="link" name="moduleType" />
</Col>
<Input
id="name"
label="Name"
caption="Provide a name for your channel"
placeholder="eg. My Channel"
/>
<Input
id="description"
label="Description"
caption="What's your channel about?"
placeholder="Channel description"
/>
{(workspace?.type === 'home') &&
<ShipSearch
groups={props.groups}
contacts={props.contacts}
id="ships"
label="Invitees"
/>}
{(workspace?.type !== 'home' && values.moduleType === 'publish') &&
<>
<ShipSearch
groups={props.groups}
contacts={props.contacts}
caption="Add writers to restrict who can write to this
notebook, or leave blank to allow all group members to write"
id="writers"
label="Writers"
/>
{errors.writers &&
<>
<Row>
<Icon
color='white'
mr='2'
backgroundColor='red'
borderRadius='999px'
icon="ExclaimationMarkBold"
/>
<Text color='red'>
{Array.from(new Set([...errors.writers]))}
</Text>
</Row>
</>
}
</>}
<Box justifySelf="start">
<AsyncButton
primary
loadingText="Creating..."
type="submit"
border
>
Create Channel
</AsyncButton>
</Box>
<FormError message="Channel creation failed" />
</Col>
</Form>}
</Formik>
</Col>
);
}