Merge pull request #2241 from urbit/mp/os1/publish-new-screen

OS1: publish new screen
This commit is contained in:
matildepark 2020-02-06 14:57:03 -05:00 committed by GitHub
commit 5b4cff7f61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 880 additions and 111 deletions

View File

@ -173,7 +173,7 @@
=/ uid (sham %publish who book eny.bol) =/ uid (sham %publish who book eny.bol)
=/ inv=invite =/ inv=invite
:* our.bol %publish /notebook/[book] who :* our.bol %publish /notebook/[book] who
'invite for notebook {<who>}/{<book>}' (crip "invite for notebook {<who>}/{<book>}")
== ==
=/ act=invite-action [%invite /publish uid inv] =/ act=invite-action [%invite /publish uid inv]
[%pass /invite %agent [who %invite-hook] %poke %invite-action !>(act)] [%pass /invite %agent [who %invite-hook] %poke %invite-action !>(act)]
@ -191,12 +191,13 @@
[%web %publish @ %publish-info ~] [%web %publish @ %publish-info ~]
=/ book-name i.t.t.pax =/ book-name i.t.t.pax
=/ old=old-info .^(old-info %cx (welp our-beak:main pax)) =/ old=old-info .^(old-info %cx (welp our-beak:main pax))
=/ write-pax /~/publish/(scot %p our.bol)/[book-name]/write =/ group-pax /~/publish/(scot %p our.bol)/[book-name]
=/ read-pax /~/publish/(scot %p our.bol)/[book-name]/read
=/ book=notebook-info =/ book=notebook-info
[title.old '' =(%open comments.old) write-pax read-pax] [title.old '' =(%open comments.old) / /]
=/ grp-car=(list card) =+ ^- [grp-car=(list card) write-pax=path read-pax=path]
(make-groups ~ writers.book ~ subscribers.book %journal) (make-groups book-name group-pax ~ %.n %.n)
=. writers.book write-pax
=. subscribers.book read-pax
=/ inv-car (send-invites book-name (~(get ju old-subs) book-name)) =/ inv-car (send-invites book-name (~(get ju old-subs) book-name))
:- :(weld car grp-car inv-car) :- :(weld car grp-car inv-car)
^- soba:clay ^- soba:clay
@ -522,12 +523,14 @@
now.bol now.bol
~ ~ ~ ~ ~ ~
== ==
=? writers.new-book =(writers.new-book /) =/ group-path=path
/~/publish/(scot %p our.bol)/[book-name]/write ?: =(writers.new-book /)
=? subscribers.new-book =(writers.new-book /) /~/publish/(scot %p our.bol)/[book-name]
/~/publish/(scot %p our.bol)/[book-name]/read writers.new-book
=/ grp-car=(list card) =+ ^- [grp-car=(list card) write-pax=path read-pax=path]
(make-groups ~ writers.new-book ~ subscribers.new-book %journal) (make-groups book-name group-path ~ %.n %.n)
=. writers.new-book write-pax
=. subscribers.new-book read-pax
=+ ^- [read-cards=(list card) notes=(map @tas note)] =+ ^- [read-cards=(list card) notes=(map @tas note)]
(watch-notes /app/publish/notebooks/[book-name]) (watch-notes /app/publish/notebooks/[book-name])
=. notes.new-book notes =. notes.new-book notes
@ -810,6 +813,11 @@
^- card ^- card
[%pass / %agent [our.bol %group-store] %poke %group-action !>(act)] [%pass / %agent [our.bol %group-store] %poke %group-action !>(act)]
:: ::
++ contact-view-create
|= act=[%create path (set ship)]
^- card
[%pass / %agent [our.bol %contact-view] %poke %contact-view-action !>(act)]
::
++ perm-hook-poke ++ perm-hook-poke
|= act=permission-hook-action |= act=permission-hook-action
^- card ^- card
@ -853,22 +861,80 @@
(perm-group-hook-poke [%associate write [[write write-type] ~ ~]]) (perm-group-hook-poke [%associate write [[write write-type] ~ ~]])
== ==
:: ::
++ make-groups ++ create-managed-group
|= $: write-grp=(set ship) write-pax=path |= [pax=path security=rw-security ships=(set ship)]
read-grp=(set ship) read-pax=path
sec=rw-security
==
^- (list card) ^- (list card)
;: weld =/ grp
:~ (group-poke [%bundle write-pax]) .^((unit group) %gx ;:(weld /=group-store/(scot %da now.bol) pax /noun))
(group-poke [%bundle read-pax]) ?^ grp
(group-poke [%add write-grp write-pax]) ~
(group-poke [%add read-grp read-pax]) ?> ?=(^ pax)
?: |(=('~' i.pax) !=(%village security))
[(group-poke [%bundle pax])]~
[(contact-view-create [%create pax ships])]~
::
++ generate-invites
|= [book=@tas invitees=(set ship)]
^- (list card)
%+ turn ~(tap in invitees)
|= who=ship
=/ uid (sham %publish who book eny.bol)
=/ inv=invite
:* our.bol %publish /notebook/[book] who
(crip "invite for notebook {<who>}/{<book>}")
== ==
(create-security read-pax write-pax sec) =/ act=invite-action [%invite /publish uid inv]
:~ (perm-hook-poke [%add-owned write-pax write-pax]) [%pass / %agent [our.bol %invite-hook] %poke %invite-action !>(act)]
(perm-hook-poke [%add-owned read-pax read-pax]) ::
++ make-groups
|= [book=@tas group=group-info]
^- [(list card) write=path read=path]
?> ?=(^ group-path.group)
?: use-preexisting.group
=/ scry-path
;:(weld /=group-store/(scot %da now.bol) group-path.group /noun)
=/ grp .^((unit ^group) %gx scry-path)
?~ grp !!
?> ?=(^ group-path.group)
?< =('~' i.group-path.group)
:_ [group-path.group group-path.group]
%- zing
:~ (create-security group-path.group group-path.group %channel)
[(perm-hook-poke [%add-owned group-path.group group-path.group])]~
(generate-invites book (~(del in u.grp) our.bol))
== ==
::
?: make-managed.group
=/ scry-path
;:(weld /=group-store/(scot %da now.bol) group-path.group /noun)
=/ grp .^((unit ^group) %gx scry-path)
?^ grp [~ group-path.group group-path.group]
?> ?=(^ group-path.group)
?< =('~' i.group-path.group)
:_ [group-path.group group-path.group]
%- zing
:~ [(contact-view-create [%create group-path.group invitees.group])]~
(create-security group-path.group group-path.group %channel)
[(perm-hook-poke [%add-owned group-path.group group-path.group])]~
(generate-invites book (~(del in invitees.group) our.bol))
==
:: make unmanaged group
?> ?=(^ group-path.group)
?> =('~' i.group-path.group)
=* write-path group-path.group
=/ read-path (weld write-path /read)
=/ scry-path=path
;:(weld /=group-store/(scot %da now.bol) group-path.group /noun)
=/ grp .^((unit ^group) %gx scry-path)
?^ grp [~ write-path read-path]
:_ [write-path read-path]
%- zing
:~ [(group-poke [%bundle write-path])]~
[(group-poke [%bundle read-path])]~
(create-security read-path write-path %journal)
[(perm-hook-poke [%add-owned write-path write-path])]~
[(perm-hook-poke [%add-owned read-path read-path])]~
(generate-invites book (~(del in invitees.group) our.bol))
== ==
:: ::
++ poke-publish-action ++ poke-publish-action
@ -881,14 +947,7 @@
?: (~(has by books) book.act) ?: (~(has by books) book.act)
~|("notebook already exists: {<book.act>}" !!) ~|("notebook already exists: {<book.act>}" !!)
=+ ^- [cards=(list card) write-pax=path read-pax=path] =+ ^- [cards=(list card) write-pax=path read-pax=path]
?. ?=(%new -.group.act) (make-groups book.act group.act)
[~ write-pax.group.act read-pax.group.act]
:_ [write-pax.group.act read-pax.group.act]
%- make-groups
:* write-grp.group.act write-pax.group.act
read-grp.group.act read-pax.group.act
sec.group.act
==
=/ new-book=notebook-info =/ new-book=notebook-info
:* title.act :* title.act
about.act about.act
@ -959,14 +1018,7 @@
=+ ^- [cards=(list card) write-pax=path read-pax=path] =+ ^- [cards=(list card) write-pax=path read-pax=path]
?~ group.act ?~ group.act
[~ writers.u.book subscribers.u.book] [~ writers.u.book subscribers.u.book]
?. ?=(%new -.u.group.act) (make-groups book.act u.group.act)
[~ write-pax.u.group.act read-pax.u.group.act]
:_ [write-pax.u.group.act read-pax.u.group.act]
%- make-groups
:* write-grp.u.group.act write-pax.u.group.act
read-grp.u.group.act read-pax.u.group.act
sec.u.group.act
==
=/ new-info=notebook-info =/ new-info=notebook-info
:* title.act :* title.act
about.act about.act

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -115,16 +115,13 @@
note+so note+so
== ==
++ group-info ++ group-info
%- of
:~ old+(ot write-pax+pa read-pax+pa ~)
:- %new
%- ot %- ot
:~ write-grp+set-ship write-pax+pa :~ group-path+pa
read-grp+set-ship read-pax+pa invitees+set-ship
sec+so use-preexisting+bo
make-managed+bo
== ==
== ++ set-ship (as (su fed:ag))
++ set-ship (ar (su fed:ag))
-- --
-- --
-- --

