Merge branch 'publish-remix-fe-encore' into os1-rc

This commit is contained in:
Matilde Park 2020-01-27 16:24:21 -05:00
commit 45f9adc10c
89 changed files with 54478 additions and 6663 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -16,7 +16,8 @@
==
::
;body
;div#root;
;div#header.w-100;
;div#root.w-100.h-100;
;script@"/~publish/index.js";
==
==

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,71 +2,6 @@
/+ elem-to-react-json
|%
::
++ front-to-post-info
|= fro=(map knot cord)
^- post-info
=/ got ~(got by fro)
~| %invalid-frontmatter
:* (slav %p (got %creator))
(got %title)
(got %collection)
(got %filename)
(comment-config (got %comments))
(slav %da (got %date-created))
(slav %da (got %last-modified))
(rash (got %pinned) (fuss %true %false))
==
::
++ front-to-comment-info
|= fro=(map knot cord)
^- comment-info
=/ got ~(got by fro)
~| %invalid-frontmatter
:* (slav %p (got %creator))
(got %collection)
(got %post)
(slav %da (got %date-created))
(slav %da (got %last-modified))
==
::
++ collection-info-to-json
|= con=collection-info
^- json
%- pairs:enjs:format
:~ :- %owner [%s (scot %p owner.con)]
:- %title [%s title.con]
:- %comments [%s comments.con]
:- %allow-edit [%s allow-edit.con]
:- %date-created (time:enjs:format date-created.con)
:- %last-modified (time:enjs:format last-modified.con)
:- %filename [%s filename.con]
==
::
++ post-info-to-json
|= info=post-info
^- json
%- pairs:enjs:format
:~ :- %creator [%s (scot %p creator.info)]
:- %title [%s title.info]
:- %comments [%s comments.info]
:- %date-created (time:enjs:format date-created.info)
:- %last-modified (time:enjs:format last-modified.info)
:- %pinned [%b pinned.info]
:- %filename [%s filename.info]
:- %collection [%s collection.info]
==
::
++ comment-info-to-json
|= info=comment-info
^- json
%- pairs:enjs:format
:~ :- %creator [%s (scot %p creator.info)]
:- %date-created (time:enjs:format date-created.info)
:- %last-modified (time:enjs:format last-modified.info)
:- %post [%s post.info]
:- %collection [%s collection.info]
==
::
++ tang-to-json
|= tan=tang
%- wall:enjs:format
@ -89,81 +24,222 @@
(add 32 a)
'-'
::
++ collection-build-to-json
|= bud=(each collection-info tang)
++ note-build-to-json
|= build=(each manx tang)
^- json
?: ?=(%.y -.bud)
(collection-info-to-json +.bud)
(tang-to-json +.bud)
::
++ post-build-to-json
|= bud=(each [post-info manx @t] tang)
^- json
?: ?=(%.y -.bud)
?: ?=(%.y -.build)
%- pairs:enjs:format
:~ info+(post-info-to-json +<.bud)
body+(elem-to-react-json +>-.bud)
raw+[%s +>+.bud]
:~ success+b+%.y
result+(elem-to-react-json p.build)
==
(tang-to-json +.bud)
::
++ comment-build-to-json
|= bud=(each (list [comment-info @t]) tang)
^- json
?: ?=(%.y -.bud)
:- %a
%+ turn p.bud
|= [com=comment-info bod=@t]
^- json
%- pairs:enjs:format
:~ info+(comment-info-to-json com)
body+s+bod
==
(tang-to-json +.bud)
::
++ total-build-to-json
|= col=collection
^- json
%- pairs:enjs:format
:~ info+(collection-build-to-json col.col)
::
:+ %posts
%o
%+ roll ~(tap in ~(key by pos.col))
|= [post=@tas out=(map @t json)]
=/ post-build (~(got by pos.col) post)
=/ comm-build (~(got by com.col) post)
%+ ~(put by out)
post
%- pairs:enjs:format
:~ post+(post-build-to-json post-build)
comments+(comment-build-to-json comm-build)
==
::
:- %order
%- pairs:enjs:format
:~ pin+a+(turn pin.order.col |=(s=@tas [%s s]))
unpin+a+(turn unpin.order.col |=(s=@tas [%s s]))
==
::
:- %contributors
%- pairs:enjs:format
:~ mod+s+mod.contributors.col
:+ %who
%a
%+ turn ~(tap in who.contributors.col)
|= who=@p
(ship:enjs:format who)
==
::
:+ %subscribers
%a
%+ turn ~(tap in subscribers.col)
|= who=@p
:~ success+b+%.n
result+(tang-to-json p.build)
==
::
++ count-unread
|= notes=(map @tas note)
^- @ud
%- ~(rep by notes)
|= [[key=@tas val=note] count=@ud]
?: read.val
count
+(count)
::
++ notebooks-list-json
|= [our=@p books=(map @tas notebook) subs=(map [@p @tas] notebook)]
^- json
=, enjs:format
:- %a
%+ weld
%+ turn ~(tap by books)
|= [name=@tas book=notebook]
(notebook-short-json book)
%+ turn ~(tap by subs)
|= [[host=@p name=@tas] book=notebook]
(notebook-short-json book)
::
++ notebooks-map-json
|= [our=@p books=(map @tas notebook) subs=(map [@p @tas] notebook)]
^- json
=, enjs:format
=/ subs-notebooks-map=json
%- ~(rep by subs)
|= [[[host=@p book-name=@tas] book=notebook] out=json]
^- json
(ship:enjs:format who)
::
[%last-update (time:enjs:format last-update.col)]
=/ host-ta (scot %p host)
?~ out
(frond host-ta (frond book-name (notebook-short-json book)))
?> ?=(%o -.out)
=/ books (~(get by p.out) host-ta)
?~ books
:- %o
(~(put by p.out) host-ta (frond book-name (notebook-short-json book)))
?> ?=(%o -.u.books)
=. p.u.books (~(put by p.u.books) book-name (notebook-short-json book))
:- %o
(~(put by p.out) host-ta u.books)
=? subs-notebooks-map ?=(~ subs-notebooks-map)
[%o ~]
=/ our-notebooks-map=json
%- ~(rep by books)
|= [[book-name=@tas book=notebook] out=json]
^- json
?~ out
(frond book-name (notebook-short-json book))
?> ?=(%o -.out)
:- %o
(~(put by p.out) book-name (notebook-short-json book))
?~ our-notebooks-map
subs-notebooks-map
?> ?=(%o -.subs-notebooks-map)
:- %o
(~(put by p.subs-notebooks-map) (scot %p our) our-notebooks-map)
::
++ notebook-short-json
|= book=notebook
^- json
=, enjs:format
%- pairs
:~ title+s+title.book
date-created+(time date-created.book)
num-notes+(numb ~(wyt by notes.book))
num-unread+(numb (count-unread notes.book))
==
::
++ notebook-full-json
|= [host=@p book-name=@tas book=notebook]
^- json
=, enjs:format
%- pairs
:~ title+s+title.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-json
|= [book=notebook note-name=@tas not=note]
^- (map @t json)
=, enjs:format
=/ 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-json 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-json name.u.prev not.u.prev)] notes]
=? notes ?=(^ next)
[[name.u.next (note-short-json name.u.next not.u.next)] notes]
%- my
:~ notes+(pairs notes)
notes-by-date+a+(turn notes-list |=([name=@tas *] s+name))
==
::
++ note-full-json
|= [note-name=@tas =note]
^- json
=, enjs:format
%- pairs
:~ note-id+s+note-name
author+s+(scot %p author.note)
title+s+title.note
date-created+(time date-created.note)
build+(note-build-to-json build.note)
file+s+file.note
num-comments+(numb ~(wyt by comments.note))
comments+(comments-page comments.note 0 50)
read+b+read.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-json
|= [note-name=@tas =note]
^- json
=, enjs:format
%- 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
:: XX snippet
==
::
++ 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-json (scag length (slag start notes-list)))
==
::
++ notes-list-json
|= 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-json 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-json
(scag end (slag start coms))
::
++ comments-list-json
|= comments=(list [@da comment])
^- json
=, enjs:format
:- %a
(turn comments comment-json)
::
++ comment-json
|= [date=@da com=comment]
^- json
=, enjs:format
%+ frond:enjs:format
(scot %da date)
%- pairs
:~ author+s+(scot %p author.com)
date-created+(time date-created.com)
content+s+content.com
==
--

View File

@ -1,11 +1,10 @@
::
:::: /hoon/action/publish/mar
::
/? 309
/- publish
/- *publish
=, format
::
|_ act=action:publish
|_ act=action
::
++ grow
|%
@ -14,179 +13,113 @@
::
++ grab
|%
++ noun action:publish
++ noun action
++ json
|= jon=^json
%- action:publish
=< (action jon)
|%
++ action
%- of:dejs
:~ new-collection+new-collection
new-post+new-post
new-comment+new-comment
::
delete-collection+delete-collection
delete-post+delete-post
delete-comment+delete-comment
::
edit-collection+edit-collection
edit-post+edit-post
::
invite+invite
reject-invite+reject-invite
::
serve+serve
unserve+unserve
::
subscribe+subscribe
unsubscribe+unsubscribe
::
read+read
=, 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
==
::
++ new-book
%- ot
:~ book+so
title+so
about+so
coms+bo
group+group-info
==
::
++ new-collection
%- ot:dejs
:~ name+so:dejs
title+so:dejs
comments+comment-config
allow-edit+edit-config
perm+perm-config
==
::
++ new-post
%- ot:dejs
:~ who+(su:dejs fed:ag)
coll+so:dejs
name+so:dejs
title+so:dejs
comments+comment-config
perm+perm-config
content+so:dejs
++ new-note
%- ot
:~ who+(su fed:ag)
book+so
note+so
title+so
body+so
==
::
++ new-comment
%- ot:dejs
:~ who+(su:dejs fed:ag)
coll+so:dejs
name+(su:dejs sym)
content+so:dejs
%- ot
:~ who+(su fed:ag)
book+so
note+so
body+so
==
::
++ delete-collection
%- ot:dejs
:~ coll+so:dejs
++ edit-book
%- ot
:~ book+so
title+so
about+so
coms+bo
group+(mu group-info)
==
::
++ delete-post
%- ot:dejs
:~ coll+so:dejs
post+so:dejs
==
::
++ delete-comment
%- ot:dejs
:~ coll+so:dejs
post+so:dejs
comment+so:dejs
==
::
++ edit-collection
%- ot:dejs
:~ name+so:dejs
title+so:dejs
==
::
++ edit-post
%- ot:dejs
:~ who+(su:dejs fed:ag)
coll+so:dejs
name+so:dejs
title+so:dejs
comments+comment-config
perm+perm-config
content+so:dejs
++ edit-note
%- ot
:~ who+(su fed:ag)
book+so
note+so
title+so
body+so
==
::
++ edit-comment
%- ot:dejs
:~ coll+so:dejs
name+so:dejs
id+so:dejs
content+so:dejs
%- ot
:~ who+(su fed:ag)
book+so
note+so
comment+(su ;~(pfix sig (cook year when:^so)))
body+so
==
::
++ comment-config
%- su:dejs
;~(pose (jest %open) (jest %closed) (jest %none))
++ del-book (ot book+so ~)
::
++ edit-config
%- su:dejs
;~(pose (jest %post) (jest %comment) (jest %all) (jest %none))
++ del-note (ot who+(su fed:ag) book+so note+so ~)
::
++ perm-config
%- ot:dejs
:~ :- %read
%- ot:dejs
:~ mod+(su:dejs ;~(pose (jest %black) (jest %white)))
who+whoms
==
:- %write
%- ot:dejs
:~ mod+(su:dejs ;~(pose (jest %black) (jest %white)))
who+whoms
== ==
::
++ whoms
|= jon=^json
^- (set whom:clay)
=/ x ((ar:dejs (su:dejs fed:ag)) jon)
%- (set whom:clay)
%- ~(run in (sy x))
|=(w=@ [& w])
::
++ invite
%- ot:dejs
:~ coll+so:dejs
title+so:dejs
who+(ar:dejs (su:dejs fed:ag))
++ del-comment
%- ot
:~ who+(su fed:ag)
book+so
note+so
comment+(su ;~(pfix sig (cook year when:^so)))
==
::
++ reject-invite
%- ot:dejs
:~ who+(su:dejs fed:ag)
coll+so:dejs
==
::
++ serve
%- ot:dejs
:~ coll+so:dejs
==
::
++ unserve
%- ot:dejs
:~ coll+so:dejs
==
::
++ subscribe
%- ot:dejs
:~ who+(su:dejs fed:ag)
coll+so:dejs
%- ot
:~ who+(su fed:ag)
book+so
==
::
++ unsubscribe
%- ot:dejs
:~ who+(su:dejs fed:ag)
coll+so:dejs
%- ot
:~ who+(su fed:ag)
book+so
==
::
++ read
%- ot:dejs
:~ who+(su:dejs fed:ag)
coll+so:dejs
post+so:dejs
%- ot
:~ who+(su fed:ag)
book+so
note+so
==
::
++ group-info
%- of
:~ old+(ot writers+pa subscribers+pa ~)
new+(ot writers+set-ship subscribers+set-ship sec+so ~)
==
++ set-ship (ar (su fed:ag))
--
--
--

View File

@ -1,6 +1,5 @@
/- publish
!:
|_ com=comment:publish
/- *publish
|_ com=comment
::
::
++ grow
@ -10,59 +9,46 @@
(as-octs:mimes:html (of-wain:format txt))
++ txt
^- wain
:* (cat 3 'creator: ' (scot %p creator.info.com))
(cat 3 'collection: ' collection.info.com)
(cat 3 'post: ' post.info.com)
(cat 3 'date-created: ' (scot %da date-created.info.com))
(cat 3 'last-modified: ' (scot %da last-modified.info.com))
:* (cat 3 'author: ' (scot %p author.com))
(cat 3 'date-created: ' (scot %da date-created.com))
'-----'
(to-wain:format body.com)
(to-wain:format content.com)
==
--
++ grab
|%
++ mime
|= [mite:eyre p=octs:eyre]
(txt (to-wain:format q.p))
++ txt
|= txs=(pole @t)
^- comment:publish
:: TODO: putting ~ instead of * breaks this but shouldn't
::
?> ?= $: creator=@t
collection=@t
post=@t
date-created=@t
last-modified=@t
line=@t
body=*
==
txs
:_ (of-wain:format (wain body.txs))
::
:* %+ rash creator.txs
;~(pfix (jest 'creator: ~') fed:ag)
::
%+ rash collection.txs
;~(pfix (jest 'collection: ') (cook crip (star next)))
::
%+ rash post.txs
;~(pfix (jest 'post: ') (cook crip (star next)))
::
%+ rash date-created.txs
;~ pfix
(jest 'date-created: ~')
(cook year when:so)
==
::
%+ rash last-modified.txs
;~ pfix
(jest 'last-modified: ~')
(cook year when:so)
==
::
==
++ noun comment:publish
|^ (rash q.p both-parser)
++ key-val
|* [key=rule val=rule]
;~(sfix ;~(pfix key val) gaq)
++ old-parser
;~ plug
(key-val (jest 'creator: ~') fed:ag)
(key-val (jest 'collection: ') sym)
(key-val (jest 'post: ') sym)
(key-val (jest 'date-created: ~') (cook year when:so))
(key-val (jest 'last-modified: ~') (cook year when:so))
;~(pfix (jest (cat 3 '-----' 10)) (cook crip (star next)))
==
++ new-parser
;~ plug
(key-val (jest 'author: ~') fed:ag)
(key-val (jest 'date-created: ~') (cook year when:so))
;~(pfix (jest (cat 3 '-----' 10)) (cook crip (star next)))
==
++ both-parser
;~ pose
new-parser
%+ cook
|= [author=@ @ @ date-created=@da @ content=@t]
^- comment
[author date-created content]
old-parser
==
--
++ noun comment
--
++ grad %mime
--

View File

