mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-15 01:52:42 +03:00
interface: revive new/join groups
This commit is contained in:
parent
a20c43d93b
commit
a45ee12d6d
@ -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) =>
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
);
|
||||
|
94
pkg/interface/src/views/landscape/components/JoinGroup.tsx
Normal file
94
pkg/interface/src/views/landscape/components/JoinGroup.tsx
Normal 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>
|
||||
);
|
||||
}
|
117
pkg/interface/src/views/landscape/components/NewGroup.tsx
Normal file
117
pkg/interface/src/views/landscape/components/NewGroup.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user