View File

@ -2,14 +2,11 @@
|% |%
:: ::
+$ group-info +$ group-info
$% [%old write-pax=path read-pax=path] $: group-path=path
$: %new invitees=(set ship)
write-grp=(set ship) write-pax=path use-preexisting=?
read-grp=(set ship) read-pax=path make-managed=?
sec=rw-security
== ==
==
::
:: ::
+$ action +$ action
$% [%new-book book=@tas title=@t about=@t coms=? group=group-info] $% [%new-book book=@tas title=@t about=@t coms=? group=group-info]

View File

@ -80,16 +80,20 @@ a {
background: rgba(42, 167, 121, 0.1); background: rgba(42, 167, 121, 0.1);
} }
.focus-b--black:focus {
border-color: #000;
}
.mix-blend-diff {
mix-blend-mode: difference;
}
.NotebookButton { .NotebookButton {
padding: 8px 12px; padding: 8px 12px;
border-radius:2px; border-radius:2px;
cursor: pointer; cursor: pointer;
} }
.NotebookTab {
padding: 16px 8px;
}
.NewPost { .NewPost {
width: 100%; width: 100%;
height: calc(100% - 103px); height: calc(100% - 103px);
@ -97,6 +101,33 @@ a {
padding-top: 8px; padding-top: 8px;
} }
.placeholder-inter::placeholder {
font-family: "Inter", sans-serif;
}
/* toggler checkbox */
.toggle::after {
content: "";
height: 12px;
width: 12px;
background: white;
position: absolute;
top: 2px;
left: 2px;
border-radius: 100%;
}
.toggle.checked::after {
content: "";
height: 12px;
width: 12px;
background: white;
position: absolute;
top: 2px;
left: 14px;
border-radius: 100%;
}
.react-codemirror2 { .react-codemirror2 {
width: 100%; width: 100%;
} }
@ -109,9 +140,10 @@ a {
cursor: text; cursor: text;
font-size: 12px; font-size: 12px;
line-height: 20px; line-height: 20px;
* { }
.CodeMirror * {
font-family: 'Source Code Pro'; font-family: 'Source Code Pro';
}
} }
.CodeMirror-selected { background:#BAE3FE !important; } .CodeMirror-selected { background:#BAE3FE !important; }
@ -175,6 +207,7 @@ a {
top: 16px; top: 16px;
color: #7f7f7f; color: #7f7f7f;
} }
.no-scrollbar { .no-scrollbar {
-ms-overflow-style: none; -ms-overflow-style: none;
scrollbar-width: none; scrollbar-width: none;

View File

@ -0,0 +1,299 @@
import React, { Component } from 'react';
import urbitOb from "urbit-ob";
import { Sigil } from "../lib/icons/sigil";
export class InviteSearch extends Component {
constructor(props) {
super(props);
this.state = {
groups: [],
peers: [],
searchValue: "",
searchResults: {
groups: [],
ships: []
},
inviteError: false
}
this.search = this.search.bind(this);
}
componentDidMount() {
this.peerUpdate();
}
componentDidUpdate(prevProps) {
if (prevProps !== this.props) {
this.peerUpdate();
}
}
peerUpdate() {
let groups = Array.from(Object.keys(this.props.groups));
groups = groups.filter(e => !e.startsWith("/~/"));
let peers = [],
peerSet = new Set();
Object.keys(this.props.groups).map(group => {
if (this.props.groups[group].size > 0) {
peerSet.add(...this.props.groups[group]);
}
});
peers = Array.from(peerSet);
this.setState({ groups: groups, peers: peers });
}
search(event) {
let searchTerm = event.target.value.toLowerCase().replace("~", "");
this.setState({searchValue: event.target.value});
if (searchTerm.length < 2) {
this.setState({searchResults: { groups: [], ships: [] }})
}
if (searchTerm.length > 2) {
if (this.state.inviteError === true) {
this.setState({inviteError: false});
}
let groupMatches = this.state.groups.filter(e => {
return e.includes(searchTerm);
});
let shipMatches = this.state.peers.filter(e => {
return e.includes(searchTerm) && !this.props.invites.ships.includes(e);
});
this.setState({
searchResults: { groups: groupMatches, ships: shipMatches }
});
}
}
deleteGroup() {
let { ships } = this.props.invites;
this.setState(
{
searchValue: "",
searchResults: { groups: [], ships: [] }
}
);
this.props.setInvite({ groups: [], ships: ships });
}
deleteShip(ship) {
let { groups, ships } = this.props.invites;
this.setState(
{
searchValue: "",
searchResults: { groups: [], ships: [] }
}
);
ships = ships.filter(e => {
return e !== ship;
});
this.props.setInvite({ groups: groups, ships: ships });
}
addGroup(group) {
this.setState(
{
searchValue: "",
searchResults: { groups: [], ships: [] }
}
);
this.props.setInvite({ groups: [group], ships: [] });
}
addShip(ship) {
let { groups, ships } = this.props.invites;
this.setState(
{
searchValue: "",
searchResults: { groups: [], ships: [] }
}
);
ships.push(ship);
if (groups.length > 0) {
return false;
}
this.props.setInvite({ groups: groups, ships: ships });
}
submitShipToAdd(ship) {
let searchTerm = ship.toLowerCase().replace("~", "").trim();
let isValid = true;
if (!urbitOb.isValidPatp("~" + searchTerm)) {
isValid = false;
}
if (!isValid) {
this.setState({ inviteError: true, searchValue: "" });
} else if (isValid) {
this.addShip(searchTerm);
this.setState({ searchValue: "" });
}
}
render() {
const { props, state } = this;
let searchDisabled = false;
if (props.invites.groups) {
if (props.invites.groups.length > 0) {
searchDisabled = true;
}
}
let participants = <div/>
let searchResults = <div/>
let invErrElem = <span />;
if (state.inviteError) {
invErrElem = (
<span className="f9 inter red2 db pt2">
Invited ships must be validly formatted ship names.
</span>
);
}
if ((state.searchResults.groups.length > 0)
|| (state.searchResults.ships.length > 0)) {
let groupHeader = (state.searchResults.groups.length > 0)
? <p className="f9 gray2 ph3">Groups</p> : "";
let shipHeader = (state.searchResults.ships.length > 0)
? <p className="f9 gray2 pv2 ph3">Ships</p> : "";
let groupResults = state.searchResults.groups.map(group => {
return (
<li
key={group}
className={
"list mono white-d f8 pv2 ph3 pointer" +
" hover-bg-gray4 hover-black-d"}
onClick={e => this.addGroup(group)}>
{group}
</li>
);
})
let shipResults = state.searchResults.ships.map(ship => {
return (
<li
key={ship}
className={
"list mono white-d f8 pv1 ph3 pointer" +
" hover-bg-gray4 hover-black-d"
}
onClick={e => this.addShip(ship)}>
<Sigil
ship={"~" + ship}
size={24}
color="#000000"
classes="mix-blend-diff v-mid"
/>
<span className="v-mid ml2">{"~" + ship}</span>
</li>
);
})
searchResults =
<div className={"absolute bg-white bg-gray0-d white-d" +
" pv3 z-1 w-100 mt1 ba b--white-d overflow-y-scroll mh-16"}>
{groupHeader}
{groupResults}
{shipHeader}
{shipResults}
</div>
}
let groupInvites = props.invites.groups || [];
let shipInvites = props.invites.ships || [];
if (groupInvites.length > 0 || shipInvites.length > 0) {
let groups = groupInvites.map(group => {
return (
<span
key={group}
className={
"f9 mono black pa2 bg-gray5 bg-gray1-d" +
" ba b--gray4 b--gray2-d white-d dib mr2 mt2 c-default"}>
{group}
<span className="white-d ml3 mono pointer"
onClick={e => this.deleteGroup(group)}>
x
</span>
</span>
);
});
let ships = shipInvites.map(ship => {
return (
<span
key={ship}
className={"f9 mono black pa2 bg-gray5 bg-gray1-d" +
" ba b--gray4 b--gray2-d white-d dib mr2 mt2 c-default"}>
{"~" + ship}
<span className="white-d ml3 mono pointer"
onClick={e => this.deleteShip(ship)}>x</span>
</span>
);
});
participants = (
<div
className={
"f9 gray2 bb bl br b--gray3 b--gray2-d bg-gray0-d " +
"white-d pa3 db w-100 inter"
}>
<span className="db gray2">Participants</span>
{groups} {ships}
</div>
);
}
return (
<div className="relative">
<img
src="/~publish/search.png"
className="absolute invert-d"
style={{
height: 16,
width: 16,
top: 14,
left: 12
}}
/>
<textarea
ref={e => {
this.textarea = e;
}}
className={"f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 w-100"
+ " db focus-b--black focus-b--white-d mono placeholder-inter"}
placeholder="Search for ships or existing groups"
disabled={searchDisabled}
rows={1}
spellCheck={false}
style={{
resize: "none",
paddingLeft: 36
}}
onKeyPress={e => {
if (e.key === "Enter" || e.key === ",") {
e.preventDefault();
this.submitShipToAdd(this.state.searchValue);
}}}
onChange={this.search}
value={state.searchValue}
/>
{searchResults}
{participants}
{invErrElem}
</div>
);
}
}
export default InviteSearch;

View File

@ -64,8 +64,6 @@ export class JoinScreen extends Component {
} }
} }
console.log('actionData', actionData);
// TODO: askHistory setting // TODO: askHistory setting
window.api.action("publish","publish-action", actionData); window.api.action("publish","publish-action", actionData);