@ -1,9 +1,9 @@
::
:::: /hoon/info/publish/mar
::
/- publish
/- *publish
!:
|_ con=collection-info:publish
|_ info=notebook-info
::
::
++ grow
@ -13,72 +13,59 @@
(as-octs:mimes:html (of-wain:format txt))
++ txt
^- wain
:~ (cat 3 'owner: ' (scot %p owner.con))
(cat 3 'title: ' title.con)
(cat 3 'filename: ' filename.con)
(cat 3 'comments: ' comments.con)
(cat 3 'allow-edit: ' allow-edit.con)
(cat 3 'date-created: ' (scot %da date-created.con))
(cat 3 'last-modified: ' (scot %da last-modified.con))
:~ (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
|= [mite:eyre p=octs:eyre]
(txt (to-wain:format q.p))
++ txt
|= txs=(pole @t)
^- collection-info:publish
:: TODO: putting ~ instead of * breaks this but shouldn't
::
?> ?= $: owner=@t
title=@t
filename=@t
comments=@t
allow-edit=@t
date-created=@t
last-modified=@t
*
==
txs
::
:* %+ rash owner.txs
;~(pfix (jest 'owner: ~') fed:ag)
::
%+ rash title.txs
;~(pfix (jest 'title: ') (cook crip (star next)))
::
%+ rash filename.txs
;~(pfix (jest 'filename: ') (cook crip (star next)))
::
%+ rash comments.txs
;~ pfix
(jest 'comments: ')
%+ cook comment-config:publish
;~(pose (jest %open) (jest %closed) (jest %none))
|^ (rash q.p both-parser)
++ key-val
|* [key=rule val=rule]
;~(sfix ;~(pfix key val) gaq)
++ old-parser
;~ plug
(key-val (jest 'owner: ~') fed:ag)
(key-val (jest 'title: ') (cook crip (star qit)))
(key-val (jest 'filename: ') sym)
%+ key-val (jest 'comments: ')
;~(pose (jest %open) (jest %closed) (jest %none))
%+ key-val (jest 'allow-edit: ')
;~(pose (jest %post) (jest %comment) (jest %all) (jest %none))
(key-val (jest 'date-created: ~') (cook year when:so))
;~ pose
(key-val (jest 'last-modified: ~') (cook year when:so))
;~(pfix (jest 'last-modified: ~') (cook year when:so))
==
==
::
%+ rash allow-edit.txs
;~ pfix
(jest 'allow-edit: ')
%+ cook edit-config:publish
;~(pose (jest %post) (jest %comment) (jest %all) (jest %none))
++ new-parser
;~ plug
(key-val (jest 'title: ') (cook crip (star qit)))
(key-val (jest 'description: ') (cook crip (star qit)))
%+ key-val (jest 'comments: ')
(cook |=(a=@ =(%on a)) ;~(pose (jest %on) (jest %off)))
(key-val (jest 'writers: ') ;~(pfix net (more net urs:ab)))
;~ pose
(key-val (jest 'subscribers: ') ;~(pfix net (more net urs:ab)))
;~(pfix (jest 'subscribers: ') ;~(pfix net (more net urs:ab)))
==
==
::
%+ rash date-created.txs
;~ pfix
(jest 'date-created: ~')
(cook year when:so)
++ both-parser
;~ pose
new-parser
%+ cook
|= [@ title=@t @ comments=@ *]
^- notebook-info
[title '' =('open' comments) / /]
old-parser
==
::
%+ rash last-modified.txs
;~ pfix
(jest 'last-modified: ~')
(cook year when:so)
==
==
++ noun collection-info:publish
--
++ noun notebook-info
--
++ grad %mime
--

View File

@ -0,0 +1,13 @@
::
:::: /hoon/action/publish/mar
::
/- *publish
=, format
::
|_ del=notebook-delta
::
++ grab
|%
++ noun notebook-delta
--
--

View File

@ -0,0 +1,83 @@
::
:::: /hoon/action/publish/mar
::
/- *publish
/+ *publish
::
|_ del=primary-delta
::
++ grab
|%
++ noun primary-delta
--
++ grow
|%
++ json
%+ frond:enjs:format -.del
?- -.del
%add-book
%+ frond:enjs:format (scot %p host.del)
%+ frond:enjs:format book.del
(notebook-short-json data.del)
::
%add-note
%+ frond:enjs:format (scot %p host.del)
%+ frond:enjs:format book.del
(note-full-json 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-json comment-date.del data.del)
==
::
%edit-book
%+ frond:enjs:format (scot %p host.del)
%+ frond:enjs:format book.del
(notebook-short-json data.del)
::
%edit-note
%+ frond:enjs:format (scot %p host.del)
%+ frond:enjs:format book.del
(note-full-json 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-json comment-date.del data.del)
==
::
%del-book
%- pairs:enjs:format
:~ host+s+(scot %p host.del)
book+s+book.del
==
::
%del-note
%- pairs:enjs:format
:~ host+s+(scot %p host.del)
book+s+book.del
note+s+note.del
==
::
%del-comment
%- pairs:enjs:format
:~ host+s+(scot %p host.del)
book+s+book.del
note+s+note.del
comment+s+(scot %da comment.del)
==
::
%read
%- pairs:enjs:format
:~ host+s+(scot %p who.del)
book+s+book.del
note+s+note.del
==
==
--
--

View File

@ -1,55 +0,0 @@
/- *publish
/+ *publish
|_ rum=rumor
++ grab
|%
++ noun rumor
--
++ grow
|%
++ noun rum
++ json
=, enjs:format
%+ frond -.rum
?- -.rum
%collection
%- pairs
:~ [%coll s+col.rum]
[%who (ship who.rum)]
[%data (collection-build-to-json dat.rum)]
==
::
%post
%- pairs
:~ [%coll s+col.rum]
[%post s+pos.rum]
[%who (ship who.rum)]
[%data (post-build-to-json dat.rum)]
==
::
%comments
%- pairs
:~ [%coll s+col.rum]
[%post s+pos.rum]
[%who (ship who.rum)]
[%data (comment-build-to-json dat.rum)]
==
::
%total
%- pairs
:~ [%coll s+col.rum]
[%who (ship who.rum)]
[%data (total-build-to-json dat.rum)]
==
::
%remove
%- pairs
:~ [%who (ship who.rum)]
[%coll s+col.rum]
[%post ?~(pos.rum ~ s+u.pos.rum)]
==
::
==
::
--
--

View File

@ -1,41 +0,0 @@
/- *publish
|_ upd=update
++ grab
|%
++ noun update
--
++ grow
|%
++ noun upd
++ json
=, enjs:format
%+ frond -.upd
::
?- -.upd
%invite
%- pairs
:~ [%who (ship who.upd)]
[%add b+add.upd]
[%coll s+col.upd]
[%title s+title.upd]
==
::
%unread
%- pairs
:~ [%add b+add.upd]
:+ %posts
%a
%+ turn ~(tap in keys.upd)
|= [who=@p coll=@tas post=@tas]
^- ^json
%- pairs
:~ [%who (ship who)]
[%coll s+coll]
[%post s+post]
==
==
::
==
::
--
--

View File

@ -1,121 +1,103 @@
/- *rw-security
|%
::
+$ action
$% $: %new-collection
name=@tas
title=@t
com=comment-config
edit=edit-config
perm=perm-config
==
::
$: %new-post
who=@p
coll=@tas
name=@tas
title=@t
com=comment-config
perm=perm-config
content=@t
==
::
[%new-comment who=@p coll=@tas post=@tas content=@t]
::
[%delete-collection coll=@tas]
[%delete-post coll=@tas post=@tas]
[%delete-comment coll=@tas post=@tas comment=@tas]
::
[%edit-collection name=@tas title=@t]
::
$: %edit-post
who=@p
coll=@tas
name=@tas
title=@t
com=comment-config
perm=perm-config
content=@t
==
::
[%invite coll=@tas title=@t who=(list ship)]
[%reject-invite who=@p coll=@tas]
::
[%serve coll=@tas]
[%unserve coll=@tas]
::
[%subscribe who=@p coll=@tas]
[%unsubscribe who=@p coll=@tas]
::
[%read who=@p coll=@tas post=@tas]
+$ group-info
$% [%old writers=path subscribers=path]
[%new writers=(set ship) subscribers=(set ship) sec=rw-security]
==
::
+$ collection-info
+$ action
$% [%new-book book=@tas title=@t about=@t coms=? group=group-info]
[%new-note who=@p book=@tas note=@tas title=@t body=@t]
[%new-comment who=@p book=@tas note=@tas body=@t]
::
[%edit-book book=@tas title=@t about=@t coms=? group=(unit group-info)]
[%edit-note who=@p book=@tas note=@tas title=@t body=@t]
[%edit-comment who=@p book=@tas note=@tas comment=@tas body=@t]
::
[%del-book book=@tas]
[%del-note who=@p book=@tas note=@tas]
[%del-comment who=@p book=@tas note=@tas comment=@tas]
::
[%subscribe who=@p book=@tas]
[%unsubscribe who=@p book=@tas]
::
[%read who=@p book=@tas note=@tas]
==
::
+$ comment
$: author=@p
date-created=@da
content=@t
==
::
+$ note
$: author=@p
title=@t
filename=@tas
date-created=@da
last-edit=@da
read=?
file=@t
build=(each manx tang)
comments=(map @da comment)
==
::
+$ notebook
$: title=@t
description=@t
comments=?
writers=path
subscribers=path
date-created=@da
notes=(map @tas note)
order=(list @tas)
unread=(set @tas)
==
::
+$ notebook-info
$: title=@t
description=@t
comments=?
writers=path
subscribers=path
==
::
+$ old-info
$: owner=@p
title=@t
filename=@tas
comments=comment-config
allow-edit=edit-config
comments=?(%open %closed %none)
allow-edit=?(%post %comment %all %none)
date-created=@da
last-modified=@da
==
::
+$ post-info
$: creator=@p
title=@t
collection=@tas
filename=@tas
comments=comment-config
date-created=@da
last-modified=@da
pinned=?
+$ old-comment
$: $: creator=@p
collection=@tas
post=@tas
date-created=@da
last-modified=@da
==
content=@t
==
::
+$ comment-info
$: creator=@p
collection=@tas
post=@tas
date-created=@da
last-modified=@da
+$ notebook-delta
$% [%add-book host=@p book=@tas data=notebook]
[%add-note host=@p book=@tas note=@tas data=note]
[%add-comment host=@p book=@tas note=@tas comment-date=@da data=comment]
::
[%edit-book host=@p book=@tas data=notebook]
[%edit-note host=@p book=@tas note=@tas data=note]
[%edit-comment host=@p book=@tas note=@tas comment-date=@da data=comment]
::
[%del-book host=@p book=@tas]
[%del-note host=@p book=@tas note=@tas]
[%del-comment host=@p book=@tas note=@tas comment=@da]
==
::
+$ comment [info=comment-info body=@t]
::
+$ perm-config [read=rule:clay write=rule:clay]
::
+$ comment-config $?(%open %closed %none)
::
+$ edit-config $?(%post %comment %all %none)
::
+$ rumor delta
::
+$ publish-dir (map path publish-file)
::
+$ publish-file
$% [%udon @t]
[%publish-info collection-info]
[%publish-comment comment]
==
::
+$ collection
$: col=(each collection-info tang)
pos=(map @tas dat=(each [post-info manx @t] tang))
com=(map @tas dat=(each (list [comment-info @t]) tang))
order=[pin=(list @tas) unpin=(list @tas)]
contributors=[mod=?(%white %black) who=(set @p)]
subscribers=(set @p)
last-update=@da
==
::
+$ delta
$% [%collection who=@p col=@tas dat=(each collection-info tang)]
[%post who=@p col=@tas pos=@tas dat=(each [post-info manx @t] tang)]
[%comments who=@p col=@tas pos=@tas dat=(each (list comment) tang)]
[%total who=@p col=@tas dat=collection]
[%remove who=@p col=@tas pos=(unit @tas)]
==
::
+$ update
$% [%invite add=? who=@p col=@tas title=@t]
[%unread add=? keys=(set [who=@p coll=@tas post=@tas])]
+$ primary-delta
$% notebook-delta
[%read who=@p book=@tas note=@tas]
==
--

View File

@ -1791,8 +1791,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
@ -1813,14 +1812,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -1835,20 +1832,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@ -1965,8 +1959,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@ -1978,7 +1971,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -1993,7 +1985,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -2001,14 +1992,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -2027,7 +2016,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -2108,8 +2096,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@ -2121,7 +2108,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -2207,8 +2193,7 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
@ -2244,7 +2229,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -2264,7 +2248,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -2308,14 +2291,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
}
}
},

View File

