Merge branch 'release/next-userspace' into lf/hark-redux

This commit is contained in:
Liam Fitzgerald 2020-11-03 10:49:25 +10:00
commit e9d9bb839f
78 changed files with 1323 additions and 4260 deletions

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:38435a0a23fb4f09d55505915cd8e772b8096fd846c2c8ff3481a5b231deedf6
size 6331042
oid sha256:0233ac5de9b947ab3ef3329e42eb6741d7a380a2c74dc97f5505ccd4456e2f70
size 6273723

3
package-lock.json generated Normal file
View File

@ -0,0 +1,3 @@
{
"lockfileVersion": 1
}

View File

@ -5,7 +5,7 @@
/- glob
/+ default-agent, verb, dbug
|%
++ hash 0v2.1vtfh.0l23v.30s7f.n57l9.dpjvi
++ hash 0vptpd9.7fcod.53cag.tfca7.grfkf
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
+$ all-states
$% state-0

View File

@ -7,14 +7,17 @@
+$ card card:agent:gall
+$ versioned-state
$% state-0
state-1
==
::
+$ state-0 [%0 network:store]
+$ state-1 [%1 network:store]
::
++ orm orm:store
++ orm-log orm-log:store
--
::
=| state-0
=| state-1
=* state -
::
%- agent:dbug
@ -27,9 +30,115 @@
++ on-init [~ this]
++ on-save !>(state)
++ on-load
|= old=vase
|= =old=vase
^- (quip card _this)
[~ this(state !<(state-0 old))]
=+ !<(old=versioned-state old-vase)
=| cards=(list card)
|^
?- -.old
%0
%_ $
-.old %1
::
validators.old
(~(put in validators.old) %graph-validator-link)
::
cards
%+ weld cards
%+ turn
~(tap in (~(put in validators.old) %graph-validator-link))
|= validator=@t
^- card
=/ =wire /validator/[validator]
=/ =rave:clay [%sing %b [%da now.bowl] /[validator]]
[%pass wire %arvo %c %warp our.bowl [%home `rave]]
::
graphs.old
%- ~(run by graphs.old)
|= [=graph:store q=(unit mark)]
^- [graph:store (unit mark)]
:- (convert-unix-timestamped-graph graph)
?^ q q
`%graph-validator-link
::
update-logs.old
%- ~(run by update-logs.old)
convert-unix-timestamped-log
==
::
%1 [cards this(state old)]
==
::
++ convert-unix-timestamped-log
|= =update-log:store
^- update-log:store
%+ gas:orm-log *update-log:store
%+ turn
(tap:orm-log update-log)
|= [=time =logged-update:store]
:- time
|^ ^- logged-update:store
:+ %0 p.logged-update
?+ -.q.logged-update q.logged-update
%add-nodes (add-nodes +.q.logged-update)
%remove-nodes (remove-nodes +.q.logged-update)
==
::
++ add-nodes
|= [rid=res nodes=(map index:store node:store)]
^- logged-update-0:store
:+ %add-nodes rid
%- ~(gas by *(map index:store node:store))
%+ turn
~(tap by nodes)
|= [=index:store =node:store]
^- [index:store node:store]
:- (convert-unix-timestamped-index index)
(convert-unix-timestamped-node node)
::
++ remove-nodes
|= [rid=res indices=(set index:store)]
^- logged-update-0:store
:+ %remove-nodes rid
%- ~(gas in *(set index:store))
%+ turn
~(tap in indices)
convert-unix-timestamped-index
--
::
++ maybe-unix-to-da
|= =atom
^- @
:: (bex 127) is roughly 226AD
?. (lte atom (bex 127))
atom
(add ~1970.1.1 (div (mul ~s1 atom) 1.000))
::
++ convert-unix-timestamped-node
|= =node:store
^- node:store
=. index.post.node
(convert-unix-timestamped-index index.post.node)
?. ?=(%graph -.children.node)
node
:+ post.node
%graph
(convert-unix-timestamped-graph p.children.node)
::
++ convert-unix-timestamped-index
|= =index:store
(turn index maybe-unix-to-da)
::
++ convert-unix-timestamped-graph
|= =graph:store
%+ gas:orm *graph:store
%+ turn
(tap:orm graph)
|= [=atom =node:store]
^- [^atom node:store]
:- (maybe-unix-to-da atom)
(convert-unix-timestamped-node node)
--
::
++ on-watch
~/ %graph-store-watch
@ -68,6 +177,7 @@
^- (quip card _state)
|^
?> ?=(%0 -.update)
=? p.update =(p.update *time) now.bowl
?- -.q.update
%add-graph (add-graph +.q.update)
%remove-graph (remove-graph +.q.update)
@ -102,7 +212,7 @@
:~ (give [/updates /keys ~] [%add-graph resource graph mark])
?~ mark ~
?: (~(has in validators) u.mark) ~
=/ wire (weld /graph (en-path:res resource))
=/ wire /validator/[u.mark]
=/ =rave:clay [%sing %b [%da now.bowl] /[u.mark]]
[%pass wire %arvo %c %warp our.bowl [%home `rave]]~
==
@ -521,7 +631,7 @@
=/ =ship (slav %p i.t.t.path)
=/ =term i.t.t.t.path
=/ =index:store
(turn t.t.t.t.path |=(=cord (slav %ud cord)))
(turn t.t.t.t.path (cury slav %ud))
=/ node=(unit node:store) (get-node ship term index)
?~ node [~ ~]
:- ~ :- ~ :- %graph-update
@ -608,15 +718,15 @@
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?+ -.sign-arvo (on-arvo:def wire sign-arvo)
%c
?+ wire (on-arvo:def wire sign-arvo)
::
:: old wire, do nothing
[%graph *] [~ this]
::
[%validator @ ~]
:_ this
?> ?=([%graph @ *] wire)
=/ =resource:store (de-path:res t.wire)
=/ gra=(unit marked-graph:store) (~(get by graphs) resource)
?~ gra ~
?~ q.u.gra ~
=/ =rave:clay [%next %b [%da now.bowl] /[u.q.u.gra]]
=* validator i.t.wire
=/ =rave:clay [%next %b [%da now.bowl] /[validator]]
[%pass wire %arvo %c %warp our.bowl [%home `rave]]~
==
::

View File

@ -45,7 +45,7 @@
=/ old !<(versioned-state old-vase)
?: ?=(%1 -.old)
`this(state old)
:- =- [%pass / %agent [our.bowl %invite-store] %poke %invite-action !>(-)]~
:- =- [%pass / %agent [our.bowl %invite-store] %poke %invite-action -]~
!> ^- action:store
[%create %graph]
%= this

View File

@ -24,6 +24,6 @@
<div id="portal-root"></div>
<script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.js"></script>
<script src="/~landscape/js/bundle/index.5fd962a0b23fc798e999.js"></script>
<script src="/~landscape/js/bundle/index.acc7ad05e12f266ea24c.js"></script>
</body>
</html>

View File

@ -10,7 +10,7 @@
:: encode group-path and app-path using (scot %t (spat group-path))
::
:: +watch paths:
:: /all assocations + updates
:: /all associations + updates
:: /updates just updates
:: /app-name/%app-name specific app's associations + updates
::
@ -57,6 +57,7 @@
+$ state-3 [%3 base-state-1]
+$ state-4 [%4 base-state-1]
+$ state-5 [%5 base-state-1]
+$ state-6 [%6 base-state-1]
+$ versioned-state
$% state-0
state-1
@ -64,10 +65,11 @@
state-3
state-4
state-5
state-6
==
--
::
=| state-5
=| state-6
=* state -
%+ verb |
%- agent:dbug
@ -86,29 +88,37 @@
=/ old !<(versioned-state vase)
=| cards=(list card)
|^
?: ?=(%5 -.old)
?: ?=(%6 -.old)
[cards this(state old)]
?: ?=(%4 -.old)
%_ $
-.old %5
::
group-indices.old
%- ~(gas ju *(jug group-path md-resource))
~(tap in ~(key by associations.old))
::
app-indices.old
%- ~(gas ju *(jug app-name [group-path app-path]))
%+ turn ~(tap in ~(key by associations.old))
|= [g=group-path r=md-resource]
^- [app-name [group-path app-path]]
[app-name.r [g app-path.r]]
?: ?=(%5 -.old)
=/ =^associations
(migrate-app-to-graph-store %publish associations.old)
%_ $
-.old %6
associations.old associations
::
resource-indices.old
%- ~(gas ju *(jug md-resource group-path))
%+ turn ~(tap in ~(key by associations.old))
|= [g=group-path r=md-resource]
^- [md-resource group-path]
[r g]
(rebuild-resource-indices associations)
::
app-indices.old
(rebuild-app-indices associations)
::
group-indices.old
(rebuild-group-indices associations)
==
?: ?=(%4 -.old)
%_ $
-.old %5
::
resource-indices.old
(rebuild-resource-indices associations.old)
::
app-indices.old
(rebuild-app-indices associations.old)
::
group-indices.old
(rebuild-group-indices associations.old)
==
?: ?=(%3 -.old)
$(old [%4 +.old])
@ -147,6 +157,43 @@
==
$(old new-state-1)
::
++ rebuild-resource-indices
|= =^associations
%- ~(gas ju *(jug md-resource group-path))
%+ turn ~(tap in ~(key by associations))
|= [g=group-path r=md-resource]
^- [md-resource group-path]
[r g]
::
++ rebuild-group-indices
|= =^associations
%- ~(gas ju *(jug group-path md-resource))
~(tap in ~(key by associations))
::
++ rebuild-app-indices
|= =^associations
%- ~(gas ju *(jug app-name [group-path app-path]))
%+ turn ~(tap in ~(key by associations))
|= [g=group-path r=md-resource]
^- [app-name [group-path app-path]]
[app-name.r [g app-path.r]]
::
++ migrate-app-to-graph-store
|= [app=@tas =^associations]
^+ associations
%- malt
%+ turn ~(tap by associations)
|= [[=group-path =md-resource] m=metadata]
^- [[^group-path ^md-resource] metadata]
?. =(app-name.md-resource app)
[[group-path md-resource] m]
=/ new-app-path=path
?. ?=([@ @ ~] app-path.md-resource)
app-path.md-resource
ship+app-path.md-resource
[[group-path [%graph new-app-path]] m(module app)]
::
++ poke-md-hook
|= act=metadata-hook-action
^- card

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
/- spider
/+ libstrand=strand, default-agent, verb, server
/+ libstrand=strand, default-agent, verb, server
=, strand=strand:libstrand
|%
+$ card card:agent:gall

View File

@ -302,9 +302,14 @@
++ node
%- ot
:~ [%post post]
:: TODO: support adding nodes with children by supporting the
:: graph key
[%children (of [%empty ul]~)]
[%children internal-graph]
==
::
++ internal-graph
^- $-(json ^internal-graph)
%- of
:~ [%empty ul]
[%graph graph]
==
::
++ post

View File

@ -1,240 +1,4 @@
/- sur=publish
/+ elem-to-react-json
^?
=< [. sur]
=, sur
|%
::
++ enjs
=, enjs:format
|%
::
++ tang
|= tan=^tang
%- wall
%- zing
%+ turn tan
|= a=^tank
(wash [0 80] a)
::
++ note-build
|= build=(each manx ^tang)
^- json
?: ?=(%.y -.build)
%- pairs
:~ success+b+%.y
result+(elem-to-react-json p.build)
==
%- pairs
:~ success+b+%.n
result+(tang p.build)
==
::
++ notebooks-list
|= [our=@p books=(map @tas notebook) subs=(map [@p @tas] notebook)]
^- json
:- %a
%+ weld
%+ turn ~(tap by books)
|= [name=@tas book=notebook]
(notebook-short book)
%+ turn ~(tap by subs)
|= [[host=@p name=@tas] book=notebook]
(notebook-short book)
::
++ notebooks-map
|= [our=@p books=(map [@p @tas] notebook)]
^- json
=/ notebooks-map=json
%- ~(rep by books)
|= [[[host=@p book-name=@tas] book=notebook] out=json]
^- json
=/ host-ta (scot %p host)
?~ out
(frond host-ta (frond book-name (notebook-short book)))
?> ?=(%o -.out)
=/ books (~(get by p.out) host-ta)
?~ books
:- %o
(~(put by p.out) host-ta (frond book-name (notebook-short book)))
?> ?=(%o -.u.books)
=. p.u.books (~(put by p.u.books) book-name (notebook-short book))
:- %o
(~(put by p.out) host-ta u.books)
=? notebooks-map ?=(~ notebooks-map)
[%o ~]
notebooks-map
::
++ notebook-short
|= book=notebook
^- json
%- pairs
:~ title+s+title.book
date-created+(time date-created.book)
about+s+description.book
num-notes+(numb ~(wyt by notes.book))
num-unread+(numb (count-unread notes.book))
comments+b+comments.book
writers-group-path+s+(spat writers.book)
subscribers-group-path+s+(spat subscribers.book)
==
::
++ notebook-full
|= [host=@p book-name=@tas book=notebook]
^- json
%- pairs
:~ title+s+title.book
about+s+description.book
date-created+(time date-created.book)
num-notes+(numb ~(wyt by notes.book))
num-unread+(numb (count-unread notes.book))
notes-by-date+(notes-by-date notes.book)
comments+b+comments.book
writers-group-path+s+(spat writers.book)
subscribers-group-path+s+(spat subscribers.book)
==
::
++ note-presentation
|= [book=notebook note-name=@tas not=note]
^- (map @t json)
=/ notes-list=(list [@tas note])
%+ sort ~(tap by notes.book)
|= [[@tas n1=note] [@tas n2=note]]
(gte date-created.n1 date-created.n2)
=/ idx=@ (need (find [note-name not]~ notes-list))
=/ next=(unit [name=@tas not=note])
?: =(idx 0) ~
`(snag (dec idx) notes-list)
=/ prev=(unit [name=@tas not=note])
?: =(+(idx) (lent notes-list)) ~
`(snag +(idx) notes-list)
=/ current=json (note-full note-name not)
?> ?=(%o -.current)
=. p.current (~(put by p.current) %prev-note ?~(prev ~ s+name.u.prev))
=. p.current (~(put by p.current) %next-note ?~(next ~ s+name.u.next))
=/ notes=(list [@t json]) [note-name current]~
=? notes ?=(^ prev)
[[name.u.prev (note-short name.u.prev not.u.prev)] notes]
=? notes ?=(^ next)
[[name.u.next (note-short name.u.next not.u.next)] notes]
%- my
:~ notes+(pairs notes)
notes-by-date+a+(turn notes-list |=([name=@tas *] s+name))
==
::
++ note-full
|= [note-name=@tas =note]
^- json
%- pairs
:~ note-id+s+note-name
author+s+(scot %p author.note)
title+s+title.note
date-created+(time date-created.note)
snippet+s+snippet.note
file+s+file.note
num-comments+(numb ~(wyt by comments.note))
comments+(comments-page:enjs comments.note 0 50)
read+b+read.note
pending+b+pending.note
==
::
++ notes-by-date
|= notes=(map @tas note)
^- json
=/ notes-list=(list [@tas note])
%+ sort ~(tap by notes)
|= [[@tas n1=note] [@tas n2=note]]
(gte date-created.n1 date-created.n2)
:- %a
%+ turn notes-list
|= [name=@tas note]
^- json
[%s name]
::
++ note-short
|= [note-name=@tas =note]
^- json
%- pairs
:~ note-id+s+note-name
author+s+(scot %p author.note)
title+s+title.note
date-created+(time date-created.note)
num-comments+(numb ~(wyt by comments.note))
read+b+read.note
snippet+s+snippet.note
pending+b+pending.note
==
::
++ notes-page
|= [notes=(map @tas note) start=@ud length=@ud]
^- (map @t json)
=/ notes-list=(list [@tas note])
%+ sort ~(tap by notes)
|= [[@tas n1=note] [@tas n2=note]]
(gte date-created.n1 date-created.n2)
%- my
:~ notes-by-date+a+(turn notes-list |=([name=@tas *] s+name))
notes+o+(^notes-list (scag length (slag start notes-list)))
==
::
++ notes-list
|= notes=(list [@tas note])
^- (map @t json)
%+ roll notes
|= [[name=@tas not=note] out-map=(map @t json)]
^- (map @t json)
(~(put by out-map) name (note-short name not))
::
++ comments-page
|= [comments=(map @da ^comment) start=@ud end=@ud]
^- json
=/ coms=(list [@da ^comment])
%+ sort ~(tap by comments)
|= [[d1=@da ^comment] [d2=@da ^comment]]
(gte d1 d2)
%- comments-list
(scag end (slag start coms))
::
++ comments-list
|= comments=(list [@da ^comment])
^- json
:- %a
(turn comments comment)
::
++ comment
|= [date=@da com=^comment]
^- json
%+ frond
(scot %da date)
%- pairs
:~ author+s+(scot %p author.com)
date-created+(time date-created.com)
content+s+content.com
pending+b+pending.com
==
--
::
++ string-to-symbol
|= tap=tape
^- @tas
%- crip
%+ turn tap
|= a=@
?: ?| &((gte a 'a') (lte a 'z'))
&((gte a '0') (lte a '9'))
==
a
?: &((gte a 'A') (lte a 'Z'))
(add 32 a)
'-'
::
++ count-unread
|= notes=(map @tas note)
^- @ud
%- ~(rep by notes)
|= [[key=@tas val=note] count=@ud]
?: read.val
count
+(count)
::
--
sur

View File

@ -0,0 +1,44 @@
/- *post
|_ i=indexed-post
++ grow
|%
++ noun i
--
++ grab
|%
:: +noun: Validate publish post
::
++ noun
|= p=*
=/ ip ;;(indexed-post p)
?+ index.p.ip !!
:: container for revisions
::
[@ %1 ~]
?> ?=(~ contents.p.ip)
ip
:: specific revision
:: first content is the title
:: revisions are numbered by the revision count
:: starting at one
[@ %1 @ ~]
?> ?=([* * *] contents.p.ip)
?> ?=(%text -.i.contents.p.ip)
ip
:: container for comments
[@ %2 ~]
?> ?=(~ contents.p.ip)
ip
:: comment
[@ %2 @ ~]
?> ?=(^ contents.p.ip)
ip
:: top level post must have no content
[@ ~]
?> ?=(~ contents.p.ip)
ip
==
--
::
++ grad %noun
--

View File

@ -1,6 +1,6 @@
::
:::: /hoon/action/publish/mar
::
:: tombstoned, now unused
/- *publish
=, format
::
@ -16,121 +16,5 @@
++ grab
|%
++ noun action
++ json
|= jon=^json
=, dejs:format
;; action
|^ %. jon
%- of
:~ new-book+new-book
new-note+new-note
new-comment+new-comment
edit-book+edit-book
edit-note+edit-note
edit-comment+edit-comment
del-book+del-book
del-note+del-note
del-comment+del-comment
subscribe+subscribe
unsubscribe+unsubscribe
read+read
groupify+groupify
==
::
++ new-book
%- ot
:~ book+so
title+so
about+so
coms+bo
group+group-info
==
::
++ new-note
%- ot
:~ who+(su fed:ag)
book+so
note+so
title+so
body+so
==
::
++ new-comment
%- ot
:~ who+(su fed:ag)
book+so
note+so
body+so
==
::
++ edit-book
%- ot
:~ book+so
title+so
about+so
coms+bo
group+(mu group-info)
==
::
++ edit-note
%- ot
:~ who+(su fed:ag)
book+so
note+so
title+so
body+so
==
::
++ edit-comment
%- ot
:~ who+(su fed:ag)
book+so
note+so
comment+so
body+so
==
::
++ del-book (ot book+so ~)
::
++ del-note (ot who+(su fed:ag) book+so note+so ~)
::
++ del-comment
%- ot
:~ who+(su fed:ag)
book+so
note+so
comment+so
==
++ subscribe
%- ot
:~ who+(su fed:ag)
book+so
==
++ unsubscribe
%- ot
:~ who+(su fed:ag)
book+so
==
++ read
%- ot
:~ who+(su fed:ag)
book+so
note+so
==
++ groupify
%- ot
:~ book+so
target+(mu pa)
inclusive+bo
==
++ group-info
%- ot
:~ group-path+pa
invitees+set-ship
use-preexisting+bo
make-managed+bo
==
++ set-ship (as (su fed:ag))
--
--
--

View File

@ -1,25 +1,12 @@
::
:::: /hoon/info/publish/mar
:: tombstoned, now unused
::
/- *publish
!:
|_ info=notebook-info
::
::
++ grow
|%
++ mime
:- /text/x-publish-info
(as-octs:mimes:html (of-wain:format txt))
++ txt
^- wain
:~ (cat 3 'title: ' title.info)
(cat 3 'description: ' description.info)
(cat 3 'comments: ' ?:(comments.info 'on' 'off'))
(cat 3 'writers: ' (spat writers.info))
(cat 3 'subscribers: ' (spat subscribers.info))
==
--
++ grab
|%
++ mime

View File

@ -13,73 +13,5 @@
++ grow
|%
++ noun del
++ json
%+ frond:enjs:format %publish-update
%+ frond:enjs:format -.del
?- -.del
%add-book
%+ frond:enjs:format (scot %p host.del)
%+ frond:enjs:format book.del
(notebook-short:enjs data.del)
::
%add-note
%+ frond:enjs:format (scot %p host.del)
%+ frond:enjs:format book.del
(note-full:enjs note.del data.del)
::
%add-comment
%- pairs:enjs:format
:~ host+s+(scot %p host.del)
book+s+book.del
note+s+note.del
comment+(comment:enjs comment-date.del data.del)
==
::
%edit-book
%+ frond:enjs:format (scot %p host.del)
%+ frond:enjs:format book.del
(notebook-short:enjs data.del)
::
%edit-note
%+ frond:enjs:format (scot %p host.del)
%+ frond:enjs:format book.del
(note-full:enjs note.del data.del)
::
%edit-comment
%- pairs:enjs:format
:~ host+s+(scot %p host.del)
book+s+book.del
note+s+note.del
comment+(comment:enjs comment-date.del data.del)
==
::
%del-book
%- pairs:enjs:format
:~ host+s+(scot %p host.del)
book+s+book.del
==
::
%del-note
%- pairs:enjs:format
:~ host+s+(scot %p host.del)
book+s+book.del
note+s+note.del
==
::
%del-comment
%- pairs:enjs:format
:~ host+s+(scot %p host.del)
book+s+book.del
note+s+note.del
comment+s+(scot %da comment.del)
==
::
%read
%- pairs:enjs:format
:~ host+s+(scot %p who.del)
book+s+book.del
note+s+note.del
==
==
--
--

View File

@ -77,10 +77,10 @@
::
:: Send invites
::
?: ?=(%group -.associated)
?: ?=(%group -.associated.action)
(pure:m !>(~))
?- -.policy.associated.action
%group (pure:m !>(~))
%open (pure:m !>(~))
%invite
=/ inv-action=action:inv
:^ %invites %graph (shaf %graph-uid eny.bowl)

View File

@ -41,6 +41,15 @@
(poke-our %graph-store %graph-update !>([%0 now.bowl %remove-graph rid]))
;< ~ bind:m
(poke-our %graph-push-hook %push-hook-action !>([%remove rid]))
;< ~ bind:m
%+ poke-our %metadata-hook
metadata-hook-action+!>([%remove (en-path:resource rid)])
;< ~ bind:m
%+ poke-our %metadata-store
:- %metadata-action
!> :+ %remove
(en-path:resource rid)
[%graph (en-path:resource rid)]
(pure:m ~)
--
::

View File

@ -1693,8 +1693,9 @@
"integrity": "sha512-3OPSdf9cejP/TSzWXuBaYbzLtAfBzQnc75SlPLkoPfwpxnv1Bvy9hiWngLY0WnKRR6lMOldnkYQCCuNWeDibYQ=="
},
"@tlon/indigo-react": {
"version": "github:urbit/indigo-react#a9ad1e2ca3c318b7455ed942d288340400e2481d",
"from": "github:urbit/indigo-react#lf/1.2.9",
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/@tlon/indigo-react/-/indigo-react-1.2.12.tgz",
"integrity": "sha512-KBsWHYKoYTkoOgzlGyIlla9WMBxXJ58NM/cDapNaHvBnb3M9jkVrH++9CcRRqkFYqLs8tHHvBaEDmwIjQzpqng==",
"requires": {
"@reach/menu-button": "^0.10.5",
"react": "^16.13.1",
@ -1702,9 +1703,9 @@
},
"dependencies": {
"tslib": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.2.tgz",
"integrity": "sha512-wAH28hcEKwna96/UacuWaVspVLkg4x1aDM9JlzqaQTOFczCktkVAb5fmXChgandR1EraDPs2w8P+ozM+oafwxg=="
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.3.tgz",
"integrity": "sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ=="
}
}
},
@ -10117,7 +10118,8 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@ -10138,12 +10140,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -10158,17 +10162,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -10285,7 +10292,8 @@
"inherits": {
"version": "2.0.4",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -10297,6 +10305,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -10311,6 +10320,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -10318,12 +10328,14 @@
"minimist": {
"version": "1.2.5",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"minipass": {
"version": "2.9.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -10342,6 +10354,7 @@
"version": "0.5.3",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "^1.2.5"
}
@ -10403,7 +10416,8 @@
"npm-normalize-package-bin": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"npm-packlist": {
"version": "1.4.8",
@ -10431,7 +10445,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -10443,6 +10458,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -10520,7 +10536,8 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@ -10556,6 +10573,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -10575,6 +10593,7 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -10618,12 +10637,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"yallist": {
"version": "3.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
}
}
},
@ -11104,7 +11125,8 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@ -11125,12 +11147,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -11145,17 +11169,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -11272,7 +11299,8 @@
"inherits": {
"version": "2.0.4",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -11284,6 +11312,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -11298,6 +11327,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -11305,12 +11335,14 @@
"minimist": {
"version": "1.2.5",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"minipass": {
"version": "2.9.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -11329,6 +11361,7 @@
"version": "0.5.3",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "^1.2.5"
}
@ -11390,7 +11423,8 @@
"npm-normalize-package-bin": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"npm-packlist": {
"version": "1.4.8",
@ -11418,7 +11452,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -11430,6 +11465,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -11507,7 +11543,8 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@ -11543,6 +11580,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -11562,6 +11600,7 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -11605,12 +11644,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"yallist": {
"version": "3.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
}
}
},

View File

@ -9,7 +9,7 @@
"@reach/menu-button": "^0.10.5",
"@reach/tabs": "^0.10.5",
"@tlon/indigo-light": "^1.0.3",
"@tlon/indigo-react": "urbit/indigo-react#lf/1.2.9",
"@tlon/indigo-react": "1.2.12",
"@tlon/sigil-js": "^1.4.2",
"aws-sdk": "^2.726.0",
"big-integer": "^1.6.48",

View File

@ -9,7 +9,6 @@ import MetadataApi from './metadata';
import ContactsApi from './contacts';
import GroupsApi from './groups';
import LaunchApi from './launch';
import PublishApi from './publish';
import GraphApi from './graph';
import S3Api from './s3';
import {HarkApi} from './hark';
@ -22,12 +21,10 @@ export default class GlobalApi extends BaseApi<StoreState> {
contacts = new ContactsApi(this.ship, this.channel, this.store);
groups = new GroupsApi(this.ship, this.channel, this.store);
launch = new LaunchApi(this.ship, this.channel, this.store);
publish = new PublishApi(this.ship, this.channel, this.store);
s3 = new S3Api(this.ship, this.channel, this.store);
graph = new GraphApi(this.ship, this.channel, this.store);
hark = new HarkApi(this.ship, this.channel, this.store);
constructor(
public ship: Patp,
public channel: any,

View File

@ -3,13 +3,13 @@ import { StoreState } from '../store/type';
import { Patp, Path, PatpNoSig } from '~/types/noun';
import _ from 'lodash';
import {makeResource, resourceFromPath} from '../lib/group';
import {GroupPolicy, Enc, Post} from '~/types';
import { deSig } from '~/logic/lib/util';
import {GroupPolicy, Enc, Post, NodeMap} from '~/types';
import { numToUd, unixToDa } from '~/logic/lib/util';
export const createPost = (contents: Object[], parentIndex: string = '') => {
return {
author: `~${window.ship}`,
index: parentIndex + '/' + Date.now(),
index: parentIndex + '/' + unixToDa(Date.now()).toString(),
'time-sent': Date.now(),
contents,
hash: null,
@ -17,6 +17,16 @@ export const createPost = (contents: Object[], parentIndex: string = '') => {
};
};
function moduleToMark(mod: string): string | undefined {
if(mod === 'link') {
return 'graph-validator-link';
}
if(mod === 'publish') {
return 'graph-validator-publish';
}
return undefined;
}
export default class GraphApi extends BaseApi<StoreState> {
private storeAction(action: any): Promise<any> {
@ -47,7 +57,8 @@ export default class GraphApi extends BaseApi<StoreState> {
title,
description,
associated,
"module": mod
"module": mod,
mark: moduleToMark(mod)
}
});
}
@ -67,7 +78,8 @@ export default class GraphApi extends BaseApi<StoreState> {
title,
description,
associated: { policy },
"module": mod
"module": mod,
mark: moduleToMark(mod)
}
});
}
@ -139,7 +151,7 @@ export default class GraphApi extends BaseApi<StoreState> {
}
addNodes(ship: Patp, name: string, nodes: Object) {
this.hookAction(ship, {
return this.hookAction(ship, {
'add-nodes': {
resource: { ship, name },
nodes
@ -204,9 +216,10 @@ export default class GraphApi extends BaseApi<StoreState> {
}
getNode(ship: string, resource: string, index: string) {
const idx = index.split('/').map(numToUd).join('/');
return this.scry<any>(
'graph-store',
`/node/${ship}/${resource}/${index}`
`/node/${ship}/${resource}${idx}`
).then((node) => {
this.store.handleEvent({
data: node

View File

@ -1,224 +0,0 @@
import BaseApi from './base';
import { PublishResponse } from '~/types/publish-response';
import { PatpNoSig, Path } from '~/types/noun';
import { BookId, NoteId } from '~/types/publish-update';
export default class PublishApi extends BaseApi {
handleEvent(data: PublishResponse) {
this.store.handleEvent({ data: { 'publish-response' : data } });
}
fetchNotebooks() {
return fetch('/publish-view/notebooks.json')
.then(response => response.json())
.then((json) => {
this.handleEvent({
type: 'notebooks',
data: json
});
});
}
fetchNotebook(host: PatpNoSig, book: BookId) {
return fetch(`/publish-view/${host}/${book}.json`)
.then(response => response.json())
.then((json) => {
this.handleEvent({
type: 'notebook',
data: json,
host: host,
notebook: book
});
});
}
fetchNote(host: PatpNoSig, book: BookId, note: NoteId) {
return fetch(`/publish-view/${host}/${book}/${note}.json`)
.then(response => response.json())
.then((json) => {
this.handleEvent({
type: 'note',
data: json,
host: host,
notebook: book,
note: note
});
});
}
fetchNotesPage(host: PatpNoSig, book: BookId, start: number, length: number) {
return fetch(`/publish-view/notes/${host}/${book}/${start}/${length}.json`)
.then(response => response.json())
.then((json) => {
this.handleEvent({
type: 'notes-page',
data: json,
host: host,
notebook: book,
startIndex: start,
length: length
});
});
}
fetchCommentsPage(host: PatpNoSig, book: BookId, note: NoteId, start: number, length: number) {
return fetch(`/publish-view/comments/${host}/${book}/${note}/${start}/${length}.json`)
.then(response => response.json())
.then((json) => {
this.handleEvent({
type: 'comments-page',
data: json,
host: host,
notebook: book,
note: note,
startIndex: start,
length: length
});
});
}
subscribeNotebook(who: PatpNoSig, book: BookId) {
return this.publishAction({
subscribe: {
who,
book
}
});
}
unsubscribeNotebook(who: PatpNoSig, book: BookId) {
return this.publishAction({
unsubscribe: {
who,
book
}
});
}
publishAction(act: any) {
return this.action('publish', 'publish-action', act);
}
groupify(bookId: string, group: Path | null) {
return this.publishAction({
groupify: {
book: bookId,
target: group,
inclusive: false
}
});
}
newBook(bookId: string, title: string, description: string, group?: Path) {
const groupInfo = group ? { 'group-path': group,
invitees: [],
'use-preexisting': true,
'make-managed': true
} : {
'group-path': `/ship/~${window.ship}/${bookId}`,
invitees: [],
'use-preexisting': false,
'make-managed': false
};
return this.publishAction({
"new-book": {
book: bookId,
title: title,
about: description,
coms: true,
group: groupInfo
}
});
}
editBook(bookId: string, title: string, description: string, coms: boolean) {
return this.publishAction({
"edit-book": {
book: bookId,
title: title,
about: description,
coms,
group: null
}
});
}
delBook(book: string) {
return this.publishAction({
"del-book": {
book
}
});
}
newNote(who: PatpNoSig, book: string, note: string, title: string, body: string) {
return this.publishAction({
'new-note': {
who,
book,
note,
title,
body
}
});
}
editNote(who: PatpNoSig, book: string, note: string, title: string, body: string) {
return this.publishAction({
'edit-note': {
who,
book,
note,
title,
body
}
});
}
delNote(who: PatpNoSig, book: string, note: string) {
return this.publishAction({
'del-note': {
who,
book,
note
}
});
}
readNote(who: PatpNoSig, book: string, note: string) {
return this.publishAction({
read: {
who,
book,
note
}
});
}
updateComment(who: PatpNoSig, book: string, note: string, comment: Path, body: string) {
return this.publishAction({
'edit-comment': {
who,
book,
note,
comment,
body
}
});
}
deleteComment(who: PatpNoSig, book: string, note: string, comment: Path ) {
return this.publishAction({
"del-comment": {
who,
book,
note,
comment
},
});
}
}

View File

@ -14,8 +14,13 @@ type MapNode<V> = NonemptyNode<V> | null;
*/
export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
private root: MapNode<V> = null;
size: number = 0;
constructor() {}
constructor(initial: [BigInteger, V][] = []) {
initial.forEach(([key, val]) => {
this.set(key, val);
});
}
/**
* Retrieve an value for a key
@ -54,6 +59,7 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
}
const [k] = node.n;
if (key.eq(k)) {
this.size--;
return {
...node,
n: [k, value],
@ -76,6 +82,7 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
return { ...node, r };
};
this.size++;
this.root = inner(this.root);
}
@ -141,6 +148,9 @@ export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
];
};
const [ret, newRoot] = inner(this.root);
if(ret) {
this.size--;
}
this.root = newRoot;
return ret;
}

View File

@ -6,7 +6,6 @@ export class OrderedMap<V> extends Map<number, V>
const sorted = Array.from(super[Symbol.iterator]()).sort(
([a], [b]) => b - a
);
let index = 0;
return {
[Symbol.iterator]: this[Symbol.iterator],

View File

@ -0,0 +1,18 @@
import { Post, GraphNode } from "~/types";
export const buntPost = (): Post => ({
author: '',
contents: [],
hash: null,
index: '',
signatures: [],
'time-sent': 0
});
export function makeNodeMap(posts: Post[]): Record<string, GraphNode> {
let nodes = {};
posts.forEach((p) => {
nodes[p.index] = { children: { empty: null }, post: p };
});
return nodes;
}

View File

@ -0,0 +1,105 @@
import { Post, GraphNode, TextContent, Graph, NodeMap } from "~/types";
import { buntPost } from '~/logic/lib/post';
import { unixToDa } from "~/logic/lib/util";
import {BigIntOrderedMap} from "./BigIntOrderedMap";
import bigInt, {BigInteger} from 'big-integer';
export function newPost(
title: string,
body: string
): [BigInteger, NodeMap] {
const now = Date.now();
const nowDa = unixToDa(now);
const root: Post = {
author: `~${window.ship}`,
index: "/" + nowDa.toString(),
"time-sent": now,
contents: [],
hash: null,
signatures: [],
};
const revContainer: Post = { ...root, index: root.index + "/1" };
const commentsContainer = { ...root, index: root.index + "/2" };
const firstRevision: Post = {
...revContainer,
index: revContainer.index + "/1",
contents: [{ text: title }, { text: body }],
};
const nodes = {
[root.index]: {
post: root,
children: {
graph: {
1: {
post: revContainer,
children: {
graph: {
1: {
post: firstRevision,
children: { empty: null },
},
},
},
},
2: {
post: commentsContainer,
children: { empty: null },
},
},
},
},
};
return [nowDa, nodes];
}
export function editPost(rev: number, noteId: BigInteger, title: string, body: string) {
const now = Date.now();
const newRev: Post = {
author: `~${window.ship}`,
index: `/${noteId.toString()}/1/${rev}`,
"time-sent": now,
contents: [{ text: title }, { text: body }],
hash: null,
signatures: [],
};
const nodes = {
[newRev.index]: {
post: newRev,
children: { empty: null }
}
};
return nodes;
}
export function getLatestRevision(node: GraphNode): [number, string, string, Post] {
const revs = node.children.get(bigInt(1));
const empty = [1, "", "", buntPost()] as [number, string, string, Post];
if(!revs) {
return empty;
}
const [revNum, rev] = [...revs.children][0];
if(!rev) {
return empty
}
const [title, body] = rev.post.contents as TextContent[];
return [revNum, title.text, body.text, rev.post];
}
export function getComments(node: GraphNode): GraphNode {
const comments = node.children.get(bigInt(2));
if(!comments) {
return { post: buntPost(), children: new BigIntOrderedMap() }
}
return comments;
}
export function getSnippet(body: string) {
const start = body.slice(0, 400);
return start === body ? start : `${start}...`;
}

View File

@ -1,3 +1,4 @@
import { useEffect } from 'react';
import _ from "lodash";
import f from "lodash/fp";
import bigInt, { BigInteger } from "big-integer";
@ -13,6 +14,10 @@ export const MOMENT_CALENDAR_DATE = {
sameElse: "DD/MM/YYYY",
};
export function appIsGraph(app: string) {
return app === 'publish' || app == 'link';
}
export function parentPath(path: string) {
return _.dropRight(path.split('/'), 1).join('/');
}
@ -29,6 +34,11 @@ export function daToUnix(da: BigInteger) {
);
}
export function unixToDa(unix: number) {
const timeSinceEpoch = bigInt(unix).multiply(DA_SECOND).divide(bigInt(1000));
return DA_UNIX_EPOCH.add(timeSinceEpoch);
}
export function makePatDa(patda: string) {
return bigInt(udToDec(patda));
}
@ -311,3 +321,36 @@ export function stringToSymbol(str: string) {
}
return result;
}
/**
* Formats a numbers as a `@ud` inserting dot where needed
*/
export function numToUd(num: number) {
return f.flow(
f.split(''),
f.reverse,
f.chunk(3),
f.reverse,
f.map(s => s.join('')),
f.join('.')
)(num.toString())
}
export function usePreventWindowUnload(shouldPreventDefault: boolean, message = "You have unsaved changes. Are you sure you want to exit?") {
useEffect(() => {
if (!shouldPreventDefault) return;
const handleBeforeUnload = event => {
event.preventDefault();
return message;
}
window.addEventListener("beforeunload", handleBeforeUnload);
window.onbeforeunload = handleBeforeUnload;
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
// @ts-ignore
window.onbeforeunload = undefined;
}
}, [shouldPreventDefault]);
}

View File

@ -1,16 +1,6 @@
import _ from 'lodash';
import { OrderedMap } from "~/logic/lib/OrderedMap";
const DA_UNIX_EPOCH = 170141184475152167957503069145530368000;
const normalizeKey = (key) => {
if(key > DA_UNIX_EPOCH) {
// new links uses milliseconds since unix epoch
// old (pre-graph-store) use @da
// ported from +time:enjs:format in hoon.hoon
return Math.round((1000 * (9223372036854775 + (key - DA_UNIX_EPOCH))) / 18446744073709551616);
}
return key;
}
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
import bigInt, { BigInteger } from "big-integer";
export const GraphReducer = (json, state) => {
const data = _.get(json, 'graph-update', false);
@ -38,33 +28,26 @@ const addGraph = (json, state) => {
const _processNode = (node) => {
// is empty
if (!node.children) {
node.children = new OrderedMap();
node.post.originalIndex = node.post.index;
node.post.index = node.post.index.split('/').map(x => x.length === 0 ? '' : normalizeKey(parseInt(x, 10))).join('/');
node.children = new BigIntOrderedMap();
return node;
}
// is graph
let converted = new OrderedMap();
let converted = new BigIntOrderedMap();
for (let i in node.children) {
let item = node.children[i];
let index = item[0].split('/').slice(1).map((ind) => {
return parseInt(ind, 10);
return bigInt(ind);
});
if (index.length === 0) { break; }
const normalKey = normalizeKey(index[index.length - 1]);
item[1].post.originalKey = index[index.length - 1];
converted.set(
normalKey,
index[index.length - 1],
_processNode(item[1])
);
}
node.children = converted;
node.post.originalIndex = node.post.index;
node.post.index = node.post.index.split('/').map(x => x.length === 0 ? '' : normalizeKey(parseInt(x, 10))).join('/');
return node;
};
@ -75,21 +58,22 @@ const addGraph = (json, state) => {
}
let resource = data.resource.ship + '/' + data.resource.name;
state.graphs[resource] = new OrderedMap();
state.graphs[resource] = new BigIntOrderedMap();
for (let i in data.graph) {
let item = data.graph[i];
let index = item[0].split('/').slice(1).map((ind) => {
return parseInt(ind, 10);
return bigInt(ind);
});
if (index.length === 0) { break; }
let node = _processNode(item[1]);
const normalKey = normalizeKey(index[index.length - 1])
node.post.originalKey = index[index.length - 1];
state.graphs[resource].set(normalKey, node);
state.graphs[resource].set(
index[index.length - 1],
node
);
}
state.graphKeys.add(resource);
}
@ -102,16 +86,16 @@ const removeGraph = (json, state) => {
if (!('graphs' in state)) {
state.graphs = {};
}
let resource = data.resource.ship + '/' + data.resource.name;
let resource = data.ship + '/' + data.name;
delete state.graphs[resource];
}
};
const mapifyChildren = (children) => {
return new OrderedMap(
return new BigIntOrderedMap(
children.map(([idx, node]) => {
const nd = {...node, children: mapifyChildren(node.children || []) };
return [normalizeKey(parseInt(idx.slice(1), 10)), nd];
return [bigInt(idx.slice(1)), nd];
}));
};
@ -119,23 +103,18 @@ const addNodes = (json, state) => {
const _addNode = (graph, index, node) => {
// set child of graph
if (index.length === 1) {
node.post.originalIndex = node.post.index;
node.post.index = node.post.index.split('/').map(x => x.length === 0 ? '' : normalizeKey(parseInt(x, 10))).join('/');
const normalKey = normalizeKey(index[0])
node.post.originalKey = index[0];
graph.set(normalKey, node);
graph.set(index[0], node);
return graph;
}
// set parent of graph
let parNode = graph.get(normalizeKey(index[0]));
let parNode = graph.get(index[0]);
if (!parNode) {
console.error('parent node does not exist, cannot add child');
return;
}
parNode.children = _addNode(parNode.children, index.slice(1), node);
graph.set(normalizeKey(index[0]), parNode);
graph.set(index[0], parNode);
return graph;
};
@ -151,7 +130,7 @@ const addNodes = (json, state) => {
if (item[0].split('/').length === 0) { return; }
let index = item[0].split('/').slice(1).map((ind) => {
return parseInt(ind, 10);
return bigInt(ind);
});
if (index.length === 0) { return; }
@ -174,9 +153,9 @@ const removeNodes = (json, state) => {
if (index.length === 1) {
graph.delete(index[0]);
} else {
const child = graph.get(normalizeKey(index[0]));
const child = graph.get(index[0]);
_remove(child.children, index.slice(1));
graph.set(normalizeKey(index[0]), child);
graph.set(index[0], child);
}
};
const data = _.get(json, 'remove-nodes', false);
@ -188,7 +167,7 @@ const removeNodes = (json, state) => {
data.indices.forEach((index) => {
if (index.split('/').length === 0) { return; }
let indexArr = index.split('/').slice(1).map((ind) => {
return parseInt(ind, 10);
return bigInt(ind);
});
_remove(state.graphs[res], indexArr);
});

View File

@ -103,7 +103,7 @@ export default class GroupReducer<S extends GroupState> {
const resourcePath = resourceAsPath(resource);
state.groups[resourcePath] = {
members: new Set(),
tags: { role: {} },
tags: { role: { admin: new Set([window.ship]) } },
policy: decodePolicy(policy),
hidden,
};

View File

@ -1,205 +0,0 @@
import _ from 'lodash';
import { StoreState } from '../../store/type';
import { Cage } from '~/types/cage';
type PublishState = Pick<StoreState, 'notebooks'>;
export default class PublishResponseReducer<S extends PublishState> {
reduce(json: Cage, state: S) {
const data = _.get(json, 'publish-response', false);
if (!data) { return; }
switch(data.type) {
case "notebooks":
this.handleNotebooks(data, state);
break;
case "notebook":
this.handleNotebook(data, state);
break;
case "note":
this.handleNote(data, state);
break;
case "notes-page":
this.handleNotesPage(data, state);
break;
case "comments-page":
this.handleCommentsPage(data, state);
break;
default:
break;
}
}
handleNotebooks(json, state) {
for (var host in state.notebooks) {
if (json.data[host]) {
for (var book in state.notebooks[host]) {
if (!json.data[host][book]) {
delete state.notebooks[host][book];
}
}
} else {
delete state.notebooks[host];
}
}
for (var host in json.data) {
if (state.notebooks[host]) {
for (var book in json.data[host]) {
if (state.notebooks[host][book]) {
state.notebooks[host][book]["title"] = json.data[host][book]["title"];
state.notebooks[host][book]["date-created"] =
json.data[host][book]["date-created"];
state.notebooks[host][book]["num-notes"] =
json.data[host][book]["num-notes"];
state.notebooks[host][book]["num-unread"] =
json.data[host][book]["num-unread"];
} else {
state.notebooks[host][book] = json.data[host][book];
}
}
} else {
state.notebooks[host] = json.data[host];
}
}
}
handleNotebook(json, state) {
if (state.notebooks[json.host]) {
if (state.notebooks[json.host][json.notebook]) {
state.notebooks[json.host][json.notebook]["notes-by-date"] =
json.data.notebook["notes-by-date"];
state.notebooks[json.host][json.notebook].subscribers =
json.data.notebook.subscribers;
state.notebooks[json.host][json.notebook].writers =
json.data.notebook.writers;
state.notebooks[json.host][json.notebook].comments =
json.data.notebook.comments;
state.notebooks[json.host][json.notebook]["subscribers-group-path"] =
json.data.notebook["subscribers-group-path"];
state.notebooks[json.host][json.notebook]["writers-group-path"] =
json.data.notebook["writers-group-path"];
state.notebooks[json.host][json.notebook].about =
json.data.notebook.about;
if (state.notebooks[json.host][json.notebook].notes) {
for (var key in json.data.notebook.notes) {
let oldNote = state.notebooks[json.host][json.notebook].notes[key];
if (!(oldNote)) {
state.notebooks[json.host][json.notebook].notes[key] =
json.data.notebook.notes[key];
} else if (!(oldNote.build)) {
state.notebooks[json.host][json.notebook].notes[key]["author"] =
json.data.notebook.notes[key]["author"];
state.notebooks[json.host][json.notebook].notes[key]["date-created"] =
json.data.notebook.notes[key]["date-created"];
state.notebooks[json.host][json.notebook].notes[key]["note-id"] =
json.data.notebook.notes[key]["note-id"];
state.notebooks[json.host][json.notebook].notes[key]["num-comments"] =
json.data.notebook.notes[key]["num-comments"];
state.notebooks[json.host][json.notebook].notes[key]["title"] =
json.data.notebook.notes[key]["title"];
}
}
} else {
state.notebooks[json.host][json.notebook].notes =
json.data.notebook.notes;
}
} else {
state.notebooks[json.host][json.notebook] = json.data.notebook;
}
} else {
state.notebooks[json.host] = {[json.notebook]: json.data.notebook};
}
}
handleNote(json, state) {
if (state.notebooks[json.host] &&
state.notebooks[json.host][json.notebook]) {
state.notebooks[json.host][json.notebook]["notes-by-date"] =
json.data["notes-by-date"];
if (state.notebooks[json.host][json.notebook].notes) {
for (var key in json.data.notes) {
let oldNote = state.notebooks[json.host][json.notebook].notes[key];
if (!(oldNote && oldNote.build && key !== json.note)) {
state.notebooks[json.host][json.notebook].notes[key] =
json.data.notes[key];
}
}
} else {
state.notebooks[json.host][json.notebook].notes = json.data.notes;
}
} else {
throw Error("tried to fetch note, but we don't have the notebook");
}
}
handleNotesPage(json, state) {
if (state.notebooks[json.host] && state.notebooks[json.host][json.notebook]) {
state.notebooks[json.host][json.notebook]["notes-by-date"] =
json.data["notes-by-date"];
if (state.notebooks[json.host][json.notebook].notes) {
for (var key in json.data.notes) {
let oldNote = state.notebooks[json.host][json.notebook].notes[key];
if (!(oldNote)) {
state.notebooks[json.host][json.notebook].notes[key] =
json.data.notes[key];
} else if (!(oldNote.build)) {
state.notebooks[json.host][json.notebook].notes[key]["author"] =
json.data.notes[key]["author"];
state.notebooks[json.host][json.notebook].notes[key]["date-created"] =
json.data.notes[key]["date-created"];
state.notebooks[json.host][json.notebook].notes[key]["note-id"] =
json.data.notes[key]["note-id"];
state.notebooks[json.host][json.notebook].notes[key]["num-comments"] =
json.data.notes[key]["num-comments"];
state.notebooks[json.host][json.notebook].notes[key]["title"] =
json.data.notes[key]["title"];
}
}
} else {
state.notebooks[json.host][json.notebook].notes =
json.data.notes;
}
} else {
throw Error("tried to fetch paginated notes, but we don't have the notebook");
}
}
handleCommentsPage(json, state) {
if (state.notebooks[json.host] &&
state.notebooks[json.host][json.notebook] &&
state.notebooks[json.host][json.notebook].notes[json.note])
{
if (state.notebooks[json.host][json.notebook].notes[json.note].comments) {
json.data.forEach((val, i) => {
let newKey = Object.keys(val)[0];
let newDate = val[newKey]["date-created"]
let oldComments = state.notebooks[json.host][json.notebook].notes[json.note].comments;
let insertIdx = -1;
for (var j=0; j<oldComments.length; j++) {
let oldKey = Object.keys(oldComments[j])[0];
let oldDate = oldComments[j][oldKey]["date-created"];
if (oldDate === newDate) {
break;
} else if (oldDate < newDate) {
insertIdx = j;
} else if ((oldDate > newDate) &&
(j === oldComments.length-1)){
insertIdx = j+1;
}
}
if (insertIdx !== -1) {
state.notebooks[json.host][json.notebook].notes[json.note].comments
.splice(insertIdx, 0, val);
}
});
} else {
state.notebooks[json.host][json.notebook].notes[json.note].comments =
json.data;
}
} else {
throw Error("tried to fetch paginated comments, but we don't have the note");
}
}
}

View File

@ -1,269 +0,0 @@
import _ from 'lodash';
import { PublishUpdate } from '~/types/publish-update';
import { Cage } from '~/types/cage';
import { StoreState } from '../../store/type';
import { getTagFromFrond } from '~/types/noun';
type PublishState = Pick<StoreState, 'notebooks'>;
export default class PublishUpdateReducer<S extends PublishState> {
reduce(data: Cage, state: S){
let json = data["publish-update"];
if(!json) {
return;
}
const tag = getTagFromFrond(json);
switch(tag){
case "add-book":
this.addBook(json["add-book"], state);
break;
case "add-note":
this.addNote(json["add-note"], state);
break;
case "add-comment":
this.addComment(json["add-comment"], state);
break;
case "edit-book":
this.editBook(json["edit-book"], state);
break;
case "edit-note":
this.editNote(json["edit-note"], state);
break;
case "edit-comment":
this.editComment(json["edit-comment"], state);
break;
case "del-book":
this.delBook(json["del-book"], state);
break;
case "del-note":
this.delNote(json["del-note"], state);
break;
case "del-comment":
this.delComment(json["del-comment"], state);
break;
case "read":
this.read(json["read"], state);
break;
default:
break;
}
}
addBook(json, state: S) {
let host = Object.keys(json)[0];
let book = Object.keys(json[host])[0];
if (state.notebooks[host]) {
state.notebooks[host][book] = json[host][book];
} else {
state.notebooks[host] = json[host];
}
}
addNote(json, state: S) {
let host = Object.keys(json)[0];
let book = Object.keys(json[host])[0];
let noteId = json[host][book]["note-id"];
if (state.notebooks[host] && state.notebooks[host][book]) {
if (state.notebooks[host][book].notes) {
if (state.notebooks[host][book].notes[noteId] &&
state.notebooks[host][book].notes[noteId].pending)
{
state.notebooks[host][book].notes[noteId].pending = false;
return;
}
if (state.notebooks[host][book]["notes-by-date"]) {
state.notebooks[host][book]["notes-by-date"].unshift(noteId);
} else {
state.notebooks[host][book]["notes-by-date"] = [noteId];
}
state.notebooks[host][book].notes[noteId] = json[host][book];
} else {
state.notebooks[host][book].notes = {[noteId]: json[host][book]};
}
state.notebooks[host][book]["num-notes"] += 1;
if (!json[host][book].read) {
state.notebooks[host][book]["num-unread"] += 1;
}
let prevNoteId = state.notebooks[host][book]["notes-by-date"][1] || null;
state.notebooks[host][book].notes[noteId]["prev-note"] = prevNoteId
state.notebooks[host][book].notes[noteId]["next-note"] = null;
if (prevNoteId && state.notebooks[host][book].notes[prevNoteId]) {
state.notebooks[host][book].notes[prevNoteId]["next-note"] = noteId;
}
}
}
addComment(json, state: S) {
let host = json.host
let book = json.book
let note = json.note
let comment = json.comment;
if (state.notebooks[host] &&
state.notebooks[host][book] &&
state.notebooks[host][book].notes &&
state.notebooks[host][book].notes[note])
{
if (state.notebooks[host][book].notes[note].comments) {
let limboCommentIdx =
_.findIndex(state.notebooks[host][book].notes[note].comments, (o) => {
let oldVal = o[getTagFromFrond(o)];
let newVal = comment[Object.keys(comment)[0]];
return (oldVal.pending &&
(oldVal.author === newVal.author) &&
(oldVal.content === newVal.content)
);
});
if (limboCommentIdx === -1) {
state.notebooks[host][book].notes[note]["num-comments"] += 1;
state.notebooks[host][book].notes[note].comments.unshift(comment);
} else {
state.notebooks[host][book].notes[note].comments[limboCommentIdx] =
comment;
}
} else if (state.notebooks[host][book].notes[note]["num-comments"] === 1) {
state.notebooks[host][book].notes[note]["num-comments"] += 1;
state.notebooks[host][book].notes[note].comments = [comment];
}
}
}
editBook(json, state) {
let host = Object.keys(json)[0];
let book = Object.keys(json[host])[0];
if (state.notebooks[host] && state.notebooks[host][book]) {
state.notebooks[host][book]["comments"] = json[host][book]["comments"];
state.notebooks[host][book]["date-created"] = json[host][book]["date-created"];
state.notebooks[host][book]["num-notes"] = json[host][book]["num-notes"];
state.notebooks[host][book]["num-unread"] = json[host][book]["num-unread"];
state.notebooks[host][book]["title"] = json[host][book]["title"];
state.notebooks[host][book]["writers-group-path"] =
json[host][book]["writers-group-path"];
state.notebooks[host][book]["subscribers-group-path"] =
json[host][book]["subscribers-group-path"];
}
}
editNote(json, state) {
let host = Object.keys(json)[0];
let book = Object.keys(json[host])[0];
let noteId = json[host][book]["note-id"];
let note = json[host][book];
if (state.notebooks[host] &&
state.notebooks[host][book] &&
state.notebooks[host][book].notes &&
state.notebooks[host][book].notes[noteId])
{
state.notebooks[host][book].notes[noteId]["author"] = note["author"];
state.notebooks[host][book].notes[noteId]["build"] = note["build"];
state.notebooks[host][book].notes[noteId]["file"] = note["file"];
state.notebooks[host][book].notes[noteId]["title"] = note["title"];
}
}
editComment(json, state) {
let host = json.host
let book = json.book
let note = json.note
let comment = json.comment;
let commentId = Object.keys(comment)[0]
if (state.notebooks[host] &&
state.notebooks[host][book] &&
state.notebooks[host][book].notes &&
state.notebooks[host][book].notes[note] &&
state.notebooks[host][book].notes[note].comments)
{
let keys = state.notebooks[host][book].notes[note].comments.map((com) => {
return Object.keys(com)[0];
});
let index = keys.indexOf(commentId);
if (index > -1) {
state.notebooks[host][book].notes[note].comments[index] = comment;
}
}
}
delBook(json, state) {
let host = json.host;
let book = json.book;
if (state.notebooks[host]) {
if (state.notebooks[host][book]) {
delete state.notebooks[host][book];
}
if (Object.keys(state.notebooks[host]).length === 0) {
delete state.notebooks[host];
}
}
}
delNote(json, state) {
let host = json.host;
let book = json.book;
let note = json.note;
if (state.notebooks[host] &&
state.notebooks[host][book] &&
state.notebooks[host][book].notes)
{
if (state.notebooks[host][book].notes[note]) {
state.notebooks[host][book]["num-notes"] -= 1;
if (!state.notebooks[host][book].notes[note].read) {
state.notebooks[host][book]["num-unread"] -= 1;
}
delete state.notebooks[host][book].notes[note];
let index = state.notebooks[host][book]["notes-by-date"].indexOf(note);
if (index > -1) {
state.notebooks[host][book]["notes-by-date"].splice(index, 1);
}
}
if (Object.keys(state.notebooks[host][book].notes).length === 0) {
delete state.notebooks[host][book].notes;
delete state.notebooks[host][book]["notes-by-date"];
}
}
}
delComment(json, state) {
let host = json.host
let book = json.book
let note = json.note
let comment = json.comment;
if (state.notebooks[host] &&
state.notebooks[host][book] &&
state.notebooks[host][book].notes &&
state.notebooks[host][book].notes[note])
{
state.notebooks[host][book].notes[note]["num-comments"] -= 1;
if (state.notebooks[host][book].notes[note].comments) {
let keys = state.notebooks[host][book].notes[note].comments.map((com) => {
return Object.keys(com)[0];
});
let index = keys.indexOf(comment);
if (index > -1) {
state.notebooks[host][book].notes[note].comments.splice(index, 1);
}
}
}
}
read(json, state){
let host = json.host;
let book = json.book;
let noteId = json.note
if (state.notebooks[host] &&
state.notebooks[host][book] &&
state.notebooks[host][book].notes &&
state.notebooks[host][book].notes[noteId])
{
if (!state.notebooks[host][book].notes[noteId]["read"]) {
state.notebooks[host][book].notes[noteId]["read"] = true;
state.notebooks[host][book]["num-unread"] -= 1;
}
}
}
}

View File

@ -12,8 +12,6 @@ import S3Reducer from '../reducers/s3-update';
import { GraphReducer } from '../reducers/graph-update';
import { HarkReducer } from '../reducers/hark-update';
import GroupReducer from '../reducers/group-update';
import PublishUpdateReducer from '../reducers/publish-update';
import PublishResponseReducer from '../reducers/publish-response';
import LaunchReducer from '../reducers/launch-update';
import ConnectionReducer from '../reducers/connection';
import {OrderedMap} from '../lib/OrderedMap';
@ -41,8 +39,6 @@ export default class GlobalStore extends BaseStore<StoreState> {
contactReducer = new ContactReducer();
s3Reducer = new S3Reducer();
groupReducer = new GroupReducer();
publishUpdateReducer = new PublishUpdateReducer();
publishResponseReducer = new PublishResponseReducer();
launchReducer = new LaunchReducer();
connReducer = new ConnectionReducer();
@ -77,7 +73,6 @@ export default class GlobalStore extends BaseStore<StoreState> {
chat: {},
contacts: {},
graph: {},
publish: {}
},
groups: {},
groupKeys: new Set(),
@ -122,8 +117,6 @@ export default class GlobalStore extends BaseStore<StoreState> {
this.contactReducer.reduce(data, this.state);
this.s3Reducer.reduce(data, this.state);
this.groupReducer.reduce(data, this.state);
this.publishUpdateReducer.reduce(data, this.state);
this.publishResponseReducer.reduce(data, this.state);
this.launchReducer.reduce(data, this.state);
this.connReducer.reduce(data, this.state);
GraphReducer(data, this.state);

View File

@ -4,14 +4,18 @@ import { Path } from '~/types/noun';
import { Invites } from '~/types/invite-update';
import { Associations } from '~/types/metadata-update';
import { Rolodex } from '~/types/contact-update';
import { Notebooks } from '~/types/publish-update';
import { Groups } from '~/types/group-update';
import { S3State } from '~/types/s3-update';
import { LaunchState, WeatherState } from '~/types/launch-update';
import { ConnectionStatus } from '~/types/connection';
import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from '~/types/local-update';
import {Graphs} from '~/types/graph-update';
import { Notifications, NotificationGraphConfig, GroupNotificationsConfig } from "~/types";
import {
Notifications,
NotificationGraphConfig,
GroupNotificationsConfig,
LocalUpdateRemoteContentPolicy,
BackgroundConfig
} from "~/types";
export interface StoreState {
// local state
@ -47,7 +51,7 @@ export interface StoreState {
userLocation: string | null;
// publish state
notebooks: Notebooks;
notebooks: any;
// Chat state
chatInitialized: boolean;

View File

@ -1,4 +1,5 @@
import {Patp} from "./noun";
import { Patp } from "./noun";
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
export interface TextContent { text: string; };
@ -10,7 +11,7 @@ export type Content = TextContent | UrlContent | CodeContent | ReferenceContent
export interface Post {
author: Patp;
contents: Content[];
hash?: string;
hash: string | null;
index: string;
pending?: boolean;
signatures: string[];
@ -23,7 +24,7 @@ export interface GraphNode {
post: Post;
}
export type Graph = Map<number, GraphNode>;
export type Graph = BigIntOrderedMap<GraphNode>;
export type Graphs = { [rid: string]: Graph };

View File

@ -1,48 +0,0 @@
import { Notebooks, Notebook, Note, BookId, NoteId } from './publish-update';
import { Patp } from './noun';
export type PublishResponse =
NotebooksResponse
| NotebookResponse
| NoteResponse
| NotesPageResponse
| CommentsPageResponse;
interface NotebooksResponse {
type: 'notebooks';
data: Notebooks;
}
interface NotebookResponse {
type: 'notebook';
data: Notebook;
host: Patp;
notebook: BookId;
}
interface NoteResponse {
type: 'note';
data: Note;
host: Patp;
notebook: BookId;
note: NoteId;
}
interface NotesPageResponse {
type: 'notes-page';
data: Note[];
host: Patp;
notebook: BookId;
startIndex: number;
length: number;
}
interface CommentsPageResponse {
type: 'comments-page';
data: Comment[];
host: Patp;
notebook: BookId;
note: NoteId;
startIndex: number;
length: number;
}

View File

@ -1,158 +0,0 @@
import { Patp, PatpNoSig, Path } from './noun';
export type NoteId = string;
export type BookId = string;
export type PublishUpdate =
PublishUpdateAddBook
| PublishUpdateAddNote
| PublishUpdateAddComment
| PublishUpdateEditBook
| PublishUpdateEditNote
| PublishUpdateEditComment
| PublishUpdateDelBook
| PublishUpdateDelNote
| PublishUpdateDelComment;
type PublishUpdateBook = {
[s in Patp]: {
[b in BookId]: {
title: string;
'date-created': number;
about: string;
'num-notes': number;
'num-unread': number;
comments: boolean;
'writers-group-path': Path;
'subscribers-group-path': Path;
};
};
}
type PublishUpdateNote = {
[s in Patp]: {
[b in BookId]: {
'note-id': NoteId;
author: Patp;
title: string;
'date-created': string;
snippet: string;
file: string;
'num-comments': number;
comments: Comment[];
read: boolean;
pending: boolean;
};
};
};
interface PublishUpdateAddBook {
'add-book': PublishUpdateBook;
}
interface PublishUpdateEditBook {
'edit-book': PublishUpdateBook;
}
interface PublishUpdateDelBook {
'del-book': {
host: Patp;
book: string;
}
}
interface PublishUpdateAddNote {
'add-note': PublishUpdateNote;
}
interface PublishUpdateEditNote {
'edit-note': PublishUpdateNote;
}
interface PublishUpdateDelNote {
'del-note': {
host: Patp;
book: BookId;
note: NoteId;
}
}
interface PublishUpdateAddComment {
'add-comment': {
who: Patp;
host: BookId;
note: NoteId;
body: string;
}
}
interface PublishUpdateEditComment {
'edit-comment': {
host: Patp;
book: BookId;
note: NoteId;
body: string;
comment: Comment;
}
}
interface PublishUpdateDelComment {
'del-comment': {
host: Patp;
book: BookId;
note: NoteId;
comment: string;
}
}
export type Notebooks = {
[host in Patp]: {
[book in BookId]: Notebook;
}
}
export interface Notebook {
about: string;
comments: boolean;
'date-created': number;
notes: Notes;
'notes-by-date': NoteId[];
'num-notes': number;
'num-unread': number;
subscribers: PatpNoSig[];
'subscribers-group-path': Path;
title: string;
'writers-group-path': Path;
}
export type Notes = {
[id in NoteId]: Note;
};
export interface Note {
author: Patp;
comments: Comment[];
'date-created': number;
file: string;
'next-note': NoteId | null;
'note-id': NoteId;
'num-comments': number;
pending: boolean;
'prev-note': NoteId | null;
read: boolean;
snippet: string;
title: string;
}
export interface Comment {
[date: string]: {
author: Patp;
content: string;
'date-created': number;
pending: boolean;
};
}

View File

@ -43,7 +43,7 @@ const Root = styled.div`
* {
scrollbar-width: thin;
scrollbar-color: ${ p => p.theme.colors.gray } ${ p => p.theme.colors.white };
scrollbar-color: ${ p => p.theme.colors.gray } transparent;
}
/* Works on Chrome/Edge/Safari */

View File

@ -1,5 +1,6 @@
import React from 'react';
import Helmet from 'react-helmet';
import styled from 'styled-components';
import { Box, Row, Icon, Text, Center } from '@tlon/indigo-react';
import { uxToHex, adjustHex } from '~/logic/lib/util';
@ -12,6 +13,14 @@ import Tile from './components/tiles/tile';
import Welcome from './components/welcome';
import Groups from './components/Groups';
const ScrollbarLessBox = styled(Box)`
scrollbar-width: none !important;
::-webkit-scrollbar {
display: none;
}
`;
export default class LaunchApp extends React.Component {
componentDidMount() {
// preload spinner asset
@ -32,30 +41,29 @@ export default class LaunchApp extends React.Component {
<Helmet>
<title>OS1 - Home</title>
</Helmet>
<Box height='100%' overflowY='scroll'>
<ScrollbarLessBox height='100%' overflowY='scroll'>
<Welcome firstTime={props.launch.firstTime} api={props.api} />
<Box
ml='2'
mx='2'
display='grid'
gridAutoRows='124px'
gridTemplateColumns='repeat(auto-fit, 124px)'
gridTemplateColumns='repeat(auto-fill, minmax(128px, 1fr))'
gridGap={3}
p={2}
>
<Tile
bg="#fff"
bg="transparent"
color="green"
to="/~landscape/home"
p={0}
>
<Box p={2} height='100%' width='100%' bg='washedGreen'>
<Box p={2} height='100%' width='100%' bg='green'>
<Row alignItems='center'>
<Icon
color="green"
fill="rgba(0,0,0,0)"
icon="Circle"
color="white"
// fill="rgba(0,0,0,0)"
icon="Home"
/>
<Text ml="1" color="green">Home</Text>
<Text ml="1" mt='1px' color="white">Home</Text>
</Row>
</Box>
</Tile>
@ -75,13 +83,10 @@ export default class LaunchApp extends React.Component {
location={props.userLocation}
weather={props.weather}
/>
<Box display={["none", "block"]} width="100%" gridColumn="1 / -1"></Box>
<Groups groups={props.groups} associations={props.associations} invites={props.invites} api={props.api}/>
</Box>
<Groups
associations={props.associations}
groups={props.groups}
invites={props.invites}
api={props.api} />
</Box>
</ScrollbarLessBox>
<Box
position="absolute"
fontFamily="mono"

View File

@ -35,17 +35,7 @@ export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
};
return (
<Box
{...boxProps}
ml='2'
display="grid"
gridAutoRows="124px"
gridTemplateColumns="repeat(auto-fit, 124px)"
gridGap={3}
px={2}
pt={2}
pb="7"
>
<>
{incomingGroups.map((invite) => (
<Box
height='100%'
@ -92,6 +82,6 @@ export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
<Text>{group.metadata.title}</Text>
</Tile>
))}
</Box>
</>
);
}

View File

@ -1,8 +1,24 @@
import React from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import defaultApps from '~/logic/lib/default-apps';
import { Box, DisclosureBox } from "@tlon/indigo-react";
import { Box } from "@tlon/indigo-react";
const SquareBox = styled(Box)`
&::before {
content: "";
display: inline-block;
width: 1px;
height: 0;
padding-bottom: 100%;
}
& > * {
position: absolute;
top: 0;
}
`;
const routeList = defaultApps.map(a => `/~${a}`);
export default class Tile extends React.Component {
@ -26,7 +42,7 @@ export default class Tile extends React.Component {
return (
<Box
<SquareBox
borderRadius={2}
overflow="hidden"
bg={bg || "white"}
@ -40,7 +56,7 @@ export default class Tile extends React.Component {
>
{childElement}
</Box>
</Box>
</SquareBox>
);
}
}

View File

@ -1,6 +1,7 @@
import React, { useEffect } from "react";
import { Box, Row, Col, Center, LoadingSpinner } from "@tlon/indigo-react";
import { Switch, Route, Link } from "react-router-dom";
import bigInt from 'big-integer';
import GlobalApi from "~/logic/api/global";
import { StoreState } from "~/logic/store/type";
@ -48,6 +49,7 @@ export function LinkResource(props: LinkResourceProps) {
? associations.graph[appPath]
: { metadata: {} };
const contactDetails = contacts[resource["group-path"]] || {};
const group = groups[resource["group-path"]] || {};
const graph = graphs[resourcePath] || null;
useEffect(() => {
@ -75,7 +77,7 @@ export function LinkResource(props: LinkResourceProps) {
const contact = contactDetails[node.post.author];
return (
<LinkItem
key={date}
key={date.toString()}
resource={resourcePath}
node={node}
nickname={contact?.nickname}
@ -83,6 +85,8 @@ export function LinkResource(props: LinkResourceProps) {
hideNicknames={hideNicknames}
baseUrl={resourceUrl}
color={uxToHex(contact?.color || '0x0')}
group={group}
api={api}
/>
);
})}
@ -99,7 +103,7 @@ export function LinkResource(props: LinkResourceProps) {
return <div>Malformed URL</div>;
}
const index = parseInt(indexArr[1], 10);
const index = bigInt(indexArr[1]);
const node = !!graph ? graph.get(index) : null;
if (!node) {
@ -124,7 +128,7 @@ export function LinkResource(props: LinkResourceProps) {
name={name}
ship={ship}
api={api}
parentIndex={node.post.originalIndex}
parentIndex={node.post.index}
/>
</Row>
<Comments

View File

@ -5,6 +5,8 @@ import { Sigil } from '~/logic/lib/sigil';
import { Link } from 'react-router-dom';
import { cite } from '~/logic/lib/util';
import { roleForShip } from "~/logic/lib/group";
export const LinkItem = (props) => {
const {
node,
@ -13,7 +15,9 @@ export const LinkItem = (props) => {
avatar,
resource,
hideAvatars,
hideNicknames
hideNicknames,
api,
group
} = props;
const URLparser = new RegExp(
@ -35,6 +39,9 @@ export const LinkItem = (props) => {
const baseUrl = props.baseUrl || `/~404/${resource}`;
const ourRole = group ? roleForShip(group, window.ship) : undefined;
const [ship, name] = resource.split("/");
return (
<Row minWidth='0' flexShrink='0' width="100%" alignItems="center" py={3} bg="white">
{img}
@ -58,6 +65,7 @@ export const LinkItem = (props) => {
<Link to={`${baseUrl}/${index}`}>
<Text color="gray">{size} comments</Text>
</Link>
{(ourRole === "admin") && <Text color='red' ml='2' cursor='pointer' onClick={() => api.graph.removeNodes(`~${ship}`, name, [node.post.index])}>Delete</Text>}
</Box>
</Col>
</Row>

View File

@ -11,6 +11,7 @@ import {
import GlobalApi from "~/logic/api/global";
import { S3State } from "~/types";
import { ImageInput } from "~/views/components/ImageInput";
import {ColorInput} from "~/views/components/ColorInput";
export type BgType = "none" | "url" | "color";
@ -48,7 +49,7 @@ export function BackgroundPicker({
<Row {...rowSpace}>
<Radio label="Color" id="color" {...radioProps} />
{bgType === "color" && (
<Input ml={4} type="text" label="Color" id="bgColor" />
<ColorInput id="bgColor" label="Color" />
)}
</Row>
<Radio label="None" id="none" {...radioProps} />

View File

@ -9,6 +9,7 @@ import { Formik, Form } from 'formik';
import * as Yup from 'yup';
import GlobalApi from '~/logic/api/global';
import { uxToHex } from '~/logic/lib/util';
import { S3State, BackgroundConfig } from '~/types';
import { BackgroundPicker, BgType } from './BackgroundPicker';
@ -17,7 +18,7 @@ const formSchema = Yup.object().shape({
.oneOf(['none', 'color', 'url'], 'invalid')
.required('Required'),
bgUrl: Yup.string().url(),
bgColor: Yup.string().matches(/#([A-F]|[a-f]|[0-9]){6}/, 'Invalid color'),
bgColor: Yup.string(),
avatars: Yup.boolean(),
nicknames: Yup.boolean()
});
@ -57,7 +58,7 @@ export default function DisplayForm(props: DisplayFormProps) {
initialValues={
{
bgType,
bgColor,
bgColor: bgColor || '',
bgUrl,
avatars: hideAvatars,
nicknames: hideNicknames
@ -66,7 +67,7 @@ export default function DisplayForm(props: DisplayFormProps) {
onSubmit={(values, actions) => {
const bgConfig: BackgroundConfig =
values.bgType === 'color'
? { type: 'color', color: values.bgColor || '' }
? { type: 'color', color: `#${uxToHex(values.bgColor || '0x0')}` }
: values.bgType === 'url'
? { type: 'url', url: values.bgUrl || '' }
: undefined;

View File

@ -1,5 +1,5 @@
import React from "react";
import { Box } from '@tlon/indigo-react';
import { Box } from "@tlon/indigo-react";
import GlobalApi from "~/logic/api/global";
import { StoreState } from "~/logic/store/type";
@ -16,8 +16,7 @@ type PublishResourceProps = StoreState & {
export function PublishResource(props: PublishResourceProps) {
const { association, api, baseUrl, notebooks } = props;
const appPath = association["app-path"];
const [, ship, book] = appPath.split("/");
const notebook = notebooks[ship]?.[book];
const [, , ship, book] = appPath.split("/");
const notebookContacts = props.contacts[association["group-path"]];
return (
@ -28,17 +27,18 @@ export function PublishResource(props: PublishResourceProps) {
book={book}
contacts={props.contacts}
groups={props.groups}
notebook={notebook}
associations={props.associations}
association={association}
notebookContacts={notebookContacts}
rootUrl={baseUrl}
baseUrl={`${baseUrl}/resource/publish/${ship}/${book}`}
baseUrl={`${baseUrl}/resource/publish/ship/${ship}/${book}`}
history={props.history}
match={props.match}
location={props.location}
hideAvatars={props.hideAvatars}
hideNicknames={props.hideNicknames}
remoteContentPolicy={props.remoteContentPolicy}
graphs={props.graphs}
/>
</Box>
);

View File

@ -6,8 +6,10 @@ import GlobalApi from "~/logic/api/global";
import { Box, Row } from "@tlon/indigo-react";
import styled from "styled-components";
import { Author } from "./Author";
import {GraphNode, TextContent} from "~/types/graph-update";
import tokenizeMessage from '~/logic/lib/tokenizeMessage';
import RichText from '~/views/components/RichText';
import {LocalUpdateRemoteContentPolicy} from "~/types";
const ClickBox = styled(Box)`
cursor: pointer;
@ -16,76 +18,51 @@ const ClickBox = styled(Box)`
interface CommentItemProps {
pending?: boolean;
comment: Comment;
comment: GraphNode;
contacts: Contacts;
book: string;
ship: string;
api: GlobalApi;
note: NoteId;
hideNicknames: boolean;
hideAvatars: boolean;
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
}
export function CommentItem(props: CommentItemProps) {
const { ship, contacts, book, note, api, remoteContentPolicy } = props;
const [editing, setEditing] = useState<boolean>(false);
const commentPath = Object.keys(props.comment)[0];
const commentData = props.comment[commentPath];
const content = tokenizeMessage(commentData.content).flat().join(' ');
const { ship, contacts, book, api, remoteContentPolicy } = props;
const commentData = props.comment?.post;
const comment = commentData.contents[0] as TextContent;
const disabled = props.pending || window.ship !== commentData.author.slice(1);
const content = tokenizeMessage(comment.text).flat().join(' ');
const onUpdate = async ({ comment }) => {
await api.publish.updateComment(
ship.slice(1),
book,
note,
commentPath,
comment
);
setEditing(false);
};
const disabled = props.pending || window.ship !== commentData.author;
const onDelete = async () => {
await api.publish.deleteComment(ship.slice(1), book, note, commentPath);
await api.graph.removeNodes(ship, book, [commentData?.index]);
};
return (
<Box mb={4} opacity={props.pending ? "60%" : "100%"}>
<Box mb={4} opacity={commentData?.pending ? "60%" : "100%"}>
<Row bg="white" my={3}>
<Author
showImage
contacts={contacts}
ship={commentData?.author}
date={commentData["date-created"]}
date={commentData?.["time-sent"]}
hideAvatars={props.hideAvatars}
hideNicknames={props.hideNicknames}
>
{!disabled && !editing && (
{!disabled && (
<>
<ClickBox color="green" onClick={() => setEditing(true)}>
Edit
</ClickBox>
<ClickBox color="red" onClick={onDelete}>
Delete
</ClickBox>
</>
)}
{editing && (
<ClickBox onClick={() => setEditing(false)} color="red">
Cancel
</ClickBox>
)}
</Author>
</Row>
<Box mb={2}>
{editing
? <CommentInput
onSubmit={onUpdate}
initial={commentData.content}
label="Update"
/>
: <RichText className="f9 white-d" remoteContentPolicy={remoteContentPolicy}>{content}</RichText>}
<RichText className="f9 white-d" remoteContentPolicy={remoteContentPolicy}>{content}</RichText>
</Box>
</Box>
);

View File

@ -8,52 +8,36 @@ import { Contacts } from "~/types/contact-update";
import _ from "lodash";
import GlobalApi from "~/logic/api/global";
import { FormikHelpers } from "formik";
import {GraphNode, Graph} from "~/types/graph-update";
import {createPost} from "~/logic/api/graph";
import { LocalUpdateRemoteContentPolicy } from "~/types";
interface CommentsProps {
comments: Comment[];
comments: GraphNode;
book: string;
noteId: NoteId;
note: Note;
note: GraphNode;
ship: string;
contacts: Contacts;
api: GlobalApi;
numComments: number;
enabled: boolean;
hideAvatars: boolean;
hideNicknames: boolean;
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
}
export function Comments(props: CommentsProps) {
const { comments, ship, book, note, api, noteId, numComments } = props;
const [pending, setPending] = useState<string[]>([]);
useEffect(() => {
_.forEach(comments, (comment: Comment) => {
const { content } = comment[Object.keys(comment)[0]];
setPending((p) => p.filter((p) => p !== content));
});
}, [numComments]);
const { comments, ship, book, note, api } = props;
const onSubmit = async (
{ comment },
actions: FormikHelpers<{ comment: string }>
) => {
setPending((p) => [...p, comment]);
const action = {
"new-comment": {
who: ship.slice(1),
book: book,
note: noteId,
body: comment,
},
};
try {
await api.publish.publishAction(action);
const post = createPost([{ text: comment }], comments?.post?.index);
await api.graph.addPost(ship, book, post)
actions.resetForm();
actions.setStatus({ success: null });
} catch (e) {
console.error(e);
actions.setStatus({ error: e.message });
}
};
@ -61,38 +45,14 @@ export function Comments(props: CommentsProps) {
return (
<Col>
<CommentInput onSubmit={onSubmit} />
{Array.from(pending).map((com, i) => {
const da = dateToDa(new Date());
const ship = `~${window.ship}`;
const comment = {
[da]: {
author: ship,
content: com,
"date-created": Math.round(new Date().getTime()),
},
} as Comment;
return (
<CommentItem
comment={comment}
key={i}
contacts={props.contacts}
ship={ship}
pending={true}
hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars}
remoteContentPolicy={props.remoteContentPolicy}
/>
);
})}
{props.comments.map((com, i) => (
{Array.from(comments.children).reverse().map(([idx, comment]) => (
<CommentItem
comment={com}
key={i}
comment={comment}
key={idx}
contacts={props.contacts}
api={api}
book={book}
ship={ship}
note={note["note-id"]}
hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars}
remoteContentPolicy={props.remoteContentPolicy}

View File

@ -1,22 +1,28 @@
import React, { Component } from "react";
import React from "react";
import _ from 'lodash';
import { PostFormSchema, PostForm } from "./NoteForm";
import { Note } from "../../../../types/publish-update";
import { FormikHelpers } from "formik";
import GlobalApi from "../../../../api/global";
import { RouteComponentProps } from "react-router-dom";
import GlobalApi from "~/logic/api/global";
import { RouteComponentProps, useLocation } from "react-router-dom";
import { GraphNode, TextContent, Association } from "~/types";
import { getLatestRevision, editPost } from "~/logic/lib/publish";
import {useWaitForProps} from "~/logic/lib/useWaitForProps";
interface EditPostProps {
ship: string;
noteId: string;
note: Note;
noteId: number;
note: GraphNode;
api: GlobalApi;
book: string;
}
export function EditPost(props: EditPostProps & RouteComponentProps) {
const { note, book, noteId, api, ship, history } = props;
const body = note.file.slice(note.file.indexOf(";>") + 3);
const [revNum, title, body] = getLatestRevision(note);
const location = useLocation();
const waiter = useWaitForProps(props);
const initial: PostFormSchema = {
title: note.title,
title,
body,
};
@ -25,11 +31,18 @@ export function EditPost(props: EditPostProps & RouteComponentProps) {
actions: FormikHelpers<PostFormSchema>
) => {
const { title, body } = values;
const host = ship.slice(1);
try {
await api.publish.editNote(host, book, noteId, title, body);
history.push(this.props.baseUrl);
const newRev = revNum + 1;
const nodes = editPost(newRev, noteId, title, body);
await api.graph.addNodes(ship, book, nodes);
await waiter(p => {
const [rev] = getLatestRevision(p.note);
return rev === newRev;
});
const noteUrl = _.dropRight(location.pathname.split('/'), 1).join('/');
history.push(noteUrl);
} catch (e) {
console.error(e);
actions.setStatus({ error: "Failed to edit notebook" });
}
};

View File

@ -6,7 +6,7 @@ import { Notebook } from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
import { MetadataForm } from "./MetadataForm";
import { Groups, Associations } from "~/types";
import { Groups, Associations, Association } from "~/types";
import { Formik, FormikHelpers, Form } from "formik";
import GroupSearch from "~/views/components/GroupSearch";
import { AsyncButton } from "~/views/components/AsyncButton";
@ -22,10 +22,10 @@ interface FormSchema {
interface GroupifyFormProps {
host: string;
book: string;
notebook: Notebook;
groups: Groups;
api: GlobalApi;
associations: Associations;
association: Association;
}
export function GroupifyForm(props: GroupifyFormProps) {
@ -34,14 +34,16 @@ export function GroupifyForm(props: GroupifyFormProps) {
actions: FormikHelpers<FormSchema>
) => {
try {
await props.api.publish.groupify(props.book, values.group);
await props.api.graph.groupifyGraph(
`~${window.ship}`, props.book, 'publish', values.group || undefined);
actions.setStatus({ success: null });
} catch (e) {
console.error(e);
actions.setStatus({ error: e.message });
}
};
const groupPath = props.notebook?.["writers-group-path"];
const groupPath = props.association?.['group-path'];
const isUnmanaged = props.groups?.[groupPath]?.hidden || false;

View File

@ -1,7 +1,9 @@
import React, { useCallback } from "react";
import { UnControlled as CodeEditor } from "react-codemirror2";
import { MOBILE_BROWSER_REGEX } from "~/logic/lib/util";
import { useFormikContext } from 'formik';
import { Prompt } from 'react-router-dom';
import { MOBILE_BROWSER_REGEX, usePreventWindowUnload } from "~/logic/lib/util";
import { PropFunc } from "~/types/util";
import CodeMirror from "codemirror";
@ -22,6 +24,17 @@ interface MarkdownEditorProps {
onBlur?: (e: any) => void;
}
const PromptIfDirty = () => {
const formik = useFormikContext();
usePreventWindowUnload(formik.dirty);
return (
<Prompt
when={formik.dirty}
message="Are you sure you want to leave? You have unsaved changes."
/>
);
};
export function MarkdownEditor(
props: MarkdownEditorProps & PropFunc<typeof Box>
) {
@ -53,7 +66,7 @@ export function MarkdownEditor(
return (
<Box
flexGrow={1}
height="100%"
position="static"
className="publish"
p={1}
@ -63,6 +76,7 @@ export function MarkdownEditor(
height={['calc(100% - 22vh)', '100%']}
{...boxProps}
>
<PromptIfDirty />
<CodeEditor
autoCursor={false}
onBlur={onBlur}

View File

@ -15,11 +15,13 @@ import { Notebook } from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
import { FormError } from "~/views/components/FormError";
import { RouteComponentProps, useHistory } from "react-router-dom";
import {Association} from "~/types";
import { uxToHex } from "~/logic/lib/util";
interface MetadataFormProps {
host: string;
book: string;
notebook: Notebook;
association: Association;
contacts: Contacts;
api: GlobalApi;
}
@ -27,13 +29,11 @@ interface MetadataFormProps {
interface FormSchema {
name: string;
description: string;
comments: boolean;
}
const formSchema = Yup.object({
name: Yup.string().required("Notebook must have a name"),
description: Yup.string(),
comments: Yup.boolean(),
description: Yup.string()
});
const ResetOnPropsChange = (props: { init: FormSchema; book: string }) => {
@ -47,21 +47,30 @@ const ResetOnPropsChange = (props: { init: FormSchema; book: string }) => {
export function MetadataForm(props: MetadataFormProps) {
const { host, notebook, api, book } = props;
const { api, book } = props;
const { metadata } = props.association || {};
const initialValues: FormSchema = {
name: notebook?.title,
description: notebook?.about,
comments: notebook?.comments,
name: metadata?.title,
description: metadata?.description,
};
const onSubmit = async (
values: FormSchema,
actions: FormikHelpers<FormSchema>
) => {
try {
const { name, description, comments } = values;
await api.publish.editBook(book, name, description, comments);
api.publish.fetchNotebook(host, book);
const { name, description } = values;
await api.metadata.metadataAdd(
"publish",
props.association["app-path"],
props.association["group-path"],
name,
description,
props.association.metadata["date-created"],
uxToHex(props.association.metadata.color)
);
actions.setStatus({ success: null });
} catch (e) {
console.log(e);
@ -86,11 +95,6 @@ export function MetadataForm(props: MetadataFormProps) {
label="Change description"
caption="Change the description of this notebook"
/>
<Checkbox
id="comments"
label="Comments"
caption="Subscribers may comment when enabled"
/>
<ResetOnPropsChange init={initialValues} book={book} />
<AsyncButton primary loadingText="Updating.." border>
Save

View File

@ -1,62 +1,61 @@
import React, { useState, useEffect } from "react";
import { Box, Text, Col } from "@tlon/indigo-react";
import ReactMarkdown from "react-markdown";
import bigInt from 'big-integer';
import { Link, RouteComponentProps } from "react-router-dom";
import { Spinner } from "~/views/components/Spinner";
import { Comments } from "./Comments";
import { NoteNavigation } from "./NoteNavigation";
import {
NoteId,
Note as INote,
Notebook,
} from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
import GlobalApi from "~/logic/api/global";
import { getLatestRevision, getComments } from '~/logic/lib/publish';
import { Author } from "./Author";
import { LocalUpdateRemoteContentPolicy } from "~/types";
import { Contacts, GraphNode, Graph, LocalUpdateRemoteContentPolicy } from "~/types";
interface NoteProps {
ship: string;
book: string;
noteId: NoteId;
note: INote;
notebook: Notebook;
note: GraphNode;
notebook: Graph;
contacts: Contacts;
api: GlobalApi;
hideAvatars: boolean;
hideNicknames: boolean;
baseUrl?: string;
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
rootUrl: string;
baseUrl: string;
}
export function Note(props: NoteProps & RouteComponentProps) {
const [deleting, setDeleting] = useState(false);
const { notebook, note, contacts, ship, book, noteId, api, rootUrl } = props;
useEffect(() => {
api.publish.readNote(ship.slice(1), book, noteId);
api.publish.fetchNote(ship, book, noteId);
}, [ship, book, noteId]);
const { notebook, note, contacts, ship, book, api, rootUrl, baseUrl } = props;
const deletePost = async () => {
setDeleting(true);
await api.publish.delNote(ship.slice(1), book, noteId);
const indices = [note.post.index]
await api.graph.removeNodes(ship, book, indices);
props.history.push(rootUrl);
};
const comments = note?.comments || [];
const file = note?.file;
const newfile = file ? file.slice(file.indexOf(";>") + 2) : "";
const comments = getComments(note);
const [revNum, title, body, post] = getLatestRevision(note);
const noteId = bigInt(note.post.index.split('/')[1]);
let editPost: JSX.Element | null = null;
const editUrl = props.location.pathname + "/edit";
if (`~${window.ship}` === note?.author) {
editPost = (
<Box display="inline-block" verticalAlign='middle'>
<Link to={editUrl}>
<Text display='inline-block' color="green">Edit</Text>
</Link>
let adminLinks: JSX.Element | null = null;
if (window.ship === note?.post?.author) {
adminLinks = (
<Box display="inline-block" verticalAlign="middle">
<Link to={`${baseUrl}/edit`}>
<Text
color="green"
ml={2}
>
Update
</Text>
</Link>
<Text
display='inline-block'
color="red"
ml={2}
onClick={deletePost}
@ -84,43 +83,38 @@ export function Note(props: NoteProps & RouteComponentProps) {
<Text>{"<- Notebook Index"}</Text>
</Link>
<Col>
<Text display="block" mb={2}>{note?.title || ""}</Text>
<Text display="block" mb={2}>{title || ""}</Text>
<Box display="flex">
<Author
hideNicknames={props?.hideNicknames}
hideAvatars={props?.hideAvatars}
ship={note?.author}
ship={post?.author}
contacts={contacts}
date={note?.["date-created"]}
date={post?.["time-sent"]}
/>
<Text ml={2}>{editPost}</Text>
<Text ml={2}>{adminLinks}</Text>
</Box>
</Col>
<Box color="black" className="md" style={{ overflowWrap: "break-word" }}>
<ReactMarkdown source={newfile} linkTarget={"_blank"} />
<ReactMarkdown source={body} linkTarget={"_blank"} />
</Box>
<NoteNavigation
notebook={notebook}
prevId={note?.["prev-note"] || undefined}
nextId={note?.["next-note"] || undefined}
ship={ship}
book={book}
noteId={noteId}
ship={props.ship}
book={props.book}
/>
<Comments
ship={ship}
book={props.book}
note={props.note}
comments={comments}
contacts={props.contacts}
api={props.api}
hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars}
remoteContentPolicy={props.remoteContentPolicy}
/>
{notebook?.comments && (
<Comments
ship={ship}
book={book}
noteId={noteId}
note={note}
comments={comments}
numComments={note?.["num-comments"]}
contacts={contacts}
api={api}
hideNicknames={props?.hideNicknames}
hideAvatars={props?.hideAvatars}
remoteContentPolicy={props?.remoteContentPolicy}
/>
)}
<Spinner
text="Deleting post..."
awaiting={deleting}

View File

@ -2,7 +2,9 @@ import React, { Component } from "react";
import moment from "moment";
import { Box } from "@tlon/indigo-react";
import { Link } from "react-router-dom";
import { Notebook } from "../../../../types/publish-update";
import { Graph, GraphNode } from "~/types";
import { getLatestRevision } from "~/logic/lib/publish";
import { BigInteger } from "big-integer";
function NavigationItem(props: {
url: string;
@ -30,48 +32,55 @@ function NavigationItem(props: {
);
}
function getAdjacentId(
graph: Graph,
child: BigInteger,
backwards = false
): BigInteger | null {
const children = Array.from(graph);
const i = children.findIndex(([index]) => index.eq(child));
const target = children[backwards ? i + 1 : i - 1];
return target?.[0] || null;
}
function makeNoteUrl(noteId: number) {
return noteId.toString();
}
interface NoteNavigationProps {
book: string;
nextId?: string;
prevId?: string;
ship: string;
notebook: Notebook;
noteId: number;
notebook: Graph;
baseUrl: string;
}
export function NoteNavigation(props: NoteNavigationProps) {
let nextComponent = <Box />;
let prevComponent = <Box />;
let nextUrl = "";
let prevUrl = "";
const { nextId, prevId, notebook } = props;
const next =
nextId && nextId in notebook?.notes ? notebook?.notes[nextId] : null;
const { noteId, notebook } = props;
if (!notebook) {
return null;
}
const nextId = getAdjacentId(notebook, noteId);
const prevId = getAdjacentId(notebook, noteId, true);
const next = nextId && notebook.get(nextId);
const prev = prevId && notebook.get(prevId);
const prev =
prevId && prevId in notebook?.notes ? notebook?.notes[prevId] : null;
if (!next && !prev) {
return null;
}
if (next) {
nextUrl = `${props.nextId}`;
nextComponent = (
<NavigationItem
title={next.title}
date={next["date-created"]}
url={nextUrl}
/>
);
if (next && nextId) {
const nextUrl = makeNoteUrl(nextId);
const [, title, , post] = getLatestRevision(next);
const date = post["time-sent"];
nextComponent = <NavigationItem title={title} date={date} url={nextUrl} />;
}
if (prev) {
prevUrl = `${props.prevId}`;
if (prev && prevId) {
const prevUrl = makeNoteUrl(prevId);
const [, title, , post] = getLatestRevision(prev);
const date = post["time-sent"];
prevComponent = (
<NavigationItem
title={prev.title}
date={prev["date-created"]}
url={prevUrl}
prev
/>
<NavigationItem title={title} date={date} url={prevUrl} prev />
);
}

View File

@ -7,13 +7,20 @@ import ReactMarkdown from "react-markdown";
import moment from "moment";
import { Link } from "react-router-dom";
import styled from "styled-components";
import { GraphNode } from "~/types/graph-update";
import {
getComments,
getLatestRevision,
getSnippet,
} from "~/logic/lib/publish";
interface NotePreviewProps {
host: string;
book: string;
note: Note;
node: GraphNode;
contact?: Contact;
hideNicknames?: boolean;
baseUrl: string;
}
const WrappedBox = styled(Box)`
@ -21,46 +28,62 @@ const WrappedBox = styled(Box)`
`;
export function NotePreview(props: NotePreviewProps) {
const { note, contact } = props;
const { node, contact } = props;
const { post } = node;
if (!post) {
return null;
}
let name = note.author;
let name = post?.author;
if (contact && !props.hideNicknames) {
name = contact.nickname.length > 0 ? contact.nickname : note.author;
name = contact.nickname.length > 0 ? contact.nickname : post?.author;
}
if (name === note.author) {
name = cite(note.author);
if (name === post?.author) {
name = cite(post?.author);
}
let comment = "No Comments";
if (note["num-comments"] == 1) {
comment = "1 Comment";
} else if (note["num-comments"] > 1) {
comment = `${note["num-comments"]} Comments`;
}
const date = moment(note["date-created"]).fromNow();
const url = `${props.book}/note/${note["note-id"]}`;
const numComments = getComments(node).children.size;
const commentDesc =
numComments === 0
? "No Comments"
: numComments === 1
? "1 Comment"
: `${numComments} Comments`;
const date = moment(post["time-sent"]).fromNow();
const url = `${props.baseUrl}/note/${post.index.split("/")[1]}`;
// stubbing pending notification-store
const isRead = true;
const [rev, title, body] = getLatestRevision(node);
const snippet = getSnippet(body);
return (
<Link to={url}>
<Col mb={4}>
<WrappedBox mb={1}>{note.title}</WrappedBox>
<WrappedBox mb={1}>{title}</WrappedBox>
<WrappedBox mb={1}>
<ReactMarkdown
unwrapDisallowed
allowedTypes={["text", "root", "break", "paragraph"]}
source={note.snippet}
source={snippet}
/>
</WrappedBox>
<Box color="gray" display="flex">
<Box
mr={3}
fontFamily={contact?.nickname && !props.hideNicknames ? "sans" : "mono"}
fontFamily={
contact?.nickname && !props.hideNicknames ? "sans" : "mono"
}
>
{name}
</Box>
<Box color={note.read ? "gray" : "green"} mr={3}>
<Box color={isRead ? "gray" : "green"} mr={3}>
{date}
</Box>
<Box>{comment}</Box>
<Box mr={3}>{commentDesc}</Box>
<Box>{rev.valueOf() === 1 ? `1 Revision` : `${rev} Revisions`}</Box>
</Box>
</Col>
</Link>

View File

@ -1,21 +1,22 @@
import React from "react";
import { Route, Switch } from "react-router-dom";
import { NoteId, Note as INote, Notebook } from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
import GlobalApi from "~/logic/api/global";
import { RouteComponentProps } from "react-router-dom";
import Note from "./Note";
import { EditPost } from "./EditPost";
import { GraphNode, Graph, Contacts, LocalUpdateRemoteContentPolicy } from "~/types";
interface NoteRoutesProps {
ship: string;
book: string;
noteId: NoteId;
note: INote;
notebook: Notebook;
note: GraphNode;
noteId: number;
notebook: Graph;
contacts: Contacts;
api: GlobalApi;
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
hideNicknames: boolean;
hideAvatars: boolean;
baseUrl?: string;
@ -26,6 +27,7 @@ export function NoteRoutes(props: NoteRoutesProps & RouteComponentProps) {
const { ship, book, noteId } = props;
const baseUrl = props.baseUrl || '/~404';
const rootUrl = props.rootUrl || '/~404';
const relativePath = (path: string) => `${baseUrl}${path}`;
return (
@ -38,7 +40,7 @@ export function NoteRoutes(props: NoteRoutesProps & RouteComponentProps) {
path={baseUrl}
exact
render={(routeProps) => {
return <Note baseUrl={baseUrl} {...routeProps} {...props} />;
return <Note baseUrl={baseUrl} {...routeProps} {...props} rootUrl={rootUrl} />;
}}
/>

View File

@ -1,26 +1,24 @@
import React, { PureComponent } from "react";
import { Link, RouteComponentProps, Route, Switch } from "react-router-dom";
import { NotebookPosts } from "./NotebookPosts";
import { Subscribers } from "./Subscribers";
import { Settings } from "./Settings";
import { Spinner } from "~/views/components/Spinner";
import { Tabs, Tab } from "~/views/components/Tab";
import { roleForShip } from "~/logic/lib/group";
import { Box, Button, Text, Row } from "@tlon/indigo-react";
import { Notebook as INotebook } from "~/types/publish-update";
import { Box, Button, Text, Row, Col } from "@tlon/indigo-react";
import { Groups } from "~/types/group-update";
import { Contacts, Rolodex } from "~/types/contact-update";
import GlobalApi from "~/logic/api/global";
import styled from "styled-components";
import { Associations } from "~/types";
import { Associations, Graph, Association } from "~/types";
import { deSig } from "~/logic/lib/util";
import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton";
interface NotebookProps {
api: GlobalApi;
ship: string;
book: string;
notebook: INotebook;
graph: Graph;
notebookContacts: Contacts;
association: Association;
associations: Associations;
contacts: Rolodex;
groups: Groups;
hideNicknames: boolean;
@ -34,185 +32,62 @@ interface NotebookState {
tab: string;
}
export class Notebook extends PureComponent<
NotebookProps & RouteComponentProps,
NotebookState
> {
constructor(props) {
super(props);
this.state = {
isUnsubscribing: false,
tab: "all",
};
this.setTab = this.setTab.bind(this);
}
export function Notebook(props: NotebookProps & RouteComponentProps) {
const {
ship,
book,
notebookContacts,
groups,
hideNicknames,
association,
graph,
} = props;
const { metadata } = association;
setTab(tab: string) {
this.setState({ tab });
}
const group = groups[association?.["group-path"]];
if (!group) return null; // Waitin on groups to populate
render() {
const {
api,
ship,
book,
notebook,
notebookContacts,
groups,
history,
hideNicknames,
associations,
} = this.props;
const { state } = this;
const relativePath = (p: string) => props.baseUrl + p;
const group = groups[notebook?.["writers-group-path"]];
if (!group) return null; // Waitin on groups to populate
const contact = notebookContacts?.[ship];
const role = group ? roleForShip(group, window.ship) : undefined;
const isOwn = `~${window.ship}` === ship;
const relativePath = (p: string) => this.props.baseUrl + p;
const isWriter =
isOwn || group.tags?.publish?.[`writers-${book}`]?.has(window.ship);
const contact = notebookContacts?.[ship];
const role = group ? roleForShip(group, window.ship) : undefined;
const isOwn = `~${window.ship}` === ship;
const isAdmin = role === "admin" || isOwn;
const showNickname = contact?.nickname && !hideNicknames;
const isWriter =
isOwn || group.tags?.publish?.[`writers-${book}`]?.has(window.ship);
const notesList = notebook?.["notes-by-date"] || [];
const notes = notebook?.notes || {};
const showNickname = contact?.nickname && !hideNicknames;
return (
<Box
pt={4}
mx="auto"
px={3}
display="grid"
gridAutoRows="min-content"
gridTemplateColumns={["100%", "1fr 1fr"]}
maxWidth="500px"
gridRowGap={[4, 6]}
gridColumnGap={3}
>
<Box display={["block", "none"]} gridColumn={["1/2", "1/3"]}>
<Link to={this.props.rootUrl}>{"<- All Notebooks"}</Link>
</Box>
return (
<Col gapY="4" pt={4} mx="auto" px={3} maxWidth="500px">
<Row justifyContent="space-between">
<Box>
<Text> {notebook?.title}</Text>
<Text> {metadata?.title}</Text>
<br />
<Text color="lightGray">by </Text>
<Text fontFamily={showNickname ? "sans" : "mono"}>
{showNickname ? contact?.nickname : ship}
</Text>
</Box>
<Row justifyContent={["flex-start", "flex-end"]}>
{isWriter && (
<Link to={relativePath("/new")}>
<Button primary border style={{ cursor: 'pointer' }}>
New Post
</Button>
</Link>
)}
{!isOwn ? (
this.state.isUnsubscribing ? (
<Spinner
awaiting={this.state.isUnsubscribing}
classes="mt2 ml2"
text="Unsubscribing..."
/>
) : (
<Button
ml={isWriter ? 2 : 0}
destructive
style={{ cursor: 'pointer' }}
onClick={() => {
this.setState({ isUnsubscribing: true });
api.publish
.unsubscribeNotebook(deSig(ship), book)
.then(() => {
history.push(this.props.baseUrl);
})
.catch(() => {
this.setState({ isUnsubscribing: false });
});
}}
>
Unsubscribe
</Button>
)
) : null}
</Row>
<Box gridColumn={["1/2", "1/3"]}>
<Tabs>
<Tab
selected={state.tab}
setSelected={this.setTab}
label="All Posts"
id="all"
/>
<Tab
selected={state.tab}
setSelected={this.setTab}
label="About"
id="about"
/>
{isAdmin && (
<>
<Tab
selected={state.tab}
setSelected={this.setTab}
label="Subscribers"
id="subscribers"
/>
<Tab
selected={state.tab}
setSelected={this.setTab}
label="Settings"
id="settings"
/>
</>
)}
</Tabs>
{state.tab === "all" && (
<NotebookPosts
notes={notes}
list={notesList}
host={ship}
book={book}
contacts={notebookContacts}
hideNicknames={hideNicknames}
/>
)}
{state.tab === "about" && (
<Box mt="3" color="black">
{notebook?.about}
</Box>
)}
{state.tab === "subscribers" && (
<Subscribers
host={ship}
book={book}
notebook={notebook}
api={api}
groups={groups}
/>
)}
{state.tab === "settings" && (
<Settings
host={ship}
book={book}
api={api}
notebook={notebook}
contacts={notebookContacts}
associations={associations}
groups={groups}
baseUrl={this.props.baseUrl}
/>
)}
</Box>
</Box>
);
}
{isWriter && (
<Link to={relativePath("/new")}>
<Button primary style={{ cursor: "pointer" }}>
New Post
</Button>
</Link>
)}
</Row>
<Box borderBottom="1" borderBottomColor="washedGray" />
<NotebookPosts
graph={graph}
host={ship}
book={book}
contacts={!!notebookContacts ? notebookContacts : {}}
hideNicknames={hideNicknames}
baseUrl={props.baseUrl}
/>
</Col>
);
}
export default Notebook;

View File

@ -1,39 +1,34 @@
import React, { Component } from "react";
import { Col } from "@tlon/indigo-react";
import { Notes, NoteId } from "../../../../types/publish-update";
import { NotePreview } from "./NotePreview";
import { Contacts } from "../../../../types/contact-update";
import { Contacts, Graph } from "~/types";
interface NotebookPostsProps {
list: NoteId[];
contacts: Contacts;
notes: Notes;
graph: Graph;
host: string;
book: string;
baseUrl: string;
hideNicknames?: boolean;
baseUrl: string;
}
export function NotebookPosts(props: NotebookPostsProps) {
return (
<Col mt="3">
{props.list.map((noteId: NoteId) => {
const note = props.notes[noteId];
if (!note) {
return null;
}
return (
<NotePreview
key={noteId}
host={props.host}
book={props.book}
note={note}
contact={props?.contacts?.[note.author.substr(1)]}
hideNicknames={props.hideNicknames}
baseUrl={props.baseUrl}
/>
);
})}
<Col>
{Array.from(props.graph || []).map(
([date, node]) =>
node && (
<NotePreview
key={date.toString()}
host={props.host}
book={props.book}
contact={props.contacts[node.post.author]}
node={node}
hideNicknames={props.hideNicknames}
baseUrl={props.baseUrl}
/>
)
)}
</Col>
);
}

View File

@ -1,11 +1,18 @@
import React, { useEffect } from "react";
import { RouteComponentProps, Link, Route, Switch } from "react-router-dom";
import { Center, LoadingSpinner } from "@tlon/indigo-react";
import { RouteComponentProps, Route, Switch } from "react-router-dom";
import GlobalApi from "~/logic/api/global";
import {
Association,
Associations,
Graphs,
Groups,
Contacts,
Rolodex,
LocalUpdateRemoteContentPolicy
} from "~/types";
import { Center, LoadingSpinner } from "@tlon/indigo-react";
import { Notebook as INotebook } from "~/types/publish-update";
import { Groups } from "~/types/group-update";
import { Contacts, Rolodex } from "~/types/contact-update";
import { LocalUpdateRemoteContentPolicy, Associations } from "~/types";
import bigInt, { BigInteger } from 'big-integer';
import Notebook from "./Notebook";
import NewPost from "./new-post";
@ -17,14 +24,15 @@ interface NotebookRoutesProps {
api: GlobalApi;
ship: string;
book: string;
notebook: INotebook;
graphs: Graphs;
notebookContacts: Contacts;
contacts: Rolodex;
groups: Groups;
baseUrl?: string;
rootUrl?: string;
baseUrl: string;
rootUrl: string;
hideAvatars: boolean;
hideNicknames: boolean;
association: Association;
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
associations: Associations;
}
@ -32,15 +40,14 @@ interface NotebookRoutesProps {
export function NotebookRoutes(
props: NotebookRoutesProps & RouteComponentProps
) {
const { ship, book, api, notebook, notebookContacts } = props;
const { ship, book, api, notebookContacts, baseUrl, rootUrl } = props;
useEffect(() => {
api.publish.fetchNotesPage(ship, book, 1, 50);
api.publish.fetchNotebook(ship, book);
ship && book && api.graph.getGraph(ship, book);
}, [ship, book]);
const baseUrl = props.baseUrl || `/~404`;
const rootUrl = props.rootUrl || '/~404';
const graph = props.graphs[`${ship.slice(1)}/${book}`];
const relativePath = (path: string) => `${baseUrl}${path}`;
return (
@ -48,9 +55,15 @@ export function NotebookRoutes(
<Route
path={baseUrl}
exact
render={(routeProps) => {
return <Notebook {...props} rootUrl={rootUrl} baseUrl={baseUrl} />;
}}
render={(routeProps) => (
<Notebook
{...props}
graph={graph}
contacts={notebookContacts}
association={props.association}
rootUrl={rootUrl}
baseUrl={baseUrl} />
)}
/>
<Route
path={relativePath("/new")}
@ -60,7 +73,8 @@ export function NotebookRoutes(
api={api}
book={book}
ship={ship}
notebook={notebook}
association={props.association}
graph={graph}
baseUrl={baseUrl}
/>
)}
@ -69,11 +83,16 @@ export function NotebookRoutes(
path={relativePath("/note/:noteId")}
render={(routeProps) => {
const { noteId } = routeProps.match.params;
const note = notebook?.notes?.[noteId];
const noteUrl = relativePath(`/note/${noteId}`);
const noteIdNum = bigInt(noteId)
if(!graph) {
return <Center height="100%"><LoadingSpinner /></Center>;
}
const note = graph.get(noteIdNum);
if(!note) {
return <Center height="100%"><LoadingSpinner /></Center>;
}
const noteUrl = `${baseUrl}/note/${noteId}`;
return (
<NoteRoutes
rootUrl={baseUrl}
@ -81,9 +100,9 @@ export function NotebookRoutes(
api={api}
book={book}
ship={ship}
noteId={noteId}
notebook={notebook}
note={note}
notebook={graph}
noteId={noteIdNum}
contacts={notebookContacts}
hideAvatars={props.hideAvatars}
hideNicknames={props.hideNicknames}

View File

@ -1,67 +0,0 @@
import React, { useEffect } from "react";
import { Box, Col, Button, Label } from "@tlon/indigo-react";
import GlobalApi from "~/logic/api/global";
import { Notebook } from "~/types/publish-update";
import { Contacts } from "~/types/contact-update";
import { MetadataForm } from "./MetadataForm";
import { Groups, Associations } from "~/types";
import GroupifyForm from "./GroupifyForm";
import { useHistory } from "react-router-dom";
interface SettingsProps {
host: string;
book: string;
notebook: Notebook;
contacts: Contacts;
groups: Groups;
api: GlobalApi;
associations: Associations;
}
const Divider = (props) => (
<Box {...props} borderBottom={1} borderBottomColor="lightGray" />
);
export function Settings(props: SettingsProps) {
const history = useHistory();
const onDelete = async () => {
await props.api.publish.delBook(props.book);
history.push(this.props.baseUrl || '/~404');
};
const groupPath = props.notebook?.["writers-group-path"];
const isUnmanaged = props.groups?.[groupPath]?.hidden || false;
return (
<Box
mx="auto"
maxWidth="300px"
my={4}
gridTemplateColumns="1fr"
gridAutoRows="auto"
display="grid"
gridRowGap={5}
>
{isUnmanaged && (
<>
<GroupifyForm {...props} />
<Divider mt={4} />
</>
)}
<MetadataForm {...props} />
<Divider />
<Col mb={4}>
<Label>Delete Notebook</Label>
<Label gray mt="2">
Permanently delete this notebook. (All current members will no longer
see this notebook.)
</Label>
<Button mt="2" onClick={onDelete} destructive style={{ cursor: 'pointer' }}>
Delete this notebook
</Button>
</Col>
</Box>
);
}
export default Settings;

View File

@ -1,105 +0,0 @@
import React, { Component } from 'react';
import { GroupView } from '~/views/components/Group';
import { resourceFromPath, roleForShip } from '~/logic/lib/group';
import {Notebook} from '~/types/publish-update';
import GlobalApi from '~/logic/api/global';
import {Groups} from '~/types/group-update';
import {Associations} from '~/types/metadata-update';
import {Rolodex} from '~/types/contact-update';
import {Box, Button} from '@tlon/indigo-react';
interface SubscribersProps {
notebook: Notebook;
api: GlobalApi;
groups: Groups;
book: string;
associations: Associations;
contacts: Rolodex;
}
export class Subscribers extends Component<SubscribersProps> {
constructor(props) {
super(props);
this.redirect = this.redirect.bind(this);
this.addUser = this.addUser.bind(this);
this.removeUser = this.removeUser.bind(this);
this.addAll = this.addAll.bind(this);
}
addUser(who, path) {
this.props.api.groups.add(path, [who]);
}
removeUser(who, path) {
this.props.api.groups.remove(path, [who]);
}
redirect(url) {
window.location.href = url;
}
addAll() {
const path = this.props.notebook['writers-group-path'];
const group = path ? this.props.groups[path] : null;
const resource = resourceFromPath(path);
this.props.api.groups.addTag(
resource,
{ app: 'publish', tag: `writers-${this.props.book}` },
[...group.members].map(m => `~${m}`)
);
}
render() {
const path = this.props.notebook['writers-group-path'];
const group = path ? this.props.groups[path] : null;
const tags = [
{
description: 'Writer',
tag: `writers-${this.props.book}`,
addDescription: 'Make Writer',
app: 'publish',
},
];
const appTags = [
{
app: 'publish',
tag: `writers-${this.props.book}`,
desc: `Writer`,
addDesc: 'Allow user to write to this notebook'
},
];
if(!group) {
return null;
}
const role = roleForShip(group, window.ship)
return (
<Box mt="3">
{ role === 'admin' && (
<Button mb={3} border onClick={this.addAll} style={{ cursor: 'pointer' }}>
Add all members as writers
</Button>
)}
<GroupView
permissions
resourcePath={path}
group={group}
tags={tags}
appTags={appTags}
contacts={this.props.contacts}
groups={this.props.groups}
associations={this.props.associations}
api={this.props.api}
/>
</Box>
);
}
}
export default Subscribers;

View File

@ -1,22 +1,25 @@
import React from "react";
import { stringToSymbol } from "~/logic/lib/util";
import { FormikHelpers } from "formik";
import GlobalApi from "~/logic/api/global";
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import { Notebook } from "~/types/publish-update";
import { RouteComponentProps } from "react-router-dom";
import { PostForm, PostFormSchema } from "./NoteForm";
import {createPost} from "~/logic/api/graph";
import {Graph} from "~/types/graph-update";
import {Association} from "~/types";
import {newPost} from "~/logic/lib/publish";
interface NewPostProps {
api: GlobalApi;
book: string;
ship: string;
notebook: Notebook;
graph: Graph;
association: Association;
baseUrl: string;
}
export default function NewPost(props: NewPostProps & RouteComponentProps) {
const { api, book, notebook, ship, history } = props;
const { api, book, ship, history } = props;
const waiter = useWaitForProps(props, 20000);
@ -24,25 +27,11 @@ export default function NewPost(props: NewPostProps & RouteComponentProps) {
values: PostFormSchema,
actions: FormikHelpers<PostFormSchema>
) => {
let noteId = stringToSymbol(values.title);
const { title, body } = values;
const host = ship.slice(1);
try {
try {
await api.publish.newNote(host, book, noteId, title, body);
} catch (e) {
if (e.includes("note already exists")) {
const timestamp = Math.floor(Date.now() / 1000);
noteId = `${noteId}-${timestamp}`;
await api.publish.newNote(host, book, noteId, title, body);
} else {
throw e;
}
}
await waiter((p) => {
return !!p?.notebook?.notes[noteId];
});
const [noteId, nodes] = newPost(title, body)
await api.graph.addNodes(ship, book, nodes)
await waiter(p => p.graph.has(noteId));
history.push(`${props.baseUrl}/note/${noteId}`);
} catch (e) {
console.error(e);

View File

@ -15,7 +15,7 @@ import { uxToHex, hexToUx } from "~/logic/lib/util";
type ColorInputProps = Parameters<typeof Col>[0] & {
id: string;
label: string;
disabled: boolean;
disabled?: boolean;
};
export function ColorInput(props: ColorInputProps) {
@ -36,9 +36,10 @@ export function ColorInput(props: ColorInputProps) {
const result = hexToUx(newValue);
setValue(result);
};
return (
<Box display="flex" flexDirection="column" {...props}>
<Box display="flex" flexDirection="column" {...rest}>
<Label htmlFor={id}>{label}</Label>
{caption ? (
<Label mt="2" gray>

View File

@ -1,12 +1,13 @@
import React from 'react';
import { useLocation } from 'react-router-dom';
import { Row, Box, Text, Icon } from '@tlon/indigo-react';
import { Row, Box, Text, Icon, Button } from '@tlon/indigo-react';
import ReconnectButton from './ReconnectButton';
import { StatusBarItem } from './StatusBarItem';
import { Sigil } from '~/logic/lib/sigil';
const StatusBar = (props) => {
const metaKey = (window.navigator.platform.includes('Mac')) ? '⌘' : 'Ctrl+';
@ -17,13 +18,15 @@ const StatusBar = (props) => {
width="100%"
gridTemplateRows="30px"
gridTemplateColumns="3fr 1fr"
py={[2,3]}
px={[2,3]}
py='3'
px='3'
pb='2'
>
<Row collapse>
<StatusBarItem mr={2} onClick={() => props.history.push('/')}>
<Icon icon='Home' color='black'/>
</StatusBarItem>
<Button borderColor='washedGray' mr='2' px='2' onClick={() => props.history.push('/')} {...props}>
<Icon icon='Spaces' color='black'/>
</Button>
<StatusBarItem mr={2} onClick={() => props.api.local.setOmnibox()}>
{ !props.doNotDisturb && props.notificationsCount > 0 &&
(<Box display="block" right="-8px" top="-8px" position="absolute" >
@ -44,7 +47,8 @@ const StatusBar = (props) => {
/>
</Row>
<Row justifyContent="flex-end" collapse>
<StatusBarItem onClick={() => props.history.push('/~profile')}>
<StatusBarItem px={'2'} flexShrink='0' onClick={() => props.history.push('/~profile')}>
<Sigil ship={props.ship} size={16} color='black' classes='mix-blend-diff' icon />
<Text ml={2} display={["none", "inline"]} fontFamily="mono">~{props.ship}</Text>
</StatusBarItem>

View File

@ -22,7 +22,7 @@ export function StatusBarItem({
color="washedGray"
bg="white"
alignItems="center"
py={1}
height='32px'
px={2}
{...props}
>

View File

@ -22,16 +22,13 @@ interface UnjoinedResourceProps {
function isJoined(app: string, path: string) {
return function (
props: Pick<UnjoinedResourceProps, "inbox" | "graphKeys" | "notebooks">
props: Pick<UnjoinedResourceProps, "inbox" | "graphKeys">
) {
let ship, name;
const graphKey = path.substr(7);
switch (app) {
case "link":
return props.graphKeys.has(graphKey);
case "publish":
[, ship, name] = path.split("/");
return !!props.notebooks[ship]?.[name];
return props.graphKeys.has(graphKey);
case "chat":
return !!props.inbox[path];
default:
@ -53,14 +50,11 @@ export function UnjoinedResource(props: UnjoinedResourceProps) {
const onJoin = async () => {
let ship, name;
switch (app) {
case "publish":
case "link":
[, , ship, name] = appPath.split("/");
await api.graph.joinGraph(ship, name);
break;
case "publish":
[, ship, name] = appPath.split("/");
await api.publish.subscribeNotebook(ship.slice(1), name);
break;
case "chat":
[, ship, name] = appPath.split("/");
await api.chat.join(ship, appPath, true);
@ -73,7 +67,7 @@ export function UnjoinedResource(props: UnjoinedResourceProps) {
};
useEffect(() => {
if (isJoined(app, appPath)({ inbox, graphKeys, notebooks })) {
if (isJoined(app, appPath)({ inbox, graphKeys })) {
history.push(`${props.baseUrl}/resource/${app}${appPath}`);
}
}, [props.association, inbox, graphKeys, notebooks]);

View File

@ -244,7 +244,18 @@ export default class VirtualScroller extends PureComponent<VirtualScrollerProps,
element.addEventListener('wheel', (event) => {
event.preventDefault();
const normalized = normalizeWheel(event);
element.scrollBy(0, normalized.pixelY * -1);
if (
!event.target.isSameNode(element)
&& (event.target.scrollHeight > event.target.clientHeight && event.target.clientHeight > 0) // If we're scrolling something with a scrollbar
&& (
(event.target.scrollTop > 0 && event.deltaY < 0) // Either we're not at the top and scrolling up
|| (event.target.scrollTop < event.target.scrollHeight - event.target.clientHeight && event.deltaY > 0) // Or we're not at the bottom and scrolling down
)
) {
event.target.scrollBy(0, normalized.pixelY);
} else {
element.scrollBy(0, normalized.pixelY * -1);
}
return false;
}, { passive: false });
window.addEventListener('keydown', this.invertedKeyHandler, { passive: false });

View File

@ -61,9 +61,7 @@ export function ChannelMenu(props: ChannelMenuProps) {
await api.chat.delete(appPath);
break;
case "publish":
await api.publish.unsubscribeNotebook(ship.slice(1), name);
await api.publish.fetchNotebooks();
await api.graph.leaveGraph(ship, name);
break;
case "link":
await api.graph.leaveGraph(ship, name);
@ -81,7 +79,7 @@ export function ChannelMenu(props: ChannelMenuProps) {
await api.chat.delete(appPath);
break;
case "publish":
await api.publish.delBook(name);
await api.graph.deleteGraph(name);
break;
case "link":
await api.graph.deleteGraph(name);

View File

@ -32,8 +32,11 @@ function DeleteGroup(props: {
}) {
const history = useHistory();
const onDelete = async () => {
await props.api.contacts.delete(props.association["group-path"]);
history.push("/");
const name = props.association['group-path'].split('/').pop();
if (prompt(`To confirm deleting this group, type ${name}`) === name) {
await props.api.contacts.delete(props.association["group-path"]);
history.push("/");
}
};
const action = props.owner ? "Delete" : "Leave";

View File

@ -102,7 +102,8 @@ export function GroupSwitcher(props: {
width="100%"
alignItems="stretch"
>
<GroupSwitcherItem to="">
{(props.baseUrl === '/~landscape/home') ?
<GroupSwitcherItem to="">
<Icon
mr={2}
color="gray"
@ -111,6 +112,16 @@ export function GroupSwitcher(props: {
/>
<Text>All Groups</Text>
</GroupSwitcherItem>
:
<GroupSwitcherItem to="/~landscape/home">
<Icon
mr={2}
color="gray"
display="block"
icon="Circle"
/>
<Text>Home</Text>
</GroupSwitcherItem>}
<RecentGroups
recent={props.recentGroups}
associations={props.associations}

View File

@ -14,11 +14,8 @@ import { Skeleton } from "./Skeleton";
import { InvitePopover } from "./InvitePopover";
import { NewChannel } from "./NewChannel";
import { Resource as IResource, Groups } from "~/types/group-update";
import { Associations } from "~/types/metadata-update";
import { resourceAsPath } from "~/logic/lib/util";
import { appIsGraph } from "~/logic/lib/util";
import { AppName } from "~/types/noun";
import { Contacts, Rolodex } from "~/types/contact-update";
import GlobalApi from "~/logic/api/global";
import { StoreState } from "~/logic/store/type";
import { UnjoinedResource } from "~/views/components/UnjoinedResource";
@ -97,11 +94,11 @@ export function GroupsPane(props: GroupsPaneProps) {
string
>;
const appName = app as AppName;
const isShip = app === "link";
const isGraph = appIsGraph(app);
const resource = `${isShip ? "/ship" : ""}/${host}/${name}`;
const resource = `${isGraph ? "/ship" : ""}/${host}/${name}`;
const association =
appName === "link"
isGraph
? associations.graph[resource]
: associations[appName][resource];
const resourceUrl = `${baseUrl}/resource/${app}${resource}`;
@ -135,9 +132,9 @@ export function GroupsPane(props: GroupsPaneProps) {
render={(routeProps) => {
const { app, host, name } = routeProps.match.params;
const appName = app as AppName;
const isShip = app === "link";
const appPath = `${isShip ? '/ship/' : '/'}${host}/${name}`;
const association = isShip ? associations.graph[appPath] : associations[appName][appPath];
const isGraph = appIsGraph(app);
const appPath = `${isGraph ? '/ship/' : '/'}${host}/${name}`;
const association = isGraph ? associations.graph[appPath] : associations[appName][appPath];
const resourceUrl = `${baseUrl}/join/${app}${appPath}`;
if (!association) {
@ -198,7 +195,7 @@ export function GroupsPane(props: GroupsPaneProps) {
display={["none", "flex"]}
p='4'
>
<Box><Text fontSize="0" color='gray'>
<Box p="4"><Text fontSize="0" color='gray'>
{description}
</Text></Box>
</Col>

View File

@ -24,14 +24,14 @@ interface FormSchema {
name: string;
description: string;
ships: string[];
type: "chat" | "publish" | "link";
moduleType: "chat" | "publish" | "link";
}
const formSchema = Yup.object({
name: Yup.string().required('Channel must have a name'),
description: Yup.string(),
ships: Yup.array(Yup.string()),
type: Yup.string().required('Must choose channel type')
moduleType: Yup.string().required('Must choose channel type')
});
interface NewChannelProps {
@ -51,8 +51,8 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
const onSubmit = async (values: FormSchema, actions) => {
const resId: string = stringToSymbol(values.name);
try {
const { name, description, type, ships } = values;
switch (type) {
const { name, description, moduleType, ships } = values;
switch (moduleType) {
case 'chat':
const appPath = `/~${window.ship}/${resId}`;
const groupPath = group || `/ship${appPath}`;
@ -68,9 +68,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
false
);
break;
case 'publish':
await props.api.publish.newBook(resId, name, description, group);
break;
case "publish":
case "link":
if (group) {
await api.graph.createManagedGraph(
@ -78,19 +76,18 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
name,
description,
group,
'link'
moduleType
);
} else {
await api.graph.createUnmanagedGraph(
resId,
name,
description,
{ invite: { pending: ships.map(s => `~${s}`) } },
'link'
{ invite: { pending: ships.map((s) => `~${s}`) } },
moduleType
);
}
break;
default:
console.log('fallthrough');
}
@ -100,7 +97,10 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
}
actions.setStatus({ success: null });
const resourceUrl = parentPath(location.pathname);
history.push(`${resourceUrl}/resource/${type}${type === 'link' ? '/ship' : ''}/~${window.ship}/${resId}`);
history.push(
`${resourceUrl}/resource/${moduleType}` +
`${moduleType !== 'chat' ? '/ship' : ''}/~${window.ship}/${resId}`
);
} catch (e) {
console.error(e);
actions.setStatus({ error: 'Channel creation failed' });
@ -114,7 +114,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
<Formik
validationSchema={formSchema}
initialValues={{
type: 'chat',
moduleType: 'chat',
name: '',
description: '',
group: '',
@ -131,9 +131,9 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
>
<Col gapY="2">
<Box color="black" mb={2}>Channel Type</Box>
<Radio label="Chat" id="chat" name="type" />
<Radio label="Notebook" id="publish" name="type" />
<Radio label="Collection" id="link" name="type" />
<Radio label="Chat" id="chat" name="moduleType" />
<Radio label="Notebook" id="publish" name="moduleType" />
<Radio label="Collection" id="link" name="moduleType" />
</Col>
<Input
id="name"

View File

@ -348,23 +348,23 @@ function Participant(props: {
{props.role === 'admin' && (
<>
{!isInvite && (
<>
<StatelessAsyncAction onClick={onBan} bg="transparent">
<Text color="red">Ban from {title}</Text>
</StatelessAsyncAction>
<StatelessAsyncAction onClick={onKick} bg="transparent">
<Text color="red">Kick from {title}</Text>
</StatelessAsyncAction>
</>
)}
{role === 'admin' ? (
<StatelessAsyncAction onClick={onDemote} bg="transparent">
Demote from Admin
</StatelessAsyncAction>
) : (
<StatelessAsyncAction onClick={onPromote} bg="transparent">
Promote to Admin
</StatelessAsyncAction>
<>
<StatelessAsyncAction onClick={onKick} bg="transparent">
<Text color="red">Kick from {title}</Text>
</StatelessAsyncAction>
<StatelessAsyncAction onClick={onPromote} bg="transparent">
Promote to Admin
</StatelessAsyncAction>
</>
)}
</>
)}

View File

@ -38,43 +38,10 @@ export function useChat(
return { lastUpdated, getStatus };
}
export function usePublish(notebooks: Notebooks): SidebarAppConfig {
const getStatus = useCallback(
(s: string) => {
const [, host, name] = s.split("/");
const notebook = notebooks?.[host]?.[name];
if (!notebook) {
return "unsubscribed";
}
if (notebook["num-unread"]) {
return "unread";
}
return undefined;
},
[notebooks]
);
const lastUpdated = useCallback(
(s: string) => {
// we can't get publish timestamps without loading posts
// so we just return the number of unreads, this ensures
// that unread notebooks don't get lost on the bottom
const [, host, name] = s.split("/");
const notebook = notebooks?.[host]?.[name];
if(!notebook) {
return 0;
}
return notebook?.["num-unread"]+1;
},
[notebooks]
);
return { getStatus, lastUpdated };
}
export function useLinks(
export function useGraphModule(
graphKeys: Set<string>,
graphs: Graphs
graphs: Graphs,
): SidebarAppConfig {
const getStatus = useCallback(
(s: string) => {

View File

@ -58,7 +58,7 @@ const SidebarStickySpacer = styled(Box)`
const inviteItems = (invites, api) => {
const returned = [];
Object.keys(invites).filter((e) => {
return e !== '/contacts';
return e !== 'contacts';
}).map((appKey) => {
const app = invites[appKey];
Object.keys(app).map((uid) => {
@ -122,12 +122,12 @@ export function Sidebar(props: SidebarProps) {
workspace={props.workspace}
/>
<SidebarListHeader
contacts={props.contacts}
contacts={props.contacts}
baseUrl={props.baseUrl}
groups={props.groups}
initialValues={config}
handleSubmit={setConfig}
selected={selected || ""}
selected={selected || ""}
workspace={workspace} />
{sidebarInvites}
<SidebarList

View File

@ -24,12 +24,12 @@ function SidebarItemIndicator(props: { status?: SidebarItemStatus }) {
}
}
const getAppIcon = (app: string, module: string) => {
const getAppIcon = (app: string, mod: string) => {
if (app === "graph") {
if (module === "link") {
if (mod === "link") {
return "Links";
}
return _.capitalize(module);
return _.capitalize(mod);
}
return _.capitalize(app);
};
@ -58,10 +58,10 @@ export function SidebarItem(props: {
const { association, path, selected, apps, groups } = props;
const title = getItemTitle(association);
const appName = association?.["app-name"];
const module = association?.metadata?.module || appName;
const mod = association?.metadata?.module || appName;
const appPath = association?.["app-path"];
const groupPath = association?.["group-path"];
const app = apps[module];
const app = apps[appName];
const isUnmanaged = groups?.[groupPath]?.hidden || false;
if (!app) {
return null;
@ -74,8 +74,8 @@ export function SidebarItem(props: {
const baseUrl = isUnmanaged ? `/~landscape/home` : `/~landscape${groupPath}`;
const to = isSynced
? `${baseUrl}/resource/${module}${appPath}`
: `${baseUrl}/join/${module}${appPath}`;
? `${baseUrl}/resource/${mod}${appPath}`
: `${baseUrl}/join/${mod}${appPath}`;
const color = selected ? "black" : isSynced ? "gray" : "lightGray";
@ -101,7 +101,7 @@ export function SidebarItem(props: {
<Icon
display="block"
color={color}
icon={getAppIcon(appName, module) as any}
icon={getAppIcon(appName, mod) as any}
/>
<Box width='100%' flexShrink={2} ml={2} display='flex' overflow='hidden'>
<Text

View File

@ -20,11 +20,11 @@ function sidebarSort(
const lastUpdated = (a: string, b: string) => {
const aAssoc = associations[a];
const bAssoc = associations[b];
const aModule = aAssoc?.metadata?.module || aAssoc?.["app-name"];
const bModule = bAssoc?.metadata?.module || bAssoc?.["app-name"];
const aAppName = aAssoc?.["app-name"];
const bAppName = bAssoc?.["app-name"];
const aUpdated = apps[aModule].lastUpdated(a);
const bUpdated = apps[bModule].lastUpdated(b);
const aUpdated = apps[aAppName].lastUpdated(a);
const bUpdated = apps[bAppName].lastUpdated(b);
return bUpdated - aUpdated || alphabetical(a, b);
};

View File

@ -13,7 +13,7 @@ import { LinkCollections } from "~/types/link-update";
import styled from "styled-components";
import GlobalSubscription from "~/logic/subscription/global";
import { Workspace, Groups, Graphs, Invites, Rolodex } from "~/types";
import { useChat, usePublish, useLinks } from "./Sidebar/Apps";
import { useChat, useGraphModule } from "./Sidebar/Apps";
import { Body } from "~/views/components/Body";
interface SkeletonProps {
@ -43,15 +43,13 @@ interface SkeletonProps {
export function Skeleton(props: SkeletonProps) {
const chatConfig = useChat(props.inbox, props.chatSynced);
const publishConfig = usePublish(props.notebooks);
const linkConfig = useLinks(props.graphKeys, props.graphs);
const graphConfig = useGraphModule(props.graphKeys, props.graphs);
const config = useMemo(
() => ({
publish: publishConfig,
link: linkConfig,
graph: graphConfig,
chat: chatConfig,
}),
[publishConfig, linkConfig, chatConfig]
[graphConfig, chatConfig]
);
return (

View File

@ -28,9 +28,7 @@ export default class Landscape extends Component<LandscapeProps, {}> {
this.props.subscription.startApp('groups')
this.props.subscription.startApp('chat')
this.props.subscription.startApp('publish');
this.props.subscription.startApp('graph');
this.props.api.publish.fetchNotebooks();
}
createandRedirectToDM(api, ship, history, allStations) {