View File

@ -1,16 +1,191 @@
import React, { Component } from 'react' import React, { Component } from 'react';
import { InviteSearch } from './invite-search';
import { Route, Link } from 'react-router-dom';
import { uuid, isPatTa, deSig, stringToSymbol } from "/lib/util";
import urbitOb from 'urbit-ob';
//TODO textarea fields for title/description
//TODO add component for ship / group search using props.groups
// (integrate props.contacts as well once contact-view is bound)
export class NewScreen extends Component { export class NewScreen extends Component {
render() { constructor(props) {
return ( super(props);
<div>
</div> this.state = {
) idName: '',
description: '',
invites: {
groups: [],
ships: []
},
createGroup: false
};
this.idChange = this.idChange.bind(this);
this.descriptionChange = this.descriptionChange.bind(this);
this.setInvite = this.setInvite.bind(this);
this.createGroupChange = this.createGroupChange.bind(this);
} }
}
export default NewScreen componentDidUpdate(prevProps) {
const { props, state } = this;
if (props.notebooks && (("~" + window.ship) in props.notebooks)) {
let notebookId = stringToSymbol(state.idName)
if (notebookId in props.notebooks["~" + window.ship]) {
let notebook = `/~${window.ship}/${notebookId}`;
props.history.push("/~publish/notebook" + notebook);
}
}
}
idChange(event) {
this.setState({
idName: event.target.value
});
}
descriptionChange(event) {
this.setState({
description: event.target.value
});
}
createGroupChange(event) {
this.setState({createGroup: !!event.target.checked});
}
setInvite(value) {
this.setState({invites: value})
}
onClickCreate() {
const { props, state } = this;
let bookId = stringToSymbol(state.idName);
let groupInfo = (state.invites.groups.length > 0)
? {
"group-path": state.invites.groups[0],
"invitees": [],
"use-preexisting": true,
"make-managed": false,
}
: {
// TODO remove /~ and set make-managed on toggle
"group-path": `/~/publish/~${window.ship}/${bookId}`,
"invitees": state.invites.ships,
"use-preexisting": false,
"make-managed": false,
};
let action = {
"new-book": {
book: bookId,
title: state.idName,
about: state.description,
coms: true,
group: groupInfo
}
}
props.api.action("publish", "publish-action", action);
}
render() {
// let createGroupClasses = this.state.createGroup
// ? "relative checked bg-green2 br3 h1 toggle v-mid z-0"
// : "relative bg-gray4 bg-gray1-d br3 h1 toggle v-mid z-0";
let createClasses = "pointer db f9 green2 bg-gray0-d ba pv3 ph4 mv7 b--green2";
if (!this.state.idName) {
createClasses = "pointer db f9 gray2 ba bg-gray0-d pa2 pv3 ph4 mv7 b--gray3";
}
// let createGroupToggle = <div/>
// if ((this.state.invites.ships.length > 0) && (this.state.invites.groups.length === 0)) {
// createGroupToggle = (
// <div className="mv7">
// <input
// type="checkbox"
// style={{ WebkitAppearance: "none", width: 28 }}
// className={createGroupClasses}
// onChange={this.createGroupChange}
// />
// <span className="dib f9 white-d inter ml3">Create Group</span>
// <p className="f9 gray2 pt1" style={{ paddingLeft: 40 }}>
// Participants will share this group across applications
// </p>
// </div>
// );
// }
let idErrElem = <span />;
if (this.state.idError) {
idErrElem = (
<span className="f9 inter red2 db pt2">
Notebook must have a valid name.
</span>
);
}
return (
<div
className={
"h-100 w-100 mw6 pa3 pt4 overflow-x-hidden flex flex-column"
}>
<div className="w-100 dn-m dn-l dn-xl inter pt1 pb6 f8">
<Link to="/~publish/">{"⟵ All Notebooks"}</Link>
</div>
<h2 className="mb3 f8">New Notebook</h2>
<div className="w-100">
<p className="f8 mt3 lh-copy db">Name</p>
<p className="f9 gray2 db mb2 pt1">
Provide a name for your notebook
</p>
<textarea
className="f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 db w-100"
placeholder="eg. My Journal"
rows={1}
style={{
resize: "none"
}}
onChange={this.idChange}
value={this.state.idName}
/>
{idErrElem}
<p className="f8 mt4 lh-copy db">
Description
<span className="gray3 ml1">(Optional)</span>
</p>
<p className="f9 gray2 db mb2 pt1">What's your notebook about?</p>
<textarea
className="f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 db w-100"
placeholder="Notebook description"
rows={1}
style={{
resize: "none"
}}
onChange={this.descriptionChange}
value={this.state.description}
/>
<p className="f8 mt4 lh-copy db">
Invite
<span className="gray3 ml1">
(Optional)
</span>
</p>
<p className="f9 gray2 db mb2 pt1">Select an initial read-only audience for your notebook</p>
<InviteSearch
groups={this.props.groups}
invites={this.state.invites}
setInvite={this.setInvite}
/>
{/* {createGroupToggle} */}
<button
onClick={this.onClickCreate.bind(this)}
className={createClasses}>
Create Notebook
</button>
</div>
</div>
);
}
}
export default NewScreen;