@ -1,281 +1,70 @@
html, body {
height: 100%;
width: 100%;
-webkit-font-smoothing: antialiased;
overflow: hidden;
font-family: "Inter", sans-serif;
}
p, h1, h2, h3, h4, h5, h6, a, input, textarea, button {
margin-block-end: unset;
margin-block-start: unset;
-webkit-margin-before: unset;
-webkit-margin-after: unset;
font-family: Inter, sans-serif;
}
button {
background: none;
color: inherit;
border: none;
cursor: pointer;
outline: inherit;
padding: 0;
}
p {
font-size: 16px;
line-height: 24px;
}
pre {
padding: 8px;
background-color: #f9f9f9;
}
code {
padding: 8px;
background-color: #f9f9f9;
white-space: pre-wrap;
}
a {
color: inherit;
text-decoration: inherit;
}
textarea, select, input, button { outline: none; }
h1 {
font-size: 48px;
line-height: 64px;
font-weight: bold;
textarea, input, button {
outline: none;
-webkit-appearance: none;
border: none;
background-color: #fff;
}
h2 {
font-size: 32px;
line-height: 48px;
font-weight: bold;
}
h3 {
font-size: 24px;
line-height: 32px;
font-weight: bold;
}
h4 {
font-size: 20px;
line-height: 32px;
font-weight: bold;
}
.header-2 {
font-size: 32px;
line-height: 48px;
font-weight: bold;
}
.body-regular {
font-size: 16px;
line-height: 24px;
font-weight: 600;
}
.body-large {
font-size: 20px;
line-height: 24px;
}
.label-regular {
font-size: 14px;
line-height: 24px;
}
.label-regular-mono {
font-size: 14px;
line-height: 24px;
font-family: "Source Code Pro", monospace;
}
.label-small-mono {
font-size: 12px;
line-height: 24px;
font-family: "Source Code Pro", monospace;
}
.label-small {
font-size: 12px;
line-height: 24px;
}
.label-small-2 {
font-size: 12px;
line-height: 16px;
}
.body-regular-400 {
font-size: 16px;
line-height: 24px;
font-weight: 400;
}
.plus-font {
font-size: 48px;
line-height: 24px;
a {
color: #000;
text-decoration: none;
}
.fw-bold {
font-weight: bold;
.inter {
font-family: Inter, sans-serif;
}
.bg-v-light-gray {
background-color: #f9f9f9;
.mono {
font-family: 'Source Code Pro', monospace;
}
.gray-50 {
color: #7F7F7F;
@media all and (max-width: 34.375em) {
.dn-s {
display: none;
}
.flex-basis-100-s, .flex-basis-full-s {
flex-basis: 100%;
}
.h-100-m-40-s {
height: calc(100% - 40px);
}
.black-s {
color: #000;
}
}
.gray-30 {
color: #B1B2B3;
}
.gray-10 {
color: #E6E6E6;
}
.green {
color: #2AA779;
}
.green-medium {
color: #2ED196;
}
.red {
color: #EE5432;
}
.w-336 {
width: 336px;
}
.w-688 {
width: 688px;
}
.mw-336 {
max-width: 336px;
}
.mw-688 {
max-width: 688px;
}
.w-680 {
width: 680px;
}
.w-16 {
width: 16px;
}
.mb-33 {
width: 33px;
}
.h-80 {
height: 80px;
}
.b-gray-30 {
border-color: #B1B2B3;
}
.header-menu-item {
float: left;
border-bottom-style: solid;
border-bottom-width: 1px;
border-color: #B1B2B3;
color: #B1B2B3;
flex-basis: 148px;
padding-bottom: 3px;
vertical-align: middle;
font-size: 14px;
line-height: 24px;
}
.publish {
float: left;
vertical-align: middle;
font-size: 20px;
line-height: 24px;
font-weight: bold;
color: #7F7F7F;
margin-left: 16px;
margin-top: 16px;
margin-bottom: 8px;
}
.create {
float: right;
font-size: 14px;
line-height: 16px;
font-weight: 600;
text-align: right;
margin-right: 16px;
margin-top: 22px;
}
.path-control {
width: 100%;
border-bottom-style: solid;
border-bottom-width: 1px;
border-color: #B1B2B3;
height: 28px;
clear: both;
}
.h-modulo-header {
height: 48px;
}
.h-publish-header {
height: 76px;
top: 48px;
}
.h-inner {
height: calc(100% - 124px);
top: 48px;
}
.h-footer {
height: 76px;
}
::placeholder {
color: #B1B2B3;
}
.bg-red {
background-color: #EE5432;
}
.bg-gray-30 {
background-color: #B1B2B3;
}
.two-lines {
display: -webkit-box;
-webkit-box-orient: vertical;
word-wrap: break-word;
-webkit-line-clamp: 2;
overflow: hidden;
}
.five-lines {
display: -webkit-box;
-webkit-box-orient: vertical;
word-wrap: break-word;
-webkit-line-clamp: 5;
overflow: hidden;
}
.one-line {
word-wrap: break-word;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
@media all and (min-width: 34.375em) {
.db-ns {
display: block;
}
.flex-basis-30-ns {
flex-basis: 30vw;
}
.h-100-m-40-ns {
height: calc(100% - 40px);
}
.mw-300-ns {
max-width: 300px;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
@import 'css/tachyons.css';
@import 'css/indigo-static.css';
@import 'css/fonts.css';
@import 'css/custom.css';
@import 'css/spinner.css';

View File

@ -1,23 +1,10 @@
import "/lib/object-extensions";
import React from 'react';
import ReactDOM from 'react-dom';
import { HeaderBar } from '/components/lib/header-bar';
import { Root } from '/components/root';
import { api } from '/api';
import { store } from '/store';
import { subscription } from "/subscription";
import * as util from '/lib/util';
import _ from 'lodash';
console.log('app running');
/*
Common variables:
station : ~zod/club
circle : club
host : zod
*/
api.setAuthTokens({
ship: window.ship
@ -25,8 +12,9 @@ api.setAuthTokens({
subscription.start();
window.util = util;
window._ = _;
ReactDOM.render((
<HeaderBar />
), document.getElementById("header"));
ReactDOM.render((
<Root />

View File

@ -42,6 +42,97 @@ class UrbitApi {
});
});
}
// TODO add error handling
handleErrors(response) {
if (!response.ok) throw Error(response.status);
return response;
}
fetchNotebooks() {
fetch('/~publish/notebooks.json')
.then((response) => response.json())
.then((json) => {
store.handleEvent({
type: 'notebooks',
data: json,
});
});
}
fetchNotebook(host, book) {
fetch(`/~publish/${host}/${book}.json`)
.then((response) => response.json())
.then((json) => {
store.handleEvent({
type: 'notebook',
data: json,
host: host,
notebook: book,
});
});
}
fetchNote(host, book, note) {
fetch(`/~publish/${host}/${book}/${note}.json`)
.then((response) => response.json())
.then((json) => {
store.handleEvent({
type: 'note',
data: json,
host: host,
notebook: book,
note: note,
});
});
}
fetchNotesPage(host, book, start, length) {
fetch(`/~publish/notes/${host}/${book}/${start}/${length}.json`)
.then((response) => response.json())
.then((json) => {
store.handleEvent({
type: 'notes-page',
data: json,
host: host,
notebook: book,
startIndex: start,
length: length,
});
});
}
fetchCommentsPage(host, book, note, start, length) {
fetch(`/~publish/comments/${host}/${book}/${note}/${start}/${length}.json`)
.then((response) => response.json())
.then((json) => {
store.handleEvent({
type: 'comments-page',
data: json,
host: host,
notebook: book,
note: note,
startIndex: start,
length: length,
});
});
}
sidebarToggle() {
let sidebarBoolean = true;
if (store.state.sidebarShown === true) {
sidebarBoolean = false;
}
store.handleEvent({
type: {
local: {
'sidebarToggle': sidebarBoolean
}
}
});
}
}
export let api = new UrbitApi();

View File

@ -1,373 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import _ from 'lodash';
import { PathControl } from '/components/lib/path-control';
import { BlogData } from '/components/lib/blog-data';
import { BlogNotes } from '/components/lib/blog-notes';
import { BlogSubs } from '/components/lib/blog-subs';
import { BlogSettings } from '/components/lib/blog-settings';
import { withRouter } from 'react-router';
import { NotFound } from '/components/not-found';
import { Link } from 'react-router-dom';
const PC = withRouter(PathControl);
const NF = withRouter(NotFound);
const BN = withRouter(BlogNotes);
const BS = withRouter(BlogSettings)
export class Blog extends Component {
constructor(props){
super(props);
this.state = {
view: 'notes',
awaiting: false,
postProps: [],
blogTitle: '',
blogHost: '',
pathData: [],
temporary: false,
awaitingSubscribe: false,
awaitingUnsubscribe: false,
notFound: false,
};
this.subscribe = this.subscribe.bind(this);
this.unsubscribe = this.unsubscribe.bind(this);
this.viewSubs = this.viewSubs.bind(this);
this.viewSettings = this.viewSettings.bind(this);
this.viewNotes = this.viewNotes.bind(this);
this.blog = null;
}
handleEvent(diff) {
if (diff.data.total) {
let blog = diff.data.total.data;
this.blog = blog;
this.setState({
postProps: this.buildPosts(blog),
blog: blog,
blogTitle: blog.info.title,
blogHost: blog.info.owner,
awaiting: false,
pathData: [
{ text: "Home", url: "/~publish/recent" },
{ text: blog.info.title,
url: `/~publish/${blog.info.owner}/${blog.info.filename}` }
],
});
this.props.setSpinner(false);
} else if (diff.data.remove) {
if (diff.data.remove.post) {
// XX TODO
} else {
this.props.history.push("/~publish/recent");
}
}
}
handleError(err) {
this.props.setSpinner(false);
this.setState({notFound: true});
}
componentDidUpdate(prevProps, prevState) {
if (this.state.notFound) return;
let ship = this.props.ship;
let blogId = this.props.blogId;
let blog = (ship === window.ship)
? _.get(this.props, `pubs["${blogId}"]`, false)
: _.get(this.props, `subs["${ship}"]["${blogId}"]`, false);
if (!(blog) && (ship === window.ship)) {
this.setState({notFound: true});
return;
} else if (this.blog && !blog) {
this.props.history.push("/~publish/recent");
return;
}
this.blog = blog;
if (this.state.awaitingSubscribe && blog) {
this.setState({
temporary: false,
awaitingSubscribe: false,
});
this.props.setSpinner(false);
}
}
componentWillMount() {
let ship = this.props.ship;
let blogId = this.props.blogId;
let blog = (ship == window.ship)
? _.get(this.props, `pubs["${blogId}"]`, false)
: _.get(this.props, `subs["${ship}"]["${blogId}"]`, false);
if (!(blog) && (ship === window.ship)) {
this.setState({notFound: true});
return;
};
let temporary = (!(blog) && (ship != window.ship));
if (temporary) {
this.setState({
awaiting: {
ship: ship,
blogId: blogId,
},
temporary: true,
});
this.props.setSpinner(true);
this.props.api.bind(`/collection/${blogId}`, "PUT", ship, "publish",
this.handleEvent.bind(this),
this.handleError.bind(this));
} else {
this.blog = blog;
}
}
buildPosts(blog){
if (!blog) {
return [];
}
let pinProps = blog.order.pin.map((postId) => {
let post = blog.posts[postId];
return this.buildPostPreviewProps(post, blog, true);
});
let unpinProps = blog.order.unpin.map((postId) => {
let post = blog.posts[postId];
return this.buildPostPreviewProps(post, blog, false);
});
return pinProps.concat(unpinProps);
}
buildPostPreviewProps(post, blog, pinned){
let unread = (-1 === _.findIndex(this.props.unread, {
post: post.post.info.filename,
coll: blog.info.filename,
who: blog.info.owner.slice(1),
}))
? false: true;
return {
postTitle: post.post.info.title,
postName: post.post.info.filename,
postBody: post.post.body,
numComments: post.comments.length,
collectionTitle: blog.info.title,
collectionName: blog.info.filename,
author: post.post.info.creator,
blogOwner: blog.info.owner,
date: post.post.info["date-created"],
pinned: pinned,
unread: unread,
}
}
buildData(){
let blog = (this.props.ship == window.ship)
? _.get(this.props, `pubs["${this.props.blogId}"]`, false)
: _.get(this.props, `subs["${this.props.ship}"]["${this.props.blogId}"]`, false);
if (this.state.temporary) {
return {
blog: this.state.blog,
postProps: this.state.postProps,
blogTitle: this.state.blogTitle,
blogHost: this.state.blogHost,
pathData: this.state.pathData,
};
} else {
if (!blog) {
return false;
}
return {
blog: blog,
postProps: this.buildPosts(blog),
blogTitle: blog.info.title,
blogHost: blog.info.owner,
pathData: [
{ text: "Home", url: "/~publish/recent" },
{ text: blog.info.title,
url: `/~publish/${blog.info.owner}/${blog.info.filename}` }
],
};
}
}
subscribe() {
let sub = {
subscribe: {
who: this.props.ship,
coll: this.props.blogId,
}
}
this.props.setSpinner(true);
this.setState({awaitingSubscribe: true}, () => {
this.props.api.action("publish", "publish-action", sub);
});
}
unsubscribe() {
let unsub = {
unsubscribe: {
who: this.props.ship,
coll: this.props.blogId,
}
}
this.props.api.action("publish", "publish-action", unsub);
this.props.history.push("/~publish/recent");
}
viewSubs() {
this.setState({view: 'subs'});
}
viewSettings() {
this.setState({view: 'settings'});
}
viewNotes() {
this.setState({view: 'notes'});
}
render() {
if (this.state.notFound) {
return (
<NF/>
);
} else if (this.state.awaiting) {
return null;
} else {
let data = this.buildData();
let contributors = `~${this.props.ship}`;
let create = (this.props.ship === window.ship);
let subNum = _.get(data.blog, 'subscribers.length', 0);
let foreign = _.get(this.props,
`subs["${this.props.ship}"]["${this.props.blogId}"]`, false);
let actionType = false;
if (this.state.temporary) {
actionType = 'subscribe';
} else if ((this.props.ship !== window.ship) && foreign) {
actionType = 'unsubscribe';
}
let viewSubs = (this.props.ship === window.ship)
? this.viewSubs
: null;
let viewSettings = (this.props.ship === window.ship)
? this.viewSettings
: null;
if (this.state.view === 'notes') {
return (
<div>
<PC pathData={data.pathData} create={create}/>
<div className="absolute w-100"
style={{top:124, paddingLeft: 16, paddingRight: 16, paddingTop: 32}}>
<div className="flex-col">
<h2 style={{wordBreak: "break-word"}}>
{data.blogTitle}
</h2>
<div className="flex" style={{marginTop: 22}}>
<BlogData
host={this.props.ship}
viewSubs={viewSubs}
subNum={subNum}
viewSettings={viewSettings}
subscribeAction={actionType}
subscribe={this.subscribe}
unsubscribe={this.unsubscribe}
/>
</div>
<BN ship={this.props.ship} posts={data.postProps} />
</div>
</div>
</div>
);
} else if (this.state.view === 'subs') {
let subscribers = _.get(data, 'blog.subscribers', []);
return (
<div>
<PC pathData={data.pathData} create={create}/>
<div className="absolute w-100"
style={{top:124, paddingLeft: 16, paddingRight: 16, paddingTop: 32}}>
<div className="flex-col">
<h2 style={{wordBreak: "break-word"}}>
{data.blogTitle}
</h2>
<div className="flex" style={{marginTop: 22}}>
<BlogData
host={this.props.ship}
viewSubs={viewSubs}
subNum={subNum}
viewSettings={viewSettings}
subscribeAction={actionType}
subscribe={this.subscribe}
unsubscribe={this.unsubscribe}
/>
</div>
<BlogSubs back={this.viewNotes}
subs={subscribers}
blogId={this.props.blogId}
title={data.blogTitle}
api={this.props.api}/>
</div>
</div>
</div>
);
} else if (this.state.view === 'settings') {
return (
<div>
<PC pathData={data.pathData} create={create}/>
<div className="absolute w-100"
style={{top:124, paddingLeft: 16, paddingRight: 16, paddingTop: 32}}>
<div className="flex-col">
<h2 style={{wordBreak: "break-word"}}>
{data.blogTitle}
</h2>
<div className="flex" style={{marginTop: 22}}>
<BlogData
host={this.props.ship}
viewSubs={viewSubs}
subNum={subNum}
viewSettings={viewSettings}
subscribeAction={actionType}
subscribe={this.subscribe}
unsubscribe={this.unsubscribe}
/>
</div>
<BS back={this.viewNotes}
blogId={this.props.blogId}
title={data.blogTitle}
api={this.props.api}/>
</div>
</div>
</div>
);
}
}
}
}

View File

@ -0,0 +1,16 @@
import React, { Component } from 'react';
//TODO "About" subcomponent of Notebook.js
//Fill in with "description" from props.notebook
export class About extends Component {
render() {
return (
<div>
</div>
)
}
}
export default About

View File

@ -1,90 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
class Subscribe extends Component {
constructor(props) {
super(props);
}
render() {
if (this.props.actionType === 'subscribe') {
return (
<p className="label-small b pointer"
onClick={this.props.subscribe}>
Subscribe
</p>
);
} else if (this.props.actionType === 'unsubscribe') {
return (
<p className="label-small b pointer"
onClick={this.props.unsubscribe}>
Unsubscribe
</p>
);
} else {
return null;
}
}
}
class Subscribers extends Component {
constructor(props) {
super(props);
}
render() {
let subscribers = (this.props.subNum === 1)
? `${this.props.subNum} Subscriber`
: `${this.props.subNum} Subscribers`;
if (this.props.action !== null) {
return (
<p className="label-small b pointer" onClick={this.props.action}>
{subscribers}
</p>
);
} else {
return (
<p className="label-small b">{subscribers}</p>
);
}
}
}
class Settings extends Component {
constructor(props) {
super(props);
}
render() {
if (this.props.action !== null) {
return (
<p className="label-small b pointer" onClick={this.props.action}>
Settings
</p>
);
} else {
return null;
}
}
}
export class BlogData extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="flex-col">
<p className="label-small">By ~{this.props.host}</p>
<Subscribers action={this.props.viewSubs} subNum={this.props.subNum}/>
<Settings action={this.props.viewSettings}/>
<Subscribe actionType={this.props.subscribeAction}
subscribe={this.props.subscribe}
unsubscribe={this.props.unsubscribe}
/>
</div>
);
}
}

View File

@ -1,50 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { PostPreview } from '/components/lib/post-preview';
import { Link } from 'react-router-dom';
export class BlogNotes extends Component {
constructor(props) {
super(props);
}
render() {
if (!this.props.posts ||
((this.props.posts.length === 0) &&
(this.props.ship === window.ship))) {
let link = {
pathname: "/~publish/new-post",
state: {
lastPath: this.props.location.pathname,
lastMatch: this.props.match.path,
lastParams: this.props.match.params,
}
}
return (
<div className="flex flex-wrap">
<div className="w-336 relative">
<hr className="gray-10" style={{marginTop: 48, marginBottom:25}}/>
<Link to={link}>
<p className="body-large b">
-> Create First Post
</p>
</Link>
</div>
</div>
);
}
let posts = this.props.posts.map((post, key) => {
return (
<PostPreview post={post} key={key}/>
);
});
return (
<div className="flex flex-wrap" style={{marginTop: 48}}>
{posts}
</div>
);
}
}

View File

@ -1,122 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
class SaveLink extends Component {
constructor(props) {
super(props);
}
render() {
if (this.props.enabled) {
return (
<button className="label-regular b"
onClick={this.props.action}>
-> Save
</button>
);
} else {
return (
<p className="label-regular b gray-50">
-> Save
</p>
);
}
}
}
export class BlogSettings extends Component {
constructor(props) {
super(props);
this.state = {
title: '',
awaitingTitleChange: false,
}
this.rename = this.rename.bind(this);
this.titleChange = this.titleChange.bind(this);
this.deleteBlog = this.deleteBlog.bind(this);
}
rename() {
let edit = {
"edit-collection": {
name: this.props.blogId,
title: this.state.title,
}
}
this.setState({
awaitingTitleChange: true,
}, () => {
this.props.api.action("publish", "publish-action", edit);
});
}
titleChange(evt) {
this.setState({title: evt.target.value});
}
deleteBlog() {
let del = {
"delete-collection": {
coll: this.props.blogId,
}
}
this.props.api.action("publish", "publish-action", del);
this.props.history.push("/~publish/recent");
}
componentDidUpdate(prevProps) {
if (this.state.awaitingTitleChange) {
if (prevProps.title !== this.props.title){
this.titleInput.value = '';
this.setState({
awaitingTitleChange: false,
});
}
}
}
render() {
let back = '<- Back to notes'
let enableSave = ((this.state.title !== '') &&
(this.state.title !== this.props.title) &&
!this.state.awaitingTitleChange);
return (
<div className="flex-col mw-688" style={{marginTop:48}}>
<hr className="gray-30" style={{marginBottom:25}}/>
<p className="label-regular pointer b" onClick={this.props.back}>
{back}
</p>
<p className="body-large b" style={{marginTop:16, marginBottom: 20}}>
Settings
</p>
<div className="flex">
<div className="flex-col w-100">
<p className="body-regular-400">Delete Notebook</p>
<p className="gray-50 label-small-2" style={{marginTop:12, marginBottom:8}}>
Permanently delete this notebook
</p>
<button className="red label-regular b" onClick={this.deleteBlog}>
-> Delete
</button>
</div>
<div className="flex-col w-100">
<p className="body-regular-400">Rename</p>
<p className="gray-50 label-small-2" style={{marginTop:12, marginBottom:23}}>
Change the name of this notebook
</p>
<p className="label-small-2">Notebook Name</p>
<input className="body-regular-400 w-100"
ref={(el) => {this.titleInput = el}}
style={{marginBottom:8}}
placeholder={this.props.title}
onChange={this.titleChange}
disabled={this.state.awaitingTitleChange}/>
<SaveLink action={this.rename} enabled={enableSave}/>
</div>
</div>
</div>
);
}
}

View File

@ -1,140 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import urbitOb from 'urbit-ob';
class InviteLink extends Component {
constructor(props) {
super(props);
}
render() {
if (this.props.enabled) {
return (
<button className="label-regular b underline"
onClick={this.props.action}>
Invite
</button>
);
} else {
return (
<p className="label-regular b underline gray-50">
Invite
</p>
);
}
}
}
export class BlogSubs extends Component {
constructor(props) {
super(props);
this.state = {
validInvites: false,
invites: [],
}
this.inviteHeight = 133;
this.invite = this.invite.bind(this);
this.inviteChange = this.inviteChange.bind(this);
}
inviteChange(evt) {
this.inviteInput.style.height = 'auto';
let newHeight = (this.inviteInput.scrollHeight < 133)
? 133 : this.inviteInput.scrollHeight + 2;
this.inviteInput.style.height = newHeight+'px';
this.inviteHeight = this.inviteInput.style.height;;
let tokens = evt.target.value
.trim()
.split(/[\s,]+/)
.map(t => t.trim());
let valid = tokens.reduce((valid, s) =>
valid && ((s !== '~') && urbitOb.isValidPatp(s) && s.includes('~')), true);
if (valid) {
this.setState({
validInvites: true,
invites: tokens.map(t => t.slice(1)),
});
} else {
this.setState({validInvites: false});
}
}
invite() {
if (this.inviteInput) this.inviteInput.value = '';
let invite = {
invite: {
coll: this.props.blogId,
title: this.props.title,
who: this.state.invites,
}
}
this.inviteHeight = 133;
this.setState({
validInvites: false,
invites: [],
}, () => {
this.props.api.action("publish", "publish-action", invite);
});
}
render() {
let back = '<- Back to notes'
let subscribers = this.props.subs.map((sub, i) => {
return (
<div className="flex w-100" key={i+1}>
<p className="label-regular-mono w-100">~{sub}</p>
</div>
);
});
subscribers.unshift(
<div className="flex w-100" key={0}>
<p className="label-regular-mono w-100">~{window.ship}</p>
<p className="label-regular-mono w-100">Host (You)</p>
</div>
);
return (
<div className="flex-col mw-688" style={{marginTop:48}}>
<hr className="gray-30" style={{marginBottom:25}}/>
<p className="label-regular pointer b" onClick={this.props.back}>
{back}
</p>
<p className="body-large b" style={{marginTop:16, marginBottom: 20}}>
Manage Notebook
</p>
<div className="flex">
<div className="flex-col w-100">
<p className="body-regular-400">Members</p>
<p className="gray-50 label-small-2"
style={{marginTop:12, marginBottom: 23}}>
Everyone subscribed to this notebook
</p>
{subscribers}
</div>
<div className="flex-col w-100">
<p className="body-regular-400">Invite</p>
<p className="gray-50 label-small-2"
style={{marginTop:12, marginBottom: 23}}>
Invite people to subscribe to this notebook
</p>
<textarea className="w-100 label-regular-mono overflow-y-hidden"
ref={(el) => {this.inviteInput = el}}
style={{resize:"none", marginBottom:8, height: this.inviteHeight}}
onChange={this.inviteChange}>
</textarea>
<InviteLink enabled={this.state.validInvites} action={this.invite}/>
</div>
</div>
</div>
);
}
}

View File

@ -1,78 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Sigil } from '/components/lib/icons/sigil';
class PostButton extends Component {
render() {
if (this.props.enabled) {
return (
<p className="body-regular pointer" onClick={this.props.post}>
-> Post
</p>
);
} else {
return (
<p className="body-regular gray-30">
-> Post
</p>
);
}
}
}
export class CommentBox extends Component {
constructor(props){
super(props);
this.commentChange = this.commentChange.bind(this);
this.commentHeight = 54;
}
componentDidUpdate(prevProps, prevState) {
if (!prevProps.enabled && this.props.enabled) {
if (this.commentInput) {
this.commentInput.value = '';
this.commentInput.style.height = 54;
}
}
}
commentChange(evt) {
this.commentInput.style.height = 'auto';
let newHeight = (this.commentInput.scrollHeight < 54)
? 54 : this.commentInput.scrollHeight+2;
this.commentInput.style.height = newHeight+'px';
this.commentHeight = this.commentInput.style.height;
this.props.action(evt);
}
render() {
let textClass = (this.props.enabled)
? "body-regular-400 w-100"
: "body-regular-400 w-100 gray-30";
return (
<div className="cb w-100 flex"
style={{paddingBottom: 8, marginTop: 32}}>
<div className="fl" style={{marginRight: 10}}>
<Sigil ship={this.props.our} size={36}/>
</div>
<div className="flex-col w-100">
<textarea className={textClass}
ref={(el) => {this.commentInput = el}}
style={{resize: "none", height: this.commentHeight}}
type="text"
name="commentBody"
defaultValue=''
onChange={this.commentChange}
disabled={(!this.props.enabled)}>
</textarea>
<PostButton
post={this.props.post}
enabled={(Boolean(this.props.content) && this.props.enabled)}
/>
</div>
</div>
);
}
}

View File

@ -0,0 +1,14 @@
import React, { Component } from 'react'
//TODO take props and render div
export class CommentItem extends Component {
render() {
return (
<div>
</div>
)
}
}
export default CommentItem

View File

@ -1,59 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Sigil } from '/components/lib/icons/sigil';
import moment from 'moment';
export class Comment extends Component {
constructor(props){
super(props);
moment.updateLocale('en', {
relativeTime: {
past: function(input) {
return input === 'just now'
? input
: input + ' ago'
},
s : 'just now',
future : 'in %s',
m : '1m',
mm : '%dm',
h : '1h',
hh : '%dh',
d : '1d',
dd : '%dd',
M : '1 month',
MM : '%d months',
y : '1 year',
yy : '%d years',
}
});
}
render(){
let body = this.props.body.split("\n").map((line, i) =>{
return (<p key={i}>{line}</p>);
});
let date = moment(this.props.date).fromNow();
return (
<div className="cb w-100 flex" style={{paddingBottom: 16}}>
<div className="fl" style={{marginRight: 10}}>
<Sigil ship={this.props.ship} size={36} />
</div>
<div className="flex-col fl">
<div className="label-small-mono gray-50">
<p className="fl label-small-mono"
style={{width: 107}}>{this.props.ship}</p>
<p className="fl label-small-mono">{date}</p>
</div>
<div className="cb body-regular-400">
{body}
</div>
</div>
</div>
);
}
}

View File

@ -1,112 +1,15 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Comment } from '/components/lib/comment';
import { CommentBox } from '/components/lib/comment-box';
import React, { Component } from 'react'
import { CommentItem } from './comment-item';
//TODO map comments into comment-items;
export class Comments extends Component {
constructor(props){
super(props);
this.state = {
show: false,
commentBody: '',
awaiting: false,
}
this.toggleDisplay = this.toggleDisplay.bind(this);
this.commentChange = this.commentChange.bind(this);
this.postComment = this.postComment.bind(this);
}
commentChange(evt) {
this.setState({commentBody: evt.target.value});
}
toggleDisplay() {
this.setState({show: !this.state.show});
}
postComment() {
this.props.setSpinner(true);
let comment = {
"new-comment": {
who: this.props.ship,
coll: this.props.blogId,
name: this.props.postId,
content: this.state.commentBody,
}
};
this.setState({
awaiting: {
ship: this.props.ship,
blogId: this.props.blogId,
postId: this.props.postId,
}
}, () => {
this.props.api.action("publish", "publish-action", comment)
});
}
componentDidUpdate(prevProps, prevState) {
if (this.state.awaiting) {
if (prevProps.comments != this.props.comments) {
this.props.setSpinner(false);
this.setState({awaiting: false, commentBody: ''});
}
}
}
render(){
if (this.state.show) {
let our = `~${window.ship}`;
let comments = this.props.comments.map((comment, i) => {
let commentProps = {
ship: comment.info.creator,
date: comment.info["date-created"],
body: comment.body,
};
return (<Comment {...commentProps} key={i} />);
});
return (
<div className="cb mt3 mb4">
<p className="gray-50 body-large b">
<span>{this.props.comments.length} </span>
<span className="black">
Comments
</span>
</p>
<p className="cl body-regular pointer" onClick={this.toggleDisplay}>
- Hide Comments
</p>
<CommentBox our={our}
action={this.commentChange}
enabled={!(Boolean(this.state.awaiting))}
content={this.state.commentBody}
post={this.postComment}/>
<div className="flex-col" style={{marginTop: 32}}>
{comments}
</div>
</div>
);
} else {
return (
<div className="cb mt3 mb4">
<p className="gray-50 body-large b">
<span>{this.props.comments.length} </span>
<span className="black">
Comments
</span>
</p>
<p className="cl body-regular pointer" onClick={this.toggleDisplay}>
+ Show Comments
</p>
</div>
);
}
render() {
return (
<div>
</div>
)
}
}
export default Comments

View File

@ -1,33 +1,48 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { IconHome } from '/components/lib/icons/icon-home';
import { IconSpinner } from '/components/lib/icons/icon-spinner';
import { Sigil } from '/components/lib/icons/sigil';
export class HeaderBar extends Component {
render() {
let spin = (this.props.spinner)
? <div className="absolute"
style={{width: 16, height: 16, top: 16, left: 55}}>
<IconSpinner/>
</div>
: null;
let popout = (window.location.href.includes("popout/"))
? "dn"
: "dn db-m db-l db-xl";
let title = (document.title === "Home")
? ""
: document.title;
return (
<div className="bg-black w-100 flex justify-between fixed z-4"
style={{ height: 48, padding: 8}}>
<a className="db"
style={{ background: '#1A1A1A',
borderRadius: 16,
width: 32,
height: 32,
top: 8 }}
href='/'>
<IconHome />
<div className={"bg-white w-100 justify-between relative tc pt3 "
+ popout}
style={{ height: 40 }}>
<a className="dib gray2 f9 inter absolute left-1"
href='/'
style={{top: 14}}>
<IconHome/>
<span className="ml2 v-top lh-title"
style={{paddingTop: 3}}>
Home
</span>
</a>
{spin}
<span className="f9 inter dib"
style={{
verticalAlign: "text-top",
paddingTop: 3
}}>
{title}
</span>
<div className="absolute right-1 lh-copy"
style={{top: 12}}>
<Sigil
ship={"~" + window.ship}
size={16}
color={"#000000"}
/>
<span className="mono f9 ml2 v-top">{"~" + window.ship}</span>
</div>
</div>
);
}
}
}

View File

@ -1,78 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { NavLink } from 'react-router-dom';
import { PublishCreate } from '/components/lib/publish-create';
import { withRouter } from 'react-router';
const PC = withRouter(PublishCreate);
export class HeaderMenu extends Component {
render () {
let recentText = (this.props.unread)
? <p className="label-regular">
<span className="green-medium body-large"></span>
<span>Recent</span>
</p>
: <p className="label-regular">Recent</p>;
let subsText = (this.props.invites)
? <p className="label-regular">
<span className="green-medium body-large"></span>
<span>Subscriptions</span>
</p>
: <p className="label-regular">Subscriptions</p>;
return (
<div className="fixed w-100 bg-white cf h-publish-header z-4"
style={{top:48}}>
<PC create={"blog"}/>
<div className="w-100 flex">
<div className="fl bb b-gray-30 w-16" >
</div>
<NavLink exact
className="header-menu-item"
to="/~publish/recent"
activeStyle={{
color: "black",
borderColor: "black",
}}
style={{flexBasis:148}}>
Recent
</NavLink>
<div className="fl bb b-gray-30 w-16" >
</div>
<NavLink exact
className="header-menu-item"
to="/~publish/subs"
activeStyle={{
color: "black",
borderColor: "black",
}}
style={{flexBasis:148}}>
{subsText}
</NavLink>
<div className="fl bb b-gray-30 w-16" >
</div>
<NavLink exact
className="header-menu-item"
to="/~publish/pubs"
activeStyle={{
color: "black",
borderColor: "black",
}}
style={{flexBasis:148}}>
Notebooks
</NavLink>
<div className="fl bb b-gray-30 w-16" style={{flexGrow:1}}>
</div>
</div>
</div>
);
}
}

View File

@ -1,74 +0,0 @@
import React, { Component } from 'react';
import { IconInbox } from '/components/lib/icons/icon-inbox';
import { IconComment } from '/components/lib/icons/icon-comment';
import { IconSig } from '/components/lib/icons/icon-sig';
import { IconDecline } from '/components/lib/icons/icon-decline';
import { IconUser } from '/components/lib/icons/icon-user';
export class Icon extends Component {
render() {
let iconElem = null;
switch(this.props.type) {
case "icon-stream-chat":
iconElem = <span className="icon-stream-chat"></span>;
break;
case "icon-stream-dm":
iconElem = <span className="icon-stream-dm"></span>;
break;
case "icon-collection-index":
iconElem = <span className="icon-collection"></span>;
break;
case "icon-collection-post":
iconElem = <span className="icon-collection-post"></span>;
break;
case "icon-collection-comment":
iconElem = <span className="icon-collection icon-collection-comment"></span>;
break;
case "icon-panini":
// TODO: Should icons be display: block, inline, or inline-blocks?
// 1) Should naturally flow inline
// 2) But can't make icon-panini naturally inline without hacks like &nbsp;
iconElem = <div className="icon-panini"></div>
break;
case "icon-x":
iconElem = <span className="icon-x"></span>
break;
case "icon-decline":
iconElem = <IconDecline />
break;
case "icon-lus":
iconElem = <span className="icon-lus"></span>
break;
case "icon-inbox":
iconElem = <IconInbox />
break;
case "icon-comment":
iconElem = <IconComment />
break;
case "icon-sig":
iconElem = <IconSig />
break;
case "icon-user":
iconElem = <IconUser />
break;
case "icon-ellipsis":
iconElem = (
<div className="icon-ellipsis-wrapper icon-label">
<div className="icon-ellipsis-dot"></div>
<div className="icon-ellipsis-dot"></div>
<div className="icon-ellipsis-dot"></div>
</div>
)
break;
}
let className = this.props.label ? "icon-label" : "";
return (
<span className={className}>
{iconElem}
</span>
)
}
}

View File

@ -1,11 +0,0 @@
import React, { Component } from 'react';
export class IconCheck extends Component {
render() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M14.9999 4.63293L13.2766 3L6.1698 9.7341L2.72327 6.46823L1 8.10117L6.16992 13L7.24512 11.9812L7.89319 11.3671L14.9999 4.63293Z" fill="white"/>
</svg>
)
}
}

View File

@ -1,17 +0,0 @@
import React, { Component } from 'react';
export class IconComment extends Component {
render() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="2" y="2" width="12" height="12">
<path fillRule="evenodd" clipRule="evenodd" d="M9.2 10.4L14 14V2H2V10.4H9.2ZM3.2 9.2H9.35486C9.48986 9.2 9.62096 9.24554 9.72686 9.32924L12.8 11.7578V3.2H3.2V9.2Z" fill="black"/>
<path d="M3.2 9.2H9.35486C9.48986 9.2 9.62096 9.24554 9.72686 9.32924L12.8 11.7578V3.2H3.2V9.2Z" fill="black"/>
</mask>
<g mask="url(#mask0)">
<rect width="16" height="16" fill="black"/>
</g>
</svg>
)
}
}

