contacts: adding yourself to group works

This commit is contained in:
Logan Allen 2020-01-22 14:44:21 -08:00
parent 8de18e4d4c
commit aac3bd9678
14 changed files with 83784 additions and 225 deletions

View File

@ -105,20 +105,37 @@
++ poke-contact-action
|= act=contact-action
^- (quip card _state)
?> ?=(%edit -.act)
:: local
:_ state
?: (team:title our.bol src.bol)
=/ ship (~(got by synced) path.act)
=/ appl ?:(=(ship our.bol) %contact-store %contact-hook)
[%pass / %agent [ship appl] %poke %contact-action !>(act)]~
:: foreign
=/ ship (~(got by synced) path.act)
?. |(=(ship our.bol) =(src.bol ship.act)) ~
:: scry group to check if ship is a member
=/ =group (need (group-scry path.act))
?. (~(has in group) ship) ~
[%pass / %agent [our.bol %contact-store] %poke %contact-action !>(act)]~
~& src+src.bol
~& act+act
|^
?+ -.act !!
%edit (process-action path.act ship.act act)
%add (process-action path.act ship.act act)
==
::
++ process-action
|= [=path =ship act=contact-action]
^- (quip card _state)
:: local
:_ state
?: (team:title our.bol src.bol)
~& 'local'
=/ shp (~(got by synced) path)
=/ appl ?:(=(shp our.bol) %contact-store %contact-hook)
~& shp+shp
~& appl+appl
[%pass / %agent [shp appl] %poke %contact-action !>(act)]~
~& 'foreign'
:: foreign
=/ shp (~(got by synced) path)
~& shp+shp
?. |(=(shp our.bol) =(src.bol ship)) ~
:: scry group to check if ship is a member
=/ =group (need (group-scry path))
~& group+group
?. (~(has in group) shp) ~
[%pass / %agent [our.bol %contact-store] %poke %contact-action !>(act)]~
--
::
++ poke-hook-action
|= act=contact-hook-action
@ -214,7 +231,9 @@
^- (quip card _state)
|^
?: (team:title our.bol src.bol)
~& update-local+fact
(local fact)
~& update-foreign+fact
(foreign fact)
::
++ local
@ -222,21 +241,22 @@
^- (quip card _state)
?+ -.fact [~ state]
%add
[(give-fact [%add path.fact ship.fact contact.fact]) state]
[(give-fact path.fact [%add path.fact ship.fact contact.fact]) state]
::
%remove
:_ state
:- [%give %kick `[%contacts path.fact] `ship.fact]
(give-fact [%remove path.fact ship.fact])
(give-fact path.fact [%remove path.fact ship.fact])
::
%edit
[(give-fact [%edit path.fact ship.fact edit-field.fact]) state]
[(give-fact path.fact [%edit path.fact ship.fact edit-field.fact]) state]
==
::
++ give-fact
|= update=contact-update
|= [=path update=contact-update]
^- (list card)
[%give %fact ~ %contact-update !>(update)]~
~& 'give-fact'
[%give %fact `[%contacts path] %contact-update !>(update)]~
::
++ foreign
|= fact=contact-update
@ -256,17 +276,17 @@
::
%add
=/ owner (~(got by synced) path.fact)
?> =(owner src.bol)
?> |(=(owner src.bol) =(src.bol ship.fact))
[~[(contact-poke [%add path.fact ship.fact contact.fact])] state]
::
%remove
=/ owner (~(got by synced) path.fact)
?> =(owner src.bol)
?> |(=(owner src.bol) =(src.bol ship.fact))
[~[(contact-poke [%remove path.fact ship.fact])] state]
::
%edit
=/ owner (~(got by synced) path.fact)
?> =(owner src.bol)
?> |(=(owner src.bol) =(src.bol ship.fact))
[~[(contact-poke [%edit path.fact ship.fact edit-field.fact])] state]
==
--
@ -330,7 +350,6 @@
++ contact-poke
|= act=contact-action
^- card
~& contact+act
[%pass / %agent [our.bol %contact-store] %poke %contact-action !>(act)]
::
++ contacts-scry

View File

@ -36,6 +36,7 @@
|= [=mark =vase]
^- (quip card _this)
?> (team:title our.bowl src.bowl)
~& %store-poke
=^ cards state
?+ mark (on-poke:def mark vase)
::%json (poke-json:cc !<(json vase))
@ -106,6 +107,7 @@
|= action=contact-action
^- (quip card _state)
?> (team:title our.bol src.bol)
~& store+action
?- -.action
%create (handle-create +.action)
%delete (handle-delete +.action)

View File

@ -149,6 +149,12 @@
:~ (group-poke [%remove [ship.act ~ ~] path.act])
(contact-poke [%remove path.act ship.act])
==
::
%share
:: determine whether to send to our contact-hook or foreign
:: send contact-action to contact-hook with %add action
~& share+act
[(share-poke recipient.act [%add path.act ship.act contact.act])]~
==
++ poke-handle-http-request
|= =inbound-request:eyre
@ -181,6 +187,11 @@
^- card
[%pass / %agent [our.bol %contact-hook] %poke %contact-hook-action !>(act)]
::
++ share-poke
|= [=ship act=contact-action]
^- card
[%pass / %agent [ship %contact-hook] %poke %contact-action !>(act)]
::
++ launch-poke
|= act=[@tas path @t]
^- card

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -103,6 +103,7 @@
[%delete delete]
[%add add]
[%remove remove]
[%share share]
==
::
++ create
@ -124,6 +125,14 @@
:~ [%path pa]
[%ship (su ;~(pfix sig fed:ag))]
==
::
++ share
%- ot
:~ [%recipient (su ;~(pfix sig fed:ag))]
[%path pa]
[%ship (su ;~(pfix sig fed:ag))]
[%contact cont]
==
--
::
++ json-to-action

View File

@ -1,17 +1,20 @@
/- *contact-store
|%
+$ contact-view-action
$% :: create in both groups and contacts
$% :: %create: create in both groups and contacts
::
[%create =path ships=(set ship)]
:: add to both groups and contacts
:: %add: add to groups and send invites
::
[%add =path ships=(set ship)]
:: remove from both groups and contacts
:: %remove: remove from both groups and contacts
::
[%remove =path =ship]
:: delete in both groups and contacts
:: %delete: delete in both groups and contacts
::
[%delete =path]
[%delete =path]
:: %share: send %add contact-action to to recipient's contact-hook
::
[%share recipient=ship =path =ship =contact]
==
--

View File

@ -18,7 +18,8 @@ class UrbitApi {
create: this.contactCreate.bind(this),
delete: this.contactDelete.bind(this),
add: this.contactAdd.bind(this),
remove: this.contactRemove.bind(this)
remove: this.contactRemove.bind(this),
share: this.contactShare.bind(this)
};
this.invite = {
@ -76,6 +77,15 @@ class UrbitApi {
this.contactViewAction({ add: { path, ships }});
}
contactShare(recipient, path, ship, contact) {
console.log(recipient, path, ship, contact);
this.contactViewAction({
share: {
recipient, path, ship, contact
}
});
}
contactDelete(path) {
this.contactViewAction({ delete: { path }});
}

View File

@ -1,22 +1,23 @@
import React, { Component } from 'react';
import { Sigil } from './icons/sigil';
import { uxToHex } from '/lib/util';
import { api } from '/api';
import { Route, Link } from 'react-router-dom';
import { EditElement } from '/components/lib/edit-element';
import { uxToHex } from '/lib/util';
export class ContactCard extends Component {
constructor(props) {
super(props);
this.state = {
edit: props.share,
colorToSet: "",
nickNameToSet: "",
emailToSet: "",
phoneToSet: "",
websiteToSet: "",
notesToSet: ""
}
colorToSet: null,
nickNameToSet: null,
emailToSet: null,
phoneToSet: null,
websiteToSet: null,
notesToSet: null
};
this.editToggle = this.editToggle.bind(this);
this.sigilColorSet = this.sigilColorSet.bind(this);
this.nickNameToSet = this.nickNameToSet.bind(this);
@ -25,51 +26,52 @@ export class ContactCard extends Component {
this.websiteToSet = this.websiteToSet.bind(this);
this.notesToSet = this.notesToSet.bind(this);
this.setField = this.setField.bind(this);
this.shareWithGroup = this.shareWithGroup.bind(this);
}
componentDidUpdate() {
const { props } = this;
// sigil color updates are done by keystroke parsing on update
// other field edits are exclusively handled by setField()
let currentColor = (props.contact.color) ? props.contact.color : "0x0";
let currentHex = uxToHex(currentColor);
let hexExp = /#?([0-9A-Fa-f]{6})/
let currentColor = (props.contact.color) ? props.contact.color : "000000";
currentColor = uxToHex(currentColor);
let hexExp = /([0-9A-Fa-f]{6})/
let hexTest = hexExp.exec(this.state.colorToSet);
if ((hexTest) && (hexTest[1] !== currentHex)) {
let ship = "~" + props.ship;
api.contactEdit(props.path, ship, {color: hexTest[1]});
if (hexTest && (hexTest[1] !== currentColor) && !props.share) {
api.contactEdit(props.path, `~${props.ship}`, {color: hexTest[1]});
}
}
editToggle() {
const { props } = this;
let editSwitch = this.state.edit;
editSwitch = !editSwitch;
this.setState({edit: editSwitch});
}
emailToSet(event) {
this.setState({ emailToSet: event.target.value });
emailToSet(value) {
this.setState({ emailToSet: value });
}
nickNameToSet(event) {
this.setState({ nickNameToSet: event.target.value });
nickNameToSet(value) {
this.setState({ nickNameToSet: value });
}
notesToSet(event) {
this.setState({ notesToSet: event.target.value });
notesToSet(value) {
this.setState({ notesToSet: value });
}
phoneToSet(event) {
this.setState({ phoneToSet: event.target.value });
phoneToSet(value) {
this.setState({ phoneToSet: value });
}
sigilColorSet(event) {
this.setState({ colorToSet: event.target.value });
sigilColorSet(value) {
this.setState({ colorToSet: value });
}
websiteToSet(event) {
this.setState({ websiteToSet: event.target.value });
websiteToSet(value) {
this.setState({ websiteToSet: value });
}
shipParser(ship) {
@ -197,8 +199,49 @@ export class ContactCard extends Component {
}
}
pickFunction(val, def) {
if (val !== null) {
return val;
}
return def;
}
shareWithGroup() {
const { props, state } = this;
let defaultVal = props.share ? {
nickname: props.rootIdentity.nickname,
email: props.rootIdentity.email,
phone: props.rootIdentity.phone,
website: props.rootIdentity.website,
notes: props.rootIdentity.notes,
color: props.rootIdentity.color
} : {
nickname: props.contact.nickname,
email: props.contact.email,
phone: props.contact.phone,
website: props.contact.website,
notes: props.contact.notes,
color: props.contact.color
};
let contact = {
nickname: this.pickFunction(state.nickNameToSet, defaultVal.nickname),
email: this.pickFunction(state.emailToSet, defaultVal.email),
phone: this.pickFunction(state.phoneToSet, defaultVal.phone),
website: this.pickFunction(state.websiteToSet, defaultVal.website),
notes: this.pickFunction(state.notesToSet, defaultVal.notes),
color: this.pickFunction(state.colorToSet, defaultVal.color),
avatar: null
};
api.contactView.share(
`~${props.ship}`, props.path, `~${window.ship}`, contact
);
this.editToggle();
}
renderEditCard() {
const { props } = this;
const { props, state } = this;
// if this is our first edit in a new group, propagate from root identity
let defaultValue = props.share ? {
nickname: props.rootIdentity.nickname,
@ -218,49 +261,52 @@ export class ContactCard extends Component {
let shipType = this.shipParser(props.ship);
let currentColor = !!defaultValue.color ? defaultValue.color : "0x0";
let hexColor = uxToHex(currentColor);
let defaultColor = !!defaultValue.color ? defaultValue.color : "000000";
defaultColor = uxToHex(defaultColor);
let currentColor = !!state.colorToSet ? state.colorToSet : defaultColor;
currentColor = uxToHex(currentColor);
let sigilColor = "";
let hasAvatar =
'avatar' in props.contact && props.contact.avatar !== "TODO";
if (!hasAvatar) {
sigilColor = (
<div className="tl mt4 mb4 w-auto ml-auto mr-auto"
style={{ width: "fit-content" }}>
<p className="f9 gray2 lh-copy">Sigil Color</p>
style={{ width: "fit-content" }}>
<p className="f9 gray2 lh-copy">Sigil Color</p>
<textarea
className="b--gray4 black f7 ba db pl2"
onChange={this.sigilColorSet}
defaultValue={"#" + hexColor}
key={hexColor}
style={{
resize: "none",
height: 40,
paddingTop: 10,
className="b--gray4 black f7 ba db pl2"
onChange={this.sigilColorSet}
defaultValue={defaultColor}
key={"default" + defaultColor}
style={{
resize: "none",
height: 40,
paddingTop: 10,
width: 114
}}></textarea>
}}>
</textarea>
</div>
)
);
}
let removeImage = "";
let avatar = (hasAvatar)
? <img className="dib h-auto" width={128} src={props.contact.avatar} />
: <Sigil ship={props.ship} size={128} color={"#" + hexColor} />;
if (hasAvatar) {
removeImage = (
let removeImage = hasAvatar ? (
<div>
<button className="f9 black pointer db"
onClick={() => this.setField("removeAvatar")}>
Remove photo
</button>
</div>
);
}
) : "";
let avatar = (hasAvatar)
? <img className="dib h-auto" width={128} src={props.contact.avatar} />
: <Sigil
ship={props.ship}
size={128}
color={currentColor}
key={"avatar" + currentColor} />;
return (
<div className="w-100 mt8 flex justify-center pa4 pt8 pt0-l pa0-xl pt4-xl">
@ -275,144 +321,45 @@ export class ContactCard extends Component {
<p className="f9 gray2 mt3">Ship Type</p>
<p className="f8">{shipType}</p>
<hr className="mv8 gray4 b--gray4 bb-0 b--solid" />
<p className="f9 gray2">Nickname</p>
<div className="w-100 flex">
<textarea
ref="nickname"
className="w-100 ba pl3 b--gray4"
style={{ resize: "none",
height: 40,
paddingTop: 10 }}
onChange={this.nickNameToSet}
defaultValue={defaultValue.nickname}/>
<button
className={"f9 pointer ml3 ba pa2 pl3 pr3 b--red2 red2 " +
((props.contact.nickname === "") ? "dn" : "dib")
}
onClick={() => this.setField("removeNickname")}>
Delete
</button>
</div>
<button
className={"pointer db mv2 f9 ba pa2 pl3 pr3 " +
((
(props.contact.nickname === this.state.nickNameToSet)
|| (this.state.nickNameToSet === "")
) ? "b--gray4 gray4" : "b--black")
}
onClick={() => this.setField("nickname")}>
Save
</button>
<p className="f9 gray2">Email</p>
<div className="w-100 flex">
<textarea
ref="email"
className="w-100 ba pl3 b--gray4"
style={{
resize: "none",
height: 40,
paddingTop: 10
}}
onChange={this.emailToSet}
defaultValue={defaultValue.email} />
<button className={"f9 pointer ml3 ba pa2 pl3 pr3 b--red2 red2 " +
((props.contact.email === "") ? "dn" : "dib")}
onClick={() => this.setField("removeEmail")}>
Delete
</button>
</div>
<button className={"pointer db mv2 f9 ba pa2 pl3 pr3 " +
(((props.contact.email === this.state.emailToSet)
|| (this.state.emailToSet === ""))
? "b--gray4 gray4"
: "b--black")}
onClick={() => this.setField("email")}>
Save
</button>
<p className="f9 gray2">Phone</p>
<div className="w-100 flex">
<textarea
ref="phone"
className="w-100 ba pl3 b--gray4"
style={{
resize: "none",
height: 40,
paddingTop: 10
}}
onChange={this.phoneToSet}
defaultValue={defaultValue.phone} />
<button className={"f9 pointer ml3 ba pa2 pl3 pr3 b--red2 red2 " +
((props.contact.phone === "") ? "dn" : "dib")}
onClick={() => this.setField("removePhone")}>
Delete
</button>
</div>
<button className={"pointer db mv2 f9 ba pa2 pl3 pr3 " +
(((props.contact.phone === this.state.phoneToSet)
|| (this.state.phoneToSet === ""))
? "b--gray4 gray4"
: "b--black")}
onClick={() => this.setField("phone")}>
Save
</button>
<p className="f9 gray2">Website</p>
<div className="w-100 flex">
<textarea
ref="website"
className="w-100 ba pl3 b--gray4"
style={{
resize: "none",
height: 40,
paddingTop: 10
}}
onChange={this.websiteToSet}
defaultValue={defaultValue.website} />
<button className={"f9 pointer ml3 ba pa2 pl3 pr3 b--red2 red2 " +
((props.contact.website === "") ? "dn" : "dib")}
onClick={() => this.setField("removeWebsite")}>
Delete
</button>
</div>
<button className={"pointer db mv2 f9 ba pa2 pl3 pr3 " +
(((props.contact.website === this.state.websiteToSet)
|| (this.state.websitetoSet === ""))
? "b--gray4 gray4"
: "b--black")}
onClick={() => this.setField("website")}>
Save
</button>
<p className="f9 gray2">Notes</p>
<div className="w-100 flex">
<textarea
ref="notes"
className="w-100 ba pl3 b--gray4"
style={{
resize: "none",
height: 40,
paddingTop: 10
}}
onChange={this.notesToSet}
defaultValue={defaultValue.notes} />
<button className={"f9 pointer ml3 ba pa2 pl3 pr3 b--red2 red2 " +
((props.contact.notes === "") ? "dn" : "dib")}
onClick={() => this.setField("removeNotes")}>
Delete
</button>
</div>
<button className={"pointer db mv2 f9 ba pa2 pl3 pr3 " +
(((props.contact.notes === this.state.notesToSet)
|| (this.state.notesToSet === ""))
? "b--gray4 gray4"
: "b--black")}
onClick={() => this.setField("notes")}>
Save
</button>
<EditElement
title="Nickname"
defaultValue={defaultValue.nickname}
onChange={this.nickNameToSet}
onDeleteClick={() => this.setField("removeNickname")}
onSaveClick={() => this.setField("nickname")}
showButtons={!props.share} />
<EditElement
title="Email"
defaultValue={defaultValue.email}
onChange={this.emailToSet}
onDeleteClick={() => this.setField("removeEmail")}
onSaveClick={() => this.setField("email")}
showButtons={!props.share} />
<EditElement
title="Phone"
defaultValue={defaultValue.phone}
onChange={this.phoneToSet}
onDeleteClick={() => this.setField("removePhone")}
onSaveClick={() => this.setField("phone")}
showButtons={!props.share} />
<EditElement
title="Website"
defaultValue={defaultValue.website}
onChange={this.websiteToSet}
onDeleteClick={() => this.setField("removeWebsite")}
onSaveClick={() => this.setField("website")}
showButtons={!props.share} />
<EditElement
title="Notes"
defaultValue={defaultValue.notes}
onChange={this.notesToSet}
onDeleteClick={() => this.setField("removeNotes")}
onSaveClick={() => this.setField("notes")}
showButtons={!props.share} />
</div>
</div>
</div>
)
);
}
renderCard() {
@ -424,7 +371,11 @@ export class ContactCard extends Component {
let avatar =
('avatar' in props.contact && props.contact.avatar !== "TODO") ?
<img className="dib h-auto" width={128} src={props.contact.avatar} /> :
<Sigil ship={props.ship} size={128} color={"#" + hexColor} />;
<Sigil
ship={props.ship}
size={128}
color={hexColor}
key={hexColor} />;
let websiteHref =
(props.contact.website && props.contact.website.includes("://")) ?
@ -514,7 +465,13 @@ export class ContactCard extends Component {
<Link to="/~contacts/">{"⟵"}</Link>
</div>
<button
onClick={this.editToggle}
onClick={() => {
if (props.share) {
this.shareWithGroup();
} else {
this.editToggle();
}
}}
className={`ml3 mt2 mb2 f9 pa1 ba br2 pointer b--black ` + ourOpt}>
{editInfoText}
</button>

View File

@ -13,8 +13,9 @@ export class ContactItem extends Component {
let name = (props.nickname) ? props.nickname : "~" + props.ship;
let prefix = props.share ? 'share' : 'view';
let suffix = !props.share ? `/${props.ship}` : '';
return (
<Link to={`/~contacts/${prefix}` + props.path}>
<Link to={`/~contacts/${prefix}` + props.path + suffix}>
<div className=
{"pl4 pt1 pb1 f9 flex justify-start content-center " + selectedClass}
>

View File

@ -41,7 +41,7 @@ export class ContactSidebar extends Component {
color={obj.color}
path={props.path}
selected={path === props.selectedContact}
share={true}
share={false}
/>
);
});

View File

@ -0,0 +1,70 @@
import React, { Component } from 'react'
import { Route, Link } from 'react-router-dom';
export class EditElement extends Component {
constructor(props) {
super(props);
this.state = {
currentValue: ''
};
}
render() {
const { props, state } = this;
// TODO: make ref clear function and make it work
let showDelete = props.defaultValue === "";
let allowSave = (
props.defaultValue !== state.currentValue &&
state.currentValue !== ""
);
return (
<div>
<p className="f9 gray2">{props.title}</p>
<div className="w-100 flex">
<textarea
ref={props.title}
className="w-100 ba pl3 b--gray4"
style={{ resize: "none", height: 40, paddingTop: 10 }}
onChange={(e) => {
let val = e.target.value;
this.setState({
currentValue: val
}, () => {
props.onChange(val);
});
}}
defaultValue={props.defaultValue} />
{!!props.showButtons ? (
<button
className={
"f9 pointer ml3 ba pa2 pl3 pr3 b--red2 red2 " +
(showDelete ? "dn" : "dib")
}
onClick={() => {
this.refs[props.title].value = "";
props.onDeleteClick();
}}>
Delete
</button>
) : null}
</div>
{!!props.showButtons ? (
<button
className={
"pointer db mv2 f9 ba pa2 pl3 pr3 " +
(allowSave ? "b--black" : "b--gray4 gray4")
}
onClick={() => {
if (!allowSave) { return; }
props.onSaveClick();
}}>
Save
</button>
) : null}
</div>
);
}
}

View File

@ -6,6 +6,7 @@ export class Sigil extends Component {
render() {
const { props } = this;
let color = "#" + props.color;
if (props.ship.length > 14) {
return (
<div className="bg-black" style={{width: props.size, height: props.size}}>
@ -13,12 +14,12 @@ export class Sigil extends Component {
);
} else {
return (
<div className="dib" style={{ flexBasis: 32, backgroundColor: props.color }}>
<div className="dib" style={{ flexBasis: 32, backgroundColor: color }}>
{sigil({
patp: props.ship,
renderer: reactRenderer,
size: props.size,
colors: [props.color, "white"]
colors: [color, "white"]
})}
</div>
);

View File

@ -63,6 +63,11 @@ export function deSig(ship) {
}
export function uxToHex(ux) {
let value = ux.substr(2).replace('.', '').padStart(6, '0');
if (ux.length > 2 && ux.substr(0,2) === '0x') {
let value = ux.substr(2).replace('.', '').padStart(6, '0');
return value;
}
let value = ux.replace('.', '').padStart(6, '0');
return value;
}
}