View File

@ -60,12 +60,12 @@ export class Notebook extends Component {
render() { render() {
let notebook = this.props.notebooks[this.props.ship][this.props.book]; let notebook = this.props.notebooks[this.props.ship][this.props.book];
let tabStyles = { let tabStyles = {
posts: "bb b--gray4 gray2 NotebookTab", posts: "bb b--gray4 gray2 pv4 ph2",
about: "bb b--gray4 gray2 NotebookTab", about: "bb b--gray4 gray2 pv4 ph2"
// subscribers: "bb b--gray4 gray2 NotebookTab", // subscribers: "bb b--gray4 gray2 pv4 ph2",
// settings: "bb b--gray4 pr2 gray2 NotebookTab", // settings: "bb b--gray4 pr2 gray2 pv4 ph2",
} };
tabStyles[this.props.view] = "bb b--black black NotebookTab"; tabStyles[this.props.view] = "bb b--black black pv4 ph2";
let inner = null; let inner = null;
switch (this.props.view) { switch (this.props.view) {
@ -111,19 +111,23 @@ export class Notebook extends Component {
</button> </button>
return ( return (
<div className="center mw6 f9 h-100" <div
style={{paddingLeft:16, paddingRight:16}}> className="center mw6 f9 h-100"
<div className="h-100 overflow-container no-scrollbar" style={{ paddingLeft: 16, paddingRight: 16 }}>
<div
className="h-100 overflow-container no-scrollbar"
onScroll={this.onScroll} onScroll={this.onScroll}
ref={(el) => {this.scrollElement = el}}> ref={el => {
<div className="flex justify-between" this.scrollElement = el;
style={{marginTop: 56, marginBottom: 32}}> }}>
<div
className="flex justify-between"
style={{ marginTop: 56, marginBottom: 32 }}>
<div className="flex-col"> <div className="flex-col">
<div className="mb1">{notebook.title}</div> <div className="mb1">{notebook.title}</div>
<span><span className="gray3 mr1">by</span> <span>
<span className="mono"> <span className="gray3 mr1">by</span>
{this.props.ship} <span className="mono">{this.props.ship}</span>
</span>
</span> </span>
</div> </div>
<div className="flex"> <div className="flex">
@ -132,23 +136,24 @@ export class Notebook extends Component {
</div> </div>
</div> </div>
<div className="flex" style={{marginBottom:24}}> <div className="flex" style={{ marginBottom: 24 }}>
<Link to={base} className={tabStyles.posts}> <Link to={base} className={tabStyles.posts}>
All Posts All Posts
</Link> </Link>
<Link to={about} className={tabStyles.about}> <Link to={about} className={tabStyles.about}>
About About
</Link> </Link>
<div className="bb b--gray4 gray2 NotebookTab" style={{flexGrow:1}}> <div
</div> className="bb b--gray4 gray2 pv4 ph2"
style={{ flexGrow: 1 }}></div>
</div> </div>
<div style={{height:"calc(100% - 188px)"}} className="f9 lh-solid"> <div style={{ height: "calc(100% - 188px)" }} className="f9 lh-solid">
{inner} {inner}
</div> </div>
</div> </div>
</div> </div>
) );
} }
} }

View File

@ -1,4 +0,0 @@
{
key:{}
}

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { BrowserRouter, Route, Link } from "react-router-dom"; import { BrowserRouter, Route, Link } from "react-router-dom";
import { store } from '/store'; import { store } from '/store';
import { api } from '/api';
import { Skeleton } from '/components/skeleton'; import { Skeleton } from '/components/skeleton';
import { NewScreen } from '/components/lib/new'; import { NewScreen } from '/components/lib/new';
import { JoinScreen } from '/components/lib/join'; import { JoinScreen } from '/components/lib/join';
@ -8,6 +9,7 @@ import { Notebook } from '/components/lib/notebook';
import { Note } from '/components/lib/note'; import { Note } from '/components/lib/note';
import { NewPost } from '/components/lib/new-post'; import { NewPost } from '/components/lib/new-post';
export class Root extends Component { export class Root extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -52,7 +54,11 @@ export class Root extends Component {
sidebarShown={true} sidebarShown={true}
notebooks={state.notebooks}> notebooks={state.notebooks}>
<NewScreen <NewScreen
groups={state.groups}/> notebooks={state.notebooks}
groups={state.groups}
api={api}
{...props}
/>
</Skeleton> </Skeleton>
) )
}}/> }}/>