View File

@ -1,11 +0,0 @@
import React, { Component } from 'react';
export class IconCross extends Component {
render() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M8 6.28568L3.71436 2L2.00012 3.71429L6.28577 7.99994L2 12.2857L3.71423 14L8 9.71423L12.2858 14L14 12.2857L9.71436 7.99997L14 3.71429L12.2856 2.00003L8 6.28568Z" fill="black"/>
</svg>
)
}
}

View File

@ -1,13 +0,0 @@
import React, { Component } from 'react';
export class IconDecline extends Component {
render() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon/Decline">
<path id="Union" fillRule="evenodd" clipRule="evenodd" d="M6.28577 7.99992L2 12.2857L3.71423 14L8 9.71422L12.2858 14L14 12.2857L9.71423 7.99997L14 3.71428L12.2856 1.99998L8 6.28568L3.71436 2L2.00012 3.71428L6.28577 7.99992Z" fill="black"/>
</g>
</svg>
);
}
}

View File

@ -3,7 +3,8 @@ import React, { Component } from 'react';
export class IconHome extends Component {
render() {
return (
<img src="/~launch/img/Home.png" width={32} height={32} />
<img
src="/~publish/Home.png" width={16} height={16} />
);
}
}

View File

@ -1,11 +0,0 @@
import React, { Component } from 'react';
export class IconInbox extends Component {
render() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M8 6C9.65686 6 11 4.65686 11 3H13C13.5523 3 14 3.44772 14 4V12C14 12.5523 13.5523 13 13 13H3C2.44771 13 2 12.5523 2 12V4C2 3.44772 2.44771 3 3 3H5C5 4.65686 6.34314 6 8 6Z" fill="black"/>
</svg>
)
}
}

View File

@ -0,0 +1,34 @@
import React, { Component } from 'react';
import { api } from '../../../api';
export class SidebarSwitcher extends Component {
render() {
let popoutSwitcher = this.props.popout
? "dn-m dn-l dn-xl"
: "dib-m dib-l dib-xl";
return (
<div className="pt2">
<a
className="pointer flex-shrink-0"
onClick={() => {
api.sidebarToggle();
}}>
<img
className={`pr3 invert-d dn ` + popoutSwitcher}
src={
this.props.sidebarShown
? "/~link/img/SwitcherOpen.png"
: "/~link/img/SwitcherClosed.png"
}
height="16"
width="16"
/>
</a>
</div>
);
}
}
export default SidebarSwitcher

View File

@ -1,11 +0,0 @@
import React, { Component } from 'react';
export class IconSig extends Component {
render() {
return (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 6H12.6564C12.4097 7.51007 11.8238 8.48322 10.7445 8.48322C8.86344 8.48322 7.78414 6 5.03965 6C2.54185 6 1.2467 7.71141 1 11H3.34361C3.59031 9.48993 4.17621 8.51678 5.25551 8.51678C7.19824 8.51678 8.18502 11 10.9912 11C13.3965 11 14.7533 9.28859 15 6Z" fill="black"/>
</svg>
)
}
}

View File

@ -1,9 +0,0 @@
import React, { Component } from 'react';
export class IconSpinner extends Component {
render() {
return (
<div className="spinner-pending"></div>
);
}
}

View File

@ -1,9 +0,0 @@
import React, { Component } from 'react';
export class IconUser extends Component {
render() {
return (
<svg fill="none" height="16" width="16" xmlns="http://www.w3.org/2000/svg"><path clipRule="evenodd" d="m8 2a3 3 0 1 0 0 6 3 3 0 0 0 0-6zm4.667 12h1.333c0-2.761-2.686-5-6-5s-6 2.239-6 5z" fill="#000" fillRule="evenodd"/></svg>
)
}
}

View File

@ -1,32 +1,26 @@
import React, { Component } from 'react';
import { sigil, reactRenderer } from 'urbit-sigil-js';
export class Sigil extends Component {
render() {
const { props } = this;
if (props.ship.length > 14) {
return (
<div className="bg-black" style={{width: 44, height: 44}}>
<div className="bg-black flex-shrink-0" style={{width: props.size, height: props.size}}>
</div>
);
} else {
return (
<div
className="bg-black"
style={{ flexBasis: 35, padding: 4 }}>
{
sigil({
<div className="dib flex-shrink-0" style={{ flexBasis: 32, backgroundColor: props.color }}>
{sigil({
patp: props.ship,
renderer: reactRenderer,
size: props.size,
colors: ['black', 'white'],
})
}
colors: [props.color, "white"]
})}
</div>
);
}
}
}

View File

@ -0,0 +1,14 @@
import React, { Component } from 'react'
//TODO textarea + join button to make an api call
export class JoinScreen extends Component {
render() {
return (
<div>
</div>
)
}
}
export default JoinScreen

View File

@ -0,0 +1,15 @@
import React, { Component } from 'react'
//TODO integrate codemirror work from GA
export class NewPost extends Component {
render() {
return (
<div>
</div>
)
}
}
export default NewPost;

View File

@ -0,0 +1,16 @@
import React, { Component } from 'react'
//TODO textarea fields for title/description
//TODO add component for ship / group search using props.groups
// (integrate props.contacts as well once contact-view is bound)
export class NewScreen extends Component {
render() {
return (
<div>
</div>
)
}
}
export default NewScreen

View File

@ -1,130 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { TitleSnippet } from '/components/lib/title-snippet';
import { PostSnippet } from '/components/lib/post-snippet';
import { Link } from 'react-router-dom';
import moment from 'moment';
class Preview extends Component {
constructor(props){
super(props);
moment.updateLocale('en', {
relativeTime: {
past: function(input) {
return input === 'just now'
? input
: input + ' ago'
},
s : 'just now',
future : 'in %s',
m : '1m',
mm : '%dm',
h : '1h',
hh : '%dh',
d : '1d',
dd : '%dd',
M : '1 month',
MM : '%d months',
y : '1 year',
yy : '%d years',
}
});
}
buildProps(postId){
let post = this.props.blog.posts[postId];
return {
postTitle: post.post.info.title,
postName: post.post.info.filename,
postBody: post.post.body,
numComments: post.comments.length,
collectionTitle: this.props.blog.info.title,
collectionName: this.props.blog.info.filename,
author: post.post.info.creator,
blogOwner: this.props.blog.info.owner,
date: post.post.info["date-created"],
pinned: false,
}
}
render(){
if (this.props.postId) {
let owner = this.props.blog.info.owner;
let blogId = this.props.blog.info.filename;
let previewProps = this.buildProps(this.props.postId);
let prevUrl = `/~publish/${owner}/${blogId}/${this.props.postId}`
let date = moment(previewProps.date).fromNow();
let authorDate = `${previewProps.author}${date}`
let collLink = "/~publish/" +
previewProps.blogOwner + "/" +
previewProps.collectionName;
let postLink = collLink + "/" + previewProps.postName;
return (
<div className="w-336">
<Link className="ml2 mr2 gray-50 body-regular db mb3" to={prevUrl}>
{this.props.text}
</Link>
<div className="w-336 relative"
style={{height:210}}>
<Link to={postLink} className="db">
<TitleSnippet badge={false} title={previewProps.postTitle} />
<div className="w-100" style={{height:16}}></div>
<PostSnippet
body={previewProps.postBody}
/>
</Link>
<p className="label-small gray-50 absolute" style={{bottom:0}}>
{authorDate}
</p>
</div>
</div>
);
} else {
return (
<div className="w-336"></div>
);
}
}
}
export class NextPrev extends Component {
constructor(props) {
super(props);
}
render() {
let posts = this.props.blog.order.unpin.slice().reverse();
let postIdx = posts.indexOf(this.props.postId);
let prevId = (postIdx > 0)
? posts[postIdx - 1]
: false;
let nextId = (postIdx < (posts.length - 1))
? posts[postIdx + 1]
: false;
if (!(prevId || nextId)){
return null;
} else {
let prevText = "<- Previous Post";
let nextText = "-> Next Post";
return (
<div>
<div className="flex">
<Preview postId={prevId} blog={this.props.blog} text={prevText}/>
<div style={{width:16}}></div>
<Preview postId={nextId} blog={this.props.blog} text={nextText}/>
</div>
<hr className="gray-50 w-680 mt4"/>
</div>
);
}
}
}

