interface: revive new/join groups

This commit is contained in:
Liam Fitzgerald 2020-10-07 17:50:32 +10:00
parent a20c43d93b
commit a45ee12d6d
7 changed files with 287 additions and 29 deletions

View File

@ -28,7 +28,7 @@ const sortGroupsRecent = (recent: string[]) => (
if(bRecency === -1) {
return -1;
}
return Math.max(0,bRecency) - Math.max(0, aRecency);
return Math.max(0, aRecency) - Math.max(0,bRecency);
};
const sortGroupsAlph = (a: Association, b: Association) =>

View File

@ -1,18 +1,13 @@
import React, { ReactNode, useState, useEffect } from "react";
import React, { useState, useEffect } from "react";
import { Button, LoadingSpinner } from "@tlon/indigo-react";
import { useFormikContext } from "formik";
interface AsyncButtonProps {
loadingText: string;
children: ReactNode;
}
export function AsyncButton({
loadingText,
children,
...rest
}: AsyncButtonProps & Parameters<typeof Button>[0]) {
}: Parameters<typeof Button>[0]) {
const { isSubmitting, status, isValid } = useFormikContext();
const [success, setSuccess] = useState<boolean | undefined>();
@ -39,8 +34,6 @@ export function AsyncButton({
<LoadingSpinner
foreground={rest.primary ? "white" : 'black'}
background="gray"
awaiting
text={loadingText}
/>
) : success === true ? (
"Done"

View File

@ -18,6 +18,7 @@ import GlobalApi from "~/logic/api/global";
import { resourceFromPath, roleForShip } from "~/logic/lib/group";
import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton";
import { ColorInput } from "~/views/components/ColorInput";
import { useHistory } from "react-router-dom";
interface FormSchema {
title: string;
@ -41,6 +42,7 @@ interface GroupSettingsProps {
export function GroupSettings(props: GroupSettingsProps) {
const { group, association } = props;
const { metadata } = association;
const history = useHistory();
const currentPrivate = "invite" in props.group.policy;
const initialValues: FormSchema = {
title: metadata?.title,
@ -55,7 +57,11 @@ export function GroupSettings(props: GroupSettingsProps) {
) => {
try {
const { title, description, color, isPrivate } = values;
await props.api.metadata.update(props.association, { title, description, color });
await props.api.metadata.update(props.association, {
title,
description,
color,
});
if (isPrivate !== currentPrivate) {
const resource = resourceFromPath(props.association["group-path"]);
const newPolicy: Enc<GroupPolicy> = isPrivate
@ -74,6 +80,13 @@ export function GroupSettings(props: GroupSettingsProps) {
const onDelete = async () => {
await props.api.contacts.delete(association["group-path"]);
history.push("/");
};
const onLeave = async () => {
const [, , ship] = association["group-path"].split("/");
await props.api.contacts.remove(association["group-path"], ship);
history.push("/");
};
const disabled =
resourceFromPath(association["group-path"]).ship.slice(1) !== window.ship &&
@ -96,21 +109,30 @@ export function GroupSettings(props: GroupSettingsProps) {
my={3}
mx={4}
>
{!disabled && (
<>
<Col>
<Label>Delete Group</Label>
<Label gray mt="2">
Permanently delete this group. (All current members will no
longer see this group.)
</Label>
<StatelessAsyncButton onClick={onDelete} mt={2} destructive>
Delete this group
</StatelessAsyncButton>
</Col>
<Box borderBottom={1} borderBottomColor="washedGray" />
</>
{!disabled ? (
<Col>
<Label>Delete Group</Label>
<Label gray mt="2">
Permanently delete this group. (All current members will no
longer see this group.)
</Label>
<StatelessAsyncButton onClick={onDelete} mt={2} destructive>
Delete this group
</StatelessAsyncButton>
</Col>
) : (
<Col>
<Label>Leave Group</Label>
<Label gray mt="2">
Leave this group. You can rejoin if it is an open group, or if
you are reinvited
</Label>
<StatelessAsyncButton onClick={onLeave} mt={2} destructive>
Leave this group
</StatelessAsyncButton>
</Col>
)}
<Box borderBottom={1} borderBottomColor="washedGray" />
<Input
id="title"
label="Group Name"

View File

@ -5,7 +5,7 @@ import {
useLocation,
RouteComponentProps,
} from "react-router-dom";
import { Center, Box } from "@tlon/indigo-react";
import { Col, Box, Text } from "@tlon/indigo-react";
import _ from "lodash";
import { Resource } from "./Resource";
@ -185,9 +185,15 @@ export function GroupsPane(props: GroupsPaneProps) {
render={(routeProps) => {
return (
<Skeleton recentGroups={recentGroups} {...props} baseUrl={baseUrl}>
<Center display={["none", "auto"]}>
Open something to get started
</Center>
<Col
alignItems="center"
justifyContent="center"
display={["none", "flex"]}
>
<Box><Text fontSize="1">
Open something, or create a channel to get started
</Text></Box>
</Col>
{popovers(routeProps, baseUrl)}
</Skeleton>
);

View File

@ -0,0 +1,94 @@
import React, { useState, useCallback } from "react";
import { Body } from "~/views/components/Body";
import {
Col,
Box,
Text,
ManagedTextInputField as Input
} from "@tlon/indigo-react";
import { Formik, Form, FormikHelpers } from "formik";
import { AsyncButton } from "~/views/components/AsyncButton";
import * as Yup from "yup";
import { Groups, Rolodex } from "~/types";
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import GlobalApi from "~/logic/api/global";
import { RouteComponentProps } from "react-router-dom";
import urbitOb from "urbit-ob";
const formSchema = Yup.object({
group: Yup.string()
.required("Must provide group to join")
.test("is-valid", "Invalid group", (group: string | null | undefined) => {
if (!group) {
return false;
}
const [patp, name] = group.split("/");
return urbitOb.isValidPatp(patp) && name.length > 0;
}),
});
interface FormSchema {
group: string;
}
interface JoinGroupProps {
groups: Groups;
contacts: Rolodex;
api: GlobalApi;
}
export function JoinGroup(props: JoinGroupProps & RouteComponentProps) {
const { api, history } = props;
const initialValues: FormSchema = {
group: "",
};
const waiter = useWaitForProps(props);
const onSubmit = useCallback(
async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
try {
const [ship, name] = values.group.split("/");
await api.contacts.join({ ship, name });
const path = `/ship/${ship}/${name}`;
await waiter(({ contacts, groups }) => {
return path in contacts && path in groups;
});
actions.setStatus({ success: null });
history.push(`/~landscape${path}`);
} catch (e) {
console.error(e);
actions.setStatus({ error: e.message });
}
},
[api, waiter, history]
);
return (
<Body>
<Col maxWidth="300px" overflowY="auto" p="3">
<Box mb={3}>
<Text fontWeight="bold">Join Group</Text>
</Box>
<Formik
validationSchema={formSchema}
initialValues={initialValues}
onSubmit={onSubmit}
>
<Form>
<Col gapY="4">
<Input
id="group"
label="Group"
caption="What group are you joining?"
placeholder="~sampel-palnet/test-group"
/>
<AsyncButton>Join Group</AsyncButton>
</Col>
</Form>
</Formik>
</Col>
</Body>
);
}

View File

@ -0,0 +1,117 @@
import React, { useState, useCallback } from "react";
import { Body } from "~/views/components/Body";
import {
Col,
Box,
Text,
ManagedTextInputField as Input,
ManagedCheckboxField as Checkbox
} from "@tlon/indigo-react";
import { Formik, Form, FormikHelpers } from "formik";
import { AsyncButton } from "~/views/components/AsyncButton";
import * as Yup from "yup";
import { Groups, Rolodex, GroupPolicy, Enc } from "~/types";
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import GlobalApi from "~/logic/api/global";
import { stringToSymbol } from "~/logic/lib/util";
import {RouteComponentProps} from "react-router-dom";
const formSchema = Yup.object({
title: Yup.string().required("Group must have a name"),
description: Yup.string(),
isPrivate: Yup.boolean(),
});
interface FormSchema {
title: string;
description: string;
isPrivate: boolean;
}
interface NewGroupProps {
groups: Groups;
contacts: Rolodex;
api: GlobalApi;
}
export function NewGroup(props: NewGroupProps & RouteComponentProps) {
const { api, history } = props;
const initialValues: FormSchema = {
title: "",
description: "",
isPrivate: false,
};
const waiter = useWaitForProps(props);
const onSubmit = useCallback(
async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
try {
const { title, description, isPrivate } = values;
const name = stringToSymbol(title.trim());
const policy: Enc<GroupPolicy> = isPrivate
? {
invite: {
pending: [],
},
}
: {
open: {
banRanks: [],
banned: [],
},
};
await api.contacts.create(name, policy, title, description);
const path = `/ship/~${window.ship}/${name}`;
await waiter(({ contacts, groups }) => {
return path in contacts && path in groups;
});
actions.setStatus({ success: null });
history.push(`/~landscape${path}`);
} catch (e) {
console.error(e);
actions.setStatus({ error: e.message });
}
},
[api, waiter, history]
);
return (
<Body>
<Col maxWidth="300px" overflowY="auto" p="3">
<Box mb={3}>
<Text fontWeight="bold">New Group</Text>
</Box>
<Formik
validationSchema={formSchema}
initialValues={initialValues}
onSubmit={onSubmit}
>
<Form>
<Col gapY="4">
<Input
id="title"
label="Name"
caption="Provide a name for your group"
placeholder="eg. My Channel"
/>
<Input
id="description"
label="Description"
caption="What's your group about?"
placeholder="Group description"
/>
<Checkbox
id="isPrivate"
label="Private Group"
caption="Is your group private?"
/>
<AsyncButton>Create Group</AsyncButton>
</Col>
</Form>
</Formik>
</Col>
</Body>
);
}

View File

@ -13,6 +13,8 @@ import { PopoverRoutes } from './components/PopoverRoutes';
import { UnjoinedResource } from '~/views/components/UnjoinedResource';
import { GroupsPane } from './components/GroupsPane';
import { Workspace } from '~/types';
import {NewGroup} from './components/NewGroup';
import {JoinGroup} from './components/JoinGroup';
type LandscapeProps = StoreState & {
@ -76,6 +78,30 @@ export default class Landscape extends Component<LandscapeProps, {}> {
);
}}
/>
<Route path="/~landscape/new"
render={routeProps=> {
return (
<NewGroup
groups={props.groups}
contacts={props.contacts}
api={props.api}
{...routeProps}
/>
);
}}
/>
<Route path="/~landscape/join"
render={routeProps=> {
return (
<JoinGroup
groups={props.groups}
contacts={props.contacts}
api={props.api}
{...routeProps}
/>
);
}}
/>
</Switch>
);
}