View File

@ -0,0 +1,68 @@
import _ from "lodash";
export class GroupReducer {
reduce(json, state) {
let data = _.get(json, "group-initial", false);
if (data) {
for (let group in data) {
state.groups[group] = new Set(data[group]);
}
}
data = _.get(json, "group-update", false);
if (data) {
this.add(data, state);
this.remove(data, state);
this.bundle(data, state);
this.unbundle(data, state);
this.keys(data, state);
this.path(data, state);
}
}
add(json, state) {
let data = _.get(json, "add", false);
if (data) {
for (let member of data.members) {
state.groups[data.path].add(member);
}
}
}
remove(json, state) {
let data = _.get(json, "remove", false);
if (data) {
for (let member of data.members) {
state.groups[data.path].delete(member);
}
}
}
bundle(json, state) {
let data = _.get(json, "bundle", false);
if (data) {
state.groups[data.path] = new Set();
}
}
unbundle(json, state) {
let data = _.get(json, "unbundle", false);
if (data) {
delete state.groups[data.path];
}
}
keys(json, state) {
let data = _.get(json, "keys", false);
if (data) {
state.groupKeys = new Set(data.keys);
}
}
path(json, state) {
let data = _.get(json, "path", false);
if (data) {
state.groups[data.path] = new Set([data.members]);
}
}
}

