mirror of
https://github.com/urbit/shrub.git
synced 2024-11-28 05:22:27 +03:00
Merge pull request #2241 from urbit/mp/os1/publish-new-screen
OS1: publish new screen
This commit is contained in:
commit
5b4cff7f61
@ -173,7 +173,7 @@
|
||||
=/ uid (sham %publish who book eny.bol)
|
||||
=/ inv=invite
|
||||
:* our.bol %publish /notebook/[book] who
|
||||
'invite for notebook {<who>}/{<book>}'
|
||||
(crip "invite for notebook {<who>}/{<book>}")
|
||||
==
|
||||
=/ act=invite-action [%invite /publish uid inv]
|
||||
[%pass /invite %agent [who %invite-hook] %poke %invite-action !>(act)]
|
||||
@ -191,12 +191,13 @@
|
||||
[%web %publish @ %publish-info ~]
|
||||
=/ book-name i.t.t.pax
|
||||
=/ old=old-info .^(old-info %cx (welp our-beak:main pax))
|
||||
=/ write-pax /~/publish/(scot %p our.bol)/[book-name]/write
|
||||
=/ read-pax /~/publish/(scot %p our.bol)/[book-name]/read
|
||||
=/ group-pax /~/publish/(scot %p our.bol)/[book-name]
|
||||
=/ book=notebook-info
|
||||
[title.old '' =(%open comments.old) write-pax read-pax]
|
||||
=/ grp-car=(list card)
|
||||
(make-groups ~ writers.book ~ subscribers.book %journal)
|
||||
[title.old '' =(%open comments.old) / /]
|
||||
=+ ^- [grp-car=(list card) write-pax=path read-pax=path]
|
||||
(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))
|
||||
:- :(weld car grp-car inv-car)
|
||||
^- soba:clay
|
||||
@ -522,12 +523,14 @@
|
||||
now.bol
|
||||
~ ~ ~
|
||||
==
|
||||
=? writers.new-book =(writers.new-book /)
|
||||
/~/publish/(scot %p our.bol)/[book-name]/write
|
||||
=? subscribers.new-book =(writers.new-book /)
|
||||
/~/publish/(scot %p our.bol)/[book-name]/read
|
||||
=/ grp-car=(list card)
|
||||
(make-groups ~ writers.new-book ~ subscribers.new-book %journal)
|
||||
=/ group-path=path
|
||||
?: =(writers.new-book /)
|
||||
/~/publish/(scot %p our.bol)/[book-name]
|
||||
writers.new-book
|
||||
=+ ^- [grp-car=(list card) write-pax=path read-pax=path]
|
||||
(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)]
|
||||
(watch-notes /app/publish/notebooks/[book-name])
|
||||
=. notes.new-book notes
|
||||
@ -810,6 +813,11 @@
|
||||
^- card
|
||||
[%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
|
||||
|= act=permission-hook-action
|
||||
^- card
|
||||
@ -853,22 +861,80 @@
|
||||
(perm-group-hook-poke [%associate write [[write write-type] ~ ~]])
|
||||
==
|
||||
::
|
||||
++ make-groups
|
||||
|= $: write-grp=(set ship) write-pax=path
|
||||
read-grp=(set ship) read-pax=path
|
||||
sec=rw-security
|
||||
==
|
||||
++ create-managed-group
|
||||
|= [pax=path security=rw-security ships=(set ship)]
|
||||
^- (list card)
|
||||
;: weld
|
||||
:~ (group-poke [%bundle write-pax])
|
||||
(group-poke [%bundle read-pax])
|
||||
(group-poke [%add write-grp write-pax])
|
||||
(group-poke [%add read-grp read-pax])
|
||||
=/ grp
|
||||
.^((unit group) %gx ;:(weld /=group-store/(scot %da now.bol) pax /noun))
|
||||
?^ grp
|
||||
~
|
||||
?> ?=(^ 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)
|
||||
:~ (perm-hook-poke [%add-owned write-pax write-pax])
|
||||
(perm-hook-poke [%add-owned read-pax read-pax])
|
||||
=/ act=invite-action [%invite /publish uid inv]
|
||||
[%pass / %agent [our.bol %invite-hook] %poke %invite-action !>(act)]
|
||||
::
|
||||
++ 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
|
||||
@ -881,14 +947,7 @@
|
||||
?: (~(has by books) book.act)
|
||||
~|("notebook already exists: {<book.act>}" !!)
|
||||
=+ ^- [cards=(list card) write-pax=path read-pax=path]
|
||||
?. ?=(%new -.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
|
||||
==
|
||||
(make-groups book.act group.act)
|
||||
=/ new-book=notebook-info
|
||||
:* title.act
|
||||
about.act
|
||||
@ -959,14 +1018,7 @@
|
||||
=+ ^- [cards=(list card) write-pax=path read-pax=path]
|
||||
?~ group.act
|
||||
[~ writers.u.book subscribers.u.book]
|
||||
?. ?=(%new -.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
|
||||
==
|
||||
(make-groups book.act u.group.act)
|
||||
=/ new-info=notebook-info
|
||||
:* title.act
|
||||
about.act
|
||||
|
File diff suppressed because one or more lines are too long
BIN
pkg/arvo/app/publish/img/search.png
Normal file
BIN
pkg/arvo/app/publish/img/search.png
Normal file
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
@ -115,16 +115,13 @@
|
||||
note+so
|
||||
==
|
||||
++ group-info
|
||||
%- of
|
||||
:~ old+(ot write-pax+pa read-pax+pa ~)
|
||||
:- %new
|
||||
%- ot
|
||||
:~ write-grp+set-ship write-pax+pa
|
||||
read-grp+set-ship read-pax+pa
|
||||
sec+so
|
||||
:~ group-path+pa
|
||||
invitees+set-ship
|
||||
use-preexisting+bo
|
||||
make-managed+bo
|
||||
==
|
||||
==
|
||||
++ set-ship (ar (su fed:ag))
|
||||
++ set-ship (as (su fed:ag))
|
||||
--
|
||||
--
|
||||
--
|
||||
|
@ -2,14 +2,11 @@
|
||||
|%
|
||||
::
|
||||
+$ group-info
|
||||
$% [%old write-pax=path read-pax=path]
|
||||
$: %new
|
||||
write-grp=(set ship) write-pax=path
|
||||
read-grp=(set ship) read-pax=path
|
||||
sec=rw-security
|
||||
$: group-path=path
|
||||
invitees=(set ship)
|
||||
use-preexisting=?
|
||||
make-managed=?
|
||||
==
|
||||
==
|
||||
::
|
||||
::
|
||||
+$ action
|
||||
$% [%new-book book=@tas title=@t about=@t coms=? group=group-info]
|
||||
|
@ -80,16 +80,20 @@ a {
|
||||
background: rgba(42, 167, 121, 0.1);
|
||||
}
|
||||
|
||||
.focus-b--black:focus {
|
||||
border-color: #000;
|
||||
}
|
||||
|
||||
.mix-blend-diff {
|
||||
mix-blend-mode: difference;
|
||||
}
|
||||
|
||||
.NotebookButton {
|
||||
padding: 8px 12px;
|
||||
border-radius:2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.NotebookTab {
|
||||
padding: 16px 8px;
|
||||
}
|
||||
|
||||
.NewPost {
|
||||
width: 100%;
|
||||
height: calc(100% - 103px);
|
||||
@ -97,6 +101,33 @@ a {
|
||||
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 {
|
||||
width: 100%;
|
||||
}
|
||||
@ -109,9 +140,10 @@ a {
|
||||
cursor: text;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
* {
|
||||
}
|
||||
|
||||
.CodeMirror * {
|
||||
font-family: 'Source Code Pro';
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror-selected { background:#BAE3FE !important; }
|
||||
@ -175,6 +207,7 @@ a {
|
||||
top: 16px;
|
||||
color: #7f7f7f;
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
299
pkg/interface/publish/src/js/components/lib/invite-search.js
Normal file
299
pkg/interface/publish/src/js/components/lib/invite-search.js
Normal 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;
|
@ -64,8 +64,6 @@ export class JoinScreen extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('actionData', actionData);
|
||||
|
||||
// TODO: askHistory setting
|
||||
window.api.action("publish","publish-action", actionData);
|
||||
|
||||
|
@ -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 {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
</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;
|
||||
|
@ -60,12 +60,12 @@ export class Notebook extends Component {
|
||||
render() {
|
||||
let notebook = this.props.notebooks[this.props.ship][this.props.book];
|
||||
let tabStyles = {
|
||||
posts: "bb b--gray4 gray2 NotebookTab",
|
||||
about: "bb b--gray4 gray2 NotebookTab",
|
||||
// subscribers: "bb b--gray4 gray2 NotebookTab",
|
||||
// settings: "bb b--gray4 pr2 gray2 NotebookTab",
|
||||
}
|
||||
tabStyles[this.props.view] = "bb b--black black NotebookTab";
|
||||
posts: "bb b--gray4 gray2 pv4 ph2",
|
||||
about: "bb b--gray4 gray2 pv4 ph2"
|
||||
// subscribers: "bb b--gray4 gray2 pv4 ph2",
|
||||
// settings: "bb b--gray4 pr2 gray2 pv4 ph2",
|
||||
};
|
||||
tabStyles[this.props.view] = "bb b--black black pv4 ph2";
|
||||
|
||||
let inner = null;
|
||||
switch (this.props.view) {
|
||||
@ -111,19 +111,23 @@ export class Notebook extends Component {
|
||||
</button>
|
||||
|
||||
return (
|
||||
<div className="center mw6 f9 h-100"
|
||||
style={{paddingLeft:16, paddingRight:16}}>
|
||||
<div className="h-100 overflow-container no-scrollbar"
|
||||
<div
|
||||
className="center mw6 f9 h-100"
|
||||
style={{ paddingLeft: 16, paddingRight: 16 }}>
|
||||
<div
|
||||
className="h-100 overflow-container no-scrollbar"
|
||||
onScroll={this.onScroll}
|
||||
ref={(el) => {this.scrollElement = el}}>
|
||||
<div className="flex justify-between"
|
||||
style={{marginTop: 56, marginBottom: 32}}>
|
||||
ref={el => {
|
||||
this.scrollElement = el;
|
||||
}}>
|
||||
<div
|
||||
className="flex justify-between"
|
||||
style={{ marginTop: 56, marginBottom: 32 }}>
|
||||
<div className="flex-col">
|
||||
<div className="mb1">{notebook.title}</div>
|
||||
<span><span className="gray3 mr1">by</span>
|
||||
<span className="mono">
|
||||
{this.props.ship}
|
||||
</span>
|
||||
<span>
|
||||
<span className="gray3 mr1">by</span>
|
||||
<span className="mono">{this.props.ship}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
@ -132,23 +136,24 @@ export class Notebook extends Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex" style={{marginBottom:24}}>
|
||||
<div className="flex" style={{ marginBottom: 24 }}>
|
||||
<Link to={base} className={tabStyles.posts}>
|
||||
All Posts
|
||||
</Link>
|
||||
<Link to={about} className={tabStyles.about}>
|
||||
About
|
||||
</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 style={{height:"calc(100% - 188px)"}} className="f9 lh-solid">
|
||||
<div style={{ height: "calc(100% - 188px)" }} className="f9 lh-solid">
|
||||
{inner}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
key:{}
|
||||
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import { BrowserRouter, Route, Link } from "react-router-dom";
|
||||
import { store } from '/store';
|
||||
import { api } from '/api';
|
||||
import { Skeleton } from '/components/skeleton';
|
||||
import { NewScreen } from '/components/lib/new';
|
||||
import { JoinScreen } from '/components/lib/join';
|
||||
@ -8,6 +9,7 @@ import { Notebook } from '/components/lib/notebook';
|
||||
import { Note } from '/components/lib/note';
|
||||
import { NewPost } from '/components/lib/new-post';
|
||||
|
||||
|
||||
export class Root extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -52,7 +54,11 @@ export class Root extends Component {
|
||||
sidebarShown={true}
|
||||
notebooks={state.notebooks}>
|
||||
<NewScreen
|
||||
groups={state.groups}/>
|
||||
notebooks={state.notebooks}
|
||||
groups={state.groups}
|
||||
api={api}
|
||||
{...props}
|
||||
/>
|
||||
</Skeleton>
|
||||
)
|
||||
}}/>
|
||||
|
68
pkg/interface/publish/src/js/reducers/group.js
Normal file
68
pkg/interface/publish/src/js/reducers/group.js
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
58
pkg/interface/publish/src/js/reducers/invite.js
Normal file
58
pkg/interface/publish/src/js/reducers/invite.js
Normal 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import _ from 'lodash';
|
||||
export class PrimaryReducer {
|
||||
reduce(json, state){
|
||||
switch(Object.keys(json)[0]){
|
||||
//publish actions
|
||||
case "add-book":
|
||||
this.addBook(json["add-book"], state);
|
||||
break;
|
||||
@ -33,6 +34,12 @@ export class PrimaryReducer {
|
||||
case "read":
|
||||
this.read(json["read"], state);
|
||||
break;
|
||||
// contacts actions
|
||||
case "contact-initial":
|
||||
this.contactInitial(json["contact-initial"], state);
|
||||
break;
|
||||
case "contact-update":
|
||||
this.contactUpdate(json["contact-update"], state);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -225,4 +232,64 @@ export class PrimaryReducer {
|
||||
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]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { InitialReducer } from '/reducers/initial';
|
||||
import { PrimaryReducer } from '/reducers/primary';
|
||||
import { ResponseReducer } from '/reducers/response';
|
||||
import { GroupReducer } from '/reducers/group';
|
||||
import { InviteReducer } from '/reducers/invite';
|
||||
|
||||
class Store {
|
||||
constructor() {
|
||||
this.state = {
|
||||
notebooks: {},
|
||||
groups: {},
|
||||
contacts: {},
|
||||
permissions: {},
|
||||
invites: {},
|
||||
spinner: false,
|
||||
@ -16,6 +19,8 @@ class Store {
|
||||
this.initialReducer = new InitialReducer();
|
||||
this.primaryReducer = new PrimaryReducer();
|
||||
this.responseReducer = new ResponseReducer();
|
||||
this.groupReducer = new GroupReducer();
|
||||
this.inviteReducer = new InviteReducer();
|
||||
this.setState = () => {};
|
||||
|
||||
this.initialReducer.reduce(window.injectedState, this.state);
|
||||
@ -26,8 +31,12 @@ class Store {
|
||||
}
|
||||
|
||||
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.inviteReducer.reduce(evt.data, this.state);
|
||||
} else if (evt.type) {
|
||||
this.responseReducer.reduce(evt, this.state);
|
||||
}
|
||||
|
@ -15,6 +15,15 @@ export class Subscription {
|
||||
api.bind(`/primary`, "PUT", api.authTokens.ship, 'publish',
|
||||
this.handleEvent.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) {
|
||||
|
Loading…
Reference in New Issue
Block a user