mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-09-21 15:38:59 +03:00
groups-js: update frontend for new group store
This commit is contained in:
parent
26c610f8d2
commit
2bf1969312
@ -1,321 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import GroupsApi from '../../api/groups';
|
||||
import GroupsSubscription from '../../subscription/groups';
|
||||
import GroupsStore from '../../store/groups';
|
||||
|
||||
import './css/custom.css';
|
||||
|
||||
import { Skeleton } from './components/skeleton';
|
||||
import { NewScreen } from './components/new';
|
||||
import { ContactSidebar } from './components/lib/contact-sidebar';
|
||||
import { ContactCard } from './components/lib/contact-card';
|
||||
import { AddScreen } from './components/lib/add-contact';
|
||||
import GroupDetail from './components/lib/group-detail';
|
||||
|
||||
export default class GroupsApp extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.title = 'OS1 - Groups';
|
||||
// preload spinner asset
|
||||
new Image().src = '/~landscape/img/Spinner.png';
|
||||
|
||||
this.props.subscription.startApp('groups')
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.subscription.stopApp('groups')
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
const contacts = props.contacts || {};
|
||||
const defaultContacts =
|
||||
(Boolean(props.contacts) && '/~/default' in props.contacts) ?
|
||||
props.contacts['/~/default'] : {};
|
||||
const groups = props.groups ? props.groups : {};
|
||||
|
||||
const invites =
|
||||
(Boolean(props.invites) && '/contacts' in props.invites) ?
|
||||
props.invites['/contacts'] : {};
|
||||
const associations = props.associations ? props.associations : {};
|
||||
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
|
||||
const s3 = props.s3 ? props.s3 : {};
|
||||
const { api } = props;
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Route exact path="/~groups"
|
||||
render={(props) => {
|
||||
return (
|
||||
<Skeleton
|
||||
activeDrawer="groups"
|
||||
selectedGroups={selectedGroups}
|
||||
history={props.history}
|
||||
api={api}
|
||||
contacts={contacts}
|
||||
groups={groups}
|
||||
invites={invites}
|
||||
associations={associations}
|
||||
>
|
||||
<div className="h-100 w-100 overflow-x-hidden 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 a group to begin.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route exact path="/~groups/new"
|
||||
render={(props) => {
|
||||
return (
|
||||
<Skeleton
|
||||
history={props.history}
|
||||
selectedGroups={selectedGroups}
|
||||
api={api}
|
||||
contacts={contacts}
|
||||
groups={groups}
|
||||
invites={invites}
|
||||
associations={associations}
|
||||
activeDrawer="rightPanel"
|
||||
>
|
||||
<NewScreen
|
||||
history={props.history}
|
||||
groups={groups}
|
||||
contacts={contacts}
|
||||
api={api}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route exact path="/~groups/(detail)?/(settings)?/:ship/:group/"
|
||||
render={(props) => {
|
||||
const groupPath =
|
||||
`/${props.match.params.ship}/${props.match.params.group}`;
|
||||
const groupContacts = contacts[groupPath] || {};
|
||||
const group = groups[groupPath] || new Set([]);
|
||||
const detail = Boolean(props.match.url.includes('/detail'));
|
||||
const settings = Boolean(props.match.url.includes('/settings'));
|
||||
|
||||
const association = (associations.contacts?.[groupPath])
|
||||
? associations.contacts[groupPath]
|
||||
: {};
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
history={props.history}
|
||||
selectedGroups={selectedGroups}
|
||||
api={api}
|
||||
contacts={contacts}
|
||||
invites={invites}
|
||||
groups={groups}
|
||||
activeDrawer={(detail || settings) ? 'detail' : 'contacts'}
|
||||
selected={groupPath}
|
||||
associations={associations}
|
||||
>
|
||||
<ContactSidebar
|
||||
contacts={groupContacts}
|
||||
defaultContacts={defaultContacts}
|
||||
group={group}
|
||||
activeDrawer={(detail || settings) ? 'detail' : 'contacts'}
|
||||
api={api}
|
||||
path={groupPath}
|
||||
{...props}
|
||||
/>
|
||||
<GroupDetail
|
||||
association={association}
|
||||
path={groupPath}
|
||||
group={group}
|
||||
activeDrawer={(detail || settings) ? 'detail' : 'contacts'}
|
||||
settings={settings}
|
||||
associations={associations}
|
||||
api={api}
|
||||
{...props}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route exact path="/~groups/add/:ship/:group"
|
||||
render={(props) => {
|
||||
const groupPath =
|
||||
`/${props.match.params.ship}/${props.match.params.group}`;
|
||||
const groupContacts = contacts[groupPath] || {};
|
||||
const group = groups[groupPath] || new Set([]);
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
history={props.history}
|
||||
selectedGroups={selectedGroups}
|
||||
api={api}
|
||||
contacts={contacts}
|
||||
groups={groups}
|
||||
invites={invites}
|
||||
activeDrawer="rightPanel"
|
||||
selected={groupPath}
|
||||
associations={associations}
|
||||
>
|
||||
<ContactSidebar
|
||||
contacts={groupContacts}
|
||||
defaultContacts={defaultContacts}
|
||||
group={group}
|
||||
activeDrawer="rightPanel"
|
||||
path={groupPath}
|
||||
api={api}
|
||||
{...props}
|
||||
/>
|
||||
<AddScreen
|
||||
api={api}
|
||||
groups={groups}
|
||||
path={groupPath}
|
||||
history={props.history}
|
||||
contacts={contacts}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route exact path="/~groups/share/:ship/:group"
|
||||
render={(props) => {
|
||||
const groupPath =
|
||||
`/${props.match.params.ship}/${props.match.params.group}`;
|
||||
const shipPath = `${groupPath}/${window.ship}`;
|
||||
const rootIdentity = defaultContacts[window.ship] || {};
|
||||
|
||||
const groupContacts = contacts[groupPath] || {};
|
||||
const contact =
|
||||
(window.ship in groupContacts) ?
|
||||
groupContacts[window.ship] : {};
|
||||
const group = groups[groupPath] || new Set([]);
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
history={props.history}
|
||||
api={api}
|
||||
selectedGroups={selectedGroups}
|
||||
contacts={contacts}
|
||||
groups={groups}
|
||||
invites={invites}
|
||||
activeDrawer="rightPanel"
|
||||
selected={groupPath}
|
||||
associations={associations}
|
||||
>
|
||||
<ContactSidebar
|
||||
activeDrawer="rightPanel"
|
||||
contacts={groupContacts}
|
||||
defaultContacts={defaultContacts}
|
||||
group={group}
|
||||
path={groupPath}
|
||||
api={api}
|
||||
selectedContact={shipPath}
|
||||
{...props}
|
||||
/>
|
||||
<ContactCard
|
||||
api={api}
|
||||
history={props.history}
|
||||
contact={contact}
|
||||
path={groupPath}
|
||||
ship={window.ship}
|
||||
share={true}
|
||||
rootIdentity={rootIdentity}
|
||||
s3={s3}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route exact path="/~groups/view/:ship/:group/:contact"
|
||||
render={(props) => {
|
||||
const groupPath =
|
||||
`/${props.match.params.ship}/${props.match.params.group}`;
|
||||
const shipPath =
|
||||
`${groupPath}/${props.match.params.contact}`;
|
||||
|
||||
const groupContacts = contacts[groupPath] || {};
|
||||
const contact =
|
||||
(props.match.params.contact in groupContacts) ?
|
||||
groupContacts[props.match.params.contact] : {};
|
||||
const group = groups[groupPath] || new Set([]);
|
||||
|
||||
const rootIdentity =
|
||||
props.match.params.contact === window.ship ?
|
||||
defaultContacts[window.ship] : null;
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
history={props.history}
|
||||
api={api}
|
||||
selectedGroups={selectedGroups}
|
||||
contacts={contacts}
|
||||
groups={groups}
|
||||
invites={invites}
|
||||
activeDrawer="rightPanel"
|
||||
selected={groupPath}
|
||||
associations={associations}
|
||||
>
|
||||
<ContactSidebar
|
||||
activeDrawer="rightPanel"
|
||||
contacts={groupContacts}
|
||||
defaultContacts={defaultContacts}
|
||||
group={group}
|
||||
path={groupPath}
|
||||
api={api}
|
||||
selectedContact={shipPath}
|
||||
{...props}
|
||||
/>
|
||||
<ContactCard
|
||||
api={api}
|
||||
history={props.history}
|
||||
contact={contact}
|
||||
path={groupPath}
|
||||
ship={props.match.params.contact}
|
||||
rootIdentity={rootIdentity}
|
||||
s3={s3}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route exact path="/~groups/me"
|
||||
render={(props) => {
|
||||
const me = defaultContacts[window.ship] || {};
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
history={props.history}
|
||||
api={api}
|
||||
selectedGroups={selectedGroups}
|
||||
contacts={contacts}
|
||||
groups={groups}
|
||||
invites={invites}
|
||||
activeDrawer="rightPanel"
|
||||
selected="me"
|
||||
associations={associations}
|
||||
>
|
||||
<ContactCard
|
||||
api={api}
|
||||
history={props.history}
|
||||
path="/~/default"
|
||||
contact={me}
|
||||
s3={s3}
|
||||
ship={window.ship}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import GroupsApi from '../../api/groups';
|
||||
import GroupsSubscription from '../../subscription/groups';
|
||||
import GroupsStore from '../../store/groups';
|
||||
|
||||
import './css/custom.css';
|
||||
|
||||
import { Skeleton } from './components/skeleton';
|
||||
@ -54,7 +50,9 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
|
||||
props.invites['/contacts'] : {};
|
||||
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
|
||||
const s3 = props.s3 ? props.s3 : {};
|
||||
const { api, associations, groups } = props;
|
||||
const groups = props.groups || {};
|
||||
const associations = props.associations || {};
|
||||
const { api } = props;
|
||||
|
||||
|
||||
return (
|
||||
|
125
pkg/interface/src/apps/groups/components/join.js
Normal file
125
pkg/interface/src/apps/groups/components/join.js
Normal file
@ -0,0 +1,125 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Spinner } from '../../../components/Spinner';
|
||||
import urbitOb from 'urbit-ob';
|
||||
|
||||
export class JoinScreen extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
group: '',
|
||||
error: false,
|
||||
awaiting: null,
|
||||
disable: false
|
||||
};
|
||||
|
||||
this.groupChange = this.groupChange.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// direct join from incoming URL
|
||||
if ((this.props.ship) && (this.props.name)) {
|
||||
const incomingGroup = `/${this.props.ship}/${this.props.notebook}`;
|
||||
this.setState({ group: incomingGroup }, () => {
|
||||
this.onClickJoin();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.groups) {
|
||||
if (this.state.awaiting) {
|
||||
const group = `/ship/${this.state.group}`;
|
||||
if (group in this.props.groups) {
|
||||
this.props.history.push(`/~groups${group}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onClickJoin() {
|
||||
const { props, state } = this;
|
||||
|
||||
const { group } = state;
|
||||
const [ship, name] = group.split('/');
|
||||
|
||||
const text = 'Joining group';
|
||||
|
||||
this.props.api.contactView.join({ ship, name }).then(() => {
|
||||
this.setState({ awaiting: text });
|
||||
});
|
||||
}
|
||||
|
||||
groupChange(event) {
|
||||
const [ship, name] = event.target.value.split('/');
|
||||
const validGroup = urbitOb.isValidPatp(ship);
|
||||
this.setState({
|
||||
group: event.target.value,
|
||||
error: !validGroup
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { state } = this;
|
||||
|
||||
let joinClasses = 'db f9 green2 ba pa2 b--green2 bg-gray0-d pointer';
|
||||
|
||||
let errElem = (<span />);
|
||||
if (state.error) {
|
||||
joinClasses = 'db f9 gray2 ba pa2 b--gray3 bg-gray0-d';
|
||||
errElem = (
|
||||
<span className="f9 inter red2 db">
|
||||
Group 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="/~groups/">{'⟵ All Groups'}</Link>
|
||||
</div>
|
||||
<h2 className="mb3 f8">Join an Existing Group</h2>
|
||||
<div className="w-100">
|
||||
<p className="f8 lh-copy mt3 db">Enter a <span className="mono">~ship/group-name</span></p>
|
||||
<p className="f9 gray2 mb4">Group 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.groupChange}
|
||||
value={this.state.group}
|
||||
/>
|
||||
{errElem}
|
||||
<br />
|
||||
<button
|
||||
disabled={this.state.error}
|
||||
onClick={this.onClickJoin.bind(this)}
|
||||
className={joinClasses}
|
||||
>Join Group</button>
|
||||
<Spinner awaiting={this.state.awaiting} classes="mt4" text={this.state.text} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { InviteSearch } from '../../../../components/InviteSearch';
|
||||
import { Spinner } from '../../../../components/Spinner';
|
||||
|
||||
export class AddScreen extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
invites: {
|
||||
groups: [],
|
||||
ships: []
|
||||
},
|
||||
awaiting: false
|
||||
};
|
||||
|
||||
this.invChange = this.invChange.bind(this);
|
||||
}
|
||||
|
||||
invChange(value) {
|
||||
this.setState({
|
||||
invites: value
|
||||
});
|
||||
}
|
||||
|
||||
onClickAdd() {
|
||||
const { props, state } = this;
|
||||
|
||||
const aud = state.invites.ships
|
||||
.map(ship => `~${ship}`);
|
||||
|
||||
if (this.textarea) {
|
||||
this.textarea.value = '';
|
||||
}
|
||||
this.setState({
|
||||
error: false,
|
||||
success: true,
|
||||
invites: {
|
||||
groups: [],
|
||||
ships: []
|
||||
},
|
||||
awaiting: true
|
||||
}, () => {
|
||||
const submit = props.api.groups.add(props.path, aud);
|
||||
submit.then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
props.history.push('/~groups' + props.path);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
return (
|
||||
<div className="h-100 w-100 flex flex-column overflow-y-scroll white-d">
|
||||
<div className="w-100 dn-m dn-l dn-xl inter pt1 pb6 pl3 pt3 f8">
|
||||
<Link to={'/~groups' + props.path}>{'⟵ All Contacts'}</Link>
|
||||
</div>
|
||||
<div className="w-100 w-70-l w-70-xl mb4 pr6 pr0-l pr0-xl">
|
||||
<h2 className="f8 pl4 pt4">Add Group Members</h2>
|
||||
<p className="f9 pl4 gray2 lh-copy">Invite ships to your group</p>
|
||||
<div className="relative pl4 mt2 pb6">
|
||||
<InviteSearch
|
||||
groups={props.groups}
|
||||
contacts={props.contacts}
|
||||
groupResults={false}
|
||||
shipResults={true}
|
||||
invites={this.state.invites}
|
||||
setInvite={this.invChange}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={this.onClickAdd.bind(this)}
|
||||
className="ml4 f8 ba pa2 b--green2 green2 pointer bg-transparent"
|
||||
>
|
||||
Add Members
|
||||
</button>
|
||||
<Link to="/~groups">
|
||||
<button className="f8 ml4 ba pa2 b--black pointer bg-transparent b--white-d white-d">Cancel</button>
|
||||
</Link>
|
||||
<Spinner awaiting={this.state.awaiting} classes="mt4 pl4" text="Inviting to group..." />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AddScreen;
|
122
pkg/interface/src/apps/groups/components/lib/add-contact.tsx
Normal file
122
pkg/interface/src/apps/groups/components/lib/add-contact.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import React, { Component } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { InviteSearch, Invites } from '../../../../components/InviteSearch';
|
||||
import { Spinner } from '../../../../components/Spinner';
|
||||
import { uuid } from '../../../../lib/util';
|
||||
import { Groups } from '../../../../types/group-update';
|
||||
import { Rolodex } from '../../../../types/contact-update';
|
||||
import { Path } from '../../../../types/noun';
|
||||
import GlobalApi from '../../../../api/global';
|
||||
import { History } from 'history';
|
||||
|
||||
interface AddScreenState {
|
||||
invites: Invites;
|
||||
awaiting: boolean;
|
||||
}
|
||||
|
||||
interface AddScreenProps {
|
||||
path: Path;
|
||||
contacts: Rolodex;
|
||||
groups: Groups;
|
||||
api: GlobalApi;
|
||||
history: History;
|
||||
}
|
||||
|
||||
export class AddScreen extends Component<AddScreenProps, AddScreenState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
invites: {
|
||||
groups: [],
|
||||
ships: [],
|
||||
},
|
||||
awaiting: false,
|
||||
};
|
||||
|
||||
this.invChange = this.invChange.bind(this);
|
||||
}
|
||||
|
||||
invChange(value) {
|
||||
this.setState({
|
||||
invites: value,
|
||||
});
|
||||
}
|
||||
|
||||
onClickAdd() {
|
||||
const { props, state } = this;
|
||||
|
||||
let [, , ship, name] = props.path.split('/');
|
||||
const resource = { ship, name };
|
||||
|
||||
const aud = state.invites.ships.map((ship) => `~${ship}`);
|
||||
|
||||
this.setState(
|
||||
{
|
||||
invites: {
|
||||
groups: [],
|
||||
ships: [],
|
||||
},
|
||||
awaiting: true,
|
||||
},
|
||||
() => {
|
||||
const submit = aud.reduce(
|
||||
(acc, recipient) =>
|
||||
acc.then(() => {
|
||||
return props.api.contacts.invite(resource, recipient);
|
||||
}),
|
||||
Promise.resolve()
|
||||
);
|
||||
submit.then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
props.history.push('/~groups' + props.path);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
return (
|
||||
<div className='h-100 w-100 flex flex-column overflow-y-scroll white-d'>
|
||||
<div className='w-100 dn-m dn-l dn-xl inter pt1 pb6 pl3 pt3 f8'>
|
||||
<Link to={'/~groups' + props.path}>{'⟵ All Contacts'}</Link>
|
||||
</div>
|
||||
<div className='w-100 w-70-l w-70-xl mb4 pr6 pr0-l pr0-xl'>
|
||||
<h2 className='f8 pl4 pt4'>Add Group Members</h2>
|
||||
<p className='f9 pl4 gray2 lh-copy'>Invite ships to your group</p>
|
||||
<div className='relative pl4 mt2 pb6'>
|
||||
<InviteSearch
|
||||
groups={props.groups}
|
||||
contacts={props.contacts}
|
||||
groupResults={false}
|
||||
shipResults={true}
|
||||
invites={this.state.invites}
|
||||
setInvite={this.invChange}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={this.onClickAdd.bind(this)}
|
||||
className='ml4 f8 ba pa2 b--green2 green2 pointer bg-transparent'
|
||||
>
|
||||
Add Members
|
||||
</button>
|
||||
<Link to='/~groups'>
|
||||
<button className='f8 ml4 ba pa2 b--black pointer bg-transparent b--white-d white-d'>
|
||||
Cancel
|
||||
</button>
|
||||
</Link>
|
||||
<Spinner
|
||||
awaiting={this.state.awaiting}
|
||||
classes='mt4 pl4'
|
||||
text='Inviting to group...'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AddScreen;
|
@ -5,8 +5,29 @@ import { ShareSheet } from './share-sheet';
|
||||
import { Sigil } from '../../../../lib/sigil';
|
||||
import { Spinner } from '../../../../components/Spinner';
|
||||
import { cite } from '../../../../lib/util';
|
||||
import { roleForShip, resourceFromPath } from '../../../../lib/group';
|
||||
import { Path, PatpNoSig } from '../../../../types/noun';
|
||||
import { Rolodex, Contacts, Contact } from '../../../../types/contact-update';
|
||||
import { Groups, Group } from '../../../../types/group-update';
|
||||
import GlobalApi from '../../../../api/global';
|
||||
|
||||
export class ContactSidebar extends Component {
|
||||
interface ContactSidebarProps {
|
||||
activeDrawer: 'contacts' | 'detail' | 'rightPanel';
|
||||
groups: Groups;
|
||||
group: Group
|
||||
contacts: Contacts;
|
||||
path: Path;
|
||||
api: GlobalApi;
|
||||
defaultContacts: Contacts;
|
||||
selectedContact?: PatpNoSig;
|
||||
}
|
||||
interface ContactSidebarState {
|
||||
awaiting: boolean;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class ContactSidebar extends Component<ContactSidebarProps, ContactSidebarState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
@ -16,10 +37,14 @@ export class ContactSidebar extends Component {
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
const group = new Set(Array.from(props.group));
|
||||
const responsiveClasses =
|
||||
props.activeDrawer === 'contacts' ? 'db' : 'dn db-ns';
|
||||
|
||||
|
||||
const group = props.groups[props.path];
|
||||
|
||||
const members = new Set(group.members || []);
|
||||
|
||||
const me = (window.ship in props.contacts)
|
||||
? props.contacts[window.ship]
|
||||
: (window.ship in props.defaultContacts)
|
||||
@ -49,13 +74,13 @@ export class ContactSidebar extends Component {
|
||||
/>
|
||||
</>
|
||||
);
|
||||
group.delete(window.ship);
|
||||
members.delete(window.ship);
|
||||
|
||||
const contactItems =
|
||||
Object.keys(props.contacts)
|
||||
.filter(c => c !== window.ship)
|
||||
.map((contact) => {
|
||||
group.delete(contact);
|
||||
members.delete(contact);
|
||||
const path = props.path + '/' + contact;
|
||||
const obj = props.contacts[contact];
|
||||
return (
|
||||
@ -72,11 +97,16 @@ export class ContactSidebar extends Component {
|
||||
);
|
||||
});
|
||||
|
||||
const adminOpt = (props.path.includes(`~${window.ship}/`))
|
||||
? 'dib' : 'dn';
|
||||
const role = roleForShip(group, window.ship);
|
||||
|
||||
const resource = resourceFromPath(props.path);
|
||||
const groupItems =
|
||||
Array.from(group).map((member) => {
|
||||
Array.from(members).map((member) => {
|
||||
const memberRole = roleForShip(group, member);
|
||||
const adminOpt = (role === 'admin' && memberRole !== 'admin')
|
||||
|| (role === 'moderator' &&
|
||||
(memberRole !== 'admin' && memberRole !== 'moderator'))
|
||||
? 'dib' : 'dn';
|
||||
return (
|
||||
<div
|
||||
key={member}
|
||||
@ -99,7 +129,7 @@ export class ContactSidebar extends Component {
|
||||
style={{ paddingTop: 6 }}
|
||||
onClick={() => {
|
||||
this.setState({ awaiting: true }, (() => {
|
||||
props.api.groups.remove(props.path, [`~${member}`])
|
||||
props.api.groups.remove(resource, [`~${member}`])
|
||||
.then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
});
|
@ -1,8 +1,10 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Spinner } from '../../../../components/Spinner';
|
||||
import { GroupView } from '../../../../components/Group';
|
||||
import { deSig, uxToHex } from '../../../../lib/util';
|
||||
|
||||
|
||||
export class GroupDetail extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -158,8 +160,8 @@ export class GroupDetail extends Component {
|
||||
<p className="f9 mw5 mw3-m mw4-l">{title}</p>
|
||||
<p className="f9 gray2">{description}</p>
|
||||
<p className="f9">
|
||||
{props.group.size + ' participant' +
|
||||
((props.group.size === 1) ? '' : 's')}
|
||||
{props.group.members.size + ' participant' +
|
||||
((props.group.members.size === 1) ? '' : 's')}
|
||||
</p>
|
||||
</div>
|
||||
<p className={'gray2 f9 mb2 pt6 ' + (isEmpty ? 'dn' : '')}>Group Channels</p>
|
||||
@ -172,25 +174,32 @@ export class GroupDetail extends Component {
|
||||
renderSettings() {
|
||||
const { props } = this;
|
||||
|
||||
const groupOwner = (deSig(props.match.params.ship) === window.ship);
|
||||
const { group, association } = props;
|
||||
|
||||
const association = props.association;
|
||||
const groupOwner = (deSig(props.match.params.ship) === window.ship);
|
||||
|
||||
const deleteButtonClasses = (groupOwner) ? 'b--red2 red2 pointer bg-gray0-d' : 'b--gray3 gray3 bg-gray0-d c-default';
|
||||
|
||||
const tags = [
|
||||
{ description: 'Admin', tag: 'admin', addDescription: 'Make Admin' },
|
||||
{ description: 'Moderator', tag: 'moderator', addDescription: 'Make Moderator' },
|
||||
{ description: 'Janitor', tag: 'janitor', addDescription: 'Make Janitor' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="pa4 w-100 h-100 white-d">
|
||||
<div className="pa4 w-100 h-100 white-d overflow-y-auto">
|
||||
<div className="f8 f9-m f9-l f9-xl w-100">
|
||||
<Link to={'/~groups/detail' + props.path}>{'⟵ Channels'}</Link>
|
||||
</div>
|
||||
{ group && <GroupView permissions className="mt6" resourcePath={props.path} group={group} tags={tags} api={props.api} /> }
|
||||
<div className={(groupOwner) ? '' : 'o-30'}>
|
||||
<p className="f8 mt3 lh-copy">Rename</p>
|
||||
<p className="f9 gray2 mb4">Change the name of this group</p>
|
||||
<p className="f9 mt3 lh-copy">Rename</p>
|
||||
<p className="f9 gray2 mb2">Change the name of this group</p>
|
||||
<div className="relative w-100 flex"
|
||||
style={{ maxWidth: '29rem' }}
|
||||
>
|
||||
<input
|
||||
className={'f8 ba b--gray3 b--gray2-d bg-gray0-d white-d ' +
|
||||
className={'f9 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}
|
||||
disabled={!groupOwner}
|
||||
@ -214,13 +223,13 @@ export class GroupDetail extends Component {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="f8 mt3 lh-copy">Change description</p>
|
||||
<p className="f9 gray2 mb4">Change the description of this group</p>
|
||||
<p className="f9 mt3 lh-copy">Change description</p>
|
||||
<p className="f9 gray2 mb2">Change the description of this group</p>
|
||||
<div className="relative w-100 flex"
|
||||
style={{ maxWidth: '29rem' }}
|
||||
>
|
||||
<input
|
||||
className={'f8 ba b--gray3 b--gray2-d bg-gray0-d white-d ' +
|
||||
className={'f9 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}
|
||||
disabled={!groupOwner}
|
||||
@ -244,8 +253,8 @@ export class GroupDetail extends Component {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="f8 mt3 lh-copy">Delete Group</p>
|
||||
<p className="f9 gray2 mb4">
|
||||
<p className="f9 mt3 lh-copy">Delete Group</p>
|
||||
<p className="f9 gray2 mb2">
|
||||
Permanently delete this group. All current members will no longer see this group.
|
||||
</p>
|
||||
<a className={'dib f9 ba pa2 ' + deleteButtonClasses}
|
||||
|
@ -8,7 +8,7 @@ export class GroupItem extends Component {
|
||||
|
||||
const selectedClass = (props.selected) ? 'bg-gray4 bg-gray1-d' : '';
|
||||
const memberCount = Math.max(
|
||||
props.group.size,
|
||||
props.group.members.size,
|
||||
Object.keys(props.contacts).length
|
||||
);
|
||||
|
||||
|
@ -13,6 +13,7 @@ export class GroupSidebar extends Component {
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const { api } = props;
|
||||
|
||||
const selectedClass = (props.selected === 'me') ? 'bg-gray4 bg-gray1-d' : 'bg-white bg-gray0-d';
|
||||
|
||||
@ -123,6 +124,9 @@ export class GroupSidebar extends Component {
|
||||
<Link to="/~groups/new" className="dib">
|
||||
<p className="f9 pt4 pl4 green2 bn">Create Group</p>
|
||||
</Link>
|
||||
<Link to="/~groups/join" className="dib">
|
||||
<p className="f9 pt4 pl4 green2 bn">Join Group</p>
|
||||
</Link>
|
||||
<Welcome contacts={props.contacts} />
|
||||
<h2 className="f9 pt4 pr4 pb2 pl4 gray2 c-default">Your Identity</h2>
|
||||
{rootIdentity}
|
||||
|
@ -3,7 +3,11 @@ import React, { Component } from 'react';
|
||||
export class SidebarInvite extends Component {
|
||||
onAccept() {
|
||||
const { props } = this;
|
||||
props.api.invite.accept('/contacts', props.uid);
|
||||
const [,,ship, name] = props.invite.path.split('/');
|
||||
const resource = { ship, name };
|
||||
props.api.contacts.join(resource).then(() => {
|
||||
props.api.invite.accept('/contacts', props.uid);
|
||||
});
|
||||
props.history.push(`/~groups${props.invite.path}`);
|
||||
}
|
||||
|
||||
|
@ -1,193 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import { InviteSearch } from '../../../components/InviteSearch';
|
||||
import { Spinner } from '../../../components/Spinner';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
type NewScreenProps = RouteComponentProps & {
|
||||
|
||||
}
|
||||
|
||||
export class NewScreen extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
groupName: '',
|
||||
title: '',
|
||||
description: '',
|
||||
invites: {
|
||||
ships: [],
|
||||
groups: [],
|
||||
},
|
||||
privacy: false,
|
||||
// color: '',
|
||||
groupNameError: false,
|
||||
awaiting: false
|
||||
};
|
||||
|
||||
this.groupNameChange = this.groupNameChange.bind(this);
|
||||
this.descriptionChange = this.descriptionChange.bind(this);
|
||||
this.invChange = this.invChange.bind(this);
|
||||
this.groupPrivacyChange = this.groupPrivacyChange.bind(this);
|
||||
}
|
||||
|
||||
groupNameChange(event) {
|
||||
const asciiSafe = event.target.value.toLowerCase()
|
||||
.replace(/[^a-z0-9~_.-]/g, '-');
|
||||
this.setState({
|
||||
groupName: asciiSafe,
|
||||
title: event.target.value
|
||||
});
|
||||
}
|
||||
|
||||
descriptionChange(event) {
|
||||
this.setState({ description: event.target.value });
|
||||
}
|
||||
|
||||
invChange(value) {
|
||||
this.setState({
|
||||
invites: value
|
||||
});
|
||||
}
|
||||
|
||||
groupPrivacyChange(event) {
|
||||
this.setState({
|
||||
privacy: event.target.checked
|
||||
});
|
||||
}
|
||||
|
||||
onClickCreate() {
|
||||
const { props, state } = this;
|
||||
|
||||
if (!state.groupName) {
|
||||
this.setState({
|
||||
groupNameError: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const aud = state.invites.ships.map(ship => `~${ship}`);
|
||||
|
||||
const policy = state.privacy
|
||||
? { 'invite': {
|
||||
pending: aud
|
||||
}}
|
||||
: { 'open': {
|
||||
'ban-ranks': [],
|
||||
'banned': []
|
||||
}};
|
||||
|
||||
if (this.textarea) {
|
||||
this.textarea.value = '';
|
||||
}
|
||||
this.setState({
|
||||
error: false,
|
||||
success: true,
|
||||
invites: '',
|
||||
awaiting: true
|
||||
}, () => {
|
||||
props.api.contacts.create(
|
||||
group,
|
||||
aud,
|
||||
this.state.title,
|
||||
this.state.description
|
||||
).then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
props.history.push(`/~groups/ship/~${window.ship}/${state.groupName}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let groupNameErrElem = (<span />);
|
||||
if (this.state.groupNameError) {
|
||||
groupNameErrElem = (
|
||||
<span className="f9 inter red2 ml3 mt1 db">
|
||||
Group must have a name.
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-100 w-100 mw6 pa3 pt4 overflow-x-hidden bg-gray0-d white-d flex flex-column">
|
||||
<div className="w-100 dn-m dn-l dn-xl inter pt1 pb6 f8">
|
||||
<Link to="/~groups/">{'⟵ All Groups'}</Link>
|
||||
</div>
|
||||
<div className="w-100 mb4 pr6 pr0-l pr0-xl">
|
||||
<h2 className="f8">Create New Group</h2>
|
||||
<h2 className="f8 pt6">Group Name</h2>
|
||||
<textarea
|
||||
className={
|
||||
'f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 db w-100 mt2 ' +
|
||||
'focus-b--black focus-b--white-d'
|
||||
}
|
||||
rows={1}
|
||||
placeholder="Jazz Maximalists Research Unit"
|
||||
style={{
|
||||
resize: 'none',
|
||||
height: 48,
|
||||
paddingTop: 14
|
||||
}}
|
||||
onChange={this.groupNameChange}
|
||||
/>
|
||||
{groupNameErrElem}
|
||||
<h2 className="f8 pt6">Description <span className="gray2">(Optional)</span></h2>
|
||||
<textarea
|
||||
className={
|
||||
'f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 db w-100 mt2 ' +
|
||||
'focus-b--black focus-b--white-d'
|
||||
}
|
||||
rows={1}
|
||||
placeholder="Two trumpeters and a microphone"
|
||||
style={{
|
||||
resize: 'none',
|
||||
height: 48,
|
||||
paddingTop: 14
|
||||
}}
|
||||
onChange={this.descriptionChange}
|
||||
/>
|
||||
<div className="mv7">
|
||||
<input
|
||||
type="checkbox"
|
||||
style={{ WebkitAppearance: 'none', width: 28 }}
|
||||
onChange={this.groupPrivacyChange}
|
||||
className={privacySwitchClasses}
|
||||
/>
|
||||
<span className="dib f9 white-d inter ml3">Private Group</span>
|
||||
<p className="f9 gray2 pt1" style={{ paddingLeft: 40 }}>
|
||||
If private, new members must be invited
|
||||
</p>
|
||||
</div>
|
||||
{ this.state.privacy && (
|
||||
<>
|
||||
<h2 className="f8 pt6">Invite <span className="gray2">(Optional)</span></h2>
|
||||
<p className="f9 gray2 lh-copy">Selected ships will be invited to your group</p>
|
||||
<div className="relative pb6 mt2">
|
||||
<InviteSearch
|
||||
groups={[]}
|
||||
contacts={this.props.contacts}
|
||||
groupResults={false}
|
||||
shipResults={true}
|
||||
invites={this.state.invites}
|
||||
setInvite={this.invChange}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={this.onClickCreate.bind(this)}
|
||||
className="f9 ba pa2 b--green2 green2 pointer bg-transparent"
|
||||
>
|
||||
Start Group
|
||||
</button>
|
||||
<Link to="/~groups">
|
||||
<button className="f9 ml3 ba pa2 b--black pointer bg-transparent b--white-d white-d">Cancel</button>
|
||||
</Link>
|
||||
<Spinner awaiting={this.state.awaiting} classes="mt4" text="Creating group..." />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
228
pkg/interface/src/apps/groups/components/new.tsx
Normal file
228
pkg/interface/src/apps/groups/components/new.tsx
Normal file
@ -0,0 +1,228 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import { InviteSearch, Invites } from '../../../components/InviteSearch';
|
||||
import { Spinner } from '../../../components/Spinner';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import { Groups, GroupPolicy } from '../../../types/group-update';
|
||||
import { Contacts, Rolodex } from '../../../types/contact-update';
|
||||
import GlobalApi from '../../../api/global';
|
||||
import { Patp, PatpNoSig, Enc } from '../../../types/noun';
|
||||
|
||||
type NewScreenProps = Pick<RouteComponentProps, 'history'> & {
|
||||
groups: Groups;
|
||||
contacts: Rolodex;
|
||||
api: GlobalApi;
|
||||
};
|
||||
|
||||
type TextChange = React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>;
|
||||
type BooleanChange = React.ChangeEvent<HTMLInputElement>;
|
||||
|
||||
|
||||
interface NewScreenState {
|
||||
groupName: string;
|
||||
title: string;
|
||||
description: string;
|
||||
invites: Invites;
|
||||
privacy: boolean;
|
||||
groupNameError: boolean;
|
||||
awaiting: boolean;
|
||||
}
|
||||
|
||||
export class NewScreen extends Component<NewScreenProps, NewScreenState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
groupName: '',
|
||||
title: '',
|
||||
description: '',
|
||||
invites: { ships: [], groups: [] },
|
||||
privacy: false,
|
||||
// color: '',
|
||||
groupNameError: false,
|
||||
awaiting: false,
|
||||
};
|
||||
|
||||
this.groupNameChange = this.groupNameChange.bind(this);
|
||||
this.descriptionChange = this.descriptionChange.bind(this);
|
||||
this.invChange = this.invChange.bind(this);
|
||||
this.groupPrivacyChange = this.groupPrivacyChange.bind(this);
|
||||
}
|
||||
|
||||
groupNameChange(event: TextChange) {
|
||||
const asciiSafe = event.target.value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9~_.-]/g, '-');
|
||||
this.setState({
|
||||
groupName: asciiSafe,
|
||||
title: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
descriptionChange(event: TextChange) {
|
||||
this.setState({ description: event.target.value });
|
||||
}
|
||||
|
||||
invChange(value: Invites) {
|
||||
this.setState({
|
||||
invites: value,
|
||||
});
|
||||
}
|
||||
|
||||
groupPrivacyChange(event: BooleanChange) {
|
||||
this.setState({
|
||||
privacy: event.target.checked,
|
||||
});
|
||||
}
|
||||
|
||||
onClickCreate() {
|
||||
const { props, state } = this;
|
||||
|
||||
if (!state.groupName) {
|
||||
this.setState({
|
||||
groupNameError: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const aud = state.invites.ships.map((ship) => `~${ship}`);
|
||||
|
||||
const policy: Enc<GroupPolicy> = state.privacy
|
||||
? {
|
||||
invite: {
|
||||
pending: aud,
|
||||
},
|
||||
}
|
||||
: {
|
||||
open: {
|
||||
banRanks: [],
|
||||
banned: [],
|
||||
},
|
||||
};
|
||||
|
||||
const { groupName } = this.state;
|
||||
this.setState(
|
||||
{
|
||||
invites: { ships: [], groups: [] },
|
||||
awaiting: true,
|
||||
},
|
||||
() => {
|
||||
props.api.contacts
|
||||
.create(groupName, policy, this.state.title, this.state.description)
|
||||
.then(() => {
|
||||
this.setState({ awaiting: false });
|
||||
props.history.push(
|
||||
`/~groups/ship/~${window.ship}/${state.groupName}`
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let groupNameErrElem = <span />;
|
||||
if (this.state.groupNameError) {
|
||||
groupNameErrElem = (
|
||||
<span className='f9 inter red2 ml3 mt1 db'>
|
||||
Group must have a name.
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const privacySwitchClasses = this.state.privacy
|
||||
? 'relative checked bg-green2 br3 h1 toggle v-mid z-0'
|
||||
: 'relative bg-gray4 bg-gray1-d br3 h1 toggle v-mid z-0';
|
||||
|
||||
return (
|
||||
<div className='h-100 w-100 mw6 pa3 pt4 overflow-x-hidden bg-gray0-d white-d flex flex-column'>
|
||||
<div className='w-100 dn-m dn-l dn-xl inter pt1 pb6 f8'>
|
||||
<Link to='/~groups/'>{'⟵ All Groups'}</Link>
|
||||
</div>
|
||||
<div className='w-100 mb4 pr6 pr0-l pr0-xl'>
|
||||
<h2 className='f8'>Create New Group</h2>
|
||||
<h2 className='f8 pt6'>Group Name</h2>
|
||||
<textarea
|
||||
className={
|
||||
'f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 db w-100 mt2 ' +
|
||||
'focus-b--black focus-b--white-d'
|
||||
}
|
||||
rows={1}
|
||||
placeholder='Jazz Maximalists Research Unit'
|
||||
style={{
|
||||
resize: 'none',
|
||||
height: 48,
|
||||
paddingTop: 14,
|
||||
}}
|
||||
onChange={this.groupNameChange}
|
||||
/>
|
||||
{groupNameErrElem}
|
||||
<h2 className='f8 pt6'>
|
||||
Description <span className='gray2'>(Optional)</span>
|
||||
</h2>
|
||||
<textarea
|
||||
className={
|
||||
'f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 db w-100 mt2 ' +
|
||||
'focus-b--black focus-b--white-d'
|
||||
}
|
||||
rows={1}
|
||||
placeholder='Two trumpeters and a microphone'
|
||||
style={{
|
||||
resize: 'none',
|
||||
height: 48,
|
||||
paddingTop: 14,
|
||||
}}
|
||||
onChange={this.descriptionChange}
|
||||
/>
|
||||
<div className='mv7'>
|
||||
<input
|
||||
type='checkbox'
|
||||
style={{ WebkitAppearance: 'none', width: 28 }}
|
||||
onChange={this.groupPrivacyChange}
|
||||
className={privacySwitchClasses}
|
||||
/>
|
||||
<span className='dib f9 white-d inter ml3'>Private Group</span>
|
||||
<p className='f9 gray2 pt1' style={{ paddingLeft: 40 }}>
|
||||
If private, new members must be invited
|
||||
</p>
|
||||
</div>
|
||||
{this.state.privacy && (
|
||||
<>
|
||||
<h2 className='f8 pt6'>
|
||||
Invite <span className='gray2'>(Optional)</span>
|
||||
</h2>
|
||||
<p className='f9 gray2 lh-copy'>
|
||||
Selected ships will be invited to your group
|
||||
</p>
|
||||
<div className='relative pb6 mt2'>
|
||||
<InviteSearch
|
||||
groups={{}}
|
||||
contacts={this.props.contacts}
|
||||
groupResults={false}
|
||||
shipResults={true}
|
||||
invites={this.state.invites}
|
||||
setInvite={this.invChange}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
onClick={this.onClickCreate.bind(this)}
|
||||
className='f9 ba pa2 b--green2 green2 pointer bg-transparent'
|
||||
>
|
||||
Start Group
|
||||
</button>
|
||||
<Link to='/~groups'>
|
||||
<button className='f9 ml3 ba pa2 b--black pointer bg-transparent b--white-d white-d'>
|
||||
Cancel
|
||||
</button>
|
||||
</Link>
|
||||
<Spinner
|
||||
awaiting={this.state.awaiting}
|
||||
classes='mt4'
|
||||
text='Creating group...'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -172,3 +172,8 @@ export type GroupUpdate =
|
||||
| GroupUpdateInitialGroup;
|
||||
|
||||
export type GroupAction = Omit<GroupUpdate, 'initialGroup' | 'initial'>;
|
||||
|
||||
export const groupBunts = {
|
||||
group: (): Group => ({ members: new Set(), tags: { role: {} }, hidden: false, policy: groupBunts.policy() }),
|
||||
policy: (): GroupPolicy => ({ open: { banned: new Set(), banRanks: new Set() } })
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user