View File

@ -0,0 +1,16 @@
import React, { Component } from 'react'
//TODO render props from notebook.js as a div of individual notes in the
//notebook
export class NoteItem extends Component {
render() {
return (
<div>
</div>
)
}
}
export default NoteItem

View File

@ -0,0 +1,15 @@
import React, { Component } from 'react';
import { NoteItem } from './note-item';
//TODO map a list of NoteItems
export class NoteList extends Component {
render() {
return (
<div>
</div>
)
}
}
export default NoteList

View File

@ -0,0 +1,18 @@
import React, { Component } from 'react';
import { Comments } from './comments';
//TODO ask for note if we don't have it
//TODO initialise note if no state
//TODO if comments are disabled on the notebook, don't render comments
export class Note extends Component {
render() {
return (
<div>
<Comments/>
</div>
)
}
}
export default Note

View File

@ -0,0 +1,14 @@
import React, { Component } from 'react';
//TODO take props and render entry in sidebar
export class NotebookItem extends Component {
render() {
return (
<div>
</div>
)
}
}
export default NotebookItem

View File

@ -0,0 +1,30 @@
import React, { Component } from 'react';
import { NoteList } from './note-list';
import { About } from './about';
import { Subscribers } from './subscribers';
import { Settings } from './settings';
//TODO subcomponents for posts, subscribers, settings
//
//TODO props.view switch for which component to render
//pass props.notebook, contacts to each component
//TODO ask for notebook if we don't have it
//
//TODO initialise notebook obj if no props.notebook
//TODO component bar above the rendered component
//don't render settings if it's ours
//current component is black, others gray2 (see Chat's tab bar for an example)
export class Notebook extends Component {
render() {
return (
<div>
</div>
)
}
}
export default Notebook

View File

@ -1,104 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import { withRouter } from 'react-router';
import { PublishCreate } from '/components/lib/publish-create';
import _ from 'lodash';
const PC = withRouter(PublishCreate);
export class PathControl extends Component {
constructor(props){
super(props);
}
buildPathData(){
let path = [
{ text: "Home", url: "/~publish/recent" },
];
let last = _.get(this.props, 'location.state', false);
let blog = false;
let finalUrl = this.props.location.pathname;
if (last) {
finalUrl = {
pathName: finalUrl,
state: last,
};
if ((last.lastMatch === '/~publish/:ship/:blog/:post') ||
(last.lastMatch === '/~publish/:ship/:blog')){
blog = (last.lastParams.ship.slice(1) == window.ship)
? _.get(this.props, `pubs["${last.lastParams.blog}"]`, false)
: _.get(this.props,
`subs["${last.lastParams.ship.slice(1)}"]["${last.lastParams.blog}"]`, false);
}
}
if (this.props.location.pathname === '/~publish/new-blog') {
path.push(
{ text: 'New Notebook', url: finalUrl }
);
} else if (this.props.location.pathname === '/~publish/new-post') {
if (blog) {
path.push({
text: blog.info.title,
url: `/~publish/${blog.info.owner}/${blog.info.filename}`,
});
}
path.push(
{ text: 'New Note', url: finalUrl }
);
}
return path;
}
render() {
let pathData = (this.props.pathData)
? this.props.pathData
: this.buildPathData();
let path = [];
let key = 0;
pathData.forEach((seg, i) => {
let style = (i == 0)
? {marginLeft: 16}
: {};
if (i === pathData.length - 1)
style.color = "black";
path.push(
<Link to={seg.url} key={key++}
className="fl gray-30 label-regular one-line mw-336" style={style}>
{seg.text}
</Link>
);
if (i < (pathData.length - 1)) {
path.push(
<img src="/~publish/arrow.png"
className="fl ml1 mr1 relative"
style={{top: 5}}
key={key++}/>
);
}
});
let create = ((window.location.pathname === '/~publish/new-blog') ||
(window.location.pathname === '/~publish/new-post')) ||
(this.props.create === false)
? false
: 'post';
return (
<div className="fixed w-100 bg-white cf h-publish-header z-4"
style={{top: 48}}>
<PC create={create}/>
<div className="path-control">
{path}
</div>
</div>
);
}
}

View File

@ -1,108 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import moment from 'moment';
import { Link } from 'react-router-dom';
export class PostBody extends Component {
constructor(props){
super(props)
}
renderA(what, node, attr, parentNode) {
let aStyle = {
textDecorationLine: "underline",
wordWrap: "break-word"
};
let children = what.map((item, key) => {
if (typeof(item) === 'string') {
return item;
} else {
let newAttr = Object.assign({style: aStyle, key: key}, item.ga);
return this.parseContent(item.c, item.gn, newAttr, node);
}
});
const element =
React.createElement(node, Object.assign({style: aStyle}, attr), children);
return element;
}
renderIMG(what, node, attr, parentNode) {
let imgStyle = {
width: "100%",
height: "auto",
marginBottom: 12,
};
let newAttr = Object.assign({style: imgStyle}, attr);
const element = React.createElement(node, newAttr);
return element;
}
renderP(what, node, attr, parentNode) {
let dStyle = {
wordWrap: "break-word",
};
if (parentNode !== 'li') {
dStyle.marginBottom = 12;
}
let children = what.map((item, key) => {
if (typeof(item) === 'string') {
return item;
} else {
let newAttr = Object.assign({key: key}, item.ga);
return this.parseContent(item.c, item.gn, newAttr, node);
}
});
const element =
React.createElement(node, Object.assign({style: dStyle}, attr), children);
return element;
}
renderHR(what, node, attr, parentNode) {
const element = React.createElement(node, attr);
return element;
}
renderDefault(what, node, attr, parentNode) {
let dStyle = {
wordWrap: "break-word",
};
let children = what.map((item, key) => {
if (typeof(item) === 'string') {
return item;
} else {
let newAttr = Object.assign({key: key}, item.ga);
return this.parseContent(item.c, item.gn, newAttr, node);
}
});
const element =
React.createElement(node, Object.assign({style: dStyle}, attr), children);
return element;
}
parseContent(what, node, attr, parentNode) {
switch (node) {
case "a":
return this.renderA(what, node, attr, parentNode);
case "img":
return this.renderIMG(what, node, attr, parentNode);
case "p":
return this.renderP(what, node, attr, parentNode);
case "hr":
return this.renderHR(what, node, attr, parentNode);
default:
return this.renderDefault(what, node, attr, parentNode);
}
}
render() {
let page = this.parseContent(this.props.body.c,
this.props.body.gn,
this.props.body.ga,
null);
return page;
}
}

View File

@ -1,61 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import moment from 'moment';
import { Link } from 'react-router-dom';
import { PostSnippet } from '/components/lib/post-snippet';
import { TitleSnippet } from '/components/lib/title-snippet';
export class PostPreview extends Component {
constructor(props) {
super(props);
moment.updateLocale('en', {
relativeTime: {
past: function(input) {
return input === 'just now'
? input
: input + ' ago'
},
s : 'just now',
future : 'in %s',
m : '1m',
mm : '%dm',
h : '1h',
hh : '%dh',
d : '1d',
dd : '%dd',
M : '1 month',
MM : '%d months',
y : '1 year',
yy : '%d years',
}
});
}
render() {
let comments = this.props.post.numComments == 1
? '1 comment'
: `${this.props.post.numComments} comments`;
let date = moment(this.props.post.date).fromNow();
let authorDate = `${this.props.post.author}${date}`
let collLink = "/~publish/" +
this.props.post.blogOwner + "/" +
this.props.post.collectionName;
let postLink = collLink + "/" + this.props.post.postName;
return (
<div className="w-336 relative"
style={{height:195, marginBottom: 72, marginRight:16}}>
<Link to={postLink}>
<TitleSnippet badge={this.props.post.unread} title={this.props.post.postTitle}/>
<PostSnippet
body={this.props.post.postBody}
/>
</Link>
<p className="label-small gray-50 absolute" style={{bottom:0}}>
{authorDate}
</p>
</div>
);
}
}

View File

@ -1,26 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
export class PostSnippet extends Component {
constructor(props) {
super(props);
}
render() {
let elem = this.props.body.c.find((elem) => {
return (elem.gn === "p" && typeof(elem.c[0]) === "string");
});
let string = (elem === undefined)
? null
: elem.c[0];
return (
<p className="body-regular-400 five-lines"
style={{WebkitBoxOrient: "vertical"}}>
{string}
</p>
);
}
}

View File

@ -1,54 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import { withRouter } from 'react-router';
export class PublishCreate extends Component {
constructor(props){
super(props);
}
render () {
if (!this.props.create) {
return (
<div className="w-100">
<p className="publish">Publish</p>
</div>
);
} else if (this.props.create == 'blog') {
let link = {
pathname: "/~publish/new-blog",
state: {
lastPath: this.props.location.pathname,
lastMatch: this.props.match.path,
lastParams: this.props.match.params,
},
};
return (
<div className="w-100">
<p className="publish">Publish</p>
<Link to={link}>
<p className="create">+New Notebook</p>
</Link>
</div>
);
} else if (this.props.create == 'post') {
let link = {
pathname: "/~publish/new-post",
state: {
lastPath: this.props.location.pathname,
lastMatch: this.props.match.path,
lastParams: this.props.match.params,
},
};
return (
<div className="w-100">
<p className="publish">Publish</p>
<Link to={link}>
<p className="create">+New Note</p>
</Link>
</div>
);
}
}
}

View File

@ -1,72 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import moment from 'moment';
import { Link } from 'react-router-dom';
import { PostSnippet } from '/components/lib/post-snippet';
import { TitleSnippet } from '/components/lib/title-snippet';
export class RecentPreview extends Component {
constructor(props) {
super(props);
moment.updateLocale('en', {
relativeTime: {
past: function(input) {
return input === 'just now'
? input
: input + ' ago'
},
s : 'just now',
future : 'in %s',
m : '1m',
mm : '%dm',
h : '1h',
hh : '%dh',
d : '1d',
dd : '%dd',
M : '1 month',
MM : '%d months',
y : '1 year',
yy : '%d years',
}
});
}
render() {
let comments = this.props.post.numComments == 1
? '1 comment'
: `${this.props.post.numComments} comments`;
let date = moment(this.props.post.date).fromNow();
let authorDate = `~${this.props.post.author}${date}`
let collLink = "/~publish/~" +
this.props.post.blogOwner + "/" +
this.props.post.collectionName;
let postLink = collLink + "/" + this.props.post.postName;
return (
<div className="w-336 relative"
style={{height:240, marginBottom: 72, marginRight: 16}}>
<Link to={postLink}>
<TitleSnippet badge={this.props.post.unread} title={this.props.post.postTitle}/>
<PostSnippet
body={this.props.post.postBody}
/>
</Link>
<div className="absolute" style={{bottom: 0}}>
<p className="label-small gray-50">
{comments}
</p>
<Link to={collLink}>
<p className="body-regular gray-50 one-line mw-336"
style={{WebkitBoxOrient: "vertical"}}>
{this.props.post.collectionTitle}
</p>
</Link>
<p className="label-small gray-50">
{authorDate}
</p>
</div>
</div>
);
}
}

View File