View File

@ -0,0 +1,58 @@
import _ from 'lodash';
export class InviteReducer {
reduce(json, state) {
let initial = _.get(json, 'invite-initial', false);
if (initial) {
this.initial(initial, state);
}
let update = _.get(json, 'invite-update', false);
if (update) {
this.create(update, state);
this.delete(update, state);
this.invite(update, state);
this.accepted(update, state);
this.decline(update, state);
}
}
initial(json, state) {
state.invites = json;;
}
create(json, state) {
let data = _.get(json, 'create', false);
if (data) {
state.invites[data.path] = {};
}
}
delete(json, state) {
let data = _.get(json, 'delete', false);
if (data) {
delete state.invites[data.path];
}
}
invite(json, state) {
let data = _.get(json, 'invite', false);
if (data) {
state.invites[data.path][data.uid] = data.invite;
}
}
accepted(json, state) {
let data = _.get(json, 'accepted', false);
if (data) {
delete state.invites[data.path][data.uid];
}
}
decline(json, state) {
let data = _.get(json, 'decline', false);
if (data) {
delete state.invites[data.path][data.uid];
}
}
}

View File

@ -3,6 +3,7 @@ import _ from 'lodash';
export class PrimaryReducer { export class PrimaryReducer {
reduce(json, state){ reduce(json, state){
switch(Object.keys(json)[0]){ switch(Object.keys(json)[0]){
//publish actions
case "add-book": case "add-book":
this.addBook(json["add-book"], state); this.addBook(json["add-book"], state);
break; break;
@ -33,6 +34,12 @@ export class PrimaryReducer {
case "read": case "read":
this.read(json["read"], state); this.read(json["read"], state);
break; break;
// contacts actions
case "contact-initial":
this.contactInitial(json["contact-initial"], state);
break;
case "contact-update":
this.contactUpdate(json["contact-update"], state);
default: default:
break; break;
} }
@ -225,4 +232,64 @@ export class PrimaryReducer {
state.notebooks[host][book].notes[noteId]["read"] = true; state.notebooks[host][book].notes[noteId]["read"] = true;
} }
} }
contactInitial(json, state) {
state.contacts = json;
}
contactUpdate(json, state) {
this.createContact(json, state);
this.deleteContact(json, state);
this.addContact(json, state);
this.removeContact(json, state);
this.editContact(json, state);
}
createContact(json, state) {
let data = _.get(json, "create", false);
if (data) {
state.contacts[data.path] = {};
}
}
deleteContact(json, state) {
let data = _.get(json, "delete", false);
if (data) {
delete state.contacts[data.path];
}
}
addContact(json, state) {
let data = _.get(json, "add", false);
if (data && data.path in state.contacts) {
state.contacts[data.path][data.ship] = data.contact;
}
}
removeContact(json, state) {
let data = _.get(json, "remove", false);
if (
data &&
data.path in state.contacts &&
data.ship in state.contacts[data.path]
) {
delete state.contacts[data.path][data.ship];
}
}
editContact(json, state) {
let data = _.get(json, "edit", false);
if (
data &&
data.path in state.contacts &&
data.ship in state.contacts[data.path]
) {
let edit = Object.keys(data["edit-field"]);
if (edit.length !== 1) {
return;
}
state.contacts[data.path][data.ship][edit[0]] =
data["edit-field"][edit[0]];
}
}
} }

