mirror of
https://github.com/urbit/shrub.git
synced 2025-01-01 17:16:47 +03:00
Merge branch 'publish-remix-fe-encore' into os1-rc
This commit is contained in:
commit
45f9adc10c
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
BIN
pkg/arvo/app/publish/img/Home.png
Normal file
BIN
pkg/arvo/app/publish/img/Home.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 679 B |
BIN
pkg/arvo/app/publish/img/SwitcherClosed.png
Normal file
BIN
pkg/arvo/app/publish/img/SwitcherClosed.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
BIN
pkg/arvo/app/publish/img/SwitcherOpen.png
Normal file
BIN
pkg/arvo/app/publish/img/SwitcherOpen.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 245 B |
BIN
pkg/arvo/app/publish/img/popout.png
Normal file
BIN
pkg/arvo/app/publish/img/popout.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
@ -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
@ -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
|
||||
==
|
||||
--
|
||||
|
@ -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))
|
||||
--
|
||||
--
|
||||
--
|
||||
|
@ -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
|
||||
--
|
||||
|
@ -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
|
||||
--
|
||||
|
13
pkg/arvo/mar/publish/notebook-delta.hoon
Normal file
13
pkg/arvo/mar/publish/notebook-delta.hoon
Normal file
@ -0,0 +1,13 @@
|
||||
::
|
||||
:::: /hoon/action/publish/mar
|
||||
::
|
||||
/- *publish
|
||||
=, format
|
||||
::
|
||||
|_ del=notebook-delta
|
||||
::
|
||||
++ grab
|
||||
|%
|
||||
++ noun notebook-delta
|
||||
--
|
||||
--
|
83
pkg/arvo/mar/publish/primary-delta.hoon
Normal file
83
pkg/arvo/mar/publish/primary-delta.hoon
Normal 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
|
||||
==
|
||||
==
|
||||
--
|
||||
--
|
@ -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)]
|
||||
==
|
||||
::
|
||||
==
|
||||
::
|
||||
--
|
||||
--
|
@ -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]
|
||||
==
|
||||
==
|
||||
::
|
||||
==
|
||||
::
|
||||
--
|
||||
--
|
@ -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]
|
||||
==
|
||||
--
|
||||
|
41
pkg/interface/publish/package-lock.json
generated
41
pkg/interface/publish/package-lock.json
generated
@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
1
pkg/interface/publish/src/css/indigo-static.css
Normal file
1
pkg/interface/publish/src/css/indigo-static.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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';
|
||||
|
@ -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 />
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
16
pkg/interface/publish/src/js/components/lib/about.js
Normal file
16
pkg/interface/publish/src/js/components/lib/about.js
Normal 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
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
14
pkg/interface/publish/src/js/components/lib/comment-item.js
Normal file
14
pkg/interface/publish/src/js/components/lib/comment-item.js
Normal 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
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export class IconSpinner extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="spinner-pending"></div>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
14
pkg/interface/publish/src/js/components/lib/join.js
Normal file
14
pkg/interface/publish/src/js/components/lib/join.js
Normal 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
|
15
pkg/interface/publish/src/js/components/lib/new-post.js
Normal file
15
pkg/interface/publish/src/js/components/lib/new-post.js
Normal 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;
|
16
pkg/interface/publish/src/js/components/lib/new.js
Normal file
16
pkg/interface/publish/src/js/components/lib/new.js
Normal 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
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
16
pkg/interface/publish/src/js/components/lib/note-item.js
Normal file
16
pkg/interface/publish/src/js/components/lib/note-item.js
Normal 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
|
15
pkg/interface/publish/src/js/components/lib/note-list.js
Normal file
15
pkg/interface/publish/src/js/components/lib/note-list.js
Normal 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
|
18
pkg/interface/publish/src/js/components/lib/note.js
Normal file
18
pkg/interface/publish/src/js/components/lib/note.js
Normal 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
|
14
pkg/interface/publish/src/js/components/lib/notebook-item.js
Normal file
14
pkg/interface/publish/src/js/components/lib/notebook-item.js
Normal 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
|
30
pkg/interface/publish/src/js/components/lib/notebook.js
Normal file
30
pkg/interface/publish/src/js/components/lib/notebook.js
Normal 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
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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 }
|
14
pkg/interface/publish/src/js/components/lib/settings.js
Normal file
14
pkg/interface/publish/src/js/components/lib/settings.js
Normal 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
|
67
pkg/interface/publish/src/js/components/lib/sidebar.js
Normal file
67
pkg/interface/publish/src/js/components/lib/sidebar.js
Normal 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;
|
@ -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
|
16
pkg/interface/publish/src/js/components/lib/subscribers.js
Normal file
16
pkg/interface/publish/src/js/components/lib/subscribers.js
Normal 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
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
7
pkg/interface/publish/src/js/reducers/initial.js
Normal file
7
pkg/interface/publish/src/js/reducers/initial.js
Normal file
@ -0,0 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export class InitialReducer {
|
||||
reduce(json, state) {
|
||||
state.notebooks = json.notebooks || null;
|
||||
}
|
||||
}
|
17
pkg/interface/publish/src/js/reducers/local.js
Normal file
17
pkg/interface/publish/src/js/reducers/local.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
229
pkg/interface/publish/src/js/reducers/primary.js
Normal file
229
pkg/interface/publish/src/js/reducers/primary.js
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
176
pkg/interface/publish/src/js/reducers/response.js
Normal file
176
pkg/interface/publish/src/js/reducers/response.js
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user