interface: move group pokes to airlock

This commit is contained in:
Liam Fitzgerald 2021-06-09 11:46:42 +10:00
parent 73f43a2829
commit 2f70a433bd
No known key found for this signature in database
GPG Key ID: D390E12C61D1CFFB
15 changed files with 98 additions and 83 deletions

View File

@ -32,6 +32,8 @@ import ModalButton from './components/ModalButton';
import Tiles from './components/tiles';
import Tile from './components/tiles/tile';
import './css/custom.css';
import airlock from '~/logic/api';
import { join } from '@urbit/api/groups';
const ScrollbarLessBox = styled(Box)`
scrollbar-width: none !important;
@ -107,7 +109,7 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
const onContinue = async (e) => {
e.stopPropagation();
if (!hasTutorialGroup({ associations })) {
await props.api.groups.join(TUTORIAL_HOST, TUTORIAL_GROUP);
await airlock.poke(join(TUTORIAL_HOST, TUTORIAL_GROUP));
await props.api.settings.putEntry('tutorial', 'joined', Date.now());
await waiter(hasTutorialGroup);
await Promise.all(

View File

@ -1,12 +1,9 @@
import { Box, Row, SegmentedProgressBar, Text } from '@tlon/indigo-react';
import {
joinError, joinProgress,
JoinRequest
} from '@urbit/api';
import { joinError, joinProgress, JoinRequest, hideGroup } from '@urbit/api';
import React, { useCallback } from 'react';
import GlobalApi from '~/logic/api/global';
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
import airlock from '~/logic/api';
interface JoiningStatusProps {
status: JoinRequest;
@ -23,13 +20,17 @@ const description: string[] = [
];
export function JoiningStatus(props: JoiningStatusProps) {
const { status, resource, api } = props;
const { status, resource } = props;
const current = joinProgress.indexOf(status.progress);
const desc = description?.[current] || '';
const isError = joinError.indexOf(status.progress as any) !== -1;
const onHide = useCallback(() => api.groups.hide(resource)
, [resource, api]);
const onHide = useCallback(
async () => {
await airlock.poke(hideGroup(resource));
},
[resource]
);
return (
<Row
display={['flex-column', 'flex']}

View File

@ -1,11 +1,12 @@
import { Box, Text } from '@tlon/indigo-react';
import { Association, Group } from '@urbit/api';
import { addTag, Association, Group } from '@urbit/api';
import { Form, Formik } from 'formik';
import React, { ReactElement } from 'react';
import GlobalApi from '~/logic/api/global';
import { resourceFromPath } from '~/logic/lib/group';
import { AsyncButton } from '~/views/components/AsyncButton';
import { ShipSearch } from '~/views/components/ShipSearch';
import airlock from '~/logic/api';
interface WritersProps {
api: GlobalApi;
@ -14,18 +15,18 @@ interface WritersProps {
}
export const Writers = (props: WritersProps): ReactElement => {
const { association, groups, api } = props;
const { association, groups } = props;
const resource = resourceFromPath(association?.group);
const onSubmit = async (values, actions) => {
try {
const ships = values.ships.map(e => `~${e}`);
await api.groups.addTag(
await airlock.poke(addTag(
resource,
{ app: 'graph', resource: association.resource, tag: 'writers' },
ships
);
));
actions.resetForm();
actions.setStatus({ success: null });
} catch (e) {

View File

@ -5,7 +5,8 @@ import {
LoadingSpinner, Row, Text
} from '@tlon/indigo-react';
import {
Invite, joinProgress,
hideGroup,
Invite, join, joinProgress,
JoinRequest,
Metadata, MetadataUpdatePreview,
resourceFromPath
@ -27,6 +28,7 @@ import { Header } from '~/views/apps/notifications/header';
import { NotificationWrapper } from '~/views/apps/notifications/notification';
import { MetadataIcon } from '~/views/landscape/components/MetadataIcon';
import { StatelessAsyncButton } from '../StatelessAsyncButton';
import airlock from '~/logic/api';
interface GroupInviteProps {
preview?: MetadataUpdatePreview;
@ -164,7 +166,7 @@ export function useInviteAccept(
return false;
}
await api.groups.join(ship, name);
await airlock.poke(join(ship, name));
await api.invite.accept(app, uid);
await waiter((p) => {
return (
@ -216,15 +218,15 @@ function InviteActions(props: {
const hideJoin = useCallback(async (e) => {
if(status?.progress === 'done') {
set(s => {
set((s) => {
// @ts-ignore investigate zustand types
delete s.pendingJoin[resource]
delete s.pendingJoin[resource];
});
e.stopPropagation();
return;
}
await api.groups.hide(resource);
}, [api, resource, status]);
await airlock.poke(hideGroup(resource));
}, [resource, status]);
if (status) {
return (

View File

@ -5,7 +5,7 @@ import {
Text
} from '@tlon/indigo-react';
import { Association, Group, metadataUpdate, PermVariation } from '@urbit/api';
import { addTag, Association, Group, metadataUpdate, PermVariation, removeTag } from '@urbit/api';
import { Form, Formik } from 'formik';
import _ from 'lodash';
import React from 'react';
@ -72,7 +72,7 @@ const formSchema = (members: string[]) => {
};
export function GraphPermissions(props: GraphPermissionsProps) {
const { api, group, association } = props;
const { group, association } = props;
const writers = _.get(
group?.tags,
@ -119,7 +119,7 @@ export function GraphPermissions(props: GraphPermissionsProps) {
actions.setStatus({ success: null });
return;
}
await api.groups.removeTag(resource, tag, allWriters);
await airlock.poke(removeTag(tag, resource, allWriters));
} else if (values.writePerms === 'self') {
if (writePerms === 'self') {
actions.setStatus({ success: null });
@ -127,8 +127,8 @@ export function GraphPermissions(props: GraphPermissionsProps) {
}
const promises: Promise<any>[] = [];
allWriters.length > 0 &&
promises.push(api.groups.removeTag(resource, tag, allWriters));
promises.push(api.groups.addTag(resource, tag, [`~${hostShip}`]));
promises.push(airlock.poke(removeTag(tag, resource, allWriters)));
promises.push(airlock.poke(addTag(resource, tag, [`~${hostShip}`])));
await Promise.all(promises);
actions.setStatus({ success: null });
} else if (values.writePerms === 'subset') {
@ -141,9 +141,9 @@ export function GraphPermissions(props: GraphPermissionsProps) {
const promises: Promise<any>[] = [];
toRemove.length > 0 &&
promises.push(api.groups.removeTag(resource, tag, toRemove));
promises.push(airlock.poke(removeTag(tag, resource, toRemove)));
toAdd.length > 0 &&
promises.push(api.groups.addTag(resource, tag, toAdd));
promises.push(airlock.poke(addTag(resource, tag, toAdd)));
await Promise.all(promises);
actions.setStatus({ success: null });

View File

@ -1,11 +1,13 @@
import { Button, Col, Icon, Label, Row, Text } from '@tlon/indigo-react';
import { Association } from '@urbit/api';
import { Association, leaveGroup } from '@urbit/api';
import { deleteGroup } from '@urbit/api/dist';
import React from 'react';
import { useHistory } from 'react-router-dom';
import GlobalApi from '~/logic/api/global';
import { resourceFromPath } from '~/logic/lib/group';
import { useModal } from '~/logic/lib/useModal';
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
import airlock from '~/logic/api';
export function DeleteGroup(props: {
owner: boolean;
@ -22,9 +24,9 @@ export function DeleteGroup(props: {
return;
}
if(props.owner) {
props.api.groups.deleteGroup(ship, name);
airlock.thread(deleteGroup(ship, name));
} else {
props.api.groups.leaveGroup(ship, name);
airlock.thread(leaveGroup(ship, name));
}
history.push('/');
};

View File

@ -4,7 +4,7 @@ import {
ManagedToggleSwitchField as Checkbox,
Text
} from '@tlon/indigo-react';
import { Enc, metadataUpdate } from '@urbit/api';
import { changePolicy, Enc, metadataUpdate } from '@urbit/api';
import { Group, GroupPolicy } from '@urbit/api/groups';
import { Association } from '@urbit/api/metadata';
import { Form, Formik, FormikHelpers } from 'formik';
@ -76,7 +76,7 @@ export function GroupAdminSettings(props: GroupAdminSettingsProps) {
? { invite: { pending: [] } }
: { open: { banRanks: [], banned: [] } };
const diff = { replace: newPolicy };
await props.api.groups.changePolicy(resource, diff);
await airlock.poke(changePolicy(resource, diff));
}
actions.setStatus({ success: null });

View File

@ -24,14 +24,14 @@ export function GroupChannelSettings(props: GroupChannelSettingsProps) {
const onChange = useCallback(
async (resource: string, preview: boolean) => {
return airlock.poke(metadataUpdate(associations.graph[resource], { preview }));
await airlock.poke(metadataUpdate(associations.graph[resource], { preview }));
},
[associations.graph]
);
const onRemove = useCallback(
async (resource: string) => {
return airlock.poke(metadataRemove('graph', resource, association.group));
await airlock.poke(metadataRemove('graph', resource, association.group));
},
[association]
);

View File

@ -5,6 +5,7 @@ import {
Row, Text
} from '@tlon/indigo-react';
import { invite } from '@urbit/api/groups';
import { Association } from '@urbit/api/metadata';
import { Form, Formik } from 'formik';
import _ from 'lodash';
@ -19,6 +20,7 @@ import { Workspace } from '~/types/workspace';
import { AsyncButton } from '~/views/components/AsyncButton';
import { FormError } from '~/views/components/FormError';
import { ShipSearch } from '~/views/components/ShipSearch';
import airlock from '~/logic/api';
interface InvitePopoverProps {
baseUrl: string;
@ -39,7 +41,7 @@ const formSchema = Yup.object({
});
export function InvitePopover(props: InvitePopoverProps) {
const { baseUrl, api, association } = props;
const { baseUrl, association } = props;
const relativePath = (p: string) => baseUrl + p;
const { title } = association?.metadata || { title: '' };
@ -55,11 +57,11 @@ export function InvitePopover(props: InvitePopoverProps) {
// TODO: how to invite via email?
try {
const { ship, name } = resourceFromPath(association.group);
await api.groups.invite(
await airlock.thread(invite(
ship, name,
_.compact(ships).map(s => `~${deSig(s)}`),
description
);
));
actions.setStatus({ success: null });
onOutsideClick();

View File

@ -4,7 +4,7 @@ import {
ManagedTextInputField as Input, Row,
Text
} from '@tlon/indigo-react';
import { MetadataUpdatePreview } from '@urbit/api';
import { join, MetadataUpdatePreview } from '@urbit/api';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import _ from 'lodash';
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
@ -22,6 +22,7 @@ import { AsyncButton } from '~/views/components/AsyncButton';
import { FormError } from '~/views/components/FormError';
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
import { GroupSummary } from './GroupSummary';
import airlock from '~/logic/api';
const formSchema = Yup.object({
group: Yup.string()
@ -80,7 +81,7 @@ export function JoinGroup(props: JoinGroupProps): ReactElement {
if (group in groups) {
return history.push(`/~landscape${group}`);
}
await api.groups.join(ship, name);
await airlock.poke(join(ship, name));
try {
await waiter((p) => {
return group in p.groups &&

View File

@ -1,4 +1,5 @@
import { Box, Col, Text } from '@tlon/indigo-react';
import { invite } from '@urbit/api/groups';
import { Form, Formik } from 'formik';
import _ from 'lodash';
import React from 'react';
@ -7,6 +8,7 @@ import { resourceFromPath } from '~/logic/lib/group';
import { deSig } from '~/logic/lib/util';
import { AsyncButton } from '~/views/components/AsyncButton';
import { ShipSearch } from '~/views/components/ShipSearch';
import airlock from '~/logic/api';
interface FormSchema {
ships: string[];
@ -17,17 +19,17 @@ const formSchema = Yup.object<FormSchema>({
});
export const MessageInvite = (props) => {
const { association, api } = props;
const { association } = props;
const initialValues: FormSchema = { ships: [] };
const onSubmit = async ({ ships }: FormSchema, actions) => {
try {
const { ship, name } = resourceFromPath(association.group);
await api.groups.invite(
await airlock.thread(invite(
ship,
name,
_.compact(ships).map(s => `~${deSig(s)}`),
`Inviting you to a DM with ~${ship}`
);
));
actions.setStatus({ success: null });
} catch (e) {
console.error(e);

View File

@ -4,6 +4,7 @@ import {
ManagedTextInputField as Input,
Text
} from '@tlon/indigo-react';
import { addTag } from '@urbit/api/dist';
import { Form, Formik } from 'formik';
import _ from 'lodash';
import React, { ReactElement } from 'react';
@ -21,6 +22,7 @@ import { FormError } from '~/views/components/FormError';
import { IconRadio } from '~/views/components/IconRadio';
import { ShipSearch, shipSearchSchema, shipSearchSchemaInGroup } from '~/views/components/ShipSearch';
import { ChannelWriteFieldSchema, ChannelWritePerms } from './ChannelWritePerms';
import airlock from '~/logic/api';
type FormSchema = {
name: string;
@ -98,10 +100,10 @@ export function NewChannel(props: NewChannelProps): ReactElement {
writers = _.compact(writers).map(s => `~${s}`);
const us = `~${window.ship}`;
if (values.writePerms === 'self') {
await api.groups.addTag(resource, tag, [us]);
await airlock.poke(addTag(resource, tag, [us]));
} else if (values.writePerms === 'subset') {
writers.push(us);
await api.groups.addTag(resource, tag, writers);
await airlock.poke(addTag(resource, tag, writers));
}
} else {
await api.graph.createUnmanagedGraph(

View File

@ -3,10 +3,10 @@ import {
ManagedCheckboxField as Checkbox, ManagedTextInputField as Input, Text
} from '@tlon/indigo-react';
import { Enc, GroupPolicy } from '@urbit/api';
import { createGroup, Enc, GroupPolicy } from '@urbit/api';
import { Form, Formik, FormikHelpers } from 'formik';
import React, { ReactElement, useCallback } from 'react';
import { RouteComponentProps, useHistory } from 'react-router-dom';
import { useHistory } from 'react-router-dom';
import * as Yup from 'yup';
import GlobalApi from '~/logic/api/global';
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
@ -14,6 +14,7 @@ import { stringToSymbol } from '~/logic/lib/util';
import useGroupState from '~/logic/state/group';
import useMetadataState from '~/logic/state/metadata';
import { AsyncButton } from '~/views/components/AsyncButton';
import airlock from '~/logic/api';
const formSchema = Yup.object({
title: Yup.string().required('Group must have a name'),
@ -32,7 +33,6 @@ interface NewGroupProps {
}
export function NewGroup(props: NewGroupProps): ReactElement {
const { api } = props;
const history = useHistory();
const initialValues: FormSchema = {
title: '',
@ -61,7 +61,7 @@ export function NewGroup(props: NewGroupProps): ReactElement {
banned: []
}
};
await api.groups.create(name, policy, title, description);
await airlock.thread(createGroup(name, policy, title, description));
const path = `/ship/~${window.ship}/${name}`;
await waiter((p) => {
return path in p.groups && path in p.associations.groups;
@ -74,7 +74,7 @@ export function NewGroup(props: NewGroupProps): ReactElement {
actions.setStatus({ error: e.message });
}
},
[api, waiter, history]
[waiter, history]
);
return (

View File

@ -7,7 +7,7 @@ import {
StatelessTextInput as Input, Text
} from '@tlon/indigo-react';
import { Contact, Contacts } from '@urbit/api/contacts';
import { Group, RoleTags } from '@urbit/api/groups';
import { addTag, removeMembers, changePolicy, Group, removeTag, RoleTags } from '@urbit/api/groups';
import { Association } from '@urbit/api/metadata';
import _ from 'lodash';
import f from 'lodash/fp';
@ -26,6 +26,7 @@ import useContactState from '~/logic/state/contact';
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
import { Dropdown } from '~/views/components/Dropdown';
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
import airlock from '~/logic/api';
const TruncText = styled(Text)`
white-space: nowrap;
@ -47,6 +48,19 @@ const searchParticipant = (search: string) => (p: Participant) => {
return p.patp.includes(s) || p.nickname.toLowerCase().includes(s);
};
const emptyContact = (patp: string, pending: boolean): Participant => ({
nickname: '',
bio: '',
status: '',
color: '0x0',
avatar: null,
cover: null,
groups: [],
patp,
'last-updated': 0,
pending
});
function getParticipants(cs: Contacts, group: Group) {
const contacts: Participant[] = _.flow(
f.omitBy<Contacts>((_c, patp) => !group.members.has(patp.slice(1))),
@ -77,19 +91,6 @@ function getParticipants(cs: Contacts, group: Group) {
] as const;
}
const emptyContact = (patp: string, pending: boolean): Participant => ({
nickname: '',
bio: '',
status: '',
color: '0x0',
avatar: null,
cover: null,
groups: [],
patp,
'last-updated': 0,
pending
});
const Tab = ({ selected, id, label, setSelected }) => (
<Box
py={2}
@ -249,7 +250,7 @@ function Participant(props: {
role?: RoleTags;
api: GlobalApi;
}) {
const { contact, association, group, api } = props;
const { contact, association, group } = props;
const { title } = association.metadata;
const { hideAvatars, hideNicknames } = useSettingsState(selectCalmState);
@ -266,34 +267,32 @@ function Participant(props: {
const onPromote = useCallback(async () => {
const resource = resourceFromPath(association.group);
await api.groups.addTag(resource, { tag: 'admin' }, [`~${contact.patp}`]);
}, [api, association]);
await airlock.poke(addTag(resource, { tag: 'admin' }, [`~${contact.patp}`]));
}, [contact.patp, association]);
const onDemote = useCallback(async () => {
const resource = resourceFromPath(association.group);
await api.groups.removeTag(resource, { tag: 'admin' }, [
`~${contact.patp}`
]);
}, [api, association]);
await airlock.poke(removeTag({ tag: 'admin' }, resource, [`~${contact.patp}`]));
}, [association, contact.patp]);
const onBan = useCallback(async () => {
const resource = resourceFromPath(association.group);
await api.groups.changePolicy(resource, {
await airlock.poke(changePolicy(resource, {
open: { banShips: [`~${contact.patp}`] }
});
}, [api, association]);
}));
}, [association, contact.patp]);
const onKick = useCallback(async () => {
const resource = resourceFromPath(association.group);
if(contact.pending) {
await api.groups.changePolicy(
await airlock.poke(changePolicy(
resource,
{ invite: { removeInvites: [`~${contact.patp}`] } }
);
));
} else {
await api.groups.remove(resource, [`~${contact.patp}`]);
await airlock.poke(removeMembers(resource, [`~${contact.patp}`]));
}
}, [api, contact, association]);
}, [contact, association]);
const avatar =
contact?.avatar && !hideAvatars ? (

View File

@ -1,4 +1,5 @@
import { Box, Button, Col, Icon, Row, Text } from '@tlon/indigo-react';
import { leaveGroup } from '@urbit/api';
import _ from 'lodash';
import React, { useCallback, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
@ -15,6 +16,7 @@ import { ModalOverlay } from '~/views/components/ModalOverlay';
import { Portal } from '~/views/components/Portal';
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
import { Triangle } from '~/views/components/Triangle';
import airlock from '~/logic/api';
const localSelector = selectLocalState([
'tutorialProgress',
@ -31,8 +33,7 @@ export function TutorialModal(props: { api: GlobalApi }) {
tutorialRef,
nextTutStep,
prevTutStep,
hideTutorial,
set: setLocalState
hideTutorial
} = useLocalState(localSelector);
const {
title,
@ -105,10 +106,10 @@ export function TutorialModal(props: { api: GlobalApi }) {
setPaused(true);
}, []);
const leaveGroup = useCallback(async () => {
await props.api.groups.leaveGroup(TUTORIAL_HOST, TUTORIAL_GROUP);
const doLeaveGroup = useCallback(async () => {
await airlock.thread(leaveGroup(TUTORIAL_HOST, TUTORIAL_GROUP));
await dismiss();
}, [props.api, dismiss]);
}, [dismiss]);
const progressIdx = progress.findIndex(p => p === tutorialProgress);
@ -149,7 +150,7 @@ export function TutorialModal(props: { api: GlobalApi }) {
<Button backgroundColor="washedGray" onClick={dismiss}>
Later
</Button>
<StatelessAsyncButton primary destructive onClick={leaveGroup}>
<StatelessAsyncButton primary destructive onClick={doLeaveGroup}>
Leave Group
</StatelessAsyncButton>
</Row>
@ -173,7 +174,7 @@ export function TutorialModal(props: { api: GlobalApi }) {
</Text>
</Col>
<Text lineHeight="tall">
You can always restart the tutorial by typing "tutorial" in Leap.
You can always restart the tutorial by typing &quot;tutorial&quot; in Leap.
</Text>
<Row mt={3} gapX={2} justifyContent="flex-end">
<Button backgroundColor="washedGray" onClick={bailExit}>