View File

@ -1,12 +1,15 @@
import { InitialReducer } from '/reducers/initial'; import { InitialReducer } from '/reducers/initial';
import { PrimaryReducer } from '/reducers/primary'; import { PrimaryReducer } from '/reducers/primary';
import { ResponseReducer } from '/reducers/response'; import { ResponseReducer } from '/reducers/response';
import { GroupReducer } from '/reducers/group';
import { InviteReducer } from '/reducers/invite';
class Store { class Store {
constructor() { constructor() {
this.state = { this.state = {
notebooks: {}, notebooks: {},
groups: {}, groups: {},
contacts: {},
permissions: {}, permissions: {},
invites: {}, invites: {},
spinner: false, spinner: false,
@ -16,6 +19,8 @@ class Store {
this.initialReducer = new InitialReducer(); this.initialReducer = new InitialReducer();
this.primaryReducer = new PrimaryReducer(); this.primaryReducer = new PrimaryReducer();
this.responseReducer = new ResponseReducer(); this.responseReducer = new ResponseReducer();
this.groupReducer = new GroupReducer();
this.inviteReducer = new InviteReducer();
this.setState = () => {}; this.setState = () => {};
this.initialReducer.reduce(window.injectedState, this.state); this.initialReducer.reduce(window.injectedState, this.state);
@ -26,8 +31,12 @@ class Store {
} }
handleEvent(evt) { handleEvent(evt) {
if (evt.from && evt.from.path === '/primary'){ if (evt.from && evt.from.path === '/all') {
this.groupReducer.reduce(evt.data, this.state);
}
else if (evt.from && evt.from.path === '/primary'){
this.primaryReducer.reduce(evt.data, this.state); this.primaryReducer.reduce(evt.data, this.state);
this.inviteReducer.reduce(evt.data, this.state);
} else if (evt.type) { } else if (evt.type) {
this.responseReducer.reduce(evt, this.state); this.responseReducer.reduce(evt, this.state);
} }

View File

@ -15,6 +15,15 @@ export class Subscription {
api.bind(`/primary`, "PUT", api.authTokens.ship, 'publish', api.bind(`/primary`, "PUT", api.authTokens.ship, 'publish',
this.handleEvent.bind(this), this.handleEvent.bind(this),
this.handleError.bind(this)); this.handleError.bind(this));
api.bind('/all', 'PUT', api.authTokens.ship, 'group-store',
this.handleEvent.bind(this),
this.handleError.bind(this));
api.bind('/primary', 'PUT', api.authTokens.ship, 'contact-view',
this.handleEvent.bind(this),
this.handleError.bind(this));
api.bind('/primary', 'PUT', api.authTokens.ship, 'invite-view',
this.handleEvent.bind(this),
this.handleError.bind(this));
} }
handleEvent(diff) { handleEvent(diff) {