@ -1,106 +0,0 @@
import React, { Component } from 'react';
import { pour } from '/vendor/sigils-1.2.5';
import _ from 'lodash';
const ReactSVGComponents = {
svg: p => {
return (
<svg key={Math.random()}
version={'1.1'}
xmlns={'http://www.w3.org/2000/svg'}
{...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</svg>
)
},
circle: p => {
return (
<circle
key={Math.random()} {...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</circle>
)
},
rect: p => {
return (
<rect
key={Math.random()}
{...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</rect>
)
},
path: p => {
return (
<path
key={Math.random()}
{...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</path>
)
},
g: p => {
return (
<g
key={Math.random()}
{...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</g>
)
},
polygon: p => {
return (
<polygon
key={Math.random()}
{...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</polygon>
)
},
line: p => {
return (
<line
key={Math.random()}
{...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</line>
)
},
polyline: p => {
return (
<polyline
key={Math.random()}
{...p.attr}>
{ _.map(_.get(p, 'children', []), child => ReactSVGComponents[child.tag](child)) }
</polyline>
)
}
}
export class SealDict {
constructor() {
this.dict = {};
}
getPrefix(patp) {
return patp.length === 3 ? patp : patp.substr(0, 3);
}
getSeal(patp, size, prefix) {
if (patp.length > 13) {
patp = "tiz";
}
let sigilShip = prefix ? this.getPrefix(patp) : patp;
let key = `${sigilShip}+${size}`;
if (!this.dict[key]) {
this.dict[key] = pour({size: size, patp: sigilShip, renderer: ReactSVGComponents, margin: 0, colorway: ["#fff", "#000"]})
}
return this.dict[key];
}
}
const sealDict = new SealDict;
export { sealDict }

View File

@ -0,0 +1,14 @@
import React, { Component } from 'react';
//TODO Settings for owned notebooks
export class Settings extends Component {
render() {
return (
<div>
</div>
)
}
}
export default Settings

View File

@ -0,0 +1,67 @@
import React, { Component } from 'react'
import { Route, Link } from 'react-router-dom';
import { NotebookItem } from './notebook-item';
export class Sidebar extends Component {
constructor(props) {
super(props);
this.state = {
sort: "oldest"
}
}
render() {
const { props, state } = this;
let activeClasses = (this.props.active === "sidebar") ? " " : "dn-s ";
let hiddenClasses = true;
if (this.props.popout) {
hiddenClasses = false;
} else {
hiddenClasses = this.props.sidebarShown;
};
//TODO render notebook list from state
// (make a new array of all notebooks from {author: {notebook}}
// prop.notebook obj, case-switch the sorting from this.state.sort, and map it)
//TODO allow for user sorting of notebook list
//
//(reactive dropdown -> amends state -> sort by state prop)
//
let notebooks = <div></div>
return (
<div className={`bn br-m br-l br-xl b--gray4 b--gray2-d lh-copy h-100
flex-shrink-0 mw-300-ns pt3 pt0-m pt0-l pt0-xl
relative ` + activeClasses + ((hiddenClasses)
? "flex-basis-100-s flex-basis-30-ns"
: "dn")}>
<a className="db dn-m dn-l dn-xl f8 pb3 pl3" href="/"> Landscape</a>
<div className="w-100 pa4">
<Link
to="/~publish/new"
className="green2 mr4 f9">
New
</Link>
<Link
to="/~publish/join"
className="f9 gray2">
Join
</Link>
</div>
<div className="overflow-y-scroll h-100">
<h2 className={`f8 pt1 pr4 pb3 pl3 black c-default bb b--gray4 mb2
dn-m dn-l dn-xl`}>
Your Notebooks
</h2>
{/*TODO Dropdown attached to this.state.sort */}
{notebooks}
</div>
</div>
);
}
}
export default Sidebar;

View File

@ -0,0 +1,15 @@
import React, { Component } from 'react'
//TODO fill sigil/avatar + name from props
export class SubscriberItem extends Component {
render() {
return (
<div>
</div>
)
}
}
export default SubscriberItem

View File

@ -0,0 +1,16 @@
import React, { Component } from 'react';
import { SubscriberItem } from './subscriber-item';
//TODO map list of subscriber-items from props
export class Subscribers extends Component {
render() {
return (
<div>
</div>
)
}
}
export default Subscribers

View File

@ -1,30 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
export class TitleSnippet extends Component {
constructor(props){
super(props);
}
render() {
if (this.props.badge) {
return (
<div className="body-large two-lines b"
style={{WebkitBoxOrient: "vertical"}}>
<span className="h2 green-medium"></span>
<span>
{this.props.title}
</span>
</div>
);
} else {
return (
<p className="body-large b two-lines"
style={{WebkitBoxOrient: "vertical"}}>
{this.props.title}
</p>
);
}
}
}

View File

@ -1,267 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import { PathControl } from '/components/lib/path-control';
import { withRouter } from 'react-router';
import urbitOb from 'urbit-ob';
import { stringToSymbol } from '/lib/util';
const PC = withRouter(PathControl);
class FormLink extends Component {
render(props){
if (this.props.enabled) {
return (
<button className="body-large b z-2 pointer" onClick={this.props.action}>
{this.props.body}
</button>
);
}
return (
<p className="gray-30 b body-large">{this.props.body}</p>
);
}
}
export class NewBlog extends Component {
constructor(props){
super(props);
this.state = {
title: '',
invites: [],
page: 'main',
awaiting: false,
validInvites: true,
};
this.titleChange = this.titleChange.bind(this);
this.invitesChange = this.invitesChange.bind(this);
this.firstPost = this.firstPost.bind(this);
this.returnHome = this.returnHome.bind(this);
this.addInvites = this.addInvites.bind(this);
this.blogSubmit = this.blogSubmit.bind(this);
this.titleHeight = 52;
}
blogSubmit() {
let ship = window.ship;
let blogTitle = this.state.title;
let blogId = stringToSymbol(blogTitle);
let permissions = {
read: {
mod: 'black',
who: [],
},
write: {
mod: 'white',
who: [],
}
}
let makeBlog = {
"new-collection" : {
name: blogId,
title: blogTitle,
comments: "open",
"allow-edit": "all",
perm: permissions,
},
};
let sendInvites = {
invite: {
coll: blogId,
title: blogTitle,
who: this.state.invites,
}
}
this.setState({
awaiting: blogId
});
this.props.setSpinner(true);
this.props.api.action("publish", "publish-action", makeBlog);
this.props.api.action("publish", "publish-action", sendInvites);
}
componentDidUpdate(prevProps, prevState) {
if (this.state.awaiting) {
if (this.props.pubs[this.state.awaiting]) {
this.props.setSpinner(false);
if (this.state.redirect === 'new-post') {
this.props.history.push("/~publish/new-post",
{
lastParams: {
ship: `~${window.ship}`,
blog: this.state.awaiting,
}
}
);
} else if (this.state.redirect === 'home') {
this.props.history.push(
`/~publish/~${window.ship}/${this.state.awaiting}`);
}
}
}
}
titleChange(evt){
this.titleInput.style.height = 'auto';
this.titleInput.style.height = (this.titleInput.scrollHeight < 52)
? 52 : this.titleInput.scrollHeight;
this.titleHeight = this.titleInput.style.height;
this.setState({title: evt.target.value});
}
invitesChange(evt){
let tokens = evt.target.value
.trim()
.split(/[\s,]+/)
.map(t => t.trim());
let valid = tokens.reduce((valid, s) =>
valid && (((s !== '~') && urbitOb.isValidPatp(s) && s.includes('~')) ||
(s === '')), true);
if (valid) {
this.setState({
validInvites: true,
invites: tokens.map(t => t.slice(1)),
});
} else {
this.setState({validInvites: false});
}
}
firstPost() {
this.setState({redirect: "new-post"});
this.blogSubmit();
}
addInvites() {
this.setState({page: 'addInvites'});
}
returnHome() {
this.setState({redirect: "home"});
this.blogSubmit();
}
render() {
if (this.state.page === 'main') {
return (
<div>
<PC pathData={false} {...this.props}/>
<div className="absolute w-100"
style={{height: 'calc(100% - 124px)', top: 124}}>
<div className="h-inner dt center mw-688 w-100">
<div className="flex-col dtc v-mid">
<textarea autoFocus
ref={(el) => {this.titleInput = el}}
className="header-2 b--none w-100"
style={{resize:"none", height: this.titleHeight}}
rows={1}
type="text"
name="blogName"
placeholder="Add a Title"
onChange={this.titleChange}>
</textarea>
<hr className="gray-30" style={{marginTop:32, marginBottom: 32}}/>
<FormLink
enabled={(this.state.title !== '')}
action={this.addInvites}
body={"-> Send Invites"}
/>
<hr className="gray-30" style={{marginTop:32, marginBottom: 32}}/>
<FormLink
enabled={(this.state.title !== '')}
action={this.firstPost}
body={"-> Create a first note"}
/>
<hr className="gray-30" style={{marginTop:32, marginBottom: 32}}/>
<Link to="/~publish/recent" className="body-large b">
Cancel
</Link>
</div>
</div>
</div>
</div>
);
} else if (this.state.page === 'addInvites') {
let enableButtons = ((this.state.title !== '') && this.state.validInvites);
let invitesStyle = (this.state.validInvites)
? "body-regular-400 b--none w-100"
: "body-regular-400 b--none w-100 red";
return (
<div>
<PC pathData={false} {...this.props}/>
<div className="absolute w-100"
style={{height: 'calc(100% - 124px)', top: 124}}>
<div className="h-inner dt center mw-688 w-100">
<div className="flex-col dtc v-mid">
<textarea autoFocus
ref={(el) => {this.titleInput = el}}
className="header-2 b--none w-100"
style={{resize:"none", height: this.titleHeight}}
rows={1}
type="text"
name="blogName"
placeholder="Add a Title"
onChange={this.titleChange}>
</textarea>
<p className="body-regular-400" style={{marginTop:25, marginBottom:27}}>
Who is invited to read this notebook?
</p>
<input className={invitesStyle}
style={{caretColor: "black"}}
type="text"
name="invites"
placeholder="~ship-name, ~ship-name"
onChange={this.invitesChange}
/>
<hr className="gray-30" style={{marginTop:32, marginBottom: 32}}/>
<FormLink
enabled={enableButtons}
action={this.firstPost}
body={"-> Save and create a first note"}
/>
<hr className="gray-30" style={{marginTop:32, marginBottom: 32}}/>
<FormLink
enabled={enableButtons}
action={this.returnHome}
body={"-> Save and return home"}
/>
<hr className="gray-30" style={{marginTop:32, marginBottom: 32}}/>
<Link to="/~publish/recent" className="body-large b">
Cancel
</Link>
</div>
</div>
</div>
</div>
);
}
}
}

View File

@ -1,311 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import _ from 'lodash';
import { PathControl } from '/components/lib/path-control';
import { withRouter } from 'react-router';
import { stringToSymbol } from '/lib/util';
const PC = withRouter(PathControl);
class SideTab extends Component {
constructor(props) {
super(props);
}
render() {
if (this.props.enabled){
return (
<div className="w1 z-2 body-regular"
style={{
flexGrow:1,
}}>
<p className="pointer" onClick={this.props.postSubmit}>
-> Post
</p>
<p className="pointer" onClick={this.props.discardPost}>
Discard note
</p>
</div>
);
}
return (
<div style={{flexGrow: 1, height:48}}></div>
);
}
}
class Error extends Component {
constructor(props) {
super(props);
}
render() {
if (this.props.error) {
let lines = this.props.error.split("\n").map((line, i) => {
return (<p key={i}>{line}</p>);
});
return (
<div className="w-100 flex-col">
<p className="w-100 bg-red label-regular pt2 pb2 pl3">
This post contains an error
</p>
<div className="label-regular-mono bg-v-light-gray pb3 pt3">
<div className="center mw-688 w-100">
{lines}
</div>
</div>
</div>
);
} else {
return null;
}
}
}
export class NewPost extends Component {
constructor(props){
super(props);
this.state = {
title: "",
body: "",
awaiting: false,
error: false,
posted: false,
};
this.titleChange = this.titleChange.bind(this);
this.bodyChange = this.bodyChange.bind(this);
this.postSubmit = this.postSubmit.bind(this);
this.discardPost = this.discardPost.bind(this);
this.windowHeight = window.innerHeight - 48 - 76;
this.bodyHeight = 54;
this.titleHeight = 102;
this.post = false;
this.comments = false;
}
postSubmit() {
let last = _.get(this.props, 'location.state', false);
let ship = window.ship;
let blogId = null;
if (last){
ship = (' ' + last.lastParams.ship.slice(1)).slice(1);
blogId = (' ' + last.lastParams.blog).slice(1);
}
let postTitle = this.state.title;
let postId = stringToSymbol(postTitle);
let awaiting = Object.assign({}, {
ship: ship,
blogId: blogId,
postId: postId,
});
let permissions = {
read: {
mod: 'black',
who: [],
},
write: {
mod: 'white',
who: [],
}
};
let content = this.state.body;
if (!this.state.error) {
let newPost = {
"new-post" : {
who: ship,
coll: blogId,
name: postId,
title: postTitle,
comments: "open",
perm: permissions,
content: content,
},
};
this.props.setSpinner(true);
this.setState({
awaiting: awaiting,
posted: {
ship: ship,
blogId: blogId,
postId: postId,
}
}, () => {
this.props.api.action("publish", "publish-action", newPost);
});
} else {
let editPost = {
"edit-post" : {
who: ship,
coll: blogId,
name: postId,
title: postTitle,
comments: "open",
perm: permissions,
content: content,
},
};
this.props.setSpinner(true);
this.setState({
awaiting: awaiting,
}, () => {
this.props.api.action("publish", "publish-action", editPost);
});
}
}
componentDidUpdate(prevProps, prevState) {
if (this.state.awaiting) {
let ship = this.state.awaiting.ship;
let blogId = this.state.awaiting.blogId;
let postId = this.state.awaiting.postId;
let post;
let comments;
if (ship == window.ship) {
post =
_.get(this.props,
`pubs["${blogId}"].posts["${postId}"].post`, false) || false;
comments =
_.get(this.props,
`pubs["${blogId}"].posts["${postId}"].comments`, false) || false;
} else {
post =
_.get(this.props,
`subs["${ship}"]["${blogId}"].posts["${postId}"].post`, false) || false;
comments =
_.get(this.props,
`subs["${ship}"]["${blogId}"].posts["${postId}"].comments`, false) || false;
}
if (!_.isEqual(this.post, post)) {
if (typeof(post) === 'string') {
this.props.setSpinner(false);
this.setState({
awaiting: false,
error: post
});
} else {
this.props.setSpinner(false);
let redirect = `/~publish/~${ship}/${blogId}/${postId}`;
this.props.history.push(redirect);
}
}
if (post) {
this.post = post;
}
if (comments) {
this.comments = comments;
}
}
}
discardPost() {
let last = _.get(this.props, 'location.state', false);
let ship = window.ship;
let blogId = null;
if (last){
ship = (' ' + last.lastParams.ship.slice(1)).slice(1);
blogId = (' ' + last.lastParams.blog).slice(1);
}
if (this.state.error && (ship === window.ship)) {
let del = {
"delete-post": {
coll: this.state.posted.blogId,
post: this.state.posted.postId,
}
};
this.props.api.action("publish", "publish-action", del);
}
let redirect = `/~publish/~${ship}/${blogId}`;
this.props.history.push(redirect);
}
titleChange(evt){
this.titleInput.style.height = 'auto';
this.titleInput.style.height = this.titleInput.scrollHeight+2+'px';
this.titleHeight = this.titleInput.style.height;
this.setState({title: evt.target.value});
}
bodyChange(evt){
this.bodyInput.style.height = 'auto';
this.bodyInput.style.height = this.bodyInput.scrollHeight+2+'px';
this.bodyHeight = this.bodyInput.style.height;
this.setState({body: evt.target.value});
}
render() {
let enabledTab = ((this.state.title !== "") && (this.state.body !== ""));
let mt = (this.windowHeight/2) - 110;
let mb = (this.windowHeight/2) - 90;
return (
<div className="relative w-100" style={{top:124}}>
<PC pathData={false} {...this.props}/>
<Error error={this.state.error}/>
<div>
<div className="w-100" style={{height: mt}}></div>
<div className="flex w-100 z-2" style={{position: 'sticky', top: 132}}>
<div className="w1 z-0" style={{flexGrow:1}}></div>
<div className="mw-688 w-100 z-0" style={{pointerEvents:'none'}}></div>
<SideTab
enabled={enabledTab}
postSubmit={this.postSubmit}
discardPost={this.discardPost}
/>
</div>
<div className="flex relative" style={{top:-74}}>
<div className="w1 z-0" style={{flexGrow:1}}></div>
<div className="flex-col w-100 mw-688 w-100 z-2">
<textarea autoFocus
className="header-2 w-100 b--none overflow-y-hidden"
ref={(el) => {this.titleInput = el}}
style={{resize:"none", marginBottom:8, height: this.titleHeight}}
placeholder="Add a Title"
onChange={this.titleChange.bind(this)}>
</textarea>
<textarea
className="body-regular-400 w-100 z-2 b--none overflow-y-hidden"
ref={(el) => {this.bodyInput = el}}
style={{resize:"none", height: this.bodyHeight}}
placeholder="And type away."
onChange={this.bodyChange.bind(this)}>
</textarea>
</div>
<div className="w1 z-0" style={{flexGrow:1}}></div>
</div>
<div className="w-100" style={{height: mb}}></div>
</div>
</div>
);
}
}

View File

@ -1,33 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { PathControl } from "/components/lib/path-control";
export class NotFound extends Component {
constructor(props) {
super(props);
}
render() {
let pathData = [{text: "Home", url: "/~publish/recent"}];
let backText = "<- Back";
let back = (this.props.history)
? <p className="body-regular pointer" style={{marginTop: 22}}
onClick={() => {this.props.history.goBack()}}>
{backText}
</p>
: null;
return (
<div>
<PathControl pathData={pathData} create={false}/>
<div className="absolute w-100" style={{top:124}}>
<div className="mw-688 center w-100">
{back}
<h2>Page Not Found</h2>
</div>
</div>
</div>
);
}
}

View File

@ -1,546 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import moment from 'moment';
import { Link } from 'react-router-dom';
import { PostBody } from '/components/lib/post-body';
import { Comments } from '/components/lib/comments';
import { PathControl } from '/components/lib/path-control';
import { NextPrev } from '/components/lib/next-prev';
import { NotFound } from '/components/not-found';
import { withRouter } from 'react-router';
import _ from 'lodash';
const NF = withRouter(NotFound);
class Admin extends Component {
constructor(props) {
super(props);
}
render() {
if (!this.props.enabled){
return null;
} else if (this.props.mode === 'view'){
return (
<div className="flex-col fr">
<p className="label-regular gray-50 pointer tr b"
onClick={this.props.editPost}>
Edit
</p>
<p className="label-regular red pointer tr b"
onClick={this.props.deletePost}>
Delete
</p>
</div>
);
} else if (this.props.mode === 'edit'){
return (
<div className="body-regular flex-col fr">
<p className="pointer"
onClick={this.props.savePost}>
-> Save
</p>
<p className="pointer"
onClick={this.props.deletePost}>
Delete note
</p>
</div>
);
}
}
}
export class Post extends Component {
constructor(props){
super(props);
moment.updateLocale('en', {
relativeTime: {
past: function(input) {
return input === 'just now'
? input
: input + ' ago'
},
s : 'just now',
future : 'in %s',
m : '1m',
mm : '%dm',
h : '1h',
hh : '%dh',
d : '1d',
dd : '%dd',
M : '1 month',
MM : '%d months',
y : '1 year',
yy : '%d years',
}
});
this.state = {
mode: 'view',
titleOriginal: '',
bodyOriginal: '',
title: '',
body: '',
awaitingEdit: false,
awaitingLoad: false,
awaitingDelete: false,
ship: this.props.ship,
blogId: this.props.blogId,
postId: this.props.postId,
blog: null,
post: null,
comments: null,
pathData: [],
temporary: false,
notFound: false,
}
this.editPost = this.editPost.bind(this);
this.deletePost = this.deletePost.bind(this);
this.savePost = this.savePost.bind(this);
this.titleChange = this.titleChange.bind(this);
this.bodyChange = this.bodyChange.bind(this);
}
editPost() {
this.setState({mode: 'edit'});
}
savePost() {
if (this.state.title == this.state.titleOriginal &&
this.state.body == this.state.bodyOriginal) {
this.setState({mode: 'view'});
return;
}
this.props.setSpinner(true);
let permissions = {
read: {
mod: 'black',
who: [],
},
write: {
mod: 'white',
who: [],
}
};
let data = {
"edit-post": {
who: this.state.ship,
coll: this.props.blogId,
name: this.props.postId,
title: this.state.title,
comments: this.state.post.info.comments,
perm: permissions,
content: this.state.body,
},
};
this.setState({
awaitingEdit: {
ship: this.state.ship,
blogId: this.props.blogId,
postId: this.props.postId,
}
}, () => {
this.props.api.action("publish", "publish-action", data)
});
}
componentWillMount() {
let ship = this.props.ship;
let blogId = this.props.blogId;
let postId = this.props.postId;
if (ship !== window.ship) {
let blog = _.get(this.props, `subs["${ship}"]["${blogId}"]`, false);
if (blog) {
let post = _.get(blog, `posts["${postId}"].post`, false);
let comments = _.get(blog, `posts["${postId}"].comments`, false);
let blogUrl = `/~publish/${blog.info.owner}/${blog.info.filename}`;
let postUrl = `${blogUrl}/${post.info.filename}`;
this.setState({
titleOriginal: post.info.title,
bodyOriginal: post.raw,
title: post.info.title,
body: post.raw,
blog: blog,
post: post,
comments: comments,
pathData: [
{ text: "Home", url: "/~publish/recent" },
{ text: blog.info.title, url: blogUrl },
{ text: post.info.title, url: postUrl },
],
});
let read = {
read: {
who: ship,
coll: blogId,
post: postId,
}
};
this.props.api.action("publish", "publish-action", read);
} else {
this.setState({
awaitingLoad: {
ship: ship,
blogId: blogId,
postId: postId,
},
temporary: true,
});
this.props.setSpinner(true);
this.props.api.bind(`/collection/${blogId}`, "PUT", ship, "publish",
this.handleEvent.bind(this),
this.handleError.bind(this));
}
} else {
let blog = _.get(this.props, `pubs["${blogId}"]`, false);
let post = _.get(blog, `posts["${postId}"].post`, false);
let comments = _.get(blog, `posts["${postId}"].comments`, false);
if (!blog || !post) {
this.setState({notFound: true});
return;
} else {
let blogUrl = `/~publish/${blog.info.owner}/${blog.info.filename}`;
let postUrl = `${blogUrl}/${post.info.filename}`;
this.setState({
titleOriginal: post.info.title,
bodyOriginal: post.raw,
title: post.info.title,
body: post.raw,
blog: blog,
post: post,
comments: comments,
pathData: [
{ text: "Home", url: "/~publish/recent" },
{ text: blog.info.title, url: blogUrl },
{ text: post.info.title, url: postUrl },
],
});
}
}
}
handleEvent(diff) {
if (diff.data.total) {
let blog = diff.data.total.data;
let post = blog.posts[this.state.postId].post;
let comments = blog.posts[this.state.postId].comments;
let blogUrl = `/~publish/${blog.info.owner}/${blog.info.filename}`;
let postUrl = `${blogUrl}/${post.info.filename}`;
this.setState({
awaitingLoad: false,
titleOriginal: post.info.title,
bodyOriginal: post.raw,
title: post.info.title,
body: post.raw,
blog: blog,
post: post,
comments: comments,
pathData: [
{ text: "Home", url: "/~publish/recent" },
{ text: blog.info.title, url: blogUrl },
{ text: post.info.title, url: postUrl },
],
});
this.props.setSpinner(false);
} else if (diff.data.collection) {
let newBlog = this.state.blog;
newBlog.info = diff.data.collection.data;
this.setState({
blog: newBlog,
});
} else if (diff.data.post) {
this.setState({
post: diff.data.post.data,
});
} else if (diff.data.comments) {
this.setState({
comments: diff.data.comments.data,
});
} else if (diff.data.remove) {
// XX TODO Handle this properly
}
}
handleError(err) {
this.props.setSpinner(false);
this.setState({notFound: true});
}
componentDidUpdate(prevProps, prevState) {
if (this.state.notFound) return;
let ship = this.props.ship;
let blogId = this.props.blogId;
let postId = this.props.postId;
let oldPost = prevState.post;
let oldComments = prevState.comments;
let oldBlog = prevState.blog;
let post;
let comments;
let blog;
if (ship === window.ship) {
blog = _.get(this.props, `pubs["${blogId}"]`, false);
post = _.get(blog, `posts["${postId}"].post`, false);
comments = _.get(blog, `posts["${postId}"].comments`, false);
} else {
blog = _.get(this.props, `subs["${ship}"]["${blogId}"]`, false);
post = _.get(blog, `posts["${postId}"].post`, false);
comments = _.get(blog, `posts["${postId}"].comments`, false);
}
if (this.state.awaitingDelete && (post === false) && oldPost) {
this.props.setSpinner(false);
let redirect = `/~publish/~${this.props.ship}/${this.props.blogId}`;
this.props.history.push(redirect);
return;
}
if (!blog || !post) {
this.setState({notFound: true});
return;
}
if (this.state.awaitingEdit &&
((post.info.title != oldPost.info.title) ||
(post.raw != oldPost.raw))) {
let blogUrl = `/~publish/${blog.info.owner}/${blog.info.filename}`;
let postUrl = `${blogUrl}/${post.info.filename}`;
this.setState({
mode: 'view',
titleOriginal: post.info.title,
bodyOriginal: post.raw,
title: post.info.title,
body: post.raw,
awaitingEdit: false,
post: post,
pathData: [
{ text: "Home", url: "/~publish/recent" },
{ text: blog.info.title, url: blogUrl },
{ text: post.info.title, url: postUrl },
],
});
this.props.setSpinner(false);
let read = {
read: {
who: ship,
coll: blogId,
post: postId,
}
};
this.props.api.action("publish", "publish-action", read);
}
if (!this.state.temporary){
if (oldPost != post) {
let blogUrl = `/~publish/${blog.info.owner}/${blog.info.filename}`;
let postUrl = `${blogUrl}/${post.info.filename}`;
this.setState({
titleOriginal: post.info.title,
bodyOriginal: post.raw,
post: post,
title: post.info.title,
body: post.raw,
pathData: [
{ text: "Home", url: "/~publish/recent" },
{ text: blog.info.title, url: blogUrl },
{ text: post.info.title, url: postUrl },
],
});
let read = {
read: {
who: ship,
coll: blogId,
post: postId,
}
};
this.props.api.action("publish", "publish-action", read);
}
if (oldComments != comments) {
this.setState({comments: comments});
}
if (oldBlog != blog) {
this.setState({blog: blog});
}
}
}
deletePost(){
let del = {
"delete-post": {
coll: this.props.blogId,
post: this.props.postId,
}
};
this.props.setSpinner(true);
this.setState({
awaitingDelete: {
ship: this.props.ship,
blogId: this.props.blogId,
postId: this.props.postId,
}
}, () => {
this.props.api.action("publish", "publish-action", del);
});
}
titleChange(evt){
this.setState({title: evt.target.value});
}
bodyChange(evt){
this.setState({body: evt.target.value});
}
render() {
let adminEnabled = (this.props.ship === window.ship);
if (this.state.notFound) {
return (
<NF/>
);
} else if (this.state.awaitingLoad) {
return null;
} else if (this.state.awaitingEdit) {
return null;
} else if (this.state.mode == 'view') {
let blogLink = `/~publish/~${this.state.ship}/${this.props.blogId}`;
let blogLinkText = `<- Back to ${this.state.blog.info.title}`;
let date = moment(this.state.post.info["date-created"]).fromNow();
let authorDate = `${this.state.post.info.creator}${date}`;
let create = (this.props.ship === window.ship);
return (
<div>
<PathControl pathData={this.state.pathData} create={create}/>
<div className="absolute w-100" style={{top:124}}>
<div className="mw-688 center mt4 flex-col" style={{flexBasis: 688}}>
<Link to={blogLink}>
<p className="body-regular one-line mw-688">
{blogLinkText}
</p>
</Link>
<h2 style={{wordWrap: "break-word"}}>{this.state.titleOriginal}</h2>
<div className="mb4">
<p className="fl label-small gray-50">{authorDate}</p>
<Admin
enabled={adminEnabled}
mode="view"
editPost={this.editPost}
deletePost={this.deletePost}
/>
</div>
<div className="cb">
<PostBody
body={this.state.post.body}
/>
</div>
<hr className="gray-50 w-680 mt4"/>
<NextPrev blog={this.state.blog} postId={this.props.postId} />
<Comments comments={this.state.comments}
api={this.props.api}
ship={this.props.ship}
blogId={this.props.blogId}
postId={this.props.postId}
setSpinner={this.props.setSpinner}
/>
</div>
</div>
</div>
);
} else if (this.state.mode == 'edit') {
let blogLink = `/~publish/~${this.state.ship}/${this.props.blogId}`;
let blogLinkText = `<- Back to ${this.state.blog.info.title}`;
let date = moment(this.state.post.info["date-created"]).fromNow();
let authorDate = `${this.state.post.info.creator}${date}`;
let create = (this.props.ship === window.ship);
return (
<div>
<PathControl pathData={this.state.pathData} create={create}/>
<div className="absolute w-100" style={{top:124}}>
<div className="mw-688 center mt4 flex-col" style={{flexBasis: 688}}>
<Link to={blogLink}>
<p className="body-regular">
{blogLinkText}
</p>
</Link>
<input autoFocus className="header-2 b--none w-100"
type="text"
name="postName"
defaultValue={this.state.titleOriginal}
onChange={this.titleChange}
/>
<div className="mb4">
<p className="fl label-small gray-50">{authorDate}</p>
<Admin
enabled={adminEnabled}
mode="edit"
savePost={this.savePost}
deletePost={this.deletePost}
/>
</div>
<textarea className="cb b--none body-regular-400 w-100 h5"
style={{resize:"none"}}
type="text"
name="postBody"
onChange={this.bodyChange}
defaultValue={this.state.bodyOriginal}>
</textarea>
<hr className="gray-50 w-680 mt4"/>
<NextPrev blog={this.state.blog} postId={this.props.postId} />
<Comments comments={this.state.comments}
api={this.props.api}
ship={this.props.ship}
blogId={this.props.blogId}
postId={this.props.postId}
setSpinner={this.props.setSpinner}
/>
</div>
</div>
</div>
);
}
}
}

View File

@ -1,109 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom'
import { withRouter } from 'react-router';
import { HeaderMenu } from '/components/lib/header-menu';
import moment from 'moment';
const HM = withRouter(HeaderMenu);
export class Pubs extends Component {
constructor(props) {
super(props);
moment.updateLocale('en', {
relativeTime: {
past: function(input) {
return input === 'just now'
? input
: input + ' ago'
},
s : 'just now',
future : 'in %s',
m : '1m',
mm : '%dm',
h : '1h',
hh : '%dh',
d : '1d',
dd : '%dd',
M : '1 month',
MM : '%d months',
y : '1 year',
yy : '%d years',
}
});
}
buildBlogData() {
let data = Object.keys(this.props.pubs).map((blogId) => {
let blog = this.props.pubs[blogId];
return {
url: `/~publish/${blog.info.owner}/${blogId}`,
title: blog.info.title,
host: blog.info.owner,
lastUpdated: moment(blog["last-update"]).fromNow(),
}
});
return data;
}
render() {
let blogData = this.buildBlogData();
let blogs = this.buildBlogData().map( (data, i) => {
let bg = (i % 2 == 0)
? "bg-v-light-gray"
: "bg-white";
let cls = "w-100 flex " + bg;
return (
<div className={cls} key={i}>
<div className="fl body-regular-400 mw-336 w-336 pr3">
<Link to={data.url}>
<p className="ml3 mw-336">
<span>{data.title}</span>
</p>
</Link>
</div>
<p className="fl body-regular-400" style={{flexBasis:336}}>
{data.host}
</p>
<p className="fl body-regular-400" style={{flexBasis:336}}>
{data.lastUpdated}
</p>
</div>
);
});
let invites = (this.props.invites.length > 0);
let unread = (this.props.unread.length > 0);
return (
<div>
<HM invites={invites} unread={unread}/>
<div className="absolute w-100" style={{top:124}}>
<div className="flex-column">
<div className="w-100">
<h2 className="gray-50"
style={{marginLeft: 16, marginTop:32, marginBottom: 16}}>
Notebooks
</h2>
</div>
<div className="w-100 flex">
<p className="fl gray-50 body-regular-400" style={{flexBasis:336}}>
<span className="ml3">Title</span>
</p>
<p className="fl gray-50 body-regular-400" style={{flexBasis:336}}>
Host
</p>
<p className="fl gray-50 body-regular-400" style={{flexBasis:336}}>
Last Updated
</p>
</div>
{blogs}
</div>
</div>
</div>
);
}
}

View File

@ -1,180 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { RecentPreview } from '/components/lib/recent-preview';
import { withRouter } from 'react-router';
import { HeaderMenu } from '/components/lib/header-menu';
const HM = withRouter(HeaderMenu);
export class Recent extends Component {
constructor(props){
super(props)
}
buildRecent() {
var recent = [];
var group = {
date: new Date(),
posts: [],
};
for (var i=0; i<this.props.latest.length; i++) {
let index = this.props.latest[i];
let post = this.retrievePost(index.post, index.coll, index.who);
let postDate = new Date(post.info["date-created"]);
let postProps = this.buildPostPreviewProps(index.post, index.coll, index.who);
if (group.posts.length == 0) {
group = {
date: this.roundDay(postDate),
posts: [postProps],
}
} else if ( this.sameDay(group.date, postDate) ) {
group.posts.push(postProps) ;
} else {
recent.push(Object.assign({}, group));
group = {
date: this.roundDay(postDate),
posts: [postProps],
}
}
if (i == (this.props.latest.length - 1)) {
recent.push(Object.assign({}, group));
}
}
return recent;
}
buildPostPreviewProps(post, coll, who){
let pos = this.retrievePost(post, coll, who);
let col = this.retrieveColl(coll, who);
let com = this.retrieveComments(post, coll, who);
let unread = (-1 === _.findIndex(this.props.unread, {
post: post,
coll: coll,
who: who,
}))
? false: true;
return {
postTitle: pos.info.title,
postName: post,
postBody: pos.body,
numComments: com.length,
collectionTitle: col.title,
collectionName: coll,
author: pos.info.creator.slice(1),
blogOwner: who,
date: pos.info["date-created"],
unread: unread,
}
}
retrievePost(post, coll, who) {
if (who === window.ship) {
return this.props.pubs[coll].posts[post].post;
} else {
return this.props.subs[who][coll].posts[post].post;
}
}
retrieveComments(post, coll, who) {
if (who === window.ship) {
return this.props.pubs[coll].posts[post].comments;
} else {
return this.props.subs[who][coll].posts[post].comments;
}
}
retrieveColl(coll, who) {
if (who === window.ship) {
return this.props.pubs[coll].info;
} else {
return this.props.subs[who][coll].info;
}
}
roundDay(d) {
let result = new Date(d.getTime());
result.setHours(0);
result.setMinutes(0);
result.setSeconds(0);
result.setMilliseconds(0);
return result
}
sameDay(d1, d2) {
return d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate() &&
d1.getFullYear() === d2.getFullYear();
}
dateLabel(d) {
let today = new Date();
let yesterday = new Date(today.getTime() - (1000*60*60*24));
if (this.sameDay(d, today)) {
return "Today";
} else if (this.sameDay(d, yesterday)) {
return "Yesterday";
} else if ( d.getFullYear() === today.getFullYear() ) {
let month = d.toLocaleString('en-us', {month: 'long'});
let day = d.getDate();
return month + ' ' + day;
} else {
let month = d.toLocaleString('en-us', {month: 'long'});
let day = d.getDate();
let year = d.getFullYear();
return month + ' ' + day + ' ' + year;
}
}
render() {
let recent = this.buildRecent();
let body = recent.map((group, i) => {
let posts = group.posts.map((post, j) => {
return (
<RecentPreview
post={post}
key={j}
/>
);
});
let date = this.dateLabel(group.date);
return (
<div key={i}>
<div className="w-100">
<h2 className="gray-50" style={{marginBottom:8}}>
{date}
</h2>
</div>
<div className="flex flex-wrap">
{posts}
</div>
</div>
);
});
let invites = (this.props.invites.length > 0);
let unread = (this.props.unread.length > 0);
return (
<div>
<HM invites={invites} unread={unread}/>
<div className="absolute w-100"
style={{top:124, marginLeft: 16, marginRight: 16, marginTop: 32}}>
<div className="flex-col">
{body}
</div>
</div>
</div>
);
}
}

View File

@ -1,142 +1,111 @@
import React, { Component } from 'react';
import { BrowserRouter, Route } from "react-router-dom";
import classnames from 'classnames';
import { api } from '/api';
import { BrowserRouter, Route, Link } from "react-router-dom";
import { store } from '/store';
import { Recent } from '/components/recent';
import { NewBlog } from '/components/new-blog';
import { NewPost } from '/components/new-post';
import { Skeleton } from '/components/skeleton';
import { Blog } from '/components/blog';
import { Post } from '/components/post';
import { Subs } from '/components/subs';
import { Pubs } from '/components/pubs';
import { Switch } from 'react-router';
import { NewScreen } from '/components/lib/new';
import { JoinScreen } from '/components/lib/join';
import { Notebook } from '/components/lib/notebook';
import { Note } from '/components/lib/note';
//TODO add new note route
export class Root extends Component {
constructor(props) {
super(props);
this.state = store.state;
store.setStateHandler(this.setState.bind(this));
this.setSpinner = this.setSpinner.bind(this);
}
setSpinner(spinner) {
this.setState({
spinner
});
}
render() {
const { props, state } = this;
return (
<BrowserRouter>
<Switch>
<Route exact path="/~publish/recent"
render={ (props) => {
return (
<Skeleton
spinner={this.state.spinner}
children={
<Recent {...this.state} />
}
/>
);
}} />
<Route exact path="/~publish/subs"
render={ (props) => {
return (
<Skeleton
spinner={this.state.spinner}
children={
<Subs {...this.state} api={api}/>
}
/>
);
}} />
<Route exact path="/~publish/pubs"
render={ (props) => {
return (
<Skeleton
spinner={this.state.spinner}
children={
<Pubs {...this.state} />
}
/>
);
}} />
<Route exact path="/~publish"
render={ (props) => {
return (
<Skeleton
popout={false}
active={"sidebar"}
rightPanelHide={true}
sidebarShown={true}
notebooks={state.notebooks}>
<div className={`h-100 w-100 overflow-x-hidden flex flex-column
bg-white bg-gray0-d dn db-ns`}>
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
<p className="f9 pt3 gray2 w-100 h-100 dtc v-mid tc">
Select or create a notebook to begin.
</p>
</div>
</div>
</Skeleton>
)
}}
/>
<Route exact path="/~publish/new"
render={ (props) => {
return (
<Skeleton
popout={false}
active={"rightPanel"}
rightPanelHide={false}
sidebarShown={true}
notebooks={state.notebooks}>
<NewScreen
groups={state.groups}/>
</Skeleton>
)
}}/>
<Route exact path="/~publish/join"
render={ (props) => {
return (
<Skeleton
popout={false}
active={"rightPanel"}
rightPanelHide={false}
sidebarShown={true}
notebooks={state.notebooks}>
<JoinScreen/>
</Skeleton>
)
}}/>
<Route exact path="/~publish/(popout)?/:ship/:notebook/:view?"
render={ (props) => {
let view = (props.match.params.view)
? props.match.params.view
: "posts";
<Route exact path="/~publish/new-blog"
render={ (props) => {
return (
<Skeleton
spinner={this.state.spinner}
children={
<NewBlog api={api}
{...this.state}
setSpinner={this.setSpinner}
{...props}/>
}
/>
);
}} />
<Route exact path="/~publish/new-post"
render={ (props) => {
return (
<Skeleton
spinner={this.state.spinner}
children={
<NewPost api={api}
setSpinner={this.setSpinner}
{...this.state}
{...props}/>
}
/>
);
}} />
<Route exact path="/~publish/:ship/:blog"
render={ (props) => {
return (
<Skeleton
spinner={this.state.spinner}
children={
<Blog
blogId = {props.match.params.blog}
ship = {props.match.params.ship.slice(1)}
api = {api}
setSpinner={this.setSpinner}
{...this.state}
{...props}
/>
}
/>
);
}} />
<Route exact path="/~publish/:ship/:blog/:post"
render={ (props) => {
return (
<Skeleton
spinner={this.state.spinner}
children={
<Post
blogId = {props.match.params.blog}
postId = {props.match.params.post}
ship = {props.match.params.ship.slice(1)}
setSpinner={this.setSpinner}
api = {api}
{...this.state}
{...props}
/>
}
/>
);
}} />
</Switch>
return (
<Skeleton
popout={false}
active={"rightPanel"}
rightPanelHide={false}
sidebarShown={true}
notebooks={state.notebooks}>
<Notebook
notebooks={state.notebooks}
view={view}/>
</Skeleton>
)
}}/>
<Route exact path="/~publish/(popout)?/:ship/:notebook/:note"
render={ (props) => {
return (
<Skeleton
popout={false}
active={"rightPanel"}
rightPanelHide={false}
sidebarShown={true}
notebooks={state.notebooks}>
<Note
notebooks={state.notebooks}/>
</Skeleton>
)
}}/>
</BrowserRouter>
);
)
}
}
export default Root

View File

@ -1,23 +1,44 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { HeaderBar } from '/components/lib/header-bar';
import { Sidebar } from './lib/sidebar';
export class Skeleton extends Component {
constructor(props){
super(props);
}
render() {
const { props, state } = this;
let rightPanelHide = props.rightPanelHide
? "dn-s"
: "";
let popout = !!props.popout
? props.popout
: false;
let popoutWindow = (popout)
? ""
: "h-100-m-40-ns ph4-m ph4-l ph4-xl pb4-m pb4-l pb4-xl"
let popoutBorder = (popout)
? ""
: "ba-m ba-l ba-xl b--gray2 br1"
return (
<div className="h-100 w-100">
<HeaderBar spinner={this.props.spinner}/>
<div className="h-inner">
{this.props.children}
</div>
<div className="h-footer">
<div className={"h-100 w-100 " + popoutWindow}>
<div className={`cf w-100 h-100 flex ` + popoutBorder}>
<Sidebar
popout={popout}
sidebarShown={props.sidebarShown}
active={props.active}
notebooks={props.notebooks}
/>
<div className={"h-100 w-100 " + rightPanelHide} style={{
flexGrow: 1,
}}>
{props.children}
</div>
</div>
</div>
);
}
}
export default Skeleton;

View File

@ -1,200 +0,0 @@
import React, { Component } from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import { withRouter } from 'react-router';
import { HeaderMenu } from '/components/lib/header-menu';
import moment from 'moment';
const HM = withRouter(HeaderMenu);
export class Subs extends Component {
constructor(props) {
super(props);
this.accept = this.accept.bind(this);
this.reject = this.reject.bind(this);
this.unsubscribe = this.unsubscribe.bind(this);
moment.updateLocale('en', {
relativeTime: {
past: function(input) {
return input === 'just now'
? input
: input + ' ago'
},
s : 'just now',
future : 'in %s',
m : '1m',
mm : '%dm',
h : '1h',
hh : '%dh',
d : '1d',
dd : '%dd',
M : '1 month',
MM : '%d months',
y : '1 year',
yy : '%d years',
}
});
}
buildBlogData() {
let invites = this.props.invites.map((inv) => {
return {
type: 'invite',
url: `/~publish/~${inv.who}/${inv.coll}`,
host: `~${inv.who}`,
title: inv.title,
blogId: inv.coll,
};
})
let data = Object.keys(this.props.subs).map((ship) => {
let perShip = Object.keys(this.props.subs[ship]).map((blogId) => {
let blog = this.props.subs[ship][blogId];
return {
type: 'regular',
url: `/~publish/${blog.info.owner}/${blogId}`,
title: blog.info.title,
host: blog.info.owner,
lastUpdated: moment(blog["last-update"]).fromNow(),
blogId: blogId,
}
});
return perShip;
});
let merged = data.flat();
return invites.concat(merged);
}
accept(host, blogId) {
let subscribe = {
subscribe: {
who: host.slice(1),
coll: blogId,
}
};
this.props.api.action("publish", "publish-action", subscribe);
}
reject(host, blogId) {
let reject = {
"reject-invite": {
who: host.slice(1),
coll: blogId,
}
};
this.props.api.action("publish", "publish-action", reject);
}
unsubscribe(host, blogId) {
let unsubscribe = {
unsubscribe: {
who: host.slice(1),
coll: blogId,
}
};
this.props.api.action("publish", "publish-action", unsubscribe);
}
render() {
let blogData = this.buildBlogData();
let blogs = this.buildBlogData().map( (data, i) => {
let bg = (i % 2 == 0)
? "bg-v-light-gray"
: "bg-white";
let cls = "w-100 flex " + bg;
if (data.type === 'regular') {
return (
<div className={cls} key={i}>
<div className="fl mw-336" style={{flexBasis: 336}}>
<Link to={data.url}>
<p className="body-regular-400 pr3 ml3">
<span>{data.title}</span>
</p>
</Link>
</div>
<p className="fl body-regular-400" style={{flexBasis:336}}>
{data.host}
</p>
<p className="fl body-regular-400" style={{flexBasis:336}}>
{data.lastUpdated}
</p>
<p className="fl body-regular-400 pointer"
style={{flexBasis:336}}
onClick={this.unsubscribe.bind(this, data.host, data.blogId)}>
Unsubscribe
</p>
</div>
);
} else if (data.type === 'invite') {
return (
<div className={cls} key={i}>
<div className="fl body-regular-400" style={{flexBasis: 336}}>
<Link to={data.url}>
<div className="mw-336 pr3">
<span className="body-large green-medium"></span>
<span className="body-regular-400">Invite to </span>
<span className="body-regular">
{data.title}
</span>
</div>
</Link>
</div>
<p className="fl body-regular-400" style={{flexBasis:336}}>
{data.host}
</p>
<p className="fl body-regular-400" style={{flexBasis:336}}>
</p>
<p className="fl body-regular-400" style={{flexBasis:336}}>
<span className="green underline pointer"
onClick={this.accept.bind(this, data.host, data.blogId)}>
Accept
</span>
<span></span>
<span className="red underline pointer"
onClick={this.reject.bind(this, data.host, data.blogId)}>
Reject
</span>
</p>
</div>
);
}
});
let invites = (this.props.invites.length > 0);
let unread = (this.props.unread.length > 0);
return (
<div>
<HM invites={invites} unread={unread}/>
<div className="absolute w-100" style={{top:124}}>
<div className="flex-column">
<div className="w-100">
<h2 className="gray-50"
style={{marginLeft: 16, marginTop: 32, marginBottom: 16}}>
Subscriptions
</h2>
</div>
<div className="w-100 flex">
<p className="fl gray-50 body-regular-400" style={{flexBasis:336}}>
<span className="ml3">Title</span>
</p>
<p className="fl gray-50 body-regular-400" style={{flexBasis:336}}>
Host
</p>
<p className="fl gray-50 body-regular-400" style={{flexBasis:336}}>
Last Updated
</p>
<p className="fl gray-50 body-regular-400" style={{flexBasis:336}}>
</p>
</div>
{blogs}
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,7 @@
import _ from 'lodash';
export class InitialReducer {
reduce(json, state) {
state.notebooks = json.notebooks || null;
}
}

View File

@ -0,0 +1,17 @@
import _ from 'lodash';
export class LocalReducer {
reduce(json, state) {
let data = _.get(json, 'local', false);
if (data) {
this.sidebarToggle(data, state);
}
}
sidebarToggle(obj, state) {
let data = _.has(obj, 'sidebarToggle', false);
if (data) {
state.sidebarShown = obj.sidebarToggle;
}
}
}

View File

@ -0,0 +1,229 @@
import _ from 'lodash';
export class PrimaryReducer {
reduce(json, state){
console.log("primaryReducer", json);
switch(Object.keys(json)[0]){
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) {
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) {
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-by-date"]) {
state.notebooks[host][book]["notes-by-date"].unshift(noteId);
} else {
state.notebooks[host][book]["notes-by-date"] = [noteId];
}
if (state.notebooks[host][book].notes) {
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 (state.notebooks[host][book].notes[prevNoteId]) {
state.notebooks[host][book].notes[prevNoteId]["next-note"] = noteId;
}
}
}
addComment(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) {
state.notebooks[host][book].notes[note].comments.unshift(comment);
} else if (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]["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"];
}
}
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])
{
state.notebooks[host][book].notes[noteId]["read"] = true;
}
}
}

View File

@ -0,0 +1,176 @@
import _ from 'lodash';
export class ResponseReducer {
reduce(json, state) {
console.log("responseReducer", json);
switch(json.type) {
case "notebooks":
this.handleNotebooks(json, state);
break;
case "notebook":
this.handleNotebook(json, state);
break;
case "note":
this.handleNote(json, state);
break;
case "notes-page":
this.handleNotesPage(json, state);
break;
case "comments-page":
this.handleCommentsPage(json, state);
break;
case "local":
this.sidebarToggle(json, state);
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"];
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) {
state.notebooks[json.host][json.notebook].notes[json.note].comments
.concat(json.data);
} 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");
}
}
sidebarToggle(json, state) {
let data = _.has(json, 'sidebarToggle', false);
if (data) {
state.sidebarShown = json.type.local.sidebarToggle;
}
}
}

View File

@ -1,336 +0,0 @@
export class RumorReducer {
reduce(json, state){
if (json.collection) {
this.reduceCollection(json.collection, state);
}
if (json.post) {
this.reducePost(json, state);
}
if (json.comments) {
this.reduceComments(json, state);
}
if (json.total) {
this.reduceTotal(json, state);
}
if (json.remove) {
this.reduceRemove(json.remove, state);
}
}
reduceRemove(json, state) {
if (json.who === window.ship) {
if (json.post) {
this.removePost(json, state);
delete state.pubs[json.coll].posts[json.post];
} else {
let postIds = Object.keys(state.pubs[json.coll].posts);
postIds.forEach((postId) => {
this.removePost({
who: json.who,
coll: json.coll,
post: postId,
}, state);
});
delete state.pubs[json.coll];
}
} else {
if (json.post) {
this.removePost(json, state);
delete state.subs[json.who][json.coll].posts[json.post];
} else {
let postIds = Object.keys(state.subs[json.who][json.coll].posts);
postIds.forEach((postId) => {
this.removePost({
who: json.who,
coll: json.coll,
post: postId,
}, state);
});
delete state.subs[json.who][json.coll];
}
}
}
removePost(json, state) {
this.removeLatest(json, state);
this.removeOrder(json, state);
this.removeUnread(json, state);
}
removeLatest(json, state) {
let idx = _.findIndex(state.latest, json);
_.pullAt(state.latest, [idx]);
}
removeUnread(json, state) {
let idx = _.findIndex(state.latest, json);
_.pullAt(state.latest, [idx]);
}
removeOrder(json, state) {
if (json.who === window.ship) {
if (state.pubs[json.coll]) {
let pinIdx = state.pubs[json.coll].order.pin.indexOf(json.post);
let unpinIdx = state.pubs[json.coll].order.unpin.indexOf(json.post);
if (pinIdx != -1) {
_.pullAt(state.pubs[json.coll].order.pin, [pinIdx]);
}
if (unpinIdx != -1) {
_.pullAt(state.pubs[json.coll].order.unpin, [unpinIdx]);
}
}
} else {
if (state.subs[json.who][json.coll]) {
let pinIdx =
state.subs[json.who][json.coll].order.pin.indexOf(json.post);
let unpinIdx =
state.subs[json.who][json.coll].order.unpin.indexOf(json.post);
if (pinIdx != -1) {
_.pullAt(state.subs[json.who][json.coll].order.pin, [pinIdx]);
}
if (unpinIdx != -1) {
_.pullAt(state.subs[json.who][json.coll].order.unpin, [unpinIdx]);
}
}
}
}
reduceCollection(json, state) {
if (json.who === window.ship) {
if (state.pubs[json.coll]) {
state.pubs[json.coll].info = json.data;
} else {
state.pubs[json.coll] = {
info: json.data,
order: { pin: [], unpin: [] },
posts: {},
}
}
} else {
if (state.subs[json.who]) {
if (state.subs[json.who][json.coll]) {
state.subs[json.who][json.coll].info = json.data;
} else {
state.subs[json.who][json.coll] = {
info: json.data,
order: { pin: [], unpin: [] },
posts: {},
}
}
} else {
state.subs[json.who] = {
[json.coll]: {
info: json.data,
order: { pin: [], unpin: [] },
posts: {},
}
}
}
}
}
reducePost(json, state) {
let who = json.post.who;
let coll = json.post.coll;
let post = json.post.post;
let data = json.post.data;
if (who === window.ship) {
if (state.pubs[coll].posts[post]) {
state.pubs[coll].posts[post].post = data;
} else {
state.pubs[coll].posts[post] = {
post: data,
comments: [],
};
}
} else {
if (state.subs[who][coll].posts[post]) {
state.subs[who][coll].posts[post].post = data;
} else {
state.subs[who][coll].posts[post] = {
post: data,
comments: [],
};
}
}
this.insertPost(json, state);
}
insertPost(json, state) {
if (typeof(json.post.data) === 'string') {
return;
}
this.insertLatest(json, state);
this.insertUnread(json, state);
this.insertOrder(json, state);
}
insertLatest(json, state) {
let newIndex = {
post: json.post.post,
coll: json.post.coll,
who: json.post.who,
}
let newDate = json.post.data.info["date-created"];
if (state.latest.length == 0) {
state.latest.push(newIndex);
return;
}
if (state.latest.indexOf(newIndex) != -1) {
return;
}
for (var i=0; i<state.latest.length; i++) {
let postId = state.latest[i].post;
let blogId = state.latest[i].coll;
let ship = state.latest[i].who;
if (newIndex.post == postId && newIndex.coll == blogId && newIndex.who == ship) {
break;
}
let idate = this.retrievePost(state, blogId, postId, ship).info["date-created"];
if (newDate >= idate) {
state.latest.splice(i, 0, newIndex);
break;
} else if (i == (state.latest.length - 1)) {
state.latest.push(newIndex);
break;
}
}
}
insertUnread(json, state) {
if (json.post.who != window.ship) {
state.unread.push({
post: json.post.post,
coll: json.post.coll,
who: json.post.who,
});
}
}
insertOrder(json, state) {
let blogId = json.post.coll;
let ship = json.post.who;
let blog = this.retrieveColl(state, blogId, ship);
let list = json.post.data.info.pinned
? blog.order.pin
: blog.order.unpin;
let newDate = json.post.data.info["date-created"];
if (list.length == 0) {
list.push(json.post.post);
}
if (list.indexOf(json.post.post) != -1) {
return;
}
for (var i=0; i<list.length; i++) {
let postId = list[i];
if (json.post.post === postId) {
break;
}
let idate = this.retrievePost(state, blogId, postId, ship).info["date-created"];
if (newDate >= idate) {
list.splice(i, 0, json.post.post);
break;
} else if (i == (state.latest.length - 1)) {
list.push(json.post.post);
break;
}
}
if (window.ship == ship) {
state.pubs[blogId].order = json.post.data.info.pinned
? {pin: list, unpin: blog.order.unpin}
: {pin: blog.order.pin, unpin: list};
} else {
state.subs[ship][blogId].order = json.post.data.info.pinned
? {pin: list, unpin: blog.order.unpin}
: {pin: blog.order.pin, unpin: list};
}
}
retrieveColl(state, coll, who) {
if (who === window.ship) {
return state.pubs[coll];
} else {
return state.subs[who][coll];
}
}
retrievePost(state, coll, post, who) {
if (who === window.ship) {
return state.pubs[coll].posts[post].post;
} else {
return state.subs[who][coll].posts[post].post;
}
}
reduceComments(json, state) {
let who = json.comments.who;
let coll = json.comments.coll;
let post = json.comments.post;
let data = json.comments.data;
if (who === window.ship) {
if (state.pubs[coll].posts[post]) {
state.pubs[coll].posts[post].comments = data;
} else {
state.pubs[coll].posts[post] = {
post: null,
comments: data,
};
}
} else {
if (state.subs[who][coll].posts[post]) {
state.subs[who][coll].posts[post].comments = data;
} else {
state.subs[who][coll].posts[post] = {
post: null,
comments: data,
};
}
}
}
reduceTotal(json, state) {
if (json.total.who == window.ship) {
state.pubs[json.total.coll] = json.total.data
} else {
if (state.subs[json.total.who]) {
state.subs[json.total.who][json.total.coll] = json.total.data;
} else {
state.subs[json.total.who] = {
[json.total.coll] : json.total.data
}
}
}
let posts = Object.keys(json.total.data.posts);
for (var i=0; i<posts.length; i++) {
let post = {
post: {
coll: json.total.coll,
post: posts[i],
who: json.total.who,
data: json.total.data.posts[posts[i]].post,
}
};
this.insertPost(post, state);
}
}
}

View File

@ -1,14 +0,0 @@
import _ from 'lodash';
export class SpinnerReducer {
reduce(json, state){
if (json.spinner == undefined) {
return;
} else if (json.spinner == true) {
state.spinner = true;
} else if (json.spinner == false) {
state.spinner = false;
}
return;
}
}

View File

@ -1,36 +0,0 @@
import _ from 'lodash';
export class UpdateReducer {
reduce(json, state){
if (json.invite) {
this.reduceInvite(json.invite, state);
} else if (json.unread) {
this.reduceUnread(json.unread, state);
}
}
reduceInvite(json, state) {
let val = {
title: json.title,
coll: json.coll,
who: json.who,
};
if (json.add) {
state.invites.push(val);
} else {
let idx = _.findIndex(state.invites, val)
_.pullAt(state.invites, [idx]);
}
}
reduceUnread(json, state) {
if (json.add) {
state.unread = _.uniq(state.unread.concat(json.posts));
} else {
let idx = json.posts.map((val) => {
return _.findIndex(state.unread, val);
});
_.pullAt(state.unread, idx);
}
}
}

View File

@ -1,31 +1,38 @@
import { UpdateReducer } from '/reducers/update';
import { RumorReducer } from '/reducers/rumor';
import { SpinnerReducer } from '/reducers/spinner';
import { InitialReducer } from '/reducers/initial';
import { PrimaryReducer } from '/reducers/primary';
import { ResponseReducer } from '/reducers/response';
class Store {
constructor() {
this.state = {
notebooks: {},
groups: {},
permissions: {},
invites: {},
spinner: false,
...window.injectedState,
sidebarShown: false,
}
this.updateReducer = new UpdateReducer();
this.rumorReducer = new RumorReducer();
this.spinnerReducer = new SpinnerReducer();
this.initialReducer = new InitialReducer();
this.primaryReducer = new PrimaryReducer();
this.responseReducer = new ResponseReducer();
this.setState = () => {};
this.initialReducer.reduce(window.injectedState, this.state);
}
setStateHandler(setState) {
this.setState = setState;
}
handleEvent(data) {
this.updateReducer.reduce(data.data, this.state);
this.rumorReducer.reduce(data.data, this.state);
this.spinnerReducer.reduce(data.data, this.state);
handleEvent(evt) {
if (evt.from && evt.from.path === '/primary'){
this.primaryReducer.reduce(evt.data, this.state);
} else if (evt.type) {
this.responseReducer.reduce(evt, this.state);
}
this.setState(this.state);
}
}
export let store = new Store();

View File

@ -5,33 +5,11 @@ import classnames from 'classnames';
export default class PublishTile extends Component {
constructor(props){
super(props);
console.log("publish-tile", this.props);
}
render(){
let info = [];
if (this.props.data.invites > 0) {
let text = (this.props.data.invites == 1)
? "Invite"
: "Invites"
info.push(
<p key={1}>
<span className="green-medium">{this.props.data.invites} </span>
<span>{text}</span>
</p>
);
}
if (this.props.data.new > 0) {
let text = (this.props.data.new == 1)
? "New Post"
: "New Posts"
info.push(
<p key={2}>
<span className="green-medium">{this.props.data.new} </span>
<span>{text}</span>
</p>
);
}
render(){
return (
<div className="w-100 h-100 relative" style={{background: "#1a1a1a"}}>
<a className="w-100 h-100 db no-underline" href="/~publish">
@ -47,7 +25,7 @@ export default class PublishTile extends Component {
height={102} />
<div className="absolute w-100 flex-col body-regular white"
style={{verticalAlign: "bottom", bottom: 8, left: 8}}>
{info}
<span className="green-medium">{this.props.data.notifications}</span>
</div>
</a>
</div>