groups: add descriptions, group settings

This commit is contained in:
Matilde Park 2020-03-05 22:26:42 -05:00
parent 831b93dc4e
commit d73d60b73b
5 changed files with 252 additions and 43 deletions

View File

@ -68,13 +68,13 @@ class UrbitApi {
return this.action("contact-view", "json", data);
}
contactCreate(path, ships = [], title) {
contactCreate(path, ships = [], title, description) {
return this.contactViewAction({
create: {
path,
ships,
title,
description: ''
description
}
});
}
@ -149,6 +149,31 @@ class UrbitApi {
});
}
metadataAction(data) {
console.log(data);
return this.action("metadata-hook", "metadata-action", data);
}
metadataAdd(appPath, groupPath, title, description, dateCreated, color) {
let creator = `~${window.ship}`;
return this.metadataAction({
add: {
"group-path": groupPath,
resource: {
"app-path": appPath,
"app-name": "contacts"
},
metadata: {
title,
description,
color,
'date-created': dateCreated,
creator
}
}
})
}
setSpinner(boolean) {
store.handleEvent({
data: {

View File

@ -1,27 +1,64 @@
import React, { Component } from 'react';
import { Route, Link } from 'react-router-dom';
import { uxToHex } from '/lib/util.js';
import { deSig, uxToHex } from '/lib/util.js';
export class GroupDetail extends Component {
render() {
constructor(props) {
super(props);
this.state = {
title: "",
description: "",
}
this.changeTitle = this.changeTitle.bind(this);
this.changeDescription = this.changeDescription.bind(this);
}
componentDidMount() {
const { props } = this;
let channelPath = `${props.path}/contacts${props.path}`;
if ((props.association) && (props.association[channelPath])) {
this.setState({
title: props.association[channelPath].metadata.title,
description: props.association[channelPath].metadata.description
})
}
}
componentDidUpdate(prevProps) {
const { props } = this;
if (prevProps !== this.props) {
let channelPath = `${props.path}/contacts${props.path}`;
if ((props.association) && (props.association[channelPath])) {
this.setState({
title: props.association[channelPath].metadata.title,
description: props.association[channelPath].metadata.description
})
}
}
}
changeTitle(event) {
this.setState({title: event.target.value})
}
changeDescription(event) {
this.setState({description: event.target.value})
}
renderDetail() {
const { props } = this;
let responsiveClass =
props.activeDrawer === "detail" ? "db" : "dn db-ns";
props.activeDrawer === "detail" ? "db " : "dn db-ns ";
let groupPath = props.path || "";
let channelsForGroup = (groupPath in props.associations) ?
props.associations[groupPath] : {};
let isEmpty = (Object.keys(channelsForGroup).length === 0) ||
((Object.keys(channelsForGroup).length === 1) &&
(Object.keys(channelsForGroup)[0].includes("contacts")));
let isEmpty = (Object.keys(props.association).length === 0) ||
((Object.keys(props.association).length === 1) &&
(Object.keys(props.association)[0].includes("contacts")));
let channelList = (<div />);
channelList = Object.keys(channelsForGroup).map((key) => {
let channel = channelsForGroup[key];
channelList = Object.keys(props.association).map((key) => {
let channel = props.association[key];
if (!('metadata' in channel)) {
return <div key={channel} />;
}
@ -37,18 +74,18 @@ export class GroupDetail extends Component {
let link = `/~${app}/join${channelPath}`
app = app.charAt(0).toUpperCase() + app.slice(1)
return(
<li key={channel} className="f9 list flex pv2 w-100">
<div className="dib"
style={{backgroundColor: `#${color}`, height: 32, width: 32}}
></div>
<div className="flex flex-column flex-auto">
<p className="f9 inter ml2 w-100">{title}</p>
<p className="f9 inter ml2 w-100"
style={{marginTop: "0.35rem"}}>
<span className="f9 di mr2 inter">{app}</span>
<a className="f9 di green2" href={link}>Open</a>
</p>
return (
<li key={channelPath} className="f9 list flex pv2 w-100">
<div className="dib"
style={{ backgroundColor: `#${color}`, height: 32, width: 32 }}
></div>
<div className="flex flex-column flex-auto">
<p className="f9 inter ml2 w-100">{title}</p>
<p className="f9 inter ml2 w-100"
style={{ marginTop: "0.35rem" }}>
<span className="f9 di mr2 inter">{app}</span>
<a className="f9 di green2" href={link}>Open</a>
</p>
</div>
</li>
)
@ -60,23 +97,137 @@ export class GroupDetail extends Component {
let emptyGroup = (
<div className={isEmpty ? "dt w-100 h-100" : "dn"}>
<p className="gray2 f9 tc v-mid dtc">
This group has no channels. To add a channel, invite this group using any application.
This group has no channels. To add a channel, invite this group using any application.
</p>
</div>
);
let title = props.path.substr(1);
let description = "";
let channel = `${props.path}/contacts${props.path}`;
if ((props.association) && (props.association[channel])) {
title = (props.association[channel].metadata.title !== "")
? props.association[channel].metadata.title
: props.path.substr(1);
description = (props.association[channel].metadata.description !== "")
? props.association[channel].metadata.description
: "";
}
return (
<div className={"h-100 w-100 overflow-x-hidden bg-white bg-gray0-d white-d pa4 "
+ responsiveClass}>
<div className="pb5 f8 db dn-m dn-l dn-xl">
<div className={"relative h-100 w-100 bg-white bg-gray0-d white-d pa4 "
+ responsiveClass +
((isEmpty) ? "overflow-hidden" : "overflow-x-hidden")}>
<div className="pb4 f8 db dn-m dn-l dn-xl">
<Link to={backLink}> Contacts</Link>
</div>
<p className={"gray2 f9 mb2 pt2 pt0-m pt0-l pt0-xl " + (isEmpty ? "dn" : "")}>Group Channels</p>
{emptyGroup}
{channelList}
</div>
<div className="w-100 lh-copy">
<Link
className="absolute right-1 f9"
to={"/~groups/settings" + props.path}>Group Settings</Link>
<p className="f9">{title}</p>
<p className="f9 gray2">{description}</p>
<p className="f9">
{props.group.size + " participant" +
((props.group.size === 1) ? "" : "s")}
</p>
</div>
<p className={"gray2 f9 mb2 pt6 " + (isEmpty ? "dn" : "")}>Group Channels</p>
{emptyGroup}
{channelList}
</div>
)
}
renderSettings() {
const { props } = this;
let groupOwner = (deSig(props.match.params.ship) === window.ship);
let channelPath = `${props.path}/contacts${props.path}`;
let association = ((props.association) && (props.association[channelPath]))
? props.association[channelPath] : {};
return (
<div className="pa4 w-100 h-100 white-d">
<div className="f9 w-100">
<Link to={"/~groups/detail" + props.path}>{"⟵ Channels"}</Link>
</div>
<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>
<div className="relative w-100 flex"
style={{maxWidth: "29rem"}}>
<input
className={"f8 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}
onChange={this.changeTitle}
/>
<span className={"f8 absolute pa3 inter " + ((groupOwner) ? "pointer" : "")}
style={{right: 12, top: 1}}
ref="rename"
onClick={() => {
if (groupOwner) {
props.api.setSpinner(true);
props.api.metadataAdd(
association['app-path'],
association['group-path'],
this.state.title,
association.metadata.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
this.refs.rename.innerText = "Saved";
props.api.setSpinner(false);
})
}
}}>Save</span>
</div>
<p className="f8 mt3 lh-copy">Change description</p>
<p className="f9 gray2 mb4">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 " +
"focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3"}
value={this.state.description}
disabled={!groupOwner}
onChange={this.changeDescription}
/>
<span className={"f8 absolute pa3 inter " + ((groupOwner) ? "pointer" : "")}
style={{ right: 12, top: 1 }}
ref="description"
onClick={() => {
if (groupOwner) {
props.api.setSpinner(true);
props.api.metadataAdd(
association['app-path'],
association['group-path'],
association.metadata.title,
this.state.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
this.refs.description.innerText = "Saved";
props.api.setSpinner(false);
})
}
}}>Save</span>
</div>
</div>
</div>
)
}
render() {
let render = (this.props.settings)
? this.renderSettings() : this.renderDetail();
return render;
}
}
export default GroupDetail

View File

@ -12,6 +12,7 @@ export class NewScreen extends Component {
this.state = {
groupName: '',
title: '',
description: '',
invites: {
groups: [],
ships: []
@ -21,6 +22,7 @@ export class NewScreen extends Component {
};
this.groupNameChange = this.groupNameChange.bind(this);
this.descriptionChange = this.descriptionChange.bind(this);
this.invChange = this.invChange.bind(this);
}
@ -33,6 +35,10 @@ export class NewScreen extends Component {
});
}
descriptionChange(event) {
this.setState({description: event.target.value});
}
invChange(value) {
this.setState({
invites: value
@ -61,8 +67,12 @@ export class NewScreen extends Component {
invites: ''
}, () => {
props.api.setSpinner(true);
let submit = props.api.contactView.create(group, aud, this.state.title);
submit.then(() => {
props.api.contactView.create(
group,
aud,
this.state.title,
this.state.description
).then(() => {
props.api.setSpinner(false);
props.history.push(`/~groups${group}`);
})
@ -102,6 +112,21 @@ export class NewScreen extends Component {
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 trumpeteers and a microphone"
style={{
resize: "none",
height: 48,
paddingTop: 14
}}
onChange={this.descriptionChange}
/>
<h2 className="f8 pt6">Add Group Members</h2>
<p className="f9 gray2 lh-copy">Invite ships to your group</p>
<div className="relative pb6 mt2">

View File

@ -82,14 +82,18 @@ export class Root extends Component {
</Skeleton>
);
}} />
<Route exact path="/~groups/(detail)?/:ship/:group/"
<Route exact path="/~groups/:detail?/:ship/:group/"
render={ (props) => {
let groupPath =
`/${props.match.params.ship}/${props.match.params.group}`;
let groupContacts = contacts[groupPath] || {};
let group = groups[groupPath] || new Set([]);
let detail = !!props.match.url.includes("/detail");
let detail = !!(props.match.url.includes("/detail"));
let settings = !!(props.match.url.includes("/settings"));
let association = (associations[groupPath])
? associations[groupPath]
: {};
return (
<Skeleton
@ -99,21 +103,24 @@ export class Root extends Component {
contacts={contacts}
invites={invites}
groups={groups}
activeDrawer={detail ? "detail" : "contacts"}
activeDrawer={(detail || settings) ? "detail" : "contacts"}
selected={groupPath}
associations={associations}>
<ContactSidebar
contacts={groupContacts}
defaultContacts={defaultContacts}
group={group}
activeDrawer={detail ? "detail" : "contacts"}
activeDrawer={(detail || settings) ? "detail" : "contacts"}
path={groupPath}
{...props}
/>
<GroupDetail
associations={associations}
association={association}
path={groupPath}
activeDrawer={detail ? "detail" : "contacts"}
group={group}
activeDrawer={(detail || settings) ? "detail" : "contacts"}
settings={settings}
api={api}
{...props}
/>
</Skeleton>

View File

@ -40,6 +40,7 @@ export class MetadataReducer {
state.associations = metadata;
}
}
update(json, state) {
let data = _.get(json, 'update-metadata', false);
if (data) {