mirror of
https://github.com/urbit/shrub.git
synced 2024-11-24 13:06:09 +03:00
Merge branch 'release/next-userspace' into lf/hark-redux
This commit is contained in:
commit
e9d9bb839f
@ -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
3
package-lock.json
generated
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"lockfileVersion": 1
|
||||
}
|
@ -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
|
||||
|
@ -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]]~
|
||||
==
|
||||
::
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
@ -1,5 +1,5 @@
|
||||
/- spider
|
||||
/+ libstrand=strand, default-agent, verb, server
|
||||
/+ libstrand=strand, default-agent, verb, server
|
||||
=, strand=strand:libstrand
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
44
pkg/arvo/mar/graph/validator/publish.hoon
Normal file
44
pkg/arvo/mar/graph/validator/publish.hoon
Normal 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
|
||||
--
|
@ -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))
|
||||
--
|
||||
--
|
||||
--
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
==
|
||||
==
|
||||
--
|
||||
--
|
||||
|
@ -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)
|
||||
|
@ -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 ~)
|
||||
--
|
||||
::
|
||||
|
99
pkg/interface/package-lock.json
generated
99
pkg/interface/package-lock.json
generated
@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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],
|
||||
|
18
pkg/interface/src/logic/lib/post.ts
Normal file
18
pkg/interface/src/logic/lib/post.ts
Normal 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;
|
||||
}
|
105
pkg/interface/src/logic/lib/publish.ts
Normal file
105
pkg/interface/src/logic/lib/publish.ts
Normal 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}...`;
|
||||
}
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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 };
|
||||
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
};
|
||||
}
|
@ -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 */
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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} />
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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" });
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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 />
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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} />;
|
||||
}}
|
||||
/>
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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;
|
@ -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;
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -22,7 +22,7 @@ export function StatusBarItem({
|
||||
color="washedGray"
|
||||
bg="white"
|
||||
alignItems="center"
|
||||
py={1}
|
||||
height='32px'
|
||||
px={2}
|
||||
{...props}
|
||||
>
|
||||
|
@ -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]);
|
||||
|
@ -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 });
|
||||
|
@ -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);
|
||||
|
@ -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";
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
@ -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) => {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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 (
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user