Merge pull request #3304 from ohAitch/patch-3

Fix {a/$foo} in type printing to [a=%foo]
This commit is contained in:
Fang 2020-08-13 10:44:36 +02:00 committed by GitHub
commit 22c66f9ac4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
106 changed files with 4504 additions and 2594 deletions

View File

@ -30,7 +30,7 @@ If applicable, add screenshots to help explain your problem.
**System (please supply the following information, if relevant):**
- OS: [e.g. macOS, linux64, FreeBSD]
- Vere and Urbit OS versions
- Your ship's `%base` hash (use `.^(@uv %cz /=base=)` to check)
- Your ship's `%base` hash (use `+trouble` to check)
**Additional context**
Add any other context about the problem here.

View File

@ -27,13 +27,13 @@ If applicable, add screenshots to help explain your problem. If possible, please
**Desktop (please complete the following information):**
- OS: [e.g. MacOS 10.15.3]
- Browser [e.g. chrome, safari]
- Base hash of your urbit ship. Run ` .^(@uv %cz /=base=)` in Dojo to see this.
- Base hash of your urbit ship. Run `+trouble` in Dojo to see this.
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Base hash of your urbit ship. Run ` .^(@uv %cz /=base=)` in Dojo to see this.
- Base hash of your urbit ship. Run `+trouble` in Dojo to see this.
**Additional context**
Add any other context about the problem here.

View File

@ -175,14 +175,15 @@ the pill to have the new files/hash. For most things, it is sufficient to run
However, if you've made a change to Landscape's JS, then you will need to build
a "glob" and upload it to bootstrap.urbit.org. To do this, run `npm install;
npm run build:prod` in `pkg/interface`, and add the resulting
`pkg/arvo/app/landscape/index.js` to a fakezod at that path (or just create a
`pkg/arvo/app/landscape/index.[hash].js` to a fakezod at that path (or just create a
new fakezod with `urbit -F zod -B bin/solid.pill -A pkg/arvo`). Run
`:glob|make`, and this will output a file in `fakezod/.urb/put/glob-0vXXX.glob`.
Upload this file to bootstrap.urbit.org, and modify `+hash` at the top of
`pkg/arvo/app/glob.hoon` to match the hash in the filename. Do not commit the
produced `index.js` and make sure it doesn't end up in your pills (they should
be less than 10MB each).
`pkg/arvo/app/glob.hoon` to match the hash in the filename of the `.glob` file.
Amend `pkg/arvo/app/landscape/index.html` to import the hashed JS bundle, instead
of the unversioned index.js. Do not commit the produced `index.js` and
make sure it doesn't end up in your pills (they should be less than 10MB each).
### Tag the resulting commit

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d862132e99ed393d786fb4bdb7c487bced2d4c9efc2cb7e174a73b6acca4799d
size 6304536
oid sha256:6cd7246753c12c7acb757e1a6ee54c177806c20a137ad8fb4300c000ac146a0f
size 6260139

View File

@ -21,8 +21,10 @@
state-4
state-5
state-6
state-7
==
::
+$ state-7 [%7 state-base]
+$ state-6 [%6 state-base]
+$ state-5 [%5 state-base]
+$ state-4 [%4 state-base]
@ -52,7 +54,7 @@
$% [%chat-update update:store]
==
--
=| state-6
=| state-7
=* state -
::
%- agent:dbug
@ -81,8 +83,14 @@
=/ old !<(versioned-state old-vase)
=| cards=(list card)
|-
?: ?=(%6 -.old)
?: ?=(%7 -.old)
[cards this(state old)]
?: ?=(%6 -.old)
=. cards
%+ weld cards
^- (list card)
[%pass /s %agent [our.bol %chat-hook] %poke %noun !>(%fix-out-of-sync)]~
$(-.old %7)
?: ?=(?(%3 %4 %5) -.old)
=. cards
%+ weld cards
@ -327,7 +335,7 @@
?+ mark (on-poke:def mark vase)
%json (poke-json:cc !<(json vase))
%chat-action (poke-chat-action:cc !<(action:store vase))
%noun (poke-fix-dms:cc %fix-dms)
%noun (poke-noun:cc !<(?(%fix-dm %fix-out-of-sync) vase))
::
%chat-hook-action
(poke-chat-hook-action:cc !<(action:hook vase))
@ -389,51 +397,80 @@
|_ bol=bowl:gall
++ grp ~(. grpl bol)
::
++ poke-fix-dms
|= a=%fix-dms
++ poke-noun
|= a=?(%fix-dm %fix-out-of-sync)
^- (quip card _state)
|^
:_ state
%- zing
%+ turn
~(tap by synced)
|= [=path host=ship]
^- (list card)
?> ?=([@ @ *] path)
=/ =ship (slav %p i.path)
?: =(ship our.bol)
:: local dm, no need to do cleanup
~
?: ?=(^ (groups-of-chat path))
:: correctly initialized, no need to do cleanup
::
~
?. =((end 3 4 i.t.path) 'dm--')
~
:- =- [%pass /fixdm %agent [our.bol %chat-view] %poke %chat-view-action -]
!> ^- action:view
[%delete path]
=/ new-dm /(scot %p our.bol)/(crip (weld "dm--" (trip (scot %p ship))))
=/ mailbox=(unit mailbox:store) (chat-scry path)
?~ mailbox
~
:~ =- [%pass /fixdm %agent [our.bol %chat-view] %poke %chat-view-action -]
!> ^- action:view
:* %create
%- crip
(zing [(trip (scot %p our.bol)) " <-> " (trip (scot %p ship)) ~])
''
new-dm
ship+new-dm
[%invite (silt ~[ship])]
(silt ~[ship])
%.y
%.n
==
::
=- [%pass /fixdm %agent [our.bol %chat-store] %poke %chat-action -]
!> ^- action:store
[%messages new-dm envelopes.u.mailbox]
?- a
%fix-dm (fix-dm %fix-dm)
%fix-out-of-sync (fix-out-of-sync %fix-out-of-sync)
==
::
++ fix-out-of-sync
|= b=%fix-out-of-sync
^- (list card)
%- zing
%+ turn ~(tap by synced)
|= [=path host=ship]
^- (list card)
?: =(host our.bol) ~
?> ?=([@ @ ~] path)
=/ =ship (slav %p i.path)
:~ =- [%pass / %agent [our.bol %chat-hook] %poke %chat-hook-action -]
!> ^- action:hook
[%remove path]
::
=- [%pass / %agent [our.bol %chat-hook] %poke %chat-hook-action -]
!> ^- action:hook
[%add-synced ship path %.y]
==
::
++ fix-dm
|= b=%fix-dm
^- (list card)
%- zing
%+ turn
~(tap by synced)
|= [=path host=ship]
^- (list card)
?> ?=([@ @ *] path)
=/ =ship (slav %p i.path)
?: =(ship our.bol)
:: local dm, no need to do cleanup
~
?: ?=(^ (groups-of-chat path))
:: correctly initialized, no need to do cleanup
::
~
?. =((end 3 4 i.t.path) 'dm--')
~
:- =- [%pass /fixdm %agent [our.bol %chat-view] %poke %chat-view-action -]
!> ^- action:view
[%delete path]
=/ new-dm /(scot %p our.bol)/(crip (weld "dm--" (trip (scot %p ship))))
=/ mailbox=(unit mailbox:store) (chat-scry path)
?~ mailbox
~
:~ =- [%pass /fixdm %agent [our.bol %chat-view] %poke %chat-view-action -]
!> ^- action:view
:* %create
%- crip
(zing [(trip (scot %p our.bol)) " <-> " (trip (scot %p ship)) ~])
''
new-dm
ship+new-dm
[%invite (silt ~[ship])]
(silt ~[ship])
%.y
%.n
==
::
=- [%pass /fixdm %agent [our.bol %chat-store] %poke %chat-action -]
!> ^- action:store
[%messages new-dm envelopes.u.mailbox]
==
--
::
++ poke-json
|= jon=json

View File

@ -268,7 +268,7 @@
%group-store
%group-push-hook
=/ =cage
:- %group-action
:- %group-update
!> ^- action:group-store
[%change-policy rid %invite %add-invites (sy ship ~)]
[%pass / %agent [entity.rid app] %poke cage]

View File

@ -7,17 +7,20 @@
$% [%clay =path]
[%glob =glob:glob]
==
+$ state-1
$: %1
=configuration:srv
+$ state-base
$: =configuration:srv
=serving
==
+$ state-2
$: %2
state-base
==
--
::
%+ verb |
%- agent:dbug
::
=| state-1
=| state-2
=* state -
^- agent:gall
|_ =bowl:gall
@ -60,12 +63,18 @@
^- [content ?]
[[%clay clay-path] public]
==
?> ?=(%1 -.old-state)
=? old-state ?=(%1 -.old-state)
%= old-state
- %2
serving (~(del by serving.old-state) /'~landscape'/js/index)
==
?> ?=(%2 -.old-state)
[~ this(state old-state)]
::
+$ versioned-state
$% state-1
state-0
$% state-0
state-1
state-2
==
::
+$ serving-0 (map url-base=path [=clay=path public=?])
@ -74,6 +83,10 @@
=configuration:srv
=serving-0
==
+$ state-1
$: %1
state-base
==
--
::
++ on-poke
@ -169,7 +182,7 @@
?~ content [not-found:gen %.n]
?- -.content.u.content
%clay
=/ scry-path
=/ scry-path=path
:* (scot %p our.bowl)
q.byk.bowl
(scot %da now.bowl)
@ -179,10 +192,16 @@
=/ file (as-octs:mimes:html .^(@ %cx scry-path))
:_ public.u.content
?+ ext.req-line not-found:gen
[~ %html] (html-response:gen file)
[~ %js] (js-response:gen file)
[~ %css] (css-response:gen file)
[~ %png] (png-response:gen file)
::
[~ %html]
%. file
%* . html-response:gen
cache
!=(/app/landscape/index/html (slag 3 scry-path))
==
==
::
%glob

View File

@ -1,7 +1,7 @@
/- glob
/+ default-agent, verb, dbug
|%
++ hash 0v5.m40lm.ha96c.aavqb.57scm.222e7
++ hash 0v2.pbthv.gd1q2.h2ura.5esrn.d361c
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
+$ all-states
$% state-0
@ -41,7 +41,7 @@
--
=| state=state-0
=. hash.state hash
=/ serve-path=path /'~landscape'/js/index
=/ serve-path=path /'~landscape'/js/bundle
^- agent:gall
%+ verb |
%- agent:dbug
@ -82,9 +82,19 @@
:_ this
=/ home=path /(scot %p our.bowl)/home/(scot %da now.bowl)
=+ .^(=tube:clay %cc (weld home /js/mime))
=+ .^(js=@t %cx (weld home /app/landscape/js/index/js))
=+ .^(arch %cy (weld home /app/landscape/js/bundle))
=/ bundle=path
%- need
^- (unit path)
%- ~(rep by dir)
|= [[file=@t ~] out=(unit path)]
?^ out out
?. =((end 3 5 file) 'index')
~
`/[file]/js
=+ .^(js=@t %cx :(weld home /app/landscape/js/bundle bundle))
=+ !<(=mime (tube !>(js)))
=/ =glob:glob (~(put by *glob:glob) /js mime)
=/ =glob:glob (~(put by *glob:glob) bundle mime)
=/ =path /(cat 3 'glob-' (scot %uv (sham glob)))/glob
[%pass /make %agent [our.bowl %hood] %poke %drum-put !>([path (jam glob)])]~
::

View File

@ -0,0 +1,572 @@
/+ store=graph-store, sigs=signatures, res=resource, default-agent, dbug
~% %graph-store-top ..is ~
|%
+$ card card:agent:gall
+$ versioned-state
$% state-0
==
::
+$ state-0 [%0 network:store]
++ orm orm:store
++ orm-log orm-log:store
--
::
=| state-0
=* state -
::
%- agent:dbug
^- agent:gall
~% %graph-store-agent ..card ~
|_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
::
++ on-init [~ this]
++ on-save !>(state)
++ on-load
|= old=vase
^- (quip card _this)
[~ this(state !<(state-0 old))]
::
++ on-watch
~/ %graph-store-watch
|= =path
^- (quip card _this)
|^
?> (team:title our.bowl src.bowl)
=/ cards=(list card)
?+ path (on-watch:def path)
[%updates ~] ~
[%keys ~] (give [%keys ~(key by graphs)])
[%tags ~] (give [%tags ~(key by tag-queries)])
==
[cards this]
::
++ give
|= =update-0:store
^- (list card)
[%give %fact ~ [%graph-update !>([%0 now.bowl update-0])]]~
--
::
++ on-poke
~/ %graph-store-poke
|= [=mark =vase]
^- (quip card _this)
|^
?> (team:title our.bowl src.bowl)
=^ cards state
?+ mark (on-poke:def mark vase)
%graph-update (graph-update !<(update:store vase))
==
[cards this]
::
++ graph-update
|= =update:store
^- (quip card _state)
|^
?> ?=(%0 -.update)
?- -.q.update
%add-graph (add-graph +.q.update)
%remove-graph (remove-graph +.q.update)
%add-nodes (add-nodes p.update +.q.update)
%remove-nodes (remove-nodes p.update +.q.update)
%add-signatures (add-signatures p.update +.q.update)
%remove-signatures (remove-signatures p.update +.q.update)
%add-tag (add-tag +.q.update)
%remove-tag (remove-tag +.q.update)
%archive-graph (archive-graph +.q.update)
%unarchive-graph (unarchive-graph +.q.update)
%run-updates (run-updates +.q.update)
%keys ~|('cannot send %keys as poke' !!)
%tags ~|('cannot send %tags as poke' !!)
%tag-queries ~|('cannot send %tag-queries as poke' !!)
==
::
++ add-graph
|= [=resource:store =graph:store mark=(unit mark:store)]
^- (quip card _state)
?< (~(has by archive) resource)
?< (~(has by graphs) resource)
?> (validate-graph graph mark)
:_ %_ state
graphs (~(put by graphs) resource [graph mark])
update-logs (~(put by update-logs) resource (gas:orm-log ~ ~))
validators
?~ mark validators
(~(put in validators) u.mark)
==
%- zing
:~ (give [/updates /keys ~] [%add-graph resource graph mark])
?~ mark ~
?: (~(has in validators) u.mark) ~
=/ wire (weld /graph (en-path:res resource))
=/ =rave:clay [%sing %b [%da now.bowl] /[u.mark]]
[%pass wire %arvo %c %warp our.bowl [%home `rave]]~
==
::
++ remove-graph
|= =resource:store
^- (quip card _state)
?< (~(has by archive) resource)
?> (~(has by graphs) resource)
:- (give [/updates /keys ~] [%remove-graph resource])
%_ state
graphs (~(del by graphs) resource)
update-logs (~(del by update-logs) resource)
==
::
++ add-nodes
|= $: =time
=resource:store
nodes=(map index:store node:store)
==
^- (quip card _state)
|^
=/ [=graph:store mark=(unit mark:store)]
(~(got by graphs) resource)
=/ =update-log:store (~(got by update-logs) resource)
=. update-log
(put:orm-log update-log time [%0 time [%add-nodes resource nodes]])
::
:- (give [/updates]~ [%add-nodes resource nodes])
%_ state
update-logs (~(put by update-logs) resource update-log)
graphs
%+ ~(put by graphs)
resource
:_ mark
(add-node-list resource graph mark (sort-nodes nodes))
==
::
++ sort-nodes
|= nodes=(map index:store node:store)
^- (list [index:store node:store])
%+ sort ~(tap by nodes)
|= [p=[=index:store *] q=[=index:store *]]
^- ?
(lth (lent index.p) (lent index.q))
::
++ add-node-list
|= $: =resource:store
=graph:store
mark=(unit mark:store)
node-list=(list [index:store node:store])
==
^- graph:store
?~ node-list graph
=* index -.i.node-list
=* node +.i.node-list
%_ $
node-list t.node-list
graph (add-node-at-index graph index node mark)
==
::
++ add-node-at-index
=| parent-hash=(unit hash:store)
|= $: =graph:store
=index:store
=node:store
mark=(unit mark:store)
==
^- graph:store
?< ?=(~ index)
~| "validation of node failed using mark {<mark>}"
?> (validate-graph (gas:orm ~ [i.index node]~) mark)
=* atom i.index
%^ put:orm
graph
atom
:: add child
::
?~ t.index
=* p post.node
=/ =validated-portion:store
[parent-hash author.p time-sent.p contents.p]
=/ =hash:store `@ux`(sham validated-portion)
?~ hash.p node(signatures.post *signatures:store)
~| "signatures do not match the calculated hash"
?> (are-signatures-valid:sigs signatures.p hash now.bowl)
~| "hash of post does not match calculated hash"
?> =(hash u.hash.p)
node
:: recurse children
::
=/ parent=node:store
~| "index does not exist to add a node to!"
(need (get:orm graph atom))
%_ parent
children
^- internal-graph:store
:- %graph
%_ $
index t.index
parent-hash hash.post.parent
graph
?: ?=(%graph -.children.parent)
p.children.parent
(gas:orm ~ ~)
==
==
--
::
++ remove-nodes
|= [=time =resource:store indices=(set index:store)]
^- (quip card _state)
|^
=/ [=graph:store mark=(unit mark:store)]
(~(got by graphs) resource)
=/ =update-log:store (~(got by update-logs) resource)
=. update-log
(put:orm-log update-log time [%0 time [%remove-nodes resource indices]])
::
:- (give [/updates]~ [%remove-nodes resource indices])
%_ state
update-logs (~(put by update-logs) resource update-log)
graphs
%+ ~(put by graphs)
resource
[(remove-indices resource graph ~(tap in indices)) mark]
==
::
++ remove-indices
|= [=resource:store =graph:store indices=(list index:store)]
^- graph:store
?~ indices graph
%_ $
indices t.indices
graph (remove-index graph i.indices)
==
::
++ remove-index
|= [=graph:store =index:store]
^- graph:store
?~ index graph
=* atom i.index
:: last index in list
::
?~ t.index
+:`[* graph:store]`(del:orm graph atom)
=/ =node:store
~| "parent index does not exist to remove a node from!"
(need (get:orm graph atom))
~| "child index does not exist to remove a node from!"
?> ?=(%graph -.children.node)
%^ put:orm
graph
atom
node(p.children $(graph p.children.node, index t.index))
--
::
++ add-signatures
|= [=time =uid:store =signatures:store]
^- (quip card _state)
|^
=* resource resource.uid
=/ [=graph:store mark=(unit mark:store)]
(~(got by graphs) resource)
=/ =update-log:store (~(got by update-logs) resource)
=. update-log
(put:orm-log update-log time [%0 time [%add-signatures uid signatures]])
::
:- (give [/updates]~ [%add-signatures uid signatures])
%_ state
update-logs (~(put by update-logs) resource update-log)
graphs
%+ ~(put by graphs) resource
[(add-at-index graph index.uid signatures) mark]
==
::
++ add-at-index
|= [=graph:store =index:store =signatures:store]
^- graph:store
?~ index graph
=* atom i.index
=/ =node:store
~| "node does not exist to add signatures to!"
(need (get:orm graph atom))
:: last index in list
::
%^ put:orm
graph
atom
?~ t.index
~| "cannot add signatures to a node missing a hash"
?> ?=(^ hash.post.node)
~| "signatures did not match public keys!"
?> (are-signatures-valid:sigs signatures u.hash.post.node now.bowl)
node(signatures.post (~(uni in signatures) signatures.post.node))
~| "child graph does not exist to add signatures to!"
?> ?=(%graph -.children.node)
node(p.children $(graph p.children.node, index t.index))
--
::
++ remove-signatures
|= [=time =uid:store =signatures:store]
^- (quip card _state)
|^
=* resource resource.uid
=/ [=graph:store mark=(unit mark:store)]
(~(got by graphs) resource)
=/ =update-log:store (~(got by update-logs) resource)
=. update-log
%^ put:orm-log update-log
time
[%0 time [%remove-signatures uid signatures]]
::
:- (give [/updates]~ [%remove-signatures uid signatures])
%_ state
update-logs (~(put by update-logs) resource update-log)
graphs
%+ ~(put by graphs) resource
[(remove-at-index graph index.uid signatures) mark]
==
::
++ remove-at-index
|= [=graph:store =index:store =signatures:store]
^- graph:store
?~ index graph
=* atom i.index
=/ =node:store
~| "node does not exist to add signatures to!"
(need (get:orm graph atom))
:: last index in list
::
%^ put:orm
graph
atom
?~ t.index
node(signatures.post (~(dif in signatures) signatures.post.node))
~| "child graph does not exist to add signatures to!"
?> ?=(%graph -.children.node)
node(p.children $(graph p.children.node, index t.index))
--
::
++ add-tag
|= [=term =resource:store]
^- (quip card _state)
?> (~(has by graphs) resource)
:- (give [/updates /tags ~] [%add-tag term resource])
%_ state
tag-queries (~(put ju tag-queries) term resource)
==
::
++ remove-tag
|= [=term =resource:store]
^- (quip card _state)
?> (~(has by graphs) resource)
:- (give [/updates /tags ~] [%remove-tag term resource])
%_ state
tag-queries (~(del ju tag-queries) term resource)
==
::
++ archive-graph
|= =resource:store
^- (quip card _state)
?< (~(has by archive) resource)
?> (~(has by graphs) resource)
:- (give [/updates /keys /tags ~] [%archive-graph resource])
%_ state
archive (~(put by archive) resource (~(got by graphs) resource))
graphs (~(del by graphs) resource)
update-logs (~(del by update-logs) resource)
tag-queries
%- ~(run by tag-queries)
|= =resources:store
(~(del in resources) resource)
==
::
++ unarchive-graph
|= =resource:store
^- (quip card _state)
?> (~(has by archive) resource)
?< (~(has by graphs) resource)
:- (give [/updates /keys ~] [%unarchive-graph resource])
%_ state
archive (~(del by archive) resource)
graphs (~(put by graphs) resource (~(got by archive) resource))
update-logs (~(put by update-logs) resource (gas:orm-log ~ ~))
==
::
++ run-updates
|= [=resource:store =update-log:store]
^- (quip card _state)
?< (~(has by archive) resource)
?> (~(has by graphs) resource)
:_ state
%+ turn (tap:orm-log update-log)
|= [=time update=logged-update:store]
^- card
?> ?=(%0 -.update)
:* %pass
/run-updates/(scot %da time)
%agent
[our.bowl %graph-store]
%poke
:- %graph-update
!>
^- update:store
?- -.q.update
%add-nodes update(resource.q resource)
%remove-nodes update(resource.q resource)
%add-signatures update(resource.uid.q resource)
%remove-signatures update(resource.uid.q resource)
==
==
::
++ validate-graph
|= [=graph:store mark=(unit mark:store)]
^- ?
?~ mark %.y
?~ graph %.y
=/ =dais:clay
.^ =dais:clay
%cb
/(scot %p our.bowl)/[q.byk.bowl]/(scot %da now.bowl)/[u.mark]
==
%+ roll (tap:orm graph)
|= [[=atom =node:store] out=?]
?& out
=(%& -:(mule |.((vale:dais [atom post.node]))))
?- -.children.node
%empty %.y
%graph ^$(graph p.children.node)
==
==
::
++ give
|= [paths=(list path) update=update-0:store]
^- (list card)
[%give %fact paths [%graph-update !>([%0 now.bowl update])]]~
--
--
::
++ on-peek
~/ %graph-store-peek
|= =path
^- (unit (unit cage))
|^
?> (team:title our.bowl src.bowl)
?+ path (on-peek:def path)
[%x %keys ~] ``noun+!>(~(key by graphs))
[%x %tags ~] ``noun+!>(~(key by tag-queries))
[%x %tag-queries ~] ``noun+!>(tag-queries)
[%x %graph @ @ ~]
=/ =ship (slav %p i.t.t.path)
=/ =term i.t.t.t.path
=/ result=(unit marked-graph:store)
(~(get by graphs) [ship term])
?~ result [~ ~]
``noun+!>(u.result)
::
[%x %graph-subset @ @ @ @ ~]
=/ =ship (slav %p i.t.t.path)
=/ =term i.t.t.t.path
=/ start=(unit atom) (rush i.t.t.t.t.path dem:ag)
=/ end=(unit atom) (rush i.t.t.t.t.t.path dem:ag)
=/ graph=(unit marked-graph:store)
(~(get by graphs) [ship term])
?~ graph [~ ~]
``noun+!>(`graph:store`(subset:orm p.u.graph start end))
::
[%x %node @ @ @ *]
=/ =ship (slav %p i.t.t.path)
=/ =term i.t.t.t.path
=/ =index:store
(turn t.t.t.t.path |=(=cord (slav %ud cord)))
=/ node=(unit node:store) (get-node ship term index)
?~ node [~ ~]
``noun+!>(u.node)
::
[%x %post @ @ @ *]
=/ =ship (slav %p i.t.t.path)
=/ =term i.t.t.t.path
=/ =index:store
(turn t.t.t.t.path |=(=cord (slav %ud cord)))
=/ node=(unit node:store) (get-node ship term index)
?~ node [~ ~]
``noun+!>(post.u.node)
::
[%x %node-children @ @ @ *]
=/ =ship (slav %p i.t.t.path)
=/ =term i.t.t.t.path
=/ =index:store
(turn t.t.t.t.path |=(=cord (slav %ud cord)))
=/ node=(unit node:store) (get-node ship term index)
?~ node [~ ~]
?- -.children.u.node
%empty [~ ~]
%graph ``noun+!>(p.children.u.node)
==
::
[%x %node-children-subset @ @ @ @ @ *]
=/ =ship (slav %p i.t.t.path)
=/ =term i.t.t.t.path
=/ start=(unit atom) (rush i.t.t.t.t.path dem:ag)
=/ end=(unit atom) (rush i.t.t.t.t.t.path dem:ag)
=/ =index:store
(turn t.t.t.t.t.t.path |=(=cord (slav %ud cord)))
=/ node=(unit node:store) (get-node ship term index)
?~ node [~ ~]
?- -.children.u.node
%empty [~ ~]
%graph ``noun+!>(`graph:store`(subset:orm p.children.u.node start end))
==
::
[%x %update-log @ @ ~]
=/ =ship (slav %p i.t.t.path)
=/ =term i.t.t.t.path
=/ update-log=(unit update-log:store) (~(get by update-logs) [ship term])
?~ update-log [~ ~]
``noun+!>(u.update-log)
::
[%x %peek-update-log @ @ ~]
=/ =ship (slav %p i.t.t.path)
=/ =term i.t.t.t.path
=/ update-log=(unit update-log:store) (~(get by update-logs) [ship term])
?~ update-log [~ ~]
=/ result=(unit [time update:store])
(peek:orm-log:store u.update-log)
?~ result [~ ~]
``noun+!>([~ -.u.result])
==
::
++ get-node
|= [=ship =term =index:store]
^- (unit node:store)
=/ parent-graph=(unit marked-graph:store)
(~(get by graphs) [ship term])
?~ parent-graph ~
=/ node=(unit node:store) ~
=/ =graph:store p.u.parent-graph
|-
?~ index
node
?~ t.index
(get:orm graph i.index)
=. node (get:orm graph i.index)
?~ node ~
?- -.children.u.node
%empty ~
%graph $(graph p.children.u.node, index t.index)
==
--
::
++ on-arvo
|= [=wire =sign-arvo]
^- (quip card _this)
?+ -.sign-arvo (on-arvo:def wire sign-arvo)
%c
:_ this
?> ?=([%graph @ *] wire)
=/ =resource:store (de-path:res t.wire)
=/ gra=(unit marked-graph:store) (~(get by graphs) resource)
?~ gra ~
?~ q.u.gra ~
=/ =rave:clay [%next %b [%da now.bowl] /[u.q.u.gra]]
[%pass wire %arvo %c %warp our.bowl [%home `rave]]~
==
::
++ on-agent on-agent:def
++ on-leave on-leave:def
++ on-fail on-fail:def
--

View File

@ -2,22 +2,16 @@
/+ drum=hood-drum, helm=hood-helm, kiln=hood-kiln
|%
+$ state
$: %8
drum=state:drum
helm=state:helm
kiln=state:kiln
==
+$ state-7
$: %7
$: %9
drum=state:drum
helm=state:helm
kiln=state:kiln
==
+$ any-state
$% state
state-7
[ver=?(%1 %2 %3 %4 %5 %6) lac=(map @tas fin-any-state)]
[%7 drum=state:drum helm=state:helm kiln=state:kiln]
[%8 drum=state:drum helm=state:helm kiln=state:kiln]
==
+$ any-state-tuple
$: drum=any-state:drum

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 B

After

Width:  |  Height:  |  Size: 582 B

View File

@ -4,7 +4,7 @@
<title>OS1</title>
<meta charset="utf-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
content="width=device-width, initial-scale=1, shrink-to-fit=no,maximum-scale=1"/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
@ -23,7 +23,7 @@
<div id="root"/>
<script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.js"></script>
<script src="/~landscape/js/index.js"></script>
<script src="/~landscape/js/bundle/index.f58fbbc4b037bb976a2a.js"></script>
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,10 @@
:: graph-store|add-graph: add new graph
::
/+ *graph-store
:- %say
|= $: [now=@da eny=@uvJ =beak]
[[=resource mark=(unit mark) ~] ~]
==
:- %graph-update
^- update
[%0 now [%add-graph resource (gas:orm ~ ~) mark]]

View File

@ -0,0 +1,20 @@
:: graph-store|add-post: add post to a graph
::
/- *graph-store
:- %say
|= $: [now=@da eny=@uvJ =beak]
[[[our=ship name=term] contents=(list content) ~] ~]
==
=/ =post *post
=: author.post our
index.post [now]~
time-sent.post now
contents.post contents
==
::
:- %graph-update
^- update
:+ %0 now
:+ %add-nodes [our name]
%- ~(gas by *(map index node))
~[[[now]~ [post [%empty ~]]]]

View File

@ -0,0 +1,10 @@
:: graph-store|add-signatures: add signatures to a node at a particular uid
::
/- *graph-store
:- %say
|= $: [now=@da eny=@uvJ =beak]
[[[=resource =index] =signatures ~] ~]
==
:- %graph-update
^- update
[%0 now [%add-signatures [resource index] signatures]]

View File

@ -0,0 +1,10 @@
:: graph-store|add-tag: tag a particular graph
::
/- *graph-store
:- %say
|= $: [now=@da eny=@uvJ =beak]
[[=term =resource ~] ~]
==
:- %graph-update
^- update
[%0 now [%add-tag term resource]]

View File

@ -0,0 +1,10 @@
:: graph-store|archive-graph: archive graph
::
/- *graph-store
:- %say
|= $: [now=@da eny=@uvJ =beak]
[[=resource ~] ~]
==
:- %graph-update
^- update
[%0 now [%archive-graph resource]]

View File

@ -0,0 +1,10 @@
:: graph-store|remove-graph: remove graph
::
/- *graph-store
:- %say
|= $: [now=@da eny=@uvJ =beak]
[[=resource ~] ~]
==
:- %graph-update
^- update
[%0 now [%remove-graph resource]]

View File

@ -0,0 +1,10 @@
:: graph-store|remove-nodes: remove nodes from a graph at indices
::
/- *graph-store
:- %say
|= $: [now=@da eny=@uvJ =beak]
[[=resource indices=(set index) ~] ~]
==
:- %graph-update
^- update
[%0 now [%remove-nodes resource indices]]

View File

@ -0,0 +1,11 @@
:: graph-store|remove-signatures: remove signatures from a node at a
:: particular uid
::
/- *graph-store
:- %say
|= $: [now=@da eny=@uvJ =beak]
[[[=resource =index] =signatures ~] ~]
==
:- %graph-update
^- update
[%0 now [%remove-signatures [resource index] signatures]]

View File

@ -0,0 +1,10 @@
:: graph-store|remove-tag: remove a tag from a particular graph
::
/- *graph-store
:- %say
|= $: [now=@da eny=@uvJ =beak]
[[=term =resource ~] ~]
==
:- %graph-update
^- update
[%0 now [%remove-tag term resource]]

View File

@ -0,0 +1,10 @@
:: graph-store|unarchive-graph: unarchive graph
::
/- *graph-store
:- %say
|= $: [now=@da eny=@uvJ =beak]
[[=resource ~] ~]
==
:- %graph-update
^- update
[%0 now [%unarchive-graph resource]]

View File

@ -0,0 +1,411 @@
/- sur=graph-store, pos=post
/+ res=resource
=< [sur .]
=< [pos .]
=, sur
=, pos
|%
:: NOTE: move these functions to zuse
++ nu :: parse number as hex
|= jon/json
?> ?=({$s *} jon)
(rash p.jon hex)
::
++ re :: recursive reparsers
|* {gar/* sef/_|.(fist:dejs-soft:format)}
|= jon/json
^- (unit _gar)
=- ~! gar ~! (need -) -
((sef) jon)
::
++ dank :: tank
^- $-(json (unit tank))
=, ^? dejs-soft:format
%+ re *tank |. ~+
%- of :~
leaf+sa
palm+(ot style+(ot mid+sa cap+sa open+sa close+sa ~) lines+(ar dank) ~)
rose+(ot style+(ot mid+sa open+sa close+sa ~) lines+(ar dank) ~)
==
::
++ orm ((ordered-map atom node) gth)
++ orm-log ((ordered-map time logged-update) gth)
::
++ enjs
=, enjs:format
|%
++ update
|= upd=^update
^- json
?> ?=(%0 -.upd)
|^ (frond %graph-update (pairs ~[(encode q.upd)]))
::
++ encode
|= upd=update-0
^- [cord json]
?- -.upd
%add-graph
:- %add-graph
%- pairs
:~ [%resource (enjs:res resource.upd)]
[%graph (graph graph.upd)]
[%mark ?~(mark.upd ~ s+u.mark.upd)]
==
::
%remove-graph
[%remove-graph (enjs:res resource.upd)]
::
%add-nodes
:- %add-nodes
%- pairs
:~ [%resource (enjs:res resource.upd)]
[%nodes (nodes nodes.upd)]
==
::
%remove-nodes
:- %remove-nodes
%- pairs
:~ [%resource (enjs:res resource.upd)]
[%indices (indices indices.upd)]
==
::
%add-signatures
:- %add-signatures
%- pairs
:~ [%uid (uid uid.upd)]
[%signatures (signatures signatures.upd)]
==
::
%remove-signatures
:- %remove-signatures
%- pairs
:~ [%uid (uid uid.upd)]
[%signatures (signatures signatures.upd)]
==
::
%add-tag
:- %add-tag
%- pairs
:~ [%term s+term.upd]
[%resource (enjs:res resource.upd)]
==
::
%remove-tag
:- %remove-tag
%- pairs
:~ [%term s+term.upd]
[%resource (enjs:res resource.upd)]
==
::
%archive-graph
[%archive-graph (enjs:res resource.upd)]
::
%unarchive-graph
[%unarchive-graph (enjs:res resource.upd)]
::
%keys
[%keys [%a (turn ~(tap in resources.upd) enjs:res)]]
::
%tags
[%tags [%a (turn ~(tap in tags.upd) |=(=term s+term))]]
::
%run-updates
[%run-updates ~]
::
%tag-queries
:- %tag-queries
%- pairs
%+ turn ~(tap by tag-queries.upd)
|= [=term =resources]
^- [cord json]
[term [%a (turn ~(tap in resources) enjs:res)]]
==
::
++ graph
|= g=^graph
^- json
:- %a
%+ turn (tap:orm g)
|= [a=atom n=^node]
^- json
:- %a
:~ (index [a]~)
(node n)
==
::
++ index
|= i=^index
^- json
=/ j=^tape ""
|-
?~ i [%s (crip j)]
=/ k=json (numb i.i)
?> ?=(%n -.k)
%_ $
i t.i
j (weld j (weld "/" (trip +.k)))
==
::
++ node
|= n=^node
^- json
%- pairs
:~ [%post (post post.n)]
:- %children
?- -.children.n
%empty ~
%graph (graph +.children.n)
==
==
::
++ post
|= p=^post
^- json
%- pairs
:~ [%author (ship author.p)]
[%index (index index.p)]
[%time-sent (time time-sent.p)]
[%contents [%a (turn contents.p content)]]
[%hash ?~(hash.p ~ s+(scot %ux u.hash.p))]
[%signatures (signatures signatures.p)]
==
::
++ content
|= c=^content
^- json
?- -.c
%text (frond %text s+text.c)
%url (frond %url s+url.c)
%reference (frond %reference (uid uid.c))
%code
%+ frond %code
%- pairs
:- [%expression s+expression.c]
:_ ~
:- %output
:: virtualize output rendering, +tank:enjs:format might crash
::
=/ result=(each (list json) tang)
(mule |.((turn output.c tank)))
?- -.result
%& a+p.result
%| a+[a+[%s '[[output rendering error]]']~]~
==
==
::
++ nodes
|= m=(map ^index ^node)
^- json
:- %a
%+ turn ~(tap by m)
|= [n=^index o=^node]
^- json
:- %a
:~ (index n)
(node o)
==
::
++ indices
|= i=(set ^index)
^- json
[%a (turn ~(tap in i) index)]
::
++ uid
|= u=^uid
^- json
%- pairs
:~ [%resource (enjs:res resource.u)]
[%index (index index.u)]
==
::
++ signatures
|= s=^signatures
^- json
[%a (turn ~(tap in s) signature)]
::
++ signature
|= s=^signature
^- json
%- pairs
:~ [%signature s+(scot %ux p.s)]
[%ship (ship q.s)]
[%life (numb r.s)]
==
--
--
::
++ dejs
=, dejs:format
|%
++ update
|= jon=json
^- ^update
:- %0
:- *time
^- update-0
=< (decode jon)
|%
++ decode
%- of
:~ [%add-graph add-graph]
[%remove-graph remove-graph]
[%add-nodes add-nodes]
[%remove-nodes remove-nodes]
[%add-signatures add-signatures]
[%remove-signatures remove-signatures]
[%add-tag add-tag]
[%remove-tag remove-tag]
[%archive-graph archive-graph]
[%unarchive-graph unarchive-graph]
[%keys keys]
[%tags tags]
[%tag-queries tag-queries]
[%run-updates run-updates]
==
::
++ add-graph
%- ot
:~ [%resource dejs:res]
[%graph graph]
[%mark (mu so)]
==
::
++ graph
|= a=json
^- ^graph
=/ or-mp ((ordered-map atom ^node) gth)
%+ gas:or-mp ~
%+ turn ~(tap by ((om node) a))
|* [b=cord c=*]
^- [atom ^node]
=> .(+< [b c]=+<)
[(rash b dem) c]
::
++ remove-graph (ot [%resource dejs:res]~)
++ archive-graph (ot [%resource dejs:res]~)
++ unarchive-graph (ot [%resource dejs:res]~)
::
++ add-nodes
%- ot
:~ [%resource dejs:res]
[%nodes nodes]
==
::
++ nodes (op ;~(pfix net (more net dem)) node)
::
++ node
%- ot
:~ [%post post]
:: TODO: support adding nodes with children by supporting the
:: graph key
[%children (of [%empty ul]~)]
==
::
++ post
%- ot
:~ [%author (su ;~(pfix sig fed:ag))]
[%index index]
[%time-sent di]
[%contents (ar content)]
[%hash (mu nu)]
[%signatures (as signature)]
==
::
++ content
%- of
:~ [%text so]
[%url so]
[%reference uid]
[%code eval]
==
::
++ eval
|= a=^json
^- [cord (list tank)]
=, ^? dejs-soft:format
=+ exp=((ot expression+so ~) a)
%- need
?~ exp [~ '' ~]
:+ ~ u.exp
:: NOTE: when sending, if output is an empty list,
:: graph-store will evaluate
(fall ((ot output+(ar dank) ~) a) ~)
::
++ remove-nodes
%- ot
:~ [%resource dejs:res]
[%indices (as index)]
==
::
++ add-signatures
%- ot
:~ [%uid uid]
[%signatures (as signature)]
==
::
++ remove-signatures
%- ot
:~ [%uid uid]
[%signatures (as signature)]
==
::
++ signature
%- ot
:~ [%hash nu]
[%ship (su ;~(pfix sig fed:ag))]
[%life ni]
==
::
++ uid
%- ot
:~ [%resource dejs:res]
[%index index]
==
::
++ index (su ;~(pfix net (more net dem)))
::
++ add-tag
%- ot
:~ [%term so]
[%resource dejs:res]
==
::
++ remove-tag
%- ot
:~ [%term so]
[%resource dejs:res]
==
::
++ keys
|= =json
*resources
::
++ tags
|= =json
*(set term)
::
++ tag-queries
|= =json
*^tag-queries
::
++ run-updates
|= a=json
^- [resource update-log]
[*resource *update-log]
--
--
::
++ create
|_ [our=ship now=time]
++ post
|= [=index contents=(list content)]
^- ^post
:* our
index
now
contents
~
*signatures
==
--
--

24
pkg/arvo/lib/graph.hoon Normal file
View File

@ -0,0 +1,24 @@
/- *resource
/+ store=graph-store
|_ =bowl:gall
++ scry-for
|* [=mold =path]
.^ mold
%gx
(scot %p our.bowl)
%graph-store
(scot %da now.bowl)
(snoc `^path`path %noun)
==
::
++ get-graph
|= res=resource
^- marked-graph:store
%+ scry-for marked-graph:store
/graph/(scot %p entity.res)/[name.res]
::
++ peek-log
|= res=resource
^- (unit time)
(scry-for (unit time) /peek-update-log/(scot %p entity.res)/[name.res])
--

View File

@ -104,6 +104,7 @@
%s3-store
%file-server
%glob
%graph-store
==
::
++ deft-fish :: default connects
@ -206,7 +207,7 @@
==
::
++ on-load
|= [hood-version=?(%1 %2 %3 %4 %5 %6 %7 %8) old=any-state]
|= [hood-version=?(%1 %2 %3 %4 %5 %6 %7 %8 %9) old=any-state]
=< se-abet =< se-view
=. sat old
=. dev (~(gut by bin) ost *source)
@ -233,6 +234,8 @@
=? ..on-load (lte hood-version %8)
=> (se-born | %home %group-push-hook)
(se-born | %home %group-pull-hook)
=? ..on-load (lte hood-version %9)
(se-born | %home %graph-store)
..on-load
::
++ reap-phat :: ack connect

View File

@ -59,7 +59,6 @@
|~ [term tang]
*[(list card) _^|(..on-init)]
:: +resource-for-update: get affected resource from an update
++ resource-for-update
|~ vase
*(unit resource)

View File

@ -80,9 +80,11 @@
++ max-1-wk ['cache-control' 'max-age=604800']
::
++ html-response
=| cache=?
|= =octs
^- simple-payload:http
[[200 [['content-type' 'text/html'] max-1-wk ~]] `octs]
:_ `octs
[200 [['content-type' 'text/html'] ?:(cache [max-1-wk ~] ~)]]
::
++ js-response
|= =octs

View File

@ -0,0 +1,43 @@
/- post
^?
=< [post .]
=, post
|%
++ sign
|= [our=ship now=time =hash]
^- signature
=/ =life .^(life %j /=life/(scot %da now)/(scot %p our))
=/ =ring .^(ring %j /=vein/(scot %da now)/(scot %ud life))
:+ `@ux`(sign:as:(nol:nu:crub:crypto ring) hash)
our
life
::
++ is-signature-valid
|= [=signature =hash now=time]
^- ?
=/ deed=(unit [a=life b=pass c=(unit @ux)])
.^ (unit [life pass (unit @ux)])
%j
/=deed/(scot %da now)/(scot %p q.signature)/(scot %ud p.signature)
==
:: we do not have a public key from ship
::
?~ deed %.y
:: we do not have a public key from ship at this life
::
?. =(a.u.deed r.signature) %.y
:: verify signature from ship at life
::
=(`hash (tear:as:crub:crypto b.u.deed p.signature))
::
++ are-signatures-valid
|= [=signatures =hash now=time]
^- ?
=/ signature-list ~(tap in signatures)
|-
?~ signature-list
%.y
?: (is-signature-valid i.signature-list hash now)
$(signature-list t.signature-list)
%.n
--

View File

@ -0,0 +1,13 @@
/+ *graph-store
|_ upd=update
++ grow
|%
++ json (update:enjs upd)
--
::
++ grab
|%
++ noun update
++ json update:dejs
--
--

View File

@ -0,0 +1,17 @@
/- *post
|_ i=indexed-post
++ grow
|%
++ noun i
--
++ grab
|%
++ noun
|= p=*
=/ ip ;;(indexed-post p)
?> ?=([@ ~] index.p.ip)
ip
--
::
++ grad %noun
--

View File

@ -0,0 +1,61 @@
/- *post
|%
+$ graph ((mop atom node) gth)
+$ marked-graph [p=graph q=(unit mark)]
::
+$ node [=post children=internal-graph]
+$ graphs (map resource marked-graph)
::
+$ tag-queries (jug term resource)
::
+$ update-log ((mop time logged-update) gth)
+$ update-logs (map resource update-log)
::
+$ internal-graph
$~ [%empty ~]
$% [%graph p=graph]
[%empty ~]
==
::
+$ network
$: =graphs
=tag-queries
=update-logs
archive=graphs
validators=(set mark)
==
::
+$ update
$% [%0 p=time q=update-0]
==
::
+$ logged-update
$% [%0 p=time q=logged-update-0]
==
::
+$ logged-update-0
$% [%add-nodes =resource nodes=(map index node)]
[%remove-nodes =resource indices=(set index)]
[%add-signatures =uid =signatures]
[%remove-signatures =uid =signatures]
==
::
+$ update-0
$% logged-update-0
[%add-graph =resource =graph mark=(unit mark)]
[%remove-graph =resource]
::
[%add-tag =term =resource]
[%remove-tag =term =resource]
::
[%archive-graph =resource]
[%unarchive-graph =resource]
[%run-updates =resource =update-log]
::
:: NOTE: cannot be sent as pokes
::
[%keys =resources]
[%tags tags=(set term)]
[%tag-queries =tag-queries]
==
--

37
pkg/arvo/sur/post.hoon Normal file
View File

@ -0,0 +1,37 @@
/- *resource
|%
+$ index (list atom)
+$ uid [=resource =index]
::
:: +sham (half sha-256) hash of +validated-portion
+$ hash @ux
::
+$ signature [p=@ux q=ship r=life]
+$ signatures (set signature)
+$ post
$: author=ship
=index
time-sent=time
contents=(list content)
hash=(unit hash)
=signatures
==
::
+$ indexed-post [a=atom p=post]
::
+$ validated-portion
$: parent-hash=(unit hash)
author=ship
time-sent=time
contents=(list content)
==
::
+$ content
$% [%text text=cord]
[%url url=cord]
[%code expression=cord output=(list tank)]
[%reference =uid]
:: TODO: maybe use a cask?
::[%cage =cage]
==
--

View File

@ -8,5 +8,4 @@
+$ update
$% [%tracking tracking=(map resource ship)]
==
::
--

View File

@ -11367,7 +11367,7 @@
::
{$face *}
=^ cox gid $(q.ham q.q.ham)
:_(gid [%palm [['/' ~] ~ ~ ~] [%leaf (trip p.q.ham)] cox ~])
:_(gid [%palm [['=' ~] ~ ~ ~] [%leaf (trip p.q.ham)] cox ~])
::
{$list *}
=^ cox gid $(q.ham q.q.ham)
@ -11379,10 +11379,10 @@
::
{$plot *}
=^ coz gid (many p.q.ham)
:_(gid [%rose [[' ' ~] ['{' ~] ['}' ~]] coz])
:_(gid [%rose [[' ' ~] ['[' ~] [']' ~]] coz])
::
{$pear *}
:_(gid [%leaf '$' ~(rend co [%$ p.q.ham q.q.ham])])
:_(gid [%leaf '%' ~(rend co [%$ p.q.ham q.q.ham])])
::
{$stop *}
=+ num=~(rend co [%$ %ud p.q.ham])

186
pkg/interface/.eslintrc.js Normal file
View File

@ -0,0 +1,186 @@
const env = {
"browser": true,
"es6": true,
"node": true
};
const rules = {
"array-bracket-spacing": ["error", "never"],
"arrow-parens": [
"error",
"as-needed",
{
"requireForBlockBody": true
}
],
"arrow-spacing": "error",
"block-spacing": ["error", "always"],
"brace-style": ["error", "1tbs"],
"camelcase": [
"error",
{
"properties": "never"
}
],
"comma-dangle": ["error", "never"],
"eol-last": ["error", "always"],
"func-name-matching": "error",
"indent": [
"off",
2,
{
"ArrayExpression": "off",
"SwitchCase": 1,
"CallExpression": {
"arguments": "off"
},
"FunctionDeclaration": {
"parameters": "off"
},
"FunctionExpression": {
"parameters": "off"
},
"MemberExpression": "off",
"ObjectExpression": "off",
"ImportDeclaration": "off"
}
],
"handle-callback-err": "off",
"linebreak-style": ["error", "unix"],
"max-lines": [
"error",
{
"max": 300,
"skipBlankLines": true,
"skipComments": true
}
],
"max-lines-per-function": [
"warn",
{
"skipBlankLines": true,
"skipComments": true
}
],
"max-statements-per-line": [
"error",
{
"max": 1
}
],
"new-cap": [
"error",
{
"newIsCap": true,
"capIsNew": false
}
],
"new-parens": "error",
"no-buffer-constructor": "error",
"no-console": "off",
"no-extra-semi": "off",
"no-fallthrough": "off",
"no-func-assign": "off",
"no-implicit-coercion": "error",
"no-multi-assign": "error",
"no-multiple-empty-lines": [
"error",
{
"max": 1
}
],
"no-nested-ternary": "error",
"no-param-reassign": "off",
"no-return-assign": "error",
"no-return-await": "off",
"no-shadow-restricted-names": "error",
"no-tabs": "error",
"no-trailing-spaces": "error",
"no-unused-vars": [
"error",
{
"vars": "all",
"args": "none",
"ignoreRestSiblings": false
}
],
"no-use-before-define": [
"error",
{
"functions": false,
"classes": false
}
],
"no-useless-escape": "off",
"no-var": "error",
"nonblock-statement-body-position": ["error", "below"],
"object-curly-spacing": ["error", "always"],
"padded-blocks": ["error", "never"],
"prefer-arrow-callback": "error",
"prefer-const": [
"error",
{
"destructuring": "all",
"ignoreReadBeforeAssign": true
}
],
"prefer-template": "off",
"quotes": ["error", "single"],
"semi": ["error", "always"],
"spaced-comment": [
"error",
"always",
{
"exceptions": ["!"]
}
],
"space-before-blocks": "error",
"unicode-bom": ["error", "never"],
"valid-jsdoc": "error",
"wrap-iife": ["error", "inside"],
"react/jsx-closing-bracket-location": 1,
"react/jsx-tag-spacing": 1,
"react/jsx-max-props-per-line": ["error", { "maximum": 2, "when": "multiline" }],
"react/prop-types": 0
};
module.exports = {
"env": env,
"extends": [
"plugin:react/recommended",
"eslint:recommended",
],
"settings": {
"react": {
"version": "^16.5.2"
}
},
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 10,
"requireConfigFile": false,
"sourceType": "module"
},
"root": true,
"rules": rules,
"overrides": [
{
"files": ["**/*.ts", "**/*.tsx"],
"env": env,
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": { "jsx": true },
"ecmaVersion": 10,
"requireConfigFile": false,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": rules
}
]
};

View File

@ -1,147 +0,0 @@
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"settings": {
"react": {
"version": "^16.5.2"
}
},
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 10,
"requireConfigFile": false,
"sourceType": "module"
},
"root": true,
"rules": {
"array-bracket-spacing": ["error", "never"],
"arrow-parens": [
"error",
"as-needed",
{
"requireForBlockBody": true
}
],
"arrow-spacing": "error",
"block-spacing": ["error", "always"],
"brace-style": ["error", "1tbs"],
"camelcase": [
"error",
{
"properties": "never"
}
],
"comma-dangle": ["error", "never"],
"eol-last": ["error", "always"],
"func-name-matching": "error",
"indent": [
"off",
2,
{
"ArrayExpression": "off",
"SwitchCase": 1,
"CallExpression": {
"arguments": "off"
},
"FunctionDeclaration": {
"parameters": "off"
},
"FunctionExpression": {
"parameters": "off"
},
"MemberExpression": "off",
"ObjectExpression": "off",
"ImportDeclaration": "off"
}
],
"handle-callback-err": "off",
"linebreak-style": ["error", "unix"],
"max-statements-per-line": [
"error",
{
"max": 1
}
],
"new-cap": [
"error",
{
"newIsCap": true,
"capIsNew": false
}
],
"new-parens": "error",
"no-buffer-constructor": "error",
"no-console": "off",
"no-extra-semi": "off",
"no-fallthrough": "off",
"no-func-assign": "off",
"no-implicit-coercion": "error",
"no-multi-assign": "error",
"no-multiple-empty-lines": [
"error",
{
"max": 1
}
],
"no-nested-ternary": "error",
"no-param-reassign": "off",
"no-return-assign": "error",
"no-return-await": "off",
"no-shadow-restricted-names": "error",
"no-tabs": "error",
"no-trailing-spaces": "error",
"no-unused-vars": [
"error",
{
"vars": "all",
"args": "none",
"ignoreRestSiblings": false
}
],
"no-use-before-define": [
"error",
{
"functions": false,
"classes": false
}
],
"no-useless-escape": "off",
"no-var": "error",
"nonblock-statement-body-position": ["error", "below"],
"object-curly-spacing": ["error", "always"],
"padded-blocks": ["error", "never"],
"prefer-arrow-callback": "error",
"prefer-const": [
"error",
{
"destructuring": "all",
"ignoreReadBeforeAssign": true
}
],
"prefer-template": "off",
"quotes": ["error", "single"],
"semi": ["error", "always"],
"spaced-comment": [
"error",
"always",
{
"exceptions": ["!"]
}
],
"space-before-blocks": "error",
"unicode-bom": ["error", "never"],
"valid-jsdoc": "error",
"wrap-iife": ["error", "inside"],
"react/jsx-closing-bracket-location": 1,
"react/jsx-tag-spacing": 1,
"react/jsx-max-props-per-line": ["error", { "maximum": 2, "when": "multiline" }],
"react/prop-types": 0
}
}

View File

@ -52,7 +52,7 @@ if(urbitrc.URL) {
...devServer,
index: '',
proxy: {
'/~landscape/js/index.js': {
'/~landscape/js/bundle/index.*.js': {
target: 'http://localhost:9000',
pathRewrite: (req, path) => '/index.js'
},

View File

@ -1,6 +1,6 @@
const path = require('path');
// const HtmlWebpackPlugin = require('html-webpack-plugin');
// const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode: 'production',
@ -49,17 +49,16 @@ module.exports = {
// historyApiFallback: true
// },
plugins: [
// new CleanWebpackPlugin(),
new CleanWebpackPlugin(),
// new HtmlWebpackPlugin({
// title: 'Hot Module Replacement',
// template: './public/index.html',
// }),
],
output: {
filename: 'index.js',
chunkFilename: 'index.js',
path: path.resolve(__dirname, '../../arvo/app/landscape/js'),
publicPath: '/'
filename: 'index.[contenthash].js',
path: path.resolve(__dirname, '../../arvo/app/landscape/js/bundle'),
publicPath: '/',
},
optimization: {
minimize: true,

View File

@ -1660,6 +1660,12 @@
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
"dev": true
},
"@types/eslint-visitor-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
"integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==",
"dev": true
},
"@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
@ -1689,6 +1695,12 @@
"integrity": "sha512-iYCgjm1dGPRuo12+BStjd1HiVQqhlRhWDOQigNxn023HcjnhsiFz9pc6CzJj4HwDCSQca9bxTL4PxJDbkdm3PA==",
"dev": true
},
"@types/json-schema": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz",
"integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==",
"dev": true
},
"@types/lodash": {
"version": "4.14.155",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.155.tgz",
@ -1795,6 +1807,93 @@
"source-map": "^0.6.1"
}
},
"@typescript-eslint/eslint-plugin": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.8.0.tgz",
"integrity": "sha512-lFb4VCDleFSR+eo4Ew+HvrJ37ZH1Y9ZyE+qyP7EiwBpcCVxwmUc5PAqhShCQ8N8U5vqYydm74nss+a0wrrCErw==",
"dev": true,
"requires": {
"@typescript-eslint/experimental-utils": "3.8.0",
"debug": "^4.1.1",
"functional-red-black-tree": "^1.0.1",
"regexpp": "^3.0.0",
"semver": "^7.3.2",
"tsutils": "^3.17.1"
},
"dependencies": {
"semver": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
"dev": true
}
}
},
"@typescript-eslint/experimental-utils": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.8.0.tgz",
"integrity": "sha512-o8T1blo1lAJE0QDsW7nSyvZHbiDzQDjINJKyB44Z3sSL39qBy5L10ScI/XwDtaiunoyKGLiY9bzRk4YjsUZl8w==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/types": "3.8.0",
"@typescript-eslint/typescript-estree": "3.8.0",
"eslint-scope": "^5.0.0",
"eslint-utils": "^2.0.0"
}
},
"@typescript-eslint/parser": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.8.0.tgz",
"integrity": "sha512-u5vjOBaCsnMVQOvkKCXAmmOhyyMmFFf5dbkM3TIbg3MZ2pyv5peE4gj81UAbTHwTOXEwf7eCQTUMKrDl/+qGnA==",
"dev": true,
"requires": {
"@types/eslint-visitor-keys": "^1.0.0",
"@typescript-eslint/experimental-utils": "3.8.0",
"@typescript-eslint/types": "3.8.0",
"@typescript-eslint/typescript-estree": "3.8.0",
"eslint-visitor-keys": "^1.1.0"
}
},
"@typescript-eslint/types": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.8.0.tgz",
"integrity": "sha512-8kROmEQkv6ss9kdQ44vCN1dTrgu4Qxrd2kXr10kz2NP5T8/7JnEfYNxCpPkArbLIhhkGLZV3aVMplH1RXQRF7Q==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.8.0.tgz",
"integrity": "sha512-MTv9nPDhlKfclwnplRNDL44mP2SY96YmPGxmMbMy6x12I+pERcxpIUht7DXZaj4mOKKtet53wYYXU0ABaiXrLw==",
"dev": true,
"requires": {
"@typescript-eslint/types": "3.8.0",
"@typescript-eslint/visitor-keys": "3.8.0",
"debug": "^4.1.1",
"glob": "^7.1.6",
"is-glob": "^4.0.1",
"lodash": "^4.17.15",
"semver": "^7.3.2",
"tsutils": "^3.17.1"
},
"dependencies": {
"semver": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
"dev": true
}
}
},
"@typescript-eslint/visitor-keys": {
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.8.0.tgz",
"integrity": "sha512-gfqQWyVPpT9NpLREXNR820AYwgz+Kr1GuF3nf1wxpHD6hdxI62tq03ToomFnDxY0m3pUB39IF7sil7D5TQexLA==",
"dev": true,
"requires": {
"eslint-visitor-keys": "^1.1.0"
}
},
"@webassemblyjs/ast": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz",
@ -1993,9 +2092,9 @@
}
},
"acorn": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
"integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz",
"integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==",
"dev": true
},
"acorn-jsx": {
@ -2931,9 +3030,9 @@
}
},
"cli-width": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz",
"integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz",
"integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==",
"dev": true
},
"cliui": {
@ -3960,6 +4059,15 @@
}
}
},
"eslint-utils": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
"integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
"dev": true,
"requires": {
"eslint-visitor-keys": "^1.1.0"
}
},
"globals": {
"version": "12.4.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
@ -3975,6 +4083,12 @@
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
"dev": true
},
"regexpp": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
"integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
"dev": true
},
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@ -4033,9 +4147,9 @@
}
},
"eslint-scope": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
"integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz",
"integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==",
"dev": true,
"requires": {
"esrecurse": "^4.1.0",
@ -4043,9 +4157,9 @@
}
},
"eslint-utils": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
"integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
"integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
"dev": true,
"requires": {
"eslint-visitor-keys": "^1.1.0"
@ -4084,9 +4198,9 @@
},
"dependencies": {
"estraverse": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz",
"integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz",
"integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==",
"dev": true
}
}
@ -5409,21 +5523,21 @@
"dev": true
},
"inquirer": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz",
"integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==",
"version": "7.3.3",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz",
"integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==",
"dev": true,
"requires": {
"ansi-escapes": "^4.2.1",
"chalk": "^3.0.0",
"chalk": "^4.1.0",
"cli-cursor": "^3.1.0",
"cli-width": "^2.0.0",
"cli-width": "^3.0.0",
"external-editor": "^3.0.3",
"figures": "^3.0.0",
"lodash": "^4.17.15",
"lodash": "^4.17.19",
"mute-stream": "0.0.8",
"run-async": "^2.4.0",
"rxjs": "^6.5.3",
"rxjs": "^6.6.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0",
"through": "^2.3.6"
@ -5440,9 +5554,9 @@
}
},
"chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@ -5470,6 +5584,12 @@
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"lodash": {
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
"dev": true
},
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
@ -5792,9 +5912,9 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"js-yaml": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
"integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
@ -6070,6 +6190,11 @@
"p-is-promise": "^2.0.0"
}
},
"memoize-one": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
"integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
},
"memory-fs": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@ -6375,6 +6500,11 @@
"resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz",
"integrity": "sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA=="
},
"mousetrap-global-bind": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/mousetrap-global-bind/-/mousetrap-global-bind-1.1.0.tgz",
"integrity": "sha1-zX3pIivQZG+i4BDVTISnTCaojt0="
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -6769,9 +6899,9 @@
}
},
"onetime": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
"integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.1.tgz",
"integrity": "sha512-ZpZpjcJeugQfWsfyQlshVoowIIQ1qBGSVll4rfDq6JJVO//fesjoX808hXWfBjY+ROZgpKDI5TRSRBSoJiZ8eg==",
"dev": true,
"requires": {
"mimic-fn": "^2.1.0"
@ -7576,6 +7706,15 @@
"tiny-warning": "^1.0.0"
}
},
"react-window": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.5.tgz",
"integrity": "sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q==",
"requires": {
"@babel/runtime": "^7.0.0",
"memoize-one": ">=3.1.1 <6"
}
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
@ -7647,9 +7786,9 @@
}
},
"regexpp": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
"integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz",
"integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==",
"dev": true
},
"regexpu-core": {
@ -7968,9 +8107,9 @@
}
},
"rxjs": {
"version": "6.5.5",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
"integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz",
"integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==",
"dev": true,
"requires": {
"tslib": "^1.9.0"
@ -8812,9 +8951,9 @@
"dev": true
},
"strip-json-comments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz",
"integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true
},
"style-loader": {
@ -9173,6 +9312,15 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
"integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA=="
},
"tsutils": {
"version": "3.17.1",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz",
"integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==",
"dev": true,
"requires": {
"tslib": "^1.8.1"
}
},
"tty-browserify": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
@ -9210,6 +9358,12 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true
},
"typescript": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
"integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
"dev": true
},
"unherit": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz",
@ -9483,9 +9637,9 @@
"dev": true
},
"v8-compile-cache": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz",
"integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz",
"integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==",
"dev": true
},
"value-equal": {

View File

@ -18,12 +18,14 @@
"markdown-to-jsx": "^6.11.4",
"moment": "^2.20.1",
"mousetrap": "^1.6.5",
"mousetrap-global-bind": "^1.1.0",
"prop-types": "^15.7.2",
"react": "^16.5.2",
"react-codemirror2": "^6.0.1",
"react-dom": "^16.8.6",
"react-markdown": "^4.3.1",
"react-router-dom": "^5.0.0",
"react-window": "^1.8.5",
"remark-disable-tokenizers": "^1.0.24",
"style-loader": "^1.2.1",
"styled-components": "^5.1.0",
@ -44,6 +46,8 @@
"@types/lodash": "^4.14.155",
"@types/react": "^16.9.38",
"@types/react-router-dom": "^5.1.5",
"@typescript-eslint/eslint-plugin": "^3.8.0",
"@typescript-eslint/parser": "^3.8.0",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"clean-webpack-plugin": "^3.0.0",
@ -55,12 +59,13 @@
"react-hot-loader": "^4.12.21",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"typescript": "^3.9.7",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3"
},
"scripts": {
"lint": "eslint ./**/*.js",
"lint": "eslint ./src/**/*.{js,ts,tsx}",
"lint-file": "eslint",
"tsc": "tsc",
"tsc:watch": "tsc --watch",

View File

@ -3,6 +3,10 @@ import 'react-hot-loader';
import * as React from 'react';
import { BrowserRouter as Router, Route, withRouter, Switch } from 'react-router-dom';
import styled, { ThemeProvider, createGlobalStyle } from 'styled-components';
import { sigil as sigiljs, stringRenderer } from 'urbit-sigil-js';
import Mousetrap from 'mousetrap';
import 'mousetrap-global-bind';
import './css/indigo-static.css';
import './css/fonts.css';
@ -17,11 +21,14 @@ import LinksApp from './apps/links/app';
import PublishApp from './apps/publish/app';
import StatusBar from './components/StatusBar';
import Omnibox from './components/Omnibox';
import ErrorComponent from './components/Error';
import GlobalStore from './store/store';
import GlobalSubscription from './subscription/global';
import GlobalApi from './api/global';
import { uxToHex } from './lib/util';
import { Sigil } from './lib/sigil';
// const Style = createGlobalStyle`
// ${cssReset}
@ -62,6 +69,7 @@ class App extends React.Component {
new GlobalSubscription(this.store, this.api, this.appChannel);
this.updateTheme = this.updateTheme.bind(this);
this.setFavicon = this.setFavicon.bind(this);
}
componentDidMount() {
@ -70,21 +78,49 @@ class App extends React.Component {
this.api.local.setDark(this.themeWatcher.matches);
this.themeWatcher.addListener(this.updateTheme);
this.api.local.getBaseHash();
Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => {
e.preventDefault();
this.api.local.setOmnibox();
});
this.setFavicon();
}
componentWillUnmount() {
this.themeWatcher.removeListener(this.updateTheme);
}
componentDidUpdate(prevProps, prevState, snapshot) {
this.setFavicon();
}
updateTheme(e) {
this.api.local.setDark(e.matches);
}
setFavicon() {
if (window.ship.length < 14) {
let background = '#ffffff';
if (this.state.contacts.hasOwnProperty('/~/default')) {
background = `#${uxToHex(this.state.contacts['/~/default'][window.ship].color)}`;
}
const foreground = Sigil.foregroundFromBackground(background);
const svg = sigiljs({
patp: window.ship,
renderer: stringRenderer,
size: 16,
colors: [background, foreground]
});
const dataurl = 'data:image/svg+xml;base64,' + btoa(svg);
const favicon = document.querySelector('[rel=icon]');
favicon.href = dataurl;
favicon.type = 'image/svg+xml';
}
}
render() {
const channel = window.channel;
const associations = this.state.associations ? this.state.associations : { contacts: {} };
const selectedGroups = this.state.selectedGroups ? this.state.selectedGroups : [];
const { state } = this;
const theme = state.dark ? dark : light;
@ -92,81 +128,99 @@ class App extends React.Component {
<ThemeProvider theme={theme}>
<Root>
<Router>
<StatusBarWithRouter props={this.props}
associations={associations}
invites={this.state.invites}
api={this.api}
connection={this.state.connection}
subscription={this.subscription}
<StatusBarWithRouter
props={this.props}
associations={associations}
invites={this.state.invites}
api={this.api}
connection={this.state.connection}
subscription={this.subscription}
/>
<Omnibox
associations={state.associations}
apps={state.launch}
api={this.api}
dark={state.dark}
show={state.omniboxShown}
/>
<Content>
<Switch>
<Route exact path="/"
render={ p => (
<LaunchApp
ship={this.ship}
api={this.api}
{...state}
{...p}
<Switch>
<Route
exact
path='/'
render={p => (
<LaunchApp
ship={this.ship}
api={this.api}
{...state}
{...p}
/>
)}
/>
)}
/>
<Route path="/~chat" render={ p => (
<ChatApp
ship={this.ship}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
<Route
path='/~chat'
render={p => (
<ChatApp
ship={this.ship}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
/>
)}
/>
)}
/>
<Route path="/~dojo" render={ p => (
<DojoApp
ship={this.ship}
channel={channel}
selectedGroups={selectedGroups}
subscription={this.subscription}
{...p}
<Route
path='/~dojo'
render={p => (
<DojoApp
ship={this.ship}
channel={channel}
subscription={this.subscription}
{...p}
/>
)}
/>
)}
/>
<Route path="/~groups" render={ p => (
<GroupsApp
ship={this.ship}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
<Route
path='/~groups'
render={p => (
<GroupsApp
ship={this.ship}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
/>
)}
/>
)}
/>
<Route path="/~link" render={ p => (
<LinksApp
ship={this.ship}
ship={this.ship}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
<Route
path='/~link'
render={p => (
<LinksApp
ship={this.ship}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
/>
)}
/>
)}
/>
<Route path="/~publish" render={ p => (
<PublishApp
ship={this.ship}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
<Route
path='/~publish'
render={p => (
<PublishApp
ship={this.ship}
api={this.api}
subscription={this.subscription}
{...state}
{...p}
/>
)}
/>
)}
/>
<Route
render={(props) => (
render={props => (
<ErrorComponent {...props} code={404} description="Not Found" />
)}
/>
/>
</Switch>
</Content>
</Router>
@ -176,5 +230,6 @@ class App extends React.Component {
}
}
export default process.env.NODE_ENV === 'production' ? App : hot(App);

View File

@ -1,6 +1,5 @@
import BaseApi from "./base";
import { StoreState } from "../store/type";
import { SelectedGroup } from "../types/local-update";
export default class LocalApi extends BaseApi<StoreState> {
getBaseHash() {
@ -9,16 +8,6 @@ export default class LocalApi extends BaseApi<StoreState> {
});
}
setSelected(selected: SelectedGroup[]) {
this.store.handleEvent({
data: {
local: {
selected
}
}
})
}
sidebarToggle() {
this.store.handleEvent({
data: {
@ -39,4 +28,14 @@ export default class LocalApi extends BaseApi<StoreState> {
});
}
setOmnibox() {
this.store.handleEvent({
data: {
local: {
omniboxShown: true
},
},
});
}
}

View File

@ -53,7 +53,6 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
const unreads = {};
let totalUnreads = 0;
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
const associations = props.associations
? props.associations
: { chat: {}, contacts: {} };
@ -74,14 +73,7 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
unreads[stat] = Boolean(unread);
if (
unread &&
stat in associations.chat &&
(selectedGroups.length === 0 ||
selectedGroups
.map((e) => {
return e[0];
})
.includes(associations.chat?.[stat]?.['group-path']) ||
props.groups[associations.chat?.[stat]?.['group-path']]?.hidden)
stat in associations.chat
) {
totalUnreads += unread;
}
@ -111,7 +103,6 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
inbox={inbox}
messagePreviews={messagePreviews}
associations={associations}
selectedGroups={selectedGroups}
contacts={contacts}
invites={invites['/chat'] || {}}
unreads={unreads}
@ -286,44 +277,6 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
);
}}
/>
<Route
exact
path="/~chat/(popout)?/members/(~)?/:ship/:station+"
render={(props) => {
let station = `/${props.match.params.ship}/${props.match.params.station}`;
const popout = props.match.url.includes('/popout/');
const association =
station in associations['chat'] ? associations.chat[station] : {};
const groupPath = association['group-path'];
const group = groups[groupPath] || {};
return (
<Skeleton
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
sidebarShown={sidebarShown}
popout={popout}
sidebar={renderChannelSidebar(props, station)}
>
<MemberScreen
{...props}
api={api}
group={group}
groups={groups}
associations={associations}
station={station}
association={association}
contacts={contacts}
popout={popout}
sidebarShown={sidebarShown}
/>
</Skeleton>
);
}}
/>
<Route
exact
path="/~chat/(popout)?/settings/(~)?/:ship/:station+"

View File

@ -1,16 +1,11 @@
import React, { Component, Fragment } from "react";
import _ from "lodash";
import moment from "moment";
import { Link, RouteComponentProps } from "react-router-dom";
import { ResubscribeElement } from "./lib/resubscribe-element";
import { BacklogElement } from "./lib/backlog-element";
import { Message } from "./lib/message";
import { SidebarSwitcher } from "../../../components/SidebarSwitch";
import { ChatTabBar } from "./lib/chat-tabbar";
import { ChatWindow } from './lib/chat-window';
import { ChatHeader } from './lib/chat-header';
import { ChatInput } from "./lib/chat-input";
import { UnreadNotice } from "./lib/unread-notice";
import { deSig } from "../../../lib/util";
import { ChatHookUpdate } from "../../../types/chat-hook-update";
import ChatApi from "../../../api/chat";
@ -21,52 +16,6 @@ import GlobalApi from "../../../api/global";
import { Association } from "../../../types/metadata-update";
import {Group} from "../../../types/group-update";
function getNumPending(props: any) {
const result = props.pendingMessages.has(props.station)
? props.pendingMessages.get(props.station).length
: 0;
return result;
}
const ACTIVITY_TIMEOUT = 60000; // a minute
const DEFAULT_BACKLOG_SIZE = 300;
const MAX_BACKLOG_SIZE = 1000;
function scrollIsAtTop(container) {
if (
(navigator.userAgent.includes("Safari") &&
navigator.userAgent.includes("Chrome")) ||
navigator.userAgent.includes("Firefox")
) {
return container.scrollTop === 0;
} else if (navigator.userAgent.includes("Safari")) {
return (
container.scrollHeight + Math.round(container.scrollTop) <=
container.clientHeight + 10
);
} else {
return false;
}
}
function scrollIsAtBottom(container) {
if (
(navigator.userAgent.includes("Safari") &&
navigator.userAgent.includes("Chrome")) ||
navigator.userAgent.includes("Firefox")
) {
return (
container.scrollHeight - Math.round(container.scrollTop) <=
container.clientHeight + 10
);
} else if (navigator.userAgent.includes("Safari")) {
return container.scrollTop === 0;
} else {
return false;
}
}
type IMessage = Envelope & { pending?: boolean };
type ChatScreenProps = RouteComponentProps<{
ship: Patp;
@ -90,47 +39,20 @@ type ChatScreenProps = RouteComponentProps<{
};
interface ChatScreenState {
numPages: number;
scrollLocked: boolean;
read: number;
active: boolean;
messages: Map<string, string>;
lastScrollHeight: number | null;
}
export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
hasAskedForMessages = false;
lastNumPending = 0;
scrollContainer: HTMLElement | null = null;
unreadMarker = null;
scrolledToMarker = false;
activityTimeout: NodeJS.Timeout | null = null;
scrollElement: HTMLElement | null = null;
constructor(props) {
super(props);
this.state = {
numPages: 1,
scrollLocked: false,
read: props.read,
active: true,
messages: new Map(),
// only for FF
lastScrollHeight: null,
};
this.onScroll = this.onScroll.bind(this);
this.setUnreadMarker = this.setUnreadMarker.bind(this);
this.handleActivity = this.handleActivity.bind(this);
this.setInactive = this.setInactive.bind(this);
moment.updateLocale("en", {
calendar: {
sameDay: "[Today]",
@ -143,450 +65,68 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
});
}
componentDidMount() {
document.addEventListener("mousemove", this.handleActivity, false);
document.addEventListener("mousedown", this.handleActivity, false);
document.addEventListener("keypress", this.handleActivity, false);
document.addEventListener("touchmove", this.handleActivity, false);
this.activityTimeout = setTimeout(this.setInactive, ACTIVITY_TIMEOUT);
}
componentWillUnmount() {
document.removeEventListener("mousemove", this.handleActivity, false);
document.removeEventListener("mousedown", this.handleActivity, false);
document.removeEventListener("keypress", this.handleActivity, false);
document.removeEventListener("touchmove", this.handleActivity, false);
if (this.activityTimeout) {
clearTimeout(this.activityTimeout);
}
}
handleActivity() {
if (!this.state.active) {
this.setState({ active: true });
}
if (this.activityTimeout) {
clearTimeout(this.activityTimeout);
}
this.activityTimeout = setTimeout(this.setInactive, ACTIVITY_TIMEOUT);
}
setInactive() {
this.activityTimeout = null;
this.setState({ active: false, scrollLocked: true });
}
receivedNewChat() {
const { props } = this;
this.hasAskedForMessages = false;
this.unreadMarker = null;
this.scrolledToMarker = false;
this.setState({ read: props.read });
const unread = props.length - props.read;
const unreadUnloaded = unread - props.envelopes.length;
const excessUnread = unreadUnloaded > MAX_BACKLOG_SIZE;
if (!excessUnread && unreadUnloaded + 20 > DEFAULT_BACKLOG_SIZE) {
this.askForMessages(unreadUnloaded + 20);
} else {
this.askForMessages(DEFAULT_BACKLOG_SIZE);
}
if (excessUnread || props.read === props.length) {
this.scrolledToMarker = true;
this.setState(
{
scrollLocked: false,
},
() => {
this.scrollToBottom();
}
);
} else {
this.setState({ scrollLocked: true, numPages: Math.ceil(unread / 100) });
}
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
if (
prevProps.match.params.station !== props.match.params.station ||
prevProps.match.params.ship !== props.match.params.ship
) {
this.receivedNewChat();
} else if (
props.chatInitialized &&
!(props.station in props.inbox) &&
Boolean(props.chatSynced) &&
!(props.station in props.chatSynced)
) {
props.history.push("/~chat");
} else if (props.envelopes.length >= prevProps.envelopes.length + 10) {
this.hasAskedForMessages = false;
} else if (
props.length !== prevProps.length &&
prevProps.length === prevState.read &&
state.active
) {
this.setState({ read: props.length });
this.props.api.chat.read(this.props.station);
}
if (!prevProps.chatInitialized && props.chatInitialized) {
this.receivedNewChat();
}
if (
props.length !== prevProps.length ||
props.envelopes.length !== prevProps.envelopes.length ||
getNumPending(props) !== this.lastNumPending ||
state.numPages !== prevState.numPages
) {
this.scrollToBottom();
if (navigator.userAgent.includes("Firefox")) {
this.recalculateScrollTop();
}
this.lastNumPending = getNumPending(props);
}
}
askForMessages(size) {
const { props, state } = this;
if (
props.envelopes.length >= props.length ||
this.hasAskedForMessages ||
props.length <= 0
) {
return;
}
const start =
props.length - props.envelopes[props.envelopes.length - 1].number;
if (start > 0) {
const end = start + size < props.length ? start + size : props.length;
this.hasAskedForMessages = true;
props.api.chat.fetchMessages(start + 1, end, props.station);
}
}
scrollToBottom() {
if (!this.state.scrollLocked && this.scrollElement) {
this.scrollElement.scrollIntoView();
}
}
// Restore chat position on FF when new messages come in
recalculateScrollTop() {
const { lastScrollHeight } = this.state;
if (!this.scrollContainer || !lastScrollHeight) {
return;
}
const target = this.scrollContainer;
const newScrollTop = this.scrollContainer.scrollHeight - lastScrollHeight;
if (target.scrollTop !== 0 || newScrollTop === target.scrollTop) {
return;
}
target.scrollTop = target.scrollHeight - lastScrollHeight;
}
onScroll(e) {
if (scrollIsAtTop(e.target)) {
// Save scroll position for FF
if (navigator.userAgent.includes("Firefox")) {
this.setState({
lastScrollHeight: e.target.scrollHeight,
});
}
this.setState(
{
numPages: this.state.numPages + 1,
scrollLocked: true,
},
() => {
this.askForMessages(DEFAULT_BACKLOG_SIZE);
}
);
} else if (scrollIsAtBottom(e.target)) {
this.dismissUnread();
this.setState({
numPages: 1,
scrollLocked: false,
});
}
}
setUnreadMarker(ref) {
if (ref && !this.scrolledToMarker) {
this.setState({ scrollLocked: true }, () => {
ref.scrollIntoView({ block: "center" });
if (ref.offsetParent && scrollIsAtBottom(ref.offsetParent)) {
this.dismissUnread();
this.setState({
numPages: 1,
scrollLocked: false,
});
}
});
this.scrolledToMarker = true;
}
this.unreadMarker = ref;
}
dismissUnread() {
this.props.api.chat.read(this.props.station);
}
chatWindow(unread) {
// Replace with just the "not Firefox" implementation
// when Firefox #1042151 is patched.
const { props, state } = this;
let messages: IMessage[] = props.envelopes.slice(0);
const lastMsgNum = messages.length > 0 ? messages.length : 0;
if (messages.length > 100 * state.numPages) {
messages = messages.slice(0, 100 * state.numPages);
}
const pendingMessages: IMessage[] = (
props.pendingMessages.get(props.station) || []
).map((value) => ({ ...value, pending: true }));
if(unread !== 0) {
unread += pendingMessages.length;
}
messages = pendingMessages.concat(messages);
const messageElements = messages.map((msg, i) => {
// Render sigil if previous message is not by the same sender
const aut = ["author"];
const renderSigil =
_.get(messages[i + 1], aut) !== _.get(msg, aut, msg.author);
const paddingTop = renderSigil;
const paddingBot =
_.get(messages[i - 1], aut) !== _.get(msg, aut, msg.author);
const when = ["when"];
const dayBreak =
moment(_.get(messages[i + 1], when)).format("YYYY.MM.DD") !==
moment(_.get(messages[i], when)).format("YYYY.MM.DD");
const messageElem = (
<Message
key={msg.uid}
msg={msg}
contacts={props.contacts}
renderSigil={renderSigil}
paddingTop={paddingTop}
paddingBot={paddingBot}
pending={Boolean(msg.pending)}
group={props.group}
association={props.association}
/>
);
if (unread > 0 && i === unread - 1) {
return (
<Fragment key={msg.uid}>
{messageElem}
<div
ref={this.setUnreadMarker}
className="mv2 green2 flex items-center f9"
>
<hr className="dn-s ma0 w2 b--green2 bt-0" />
<p className="mh4">New messages below</p>
<hr className="ma0 flex-grow-1 b--green2 bt-0" />
{dayBreak && (
<p className="gray2 mh4">
{moment(_.get(messages[i], when)).calendar()}
</p>
)}
<hr
style={{ width: "calc(50% - 48px)" }}
className="b--green2 ma0 bt-0"
/>
</div>
</Fragment>
);
} else if (dayBreak) {
return (
<Fragment key={msg.uid}>
{messageElem}
<div
className="pv3 gray2 b--gray2 flex items-center justify-center f9 "
>
<p>{moment(_.get(messages[i], when)).calendar()}</p>
</div>
</Fragment>
);
} else {
return messageElem;
}
});
if (navigator.userAgent.includes("Firefox")) {
return (
<div
className="relative overflow-y-scroll h-100"
onScroll={this.onScroll}
ref={(e) => {
this.scrollContainer = e;
}}
>
<div
className="bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
style={{ resize: "vertical" }}
>
<div
ref={(el) => {
this.scrollElement = el;
}}
></div>
{props.chatInitialized && !(props.station in props.inbox) && (
<BacklogElement />
)}
{props.chatSynced &&
!(props.station in props.chatSynced) &&
messages.length > 0 ? (
<ResubscribeElement
api={props.api}
host={props.match.params.ship}
station={props.station}
/>
) : (
<div />
)}
{messageElements}
</div>
</div>
);
} else {
return (
<div
className="overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse relative"
style={{ height: "100%", resize: "vertical" }}
onScroll={this.onScroll}
>
<div
ref={(el) => {
this.scrollElement = el;
}}
></div>
{props.chatInitialized && !(props.station in props.inbox) && (
<BacklogElement />
)}
{props.chatSynced &&
!(props.station in props.chatSynced) &&
messages.length > 0 ? (
<ResubscribeElement
api={props.api}
host={props.match.params.ship}
station={props.station}
/>
) : (
<div />
)}
{messageElements}
</div>
);
}
}
render() {
const { props, state } = this;
const messages = props.envelopes.slice(0);
const lastMsgNum = messages.length > 0 ? messages.length : 0;
const group = Array.from(props.group.members);
const isinPopout = props.popout ? "popout/" : "";
const lastMsgNum = props.envelopes.length > 0 ? props.envelopes.length : 0;
const ownerContact =
window.ship in props.contacts ? props.contacts[window.ship] : false;
let title = props.station.substr(1);
const pendingMessages = (props.pendingMessages.get(props.station) || [])
.map((value) => ({
...value,
pending: true
}));
if (props.association && "metadata" in props.association) {
title =
props.association.metadata.title !== ""
? props.association.metadata.title
: props.station.substr(1);
}
const isChatMissing =
props.chatInitialized &&
!(props.station in props.inbox) &&
props.chatSynced &&
!(props.station in props.chatSynced);
const unread = props.length - state.read;
const isChatLoading =
props.chatInitialized &&
!(props.station in props.inbox) &&
props.chatSynced &&
(props.station in props.chatSynced);
const unreadMsg = unread > 0 && messages[unread - 1];
const isChatUnsynced =
props.chatSynced &&
!(props.station in props.chatSynced) &&
props.envelopes.length > 0;
const showUnreadNotice =
props.length !== props.read && props.read === state.read;
const unreadCount = props.length - props.read;
const unreadMsg = unreadCount > 0 && props.envelopes[unreadCount - 1];
return (
<div
key={props.station}
className="h-100 w-100 overflow-hidden flex flex-column relative"
>
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: "1rem" }}
>
<Link to="/~chat/">{"⟵ All Chats"}</Link>
</div>
<div
className={
"pl4 pt2 bb b--gray4 b--gray1-d bg-gray0-d flex relative " +
"overflow-x-auto overflow-y-hidden flex-shrink-0 "
}
style={{ height: 48 }}
>
<SidebarSwitcher
sidebarShown={props.sidebarShown}
popout={props.popout}
api={props.api}
/>
<Link
to={"/~chat/" + isinPopout + "room" + props.station}
className="pt2 white-d"
>
<h2
className={
"dib f9 fw4 lh-solid v-top " +
(title === props.station.substr(1) ? "mono" : "")
}
style={{ width: "max-content" }}
>
{title}
</h2>
</Link>
<ChatTabBar
{...props}
station={props.station}
numPeers={group.length}
isOwner={deSig(props.match.params.ship) === window.ship}
popout={props.popout}
api={props.api}
/>
</div>
{!!unreadMsg && showUnreadNotice && (
<UnreadNotice
unread={unread}
unreadMsg={unreadMsg}
onRead={() => this.dismissUnread()}
/>
)}
{this.chatWindow(unread)}
className="h-100 w-100 overflow-hidden flex flex-column relative">
<ChatHeader
match={props.match}
location={props.location}
api={props.api}
group={props.group}
association={props.association}
station={props.station}
sidebarShown={props.sidebarShown}
popout={props.popout} />
<ChatWindow
history={props.history}
isChatMissing={isChatMissing}
isChatLoading={isChatLoading}
isChatUnsynced={isChatUnsynced}
unreadCount={unreadCount}
unreadMsg={unreadMsg}
pendingMessages={pendingMessages}
messages={props.envelopes}
length={props.length}
contacts={props.contacts}
association={props.association}
group={props.group}
ship={props.match.params.ship}
station={props.station}
api={props.api} />
<ChatInput
api={props.api}
numMsgs={lastMsgNum}
@ -595,13 +135,15 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
ownerContact={ownerContact}
envelopes={props.envelopes}
contacts={props.contacts}
onEnter={() => this.setState({ scrollLocked: false })}
onUnmount={(msg: string) => this.setState({
messages: this.state.messages.set(props.station, msg)
})}
s3={props.s3}
placeholder="Message..."
message={this.state.messages.get(props.station) || ""}
deleteMessage={() => this.setState({
messages: this.state.messages.set(props.station, "")
})}
/>
</div>
);

View File

@ -1,21 +1,22 @@
import React, { Component } from 'react';
export class BacklogElement extends Component {
render() {
return (
<div className="center mw6">
<div className="db pa3 ma3 ba b--gray4 bg-gray5 b--gray2-d bg-gray1-d white-d flex items-center">
<img className="invert-d spin-active v-mid"
src="/~chat/img/Spinner.png"
width={16}
height={16}
/>
<p className="lh-copy db ml3">
Past messages are being restored
</p>
</div>
</div>
);
export const BacklogElement = (props) => {
if (!props.isChatLoading) {
return null;
}
return (
<div className="center mw6">
<div className={
"db pa3 ma3 ba b--gray4 bg-gray5 b--gray2-d bg-gray1-d " +
"white-d flex items-center"
}>
<img className="invert-d spin-active v-mid"
src="/~chat/img/Spinner.png"
width={16}
height={16}
/>
<p className="lh-copy db ml3">Past messages are being restored</p>
</div>
</div>
);
}

View File

@ -0,0 +1,141 @@
import React, { Component } from 'react';
import { UnControlled as CodeEditor } from 'react-codemirror2';
import CodeMirror from 'codemirror';
import 'codemirror/mode/markdown/markdown';
import 'codemirror/addon/display/placeholder';
import 'codemirror/lib/codemirror.css';
const BROWSER_REGEX =
new RegExp(String(/Android|webOS|iPhone|iPad|iPod|BlackBerry/i));
const MARKDOWN_CONFIG = {
name: 'markdown',
tokenTypeOverrides: {
header: 'presentation',
quote: 'presentation',
list1: 'presentation',
list2: 'presentation',
list3: 'presentation',
hr: 'presentation',
image: 'presentation',
imageAltText: 'presentation',
imageMarker: 'presentation',
formatting: 'presentation',
linkInline: 'presentation',
linkEmail: 'presentation',
linkText: 'presentation',
linkHref: 'presentation'
}
};
export default class ChatEditor extends Component {
constructor(props) {
super(props);
this.state = {
message: props.message
};
this.editor = null;
}
componentWillUnmount() {
this.props.onUnmount(this.state.message);
}
componentDidUpdate(prevProps) {
const { props } = this;
if (prevProps.message !== props.message) {
this.editor.setValue(props.message);
this.editor.setOption('mode', MARKDOWN_CONFIG);
return;
}
if (!props.inCodeMode) {
this.editor.setOption('mode', MARKDOWN_CONFIG);
this.editor.setOption('placeholder', this.props.placeholder);
} else {
this.editor.setOption('mode', null);
this.editor.setOption('placeholder', 'Code...');
}
const value = this.editor.getValue();
// Force redraw of placeholder
if(value.length === 0) {
this.editor.setValue(' ');
this.editor.setValue('');
}
}
submit() {
if(!this.editor) {
return;
}
let editorMessage = this.editor.getValue();
if (editorMessage === '') {
return;
}
this.setState({ message: '' });
this.props.submit(editorMessage);
this.editor.setValue('');
}
messageChange(editor, data, value) {
if (this.state.message !== '' && value == '') {
this.setState({
message: value
});
}
if (value == this.props.message || value == '' || value == ' ') {
return;
}
this.setState({
message: value
});
}
render() {
const { props } = this;
const codeTheme = props.inCodeMode ? ' code' : '';
const options = {
mode: MARKDOWN_CONFIG,
theme: 'tlon' + codeTheme,
lineNumbers: false,
lineWrapping: true,
scrollbarStyle: 'native',
cursorHeight: 0.85,
placeholder: props.inCodeMode ? 'Code...' : props.placeholder,
extraKeys: {
'Enter': () => {
this.submit();
}
}
};
return (
<div
className="chat fr h-100 flex bg-gray0-d lh-copy pl2 w-100 items-center"
style={{ flexGrow: 1, maxHeight: '224px', width: 'calc(100% - 72px)' }}
>
<CodeEditor
value={props.message}
options={options}
onChange={(e, d, v) => this.messageChange(e, d, v)}
editorDidMount={(editor) => {
this.editor = editor;
if (!(BROWSER_REGEX.test(navigator.userAgent))) {
editor.focus();
}
}}
/>
</div>
);
}
}

View File

@ -0,0 +1,58 @@
import React, { Component, Fragment } from "react";
import { Link } from "react-router-dom";
import { ChatTabBar } from "./chat-tabbar";
import { SidebarSwitcher } from "../../../../components/SidebarSwitch";
import { deSig } from "../../../../lib/util";
export const ChatHeader = (props) => {
const isInPopout = props.popout ? "popout/" : "";
const group = Array.from(props.group.members);
let title = props.station.substr(1);
if (props.association &&
"metadata" in props.association &&
props.association.metadata.tile !== "") {
title = props.association.metadata.title
}
return (
<Fragment>
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: "1rem" }}>
<Link to="/~chat/">{"⟵ All Chats"}</Link>
</div>
<div
className={
"pl4 pt2 bb b--gray4 b--gray1-d bg-gray0-d flex relative " +
"overflow-x-auto overflow-y-hidden flex-shrink-0 "
}
style={{ height: 48 }}>
<SidebarSwitcher
sidebarShown={props.sidebarShown}
popout={props.popout}
api={props.api}
/>
<Link
to={"/~chat/" + isInPopout + "room" + props.station}
className="pt2 white-d">
<h2
className={
"dib f9 fw4 lh-solid v-top " +
(title === props.station.substr(1) ? "mono" : "")
}
style={{ width: "max-content" }}>
{title}
</h2>
</Link>
<ChatTabBar
location={props.location}
station={props.station}
isOwner={deSig(props.match.params.ship) === window.ship}
popout={props.popout}
/>
</div>
</Fragment>
);
}

View File

@ -1,161 +1,41 @@
import React, { Component } from 'react';
import _ from 'lodash';
import moment from 'moment';
import { UnControlled as CodeEditor } from 'react-codemirror2';
import CodeMirror from 'codemirror';
import 'codemirror/mode/markdown/markdown';
import 'codemirror/addon/display/placeholder';
import 'codemirror/lib/codemirror.css';
import { Sigil } from '../../../../lib/sigil';
import { ShipSearch } from './ship-search';
import ChatEditor from './chat-editor';
import { S3Upload } from './s3-upload';
import { uxToHex } from '../../../../lib/util';
const MARKDOWN_CONFIG = {
name: 'markdown',
tokenTypeOverrides: {
header: 'presentation',
quote: 'presentation',
list1: 'presentation',
list2: 'presentation',
list3: 'presentation',
hr: 'presentation',
image: 'presentation',
imageAltText: 'presentation',
imageMarker: 'presentation',
formatting: 'presentation',
linkInline: 'presentation',
linkEmail: 'presentation',
linkText: 'presentation',
linkHref: 'presentation'
}
};
const URL_REGEX = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source));
export class ChatInput extends Component {
constructor(props) {
super(props);
this.state = {
message: props.message,
patpSearch: null
inCodeMode: false,
};
this.textareaRef = React.createRef();
this.messageSubmit = this.messageSubmit.bind(this);
this.messageChange = this.messageChange.bind(this);
this.patpAutocomplete = this.patpAutocomplete.bind(this);
this.completePatp = this.completePatp.bind(this);
this.clearSearch = this.clearSearch.bind(this);
this.submit = this.submit.bind(this);
this.toggleCode = this.toggleCode.bind(this);
this.editor = null;
// perf testing:
/* let closure = () => {
let x = 0;
for (var i = 0; i < 30; i++) {
x++;
props.api.chat.message(
props.station,
`~${window.ship}`,
Date.now(),
{
text: `${x}`
}
);
}
setTimeout(closure, 1000);
};
this.closure = closure.bind(this);*/
moment.updateLocale('en', {
relativeTime : {
past: function(input) {
return input === 'just now'
? input
: input + ' ago';
},
s : 'just now',
future: 'in %s',
ss : '%d sec',
m: 'a minute',
mm: '%d min',
h: 'an hr',
hh: '%d hrs',
d: 'a day',
dd: '%d days',
M: 'a month',
MM: '%d months',
y: 'a year',
yy: '%d years'
}
});
}
componentWillUnmount() {
this.props.onUnmount(this.state.message);
uploadSuccess(url) {
const { props } = this;
props.api.chat.message(
props.station,
`~${window.ship}`,
Date.now(),
{ url }
);
}
nextAutocompleteSuggestion(backward = false) {
const { patpSuggestions } = this.state;
let idx = patpSuggestions.findIndex(s => s === this.state.selectedSuggestion);
idx = backward ? idx - 1 : idx + 1;
idx = idx % patpSuggestions.length;
if(idx < 0) {
idx = patpSuggestions.length - 1;
}
this.setState({ selectedSuggestion: patpSuggestions[idx] });
uploadError(error) {
// no-op for now
}
patpAutocomplete(message) {
const match = /~([a-zA-Z\-]*)$/.exec(message);
if (!match ) {
this.setState({ patpSearch: null });
return;
}
this.setState({ patpSearch: match[1].toLowerCase() });
}
clearSearch() {
toggleCode() {
this.setState({
patpSearch: null
});
}
completePatp(suggestion) {
if(!this.editor) {
return;
}
const newMessage = this.editor.getValue().replace(
/[a-zA-Z\-]*$/,
suggestion
);
this.editor.setValue(newMessage);
const lastRow = this.editor.lastLine();
const lastCol = this.editor.getLineHandle(lastRow).text.length;
this.editor.setCursor(lastRow, lastCol);
this.setState({
patpSearch: null
});
}
messageChange(editor, data, value) {
const { patpSearch } = this.state;
if(patpSearch !== null) {
this.patpAutocomplete(value, false);
}
this.setState({
message: value
inCodeMode: !this.state.inCodeMode
});
}
@ -172,7 +52,7 @@ export class ChatInput extends Component {
me: letter
};
} else if (this.isUrl(letter)) {
return {
return {
url: letter
};
} else {
@ -184,98 +64,117 @@ export class ChatInput extends Component {
isUrl(string) {
try {
const websiteTest = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source)
);
return websiteTest.test(string);
return URL_REGEX.test(string);
} catch (e) {
return false;
}
}
messageSubmit() {
if(!this.editor) {
return;
}
submit(text) {
const { props, state } = this;
const editorMessage = this.editor.getValue();
if (editorMessage === '') {
return;
}
props.onEnter();
if(state.code) {
props.api.chat.message(props.station, `~${window.ship}`, Date.now(), {
code: {
expression: editorMessage,
output: undefined
}
if (state.inCodeMode) {
this.setState({
inCodeMode: false
}, () => {
props.api.chat.message(
props.station,
`~${window.ship}`,
Date.now(), {
code: {
expression: text,
output: undefined
}
}
);
});
this.editor.setValue('');
return;
}
let messages = [];
let message = [];
editorMessage.split(' ').map((each) => {
if (this.isUrl(each)) {
if (message.length > 0) {
message = message.join(' ');
message = this.getLetterType(message);
props.api.chat.message(
props.station,
`~${window.ship}`,
Date.now(),
message
);
message = [];
}
const URL = this.getLetterType(each);
let isInCodeBlock = false;
let endOfCodeBlock = false;
text.split(/\r?\n/).forEach((line, index) => {
if (index !== 0) {
message.push('\n');
}
// A line of backticks enters and exits a codeblock
if (line.startsWith('```')) {
// But we need to check if we've ended a codeblock
endOfCodeBlock = isInCodeBlock;
isInCodeBlock = (!isInCodeBlock);
} else {
endOfCodeBlock = false;
}
if (isInCodeBlock || endOfCodeBlock) {
message.push(line);
} else {
line.split(/\s/).forEach((str) => {
if (
(str.startsWith('`') && str !== '`')
|| (str === '`' && !isInCodeBlock)
) {
isInCodeBlock = true;
} else if (
(str.endsWith('`') && str !== '`')
|| (str === '`' && isInCodeBlock)
) {
isInCodeBlock = false;
}
if (this.isUrl(str) && !isInCodeBlock) {
if (message.length > 0) {
// If we're in the middle of a message, add it to the stack and reset
messages.push(message);
message = [];
}
messages.push([str]);
message = [];
} else {
message.push(str);
}
});
}
});
if (message.length) {
// Add any remaining message
messages.push(message);
}
props.deleteMessage();
messages.forEach((message) => {
if (message.length > 0) {
message = this.getLetterType(message.join(' '));
props.api.chat.message(
props.station,
`~${window.ship}`,
Date.now(),
URL
message
);
} else {
return message.push(each);
}
});
if (message.length > 0) {
message = message.join(' ');
message = this.getLetterType(message);
props.api.chat.message(
props.station,
`~${window.ship}`,
Date.now(),
message
);
message = [];
}
// perf:
// setTimeout(this.closure, 2000);
this.editor.setValue('');
}
toggleCode() {
if(this.state.code) {
this.setState({ code: false });
this.editor.setOption('mode', MARKDOWN_CONFIG);
this.editor.setOption('placeholder', this.props.placeholder);
} else {
this.setState({ code: true });
this.editor.setOption('mode', null);
this.editor.setOption('placeholder', 'Code...');
}
const value = this.editor.getValue();
// Force redraw of placeholder
if(value.length === 0) {
this.editor.setValue(' ');
this.editor.setValue('');
}
// perf testing:
/*let closure = () => {
let x = 0;
for (var i = 0; i < 30; i++) {
x++;
props.api.chat.message(
props.station,
`~${window.ship}`,
Date.now(),
{
text: `${x}`
}
);
}
setTimeout(closure, 1000);
};
this.closure = closure.bind(this);
setTimeout(this.closure, 2000);*/
}
uploadSuccess(url) {
@ -301,7 +200,7 @@ export class ChatInput extends Component {
const sigilClass = props.ownerContact
? '' : 'mix-blend-diff';
const img = (props.ownerContact && (props.ownerContact.avatar !== null))
const avatar = (props.ownerContact && (props.ownerContact.avatar !== null))
? <img src={props.ownerContact.avatar} height={24} width={24} className="dib" />
: <Sigil
ship={window.ship}
@ -310,82 +209,33 @@ export class ChatInput extends Component {
classes={sigilClass}
/>;
const candidates = _.chain(this.props.envelopes)
.defaultTo([])
.map('author')
.uniq()
.reverse()
.value();
const codeTheme = state.code ? ' code' : '';
const options = {
mode: MARKDOWN_CONFIG,
theme: 'tlon' + codeTheme,
lineNumbers: false,
lineWrapping: true,
scrollbarStyle: 'native',
cursorHeight: 0.85,
placeholder: state.code ? 'Code...' : props.placeholder,
extraKeys: {
Tab: cm =>
this.patpAutocomplete(cm.getValue(), true),
'Enter': () => {
this.messageSubmit();
if (this.state.code) {
this.toggleCode();
}
},
'Shift-3': cm =>
cm.getValue().length === 0
? this.toggleCode()
: CodeMirror.Pass
}
};
return (
<div className="chat pa3 cf flex black white-d bt b--gray4 b--gray1-d bg-white bg-gray0-d relative"
style={{ flexGrow: 1 }}
>
<ShipSearch
popover
onSelect={this.completePatp}
onClear={this.clearSearch}
contacts={props.contacts}
candidates={candidates}
searchTerm={this.state.patpSearch}
cm={this.editor}
/>
<div
className="fl"
style={{
marginTop: 6,
flexBasis: 24,
height: 24
}}
>
{img}
</div>
<div
className="fr h-100 flex bg-gray0-d lh-copy pl2 w-100 items-center"
style={{ flexGrow: 1, maxHeight: '224px', width: 'calc(100% - 72px)' }}
>
<CodeEditor
value={this.props.message}
options={options}
editorDidMount={(editor) => {
this.editor = editor;
if (!/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(
navigator.userAgent
)) {
editor.focus();
}
}}
onChange={(e, d, v) => this.messageChange(e, d, v)}
/>
<div className={
"pa3 cf flex black white-d bt b--gray4 b--gray1-d bg-white " +
"bg-gray0-d relative"
}
style={{ flexGrow: 1 }}>
<div className="fl"
style={{
marginTop: 6,
flexBasis: 24,
height: 24
}}>
{avatar}
</div>
<ChatEditor
inCodeMode={state.inCodeMode}
submit={this.submit}
onUnmount={props.onUnmount}
message={props.message}
placeholder='Message...' />
<div className="ml2 mr2"
style={{ height: '16px', width: '16px', flexBasis: 16, marginTop: 10 }}>
style={{
height: '16px',
width: '16px',
flexBasis: 16,
marginTop: 10
}}>
<S3Upload
configuration={props.s3.configuration}
credentials={props.s3.credentials}
@ -393,13 +243,20 @@ export class ChatInput extends Component {
uploadError={this.uploadError.bind(this)}
/>
</div>
<div style={{ height: '16px', width: '16px', flexBasis: 16, marginTop: 10 }}>
<img
style={{ filter: state.code && 'invert(100%)', height: '14px', width: '14px' }}
onClick={this.toggleCode}
src="/~chat/img/CodeEval.png"
className="contrast-10-d bg-white bg-none-d ba b--gray1-d br1"
/>
<div style={{
height: '16px',
width: '16px',
flexBasis: 16,
marginTop: 10
}}>
<img style={{
filter: state.inCodeMode && 'invert(100%)',
height: '14px',
width: '14px',
}}
onClick={this.toggleCode}
src="/~chat/img/CodeEval.png"
className="contrast-10-d bg-white bg-none-d ba b--gray1-d br1" />
</div>
</div>
);

View File

@ -0,0 +1,84 @@
import React, { PureComponent, Fragment } from "react";
import moment from "moment";
import { Message } from "./message";
type IMessage = Envelope & { pending?: boolean };
export const ChatMessage = (props) => {
const {
msg,
previousMsg,
nextMsg,
isLastUnread,
group,
association,
contacts,
unreadRef
} = props;
// Render sigil if previous message is not by the same sender
const aut = ["author"];
const renderSigil =
_.get(nextMsg, aut) !== _.get(msg, aut, msg.author);
const paddingTop = renderSigil;
const paddingBot =
_.get(previousMsg, aut) !== _.get(msg, aut, msg.author);
const when = ["when"];
const dayBreak =
moment(_.get(nextMsg, when)).format("YYYY.MM.DD") !==
moment(_.get(msg, when)).format("YYYY.MM.DD");
const messageElem = (
<Message
key={msg.uid}
msg={msg}
renderSigil={renderSigil}
paddingTop={paddingTop}
paddingBot={paddingBot}
pending={Boolean(msg.pending)}
group={group}
contacts={contacts}
association={association}
/>
);
if (props.isLastUnread) {
return (
<Fragment key={msg.uid}>
{messageElem}
<div ref={unreadRef}
className="mv2 green2 flex items-center f9">
<hr className="dn-s ma0 w2 b--green2 bt-0" />
<p className="mh4">New messages below</p>
<hr className="ma0 flex-grow-1 b--green2 bt-0" />
{dayBreak && (
<p className="gray2 mh4">
{moment(_.get(msg, when)).calendar()}
</p>
)}
<hr
style={{ width: "calc(50% - 48px)" }}
className="b--green2 ma0 bt-0"
/>
</div>
</Fragment>
);
} else if (dayBreak) {
return (
<Fragment key={msg.uid}>
{messageElem}
<div
className="pv3 gray2 b--gray2 flex items-center justify-center f9 "
>
<p>{moment(_.get(msg, when)).calendar()}</p>
</div>
</Fragment>
);
} else {
return messageElem;
}
};

View File

@ -0,0 +1,143 @@
import React, { Component, Fragment } from "react";
import { scrollIsAtTop, scrollIsAtBottom } from "../../../../lib/util";
// Restore chat position on FF when new messages come in
const recalculateScrollTop = (lastScrollHeight, scrollContainer) => {
if (!scrollContainer || !lastScrollHeight) {
return;
}
const newScrollTop = scrollContainer.scrollHeight - lastScrollHeight;
if (scrollContainer.scrollTop !== 0 ||
scrollContainer.scrollTop === newScrollTop) {
return;
}
scrollContainer.scrollTop = scrollContainer.scrollHeight - lastScrollHeight;
};
export class ChatScrollContainer extends Component {
constructor(props) {
super(props);
// only for FF
this.state = {
lastScrollHeight: null
};
this.isTriggeredScroll = false;
this.isAtBottom = true;
this.isAtTop = false;
this.containerDidScroll = this.containerDidScroll.bind(this);
this.containerRef = React.createRef();
this.scrollRef = React.createRef();
}
containerDidScroll(e) {
const { props } = this;
if (scrollIsAtTop(e.target)) {
// Save scroll position for FF
if (navigator.userAgent.includes("Firefox")) {
this.setState({
lastScrollHeight: e.target.scrollHeight,
});
}
if (!this.isAtTop) {
props.scrollIsAtTop();
}
this.isTriggeredScroll = false;
this.isAtBottom = false;
this.isAtTop = true;
} else if (scrollIsAtBottom(e.target) && !this.isTriggeredScroll) {
if (!this.isAtBottom) {
props.scrollIsAtBottom();
}
this.isTriggeredScroll = false;
this.isAtBottom = true;
this.isAtTop = false;
} else {
this.isAtBottom = false;
this.isAtTop = false;
this.isTriggeredScroll = false;
}
}
render() {
// Replace with just the "not Firefox" implementation
// when Firefox #1042151 is patched.
if (navigator.userAgent.includes("Firefox")) {
return this.firefoxScrollContainer();
} else {
return this.normalScrollContainer();
}
}
firefoxScrollContainer() {
return (
<div
className="relative overflow-y-scroll h-100"
onScroll={this.containerDidScroll}
ref={this.containerRef}>
<div
className="bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
style={{ resize: "vertical" }}>
<div ref={this.scrollRef}></div>
{this.props.children}
</div>
</div>
);
}
normalScrollContainer() {
return (
<div
className={
"overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex " +
"flex-column-reverse relative"
}
style={{ height: "100%", resize: "vertical" }}
onScroll={this.containerDidScroll}>
<div ref={this.scrollRef}></div>
{this.props.children}
</div>
);
}
scrollToBottom() {
this.isTriggeredScroll = true;
if (this.scrollRef.current) {
this.scrollRef.current.scrollIntoView(false);
}
if (navigator.userAgent.includes("Firefox")) {
recalculateScrollTop(
this.state.lastScrollHeight,
this.scrollContainer
);
}
}
scrollToReference(ref) {
this.isTriggeredScroll = true;
if (this.scrollRef.current && ref.current) {
ref.current.scrollIntoView({ block: 'center' });
}
if (navigator.userAgent.includes("Firefox")) {
recalculateScrollTop(
this.state.lastScrollHeight,
this.scrollContainer
);
}
}
}

View File

@ -1,66 +1,41 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
export class ChatTabBar extends Component {
render() {
const props = this.props;
export const ChatTabBar = (props) => {
const {
location,
station
} = props;
let setColor = '', popout = '';
let memColor = '',
setColor = '',
popout = '';
if (props.location.pathname.includes('/settings')) {
memColor = 'gray3';
setColor = 'black white-d';
} else if (props.location.pathname.includes('/members')) {
memColor = 'black white-d';
setColor = 'gray3';
} else {
memColor = 'gray3';
setColor = 'gray3';
}
popout = props.location.pathname.includes('/popout')
? 'popout/' : '';
const hidePopoutIcon = (this.props.popout)
? 'dn-m dn-l dn-xl' : 'dib-m dib-l dib-xl';
return (
<div className="dib flex-shrink-0 flex-grow-1">
{props.isOwner ? (
<div className={'dib pt2 f9 pl6 lh-solid'}>
<Link
className={'no-underline ' + memColor}
to={'/~chat/' + popout + 'members' + props.station}
>
Members
</Link>
</div>
) : (
<div className="dib" style={{ width: 0 }}></div>
)}
<div className={'dib pt2 f9 pl6 pr6 lh-solid'}>
<Link
className={'no-underline ' + setColor}
to={'/~chat/' + popout + 'settings' + props.station}
>
Settings
</Link>
</div>
<a href={'/~chat/popout/room' + props.station} rel="noopener noreferrer"
target="_blank"
className="dib fr pr1"
style={{ paddingTop: '8px' }}
>
<img
className={'flex-shrink-0 pr3 dn ' + hidePopoutIcon}
src="/~chat/img/popout.png"
height="16"
width="16"
/>
</a>
</div>
);
if (location.pathname.includes('/settings')) {
setColor = 'black white-d';
} else {
setColor = 'gray3';
}
const hidePopoutIcon = (popout)
? 'dn-m dn-l dn-xl' : 'dib-m dib-l dib-xl';
return (
<div className="dib flex-shrink-0 flex-grow-1">
<div className={'dib pt2 f9 pl6 pr6 lh-solid'}>
<Link
className={'no-underline ' + setColor}
to={'/~chat/' + popout + 'settings' + station}>
Settings
</Link>
</div>
<a href={'/~chat/popout/room' + station} rel="noopener noreferrer"
target="_blank"
className="dib fr pr1"
style={{ paddingTop: '8px' }}>
<img
className={'flex-shrink-0 pr3 dn ' + hidePopoutIcon}
src="/~chat/img/popout.png"
height="16"
width="16" />
</a>
</div>
);
}

View File

@ -0,0 +1,194 @@
import React, { Component, Fragment } from "react";
import { ChatMessage } from './chat-message';
import { ChatScrollContainer } from "./chat-scroll-container";
import { UnreadNotice } from "./unread-notice";
import { ResubscribeElement } from "./resubscribe-element";
import { BacklogElement } from "./backlog-element";
const MAX_BACKLOG_SIZE = 1000;
const DEFAULT_BACKLOG_SIZE = 200;
const PAGE_SIZE = 50;
const INITIAL_LOAD = 20;
export class ChatWindow extends Component {
constructor(props) {
super(props);
this.state = {
numPages: 1,
};
this.hasAskedForMessages = false;
this.dismissUnread = this.dismissUnread.bind(this);
this.scrollIsAtBottom = this.scrollIsAtBottom.bind(this);
this.scrollIsAtTop = this.scrollIsAtTop.bind(this);
this.scrollReference = React.createRef();
this.unreadReference = React.createRef();
}
componentDidMount() {
this.initialFetch();
if (this.state.numPages === 1 && this.props.unreadCount < INITIAL_LOAD) {
this.dismissUnread();
this.scrollToBottom();
}
}
initialFetch() {
const { props } = this;
if (props.messages.length > 0) {
const unreadUnloaded = props.unreadCount - props.messages.length;
if (unreadUnloaded <= MAX_BACKLOG_SIZE &&
unreadUnloaded + INITIAL_LOAD > DEFAULT_BACKLOG_SIZE) {
this.fetchBacklog(unreadUnloaded + INITIAL_LOAD);
} else {
this.fetchBacklog(DEFAULT_BACKLOG_SIZE);
}
} else {
setTimeout(() => {
this.initialFetch();
}, 2000);
}
}
componentDidUpdate(prevProps, prevState) {
const { props, state } = this;
if (props.isChatMissing) {
props.history.push("/~chat");
} else if (props.messages.length >= prevProps.messages.length + 10) {
this.hasAskedForMessages = false;
let numPages = props.unreadCount > 0 ?
Math.ceil(props.unreadCount / PAGE_SIZE) : this.state.numPages;
if (this.state.numPages === numPages) {
if (props.unreadCount > 20) {
this.scrollToUnread();
}
} else {
this.setState({ numPages }, () => {
if (props.unreadCount > 20) {
this.scrollToUnread();
}
});
}
} else if (
state.numPages === 1 &&
this.props.unreadCount < INITIAL_LOAD &&
this.props.unreadCount > 0
) {
this.dismissUnread();
this.scrollToBottom();
}
}
scrollIsAtTop() {
const { props, state } = this;
this.setState({ numPages: state.numPages + 1 }, () => {
if (state.numPages * PAGE_SIZE < props.length) {
this.fetchBacklog(DEFAULT_BACKLOG_SIZE);
}
});
}
scrollIsAtBottom() {
if (this.state.numPages !== 1) {
this.setState({ numPages: 1 });
this.dismissUnread();
}
}
scrollToBottom() {
if (this.scrollReference.current) {
this.scrollReference.current.scrollToBottom();
}
if (this.state.numPages !== 1) {
this.setState({ numPages: 1 });
}
}
scrollToUnread() {
if (this.scrollReference.current && this.unreadReference.current) {
this.scrollReference.current.scrollToReference(this.unreadReference);
}
}
dismissUnread() {
this.props.api.chat.read(this.props.station);
}
fetchBacklog(size) {
const { props } = this;
if (
props.messages.length >= props.length ||
this.hasAskedForMessages ||
props.length <= 0
) {
return;
}
const start =
props.length - props.messages[props.messages.length - 1].number;
if (start > 0) {
const end = start + size < props.length ? start + size : props.length;
props.api.chat.fetchMessages(start + 1, end, props.station);
this.hasAskedForMessages = true;
}
}
render() {
const { props, state } = this;
const sliceLength = Math.min(
state.numPages * PAGE_SIZE,
props.messages.length + props.pendingMessages.length
);
const messages =
props.pendingMessages
.concat(props.messages)
.slice(0, sliceLength);
return (
<Fragment>
<UnreadNotice
unreadCount={props.unreadCount}
unreadMsg={props.unreadMsg}
dismissUnread={this.dismissUnread} />
<ChatScrollContainer
ref={this.scrollReference}
scrollIsAtBottom={this.scrollIsAtBottom}
scrollIsAtTop={this.scrollIsAtTop}>
<BacklogElement isChatLoading={props.isChatLoading} />
<ResubscribeElement
api={props.api}
host={props.ship}
station={props.station}
isChatUnsynced={props.isChatUnsynced}
/>
{ messages.map((msg, i) => (
<ChatMessage
unreadRef={this.unreadReference}
isLastUnread={
props.unreadCount > 0 &&
i === props.unreadCount - 1 &&
state.numPages !== 1
}
msg={msg}
previousMsg={messages[i - 1]}
nextMsg={messages[i + 1]}
association={props.association}
group={props.group}
contacts={props.contacts} />
))
}
</ChatScrollContainer>
</Fragment>
);
}
}

View File

@ -0,0 +1,28 @@
import React, { Component } from 'react';
export default class CodeContent extends Component {
render() {
const { props } = this;
const content = props.content;
const outputElement =
(Boolean(content.code.output) &&
content.code.output.length && content.code.output.length > 0) ?
(
<pre className={`f7 clamp-attachment pa1 mt0 mb0 b--gray4 b--gray1-d bl br bb`}>
{content.code.output[0].join('\n')}
</pre>
) : null;
return (
<div className="mv2">
<pre className={`f7 clamp-attachment pa1 mt0 mb0 bg-light-gray b--gray4 b--gray1-d ba`}>
{content.code.expression}
</pre>
{outputElement}
</div>
);
}
}

View File

@ -0,0 +1,65 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import ReactMarkdown from 'react-markdown';
import RemarkDisableTokenizers from 'remark-disable-tokenizers';
import urbitOb from 'urbit-ob';
const DISABLED_BLOCK_TOKENS = [
'indentedCode',
'blockquote',
'atxHeading',
'thematicBreak',
'list',
'setextHeading',
'html',
'definition',
'table'
];
const DISABLED_INLINE_TOKENS = [
'autoLink',
'url',
'email',
'link',
'reference'
];
const MessageMarkdown = React.memo(props => (
<ReactMarkdown
{...props}
plugins={[[
RemarkDisableTokenizers,
{ block: DISABLED_BLOCK_TOKENS, inline: DISABLED_INLINE_TOKENS }
]]} />
));
export default class TextContent extends Component {
render() {
const { props } = this;
const content = props.content;
const group = content.text.match(
/([~][/])?(~[a-z]{3,6})(-[a-z]{6})?([/])(([a-z])+([/-])?)+/
);
if ((group !== null) // matched possible chatroom
&& (group[2].length > 2) // possible ship?
&& (urbitOb.isValidPatp(group[2]) // valid patp?
&& (group[0] === content.text))) { // entire message is room name?
return (
<Link
className="bb b--black b--white-d f7 mono lh-copy v-top"
to={'/~groups/join/' + group.input}>
{content.text}
</Link>
);
} else {
return (
<section className="chat-md-message">
<MessageMarkdown source={content.text} />
</section>
);
}
}
}

View File

@ -0,0 +1,102 @@
import React, { Component } from 'react';
const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i);
const YOUTUBE_REGEX =
new RegExp(
String(/(?:https?:\/\/(?:[a-z]+.)?)/.source) // protocol
+ /(?:youtu\.?be(?:\.com)?\/)(?:embed\/)?/.source // short and long-links
+ /(?:(?:(?:(?:watch\?)?(?:time_continue=(?:[0-9]+))?.+v=)?([a-zA-Z0-9_-]+))(?:\?t\=(?:[0-9a-zA-Z]+))?)/.source // id
);
export default class UrlContent extends Component {
constructor() {
super();
this.state = {
unfold: false,
copied: false
};
this.unfoldEmbed = this.unfoldEmbed.bind(this);
}
unfoldEmbed(id) {
let unfoldState = this.state.unfold;
unfoldState = !unfoldState;
this.setState({ unfold: unfoldState });
this.iframe.setAttribute('src', this.iframe.dataset.src);
}
render() {
const { props } = this;
const content = props.content;
const imgMatch = IMAGE_REGEX.exec(props.content.url);
const ytMatch = YOUTUBE_REGEX.exec(props.content.url);
let contents = content.url;
if (imgMatch) {
contents = (
<img
className="o-80-d"
src={content.url}
style={{
maxWidth: '18rem'
}}
></img>
);
return (
<a className='f7 lh-copy v-top word-break-all'
href={content.url}
target="_blank"
rel="noopener noreferrer"
>
{contents}
</a>
);
} else if (ytMatch) {
contents = (
<div className={'embed-container mb2 w-100 w-75-l w-50-xl ' +
((this.state.unfold === true)
? 'db' : 'dn')}
>
<iframe
ref={(el) => {
this.iframe = el;
}}
width="560"
height="315"
data-src={`https://www.youtube.com/embed/${ytMatch[1]}`}
frameBorder="0" allow="picture-in-picture, fullscreen"
>
</iframe>
</div>
);
return (
<div>
<a href={content.url}
className='f7 lh-copy v-top bb b--white-d word-break-all'
target="_blank"
rel="noopener noreferrer"
>
{content.url}
</a>
<a className="bs ml2 f7 pointer lh-copy v-top"
onClick={e => this.unfoldEmbed()}
>
[embed]
</a>
{contents}
</div>
);
} else {
return (
<a className='f7 lh-copy v-top bb b--white-d b--black word-break-all'
href={content.url}
target="_blank"
rel="noopener noreferrer"
>
{contents}
</a>
);
}
}
}

View File

@ -0,0 +1,51 @@
import React, { Component } from 'react';
export const DeleteButton = (props) => {
const { isOwner, station, changeLoading, api } = props;
const leaveButtonClasses = (!isOwner) ? 'pointer' : 'c-default';
const deleteButtonClasses = (isOwner) ?
'b--red2 red2 pointer bg-gray0-d' :
'b--gray3 gray3 bg-gray0-d c-default';
const deleteChat = () => {
changeLoading(
true,
true,
isOwner ? 'Deleting chat...' : 'Leaving chat...',
() => {
api.chat.delete(station);
}
);
};
return (
<div className="w-100 cf">
<div className={'w-100 fl mt3 ' + ((isOwner) ? 'o-30' : '')}>
<p className="f8 mt3 lh-copy db">Leave Chat</p>
<p className="f9 gray2 db mb4">
Remove this chat from your chat list.{' '}
You will need to request for access again.
</p>
<a onClick={(!isOwner) ? deleteChat : null}
className={
'dib f9 black gray4-d bg-gray0-d ba pa2 b--black b--gray1-d ' +
leaveButtonClasses
}>
Leave this chat
</a>
</div>
<div className={'w-100 fl mt3 ' + ((!isOwner) ? 'o-30' : '')}>
<p className="f8 mt3 lh-copy db">Delete Chat</p>
<p className="f9 gray2 db mb4">
Permanently delete this chat.{' '}
All current members will no longer see this chat.
</p>
<a onClick={(isOwner) ? deleteChat : null}
className={'dib f9 ba pa2 ' + deleteButtonClasses}
>Delete this chat</a>
</div>
</div>
);
};

View File

@ -0,0 +1,104 @@
import React, { Component } from 'react';
import Toggle from '../../../../components/toggle';
import { InviteSearch } from '../../../../components/InviteSearch';
export class GroupifyButton extends Component {
constructor(props) {
super(props);
this.state = {
inclusive: false,
targetGroup: null
};
}
changeTargetGroup(target) {
if (target.groups.length === 1) {
this.setState({ targetGroup: target.groups[0] });
} else {
this.setState({ targetGroup: null });
}
}
changeInclusive(event) {
this.setState({ inclusive: Boolean(event.target.checked) });
}
renderInclusiveToggle() {
return this.state.targetGroup ? (
<div className="mt4">
<Toggle
boolean={inclusive}
change={this.changeInclusive.bind(this)}
/>
<span className="dib f9 white-d inter ml3">
Add all members to group
</span>
<p className="f9 gray2 pt1" style={{ paddingLeft: 40 }}>
Add chat members to the group if they aren't in it yet
</p>
</div>
) : <div />;
}
render() {
const { inclusive, targetGroup } = this.state;
const {
api,
isOwner,
association,
associations,
contacts,
groups,
station
} = this.props;
const groupPath = association['group-path'];
const ownedUnmanagedVillage =
isOwner &&
!contacts[groupPath];
if (!ownedUnmanagedVillage) {
return null;
}
return (
<div className={'w-100 fl mt3'} style={{ maxWidth: '29rem' }}>
<p className="f8 mt3 lh-copy db">Convert Chat</p>
<p className="f9 gray2 db mb4">
Convert this chat into a group with associated chat, or select a
group to add this chat to.
</p>
<InviteSearch
groups={groups}
contacts={contacts}
associations={associations}
groupResults={true}
shipResults={false}
invites={{
groups: targetGroup ? [targetGroup] : [],
ships: []
}}
setInvite={this.changeTargetGroup.bind(this)}
/>
{this.renderInclusiveToggle()}
<a onClick={() => {
changeLoading(true, true, 'Converting to group...', () => {
api.chat.groupify(
station, targetGroup, inclusive
).then(() => {
changeLoading(false, false, '', () => {});
});
});
}}
className={
'dib f9 black gray4-d bg-gray0-d ba pa2 mt4 b--black ' +
'b--gray1-d pointer'
}>Convert to group</a>
</div>
);
}
}

View File

@ -0,0 +1,33 @@
import React, { Component } from 'react';
import TextContent from './content/text';
import CodeContent from './content/code';
import UrlContent from './content/url';
export default class MessageContent extends Component {
render() {
const { props } = this;
const content = props.letter;
if ('code' in content) {
return <CodeContent content={content} />;
} else if ('url' in content) {
return <UrlContent content={content} />;
} else if ('me' in content) {
return (
<p className='f7 i lh-copy v-top'>
{content.me}
</p>
);
}
else if ('text' in content) {
return <TextContent content={content} />;
} else {
return null;
}
}
}

View File

@ -1,275 +1,101 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { OverlaySigil } from './overlay-sigil';
import MessageContent from './message-content';
import { uxToHex, cite, writeText } from '../../../../lib/util';
import moment from 'moment';
import ReactMarkdown from 'react-markdown';
import RemarkDisableTokenizers from 'remark-disable-tokenizers';
import urbitOb from 'urbit-ob';
const DISABLED_BLOCK_TOKENS = [
'indentedCode',
'blockquote',
'atxHeading',
'thematicBreak',
'list',
'setextHeading',
'html',
'definition',
'table'
];
const DISABLED_INLINE_TOKENS = [
'autoLink',
'url',
'email',
'link',
'reference'
];
export const Message = (props) => {
const pending = props.msg.pending ? ' o-40' : '';
const containerClass =
props.renderSigil ?
`w-100 f7 pl3 pt4 pr3 cf flex lh-copy ` + pending :
'w-100 pr3 cf hide-child flex' + pending;
const MessageMarkdown = React.memo(
props => (<ReactMarkdown
{...props}
plugins={[[RemarkDisableTokenizers, { block: DISABLED_BLOCK_TOKENS, inline: DISABLED_INLINE_TOKENS }]]}
/>));
const timestamp =
moment.unix(props.msg.when / 1000).format(
props.renderSigil ? 'hh:mm a' : 'hh:mm'
);
export class Message extends Component {
constructor() {
super();
this.state = {
unfold: false,
copied: false
};
this.unFoldEmbed = this.unFoldEmbed.bind(this);
}
unFoldEmbed(id) {
let unfoldState = this.state.unfold;
unfoldState = !unfoldState;
this.setState({ unfold: unfoldState });
const iframe = this.refs.iframe;
iframe.setAttribute('src', iframe.getAttribute('data-src'));
}
renderContent() {
const { props } = this;
const letter = props.msg.letter;
if ('code' in letter) {
const outputElement =
(Boolean(letter.code.output) &&
letter.code.output.length && letter.code.output.length > 0) ?
(
<pre className="f7 clamp-attachment pa1 mt0 mb0 b--gray4 b--gray1-d bl br bb">
{letter.code.output[0].join('\n')}
</pre>
) : null;
return (
<div className="mv2">
<pre className="f7 clamp-attachment pa1 mt0 mb0 bg-light-gray b--gray4 b--gray1-d ba">
{letter.code.expression}
</pre>
{outputElement}
</div>
);
} else if ('url' in letter) {
const imgMatch =
/(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF|GIF|webp|WEBP|svg|SVG)$/
.exec(letter.url);
const youTubeRegex = new RegExp(String(/(?:https?:\/\/(?:[a-z]+.)?)/.source) // protocol
+ /(?:youtu\.?be(?:\.com)?\/)(?:embed\/)?/.source // short and long-links
+ /(?:(?:(?:(?:watch\?)?(?:time_continue=(?:[0-9]+))?.+v=)?([a-zA-Z0-9_-]+))(?:\?t\=(?:[0-9a-zA-Z]+))?)/.source // id
);
const ytMatch =
youTubeRegex.exec(letter.url);
let contents = letter.url;
if (imgMatch) {
contents = (
<img
className="o-80-d"
src={letter.url}
style={{
height: 'min(250px, 20vh)',
maxWidth: 'calc(100% - 36px - 1.5rem)',
objectFit: 'contain'
}}
></img>
);
return (
<a className="f7 lh-copy v-top word-break-all"
href={letter.url}
target="_blank"
rel="noopener noreferrer"
>
{contents}
</a>
);
} else if (ytMatch) {
contents = (
<div className={'embed-container mb2 w-100 w-75-l w-50-xl ' +
((this.state.unfold === true)
? 'db' : 'dn')}
>
<iframe
ref="iframe"
width="560"
height="315"
data-src={`https://www.youtube.com/embed/${ytMatch[1]}`}
frameBorder="0" allow="picture-in-picture, fullscreen"
>
</iframe>
</div>
);
return (
<div>
<a href={letter.url}
className="f7 lh-copy v-top bb b--white-d word-break-all"
target="_blank"
rel="noopener noreferrer"
>
{letter.url}
</a>
<a className="ml2 f7 pointer lh-copy v-top"
onClick={e => this.unFoldEmbed()}
>
[embed]
</a>
{contents}
</div>
);
} else {
return (
<a className="f7 lh-copy v-top bb b--white-d b--black word-break-all"
href={letter.url}
target="_blank"
rel="noopener noreferrer"
>
{contents}
</a>
);
}
} else if ('me' in letter) {
return (
<p className='f7 i lh-copy v-top'>
{letter.me}
</p>
);
} else {
const group = letter.text.match(
/([~][/])?(~[a-z]{3,6})(-[a-z]{6})?([/])(([a-z])+([/-])?)+/
);
if ((group !== null) // matched possible chatroom
&& (group[2].length > 2) // possible ship?
&& (urbitOb.isValidPatp(group[2]) // valid patp?
&& (group[0] === letter.text))) { // entire message is room name?
return (
<Link
className="bb b--black b--white-d f7 mono lh-copy v-top"
to={'/~groups/join/' + group.input}
>
{letter.text}
</Link>
);
} else {
return (
<section className="chat-md-message">
<MessageMarkdown
source={letter.text}
/>
</section>
);
}
}
}
render() {
const { props, state } = this;
const pending = props.msg.pending ? ' o-40' : '';
const datestamp = '~' + moment.unix(props.msg.when / 1000).format('YYYY.M.D');
const paddingTop = props.paddingTop ? { 'paddingTop': '6px' } : '';
if (props.renderSigil) {
const timestamp = moment.unix(props.msg.when / 1000).format('hh:mm a');
const contact = props.msg.author in props.contacts
? props.contacts[props.msg.author] : false;
let name = `~${props.msg.author}`;
let color = '#000000';
let sigilClass = 'mix-blend-diff';
if (contact) {
name = (contact.nickname.length > 0)
? contact.nickname : `~${props.msg.author}`;
color = `#${uxToHex(contact.color)}`;
sigilClass = '';
}
if (`~${props.msg.author}` === name) {
name = cite(props.msg.author);
}
return (
<div
ref={this.containerRef}
className={
'w-100 f7 pl3 pt4 pr3 cf flex lh-copy ' + ' ' + pending
}
style={{
minHeight: 'min-content'
}}
>
<OverlaySigil
ship={props.msg.author}
contact={contact}
color={color}
sigilClass={sigilClass}
association={props.association}
group={props.group}
className="fl pr3 v-top bg-white bg-gray0-d"
/>
<div
className="fr clamp-message white-d"
style={{ flexGrow: 1, marginTop: -8 }}
>
<div className="hide-child" style={paddingTop}>
<p className="v-mid f9 gray2 dib mr3 c-default">
<span
className={'pointer ' + (contact.nickname || state.copied ? null : 'mono')}
onClick={() => {
writeText(props.msg.author);
this.setState({ copied: true });
setTimeout(() => {
this.setState({ copied: false });
}, 800);
}}
title={`~${props.msg.author}`}
>
{state.copied && 'Copied' || name}
</span>
</p>
<p className="v-mid mono f9 gray2 dib">{timestamp}</p>
<p className="v-mid mono f9 ml2 gray2 dib child dn-s">{datestamp}</p>
return (
<div className={containerClass}
style={{
minHeight: 'min-content'
}}>
{
props.renderSigil ? (
renderWithSigil(props, timestamp)
) : (
<div className="flex w-100">
<p className="child pt2 pl2 pr1 mono f9 gray2 dib">{timestamp}</p>
<div className="fr f7 clamp-message white-d pr3 lh-copy"
style={{ flexGrow: 1 }}>
<MessageContent letter={props.msg.letter} />
</div>
{this.renderContent()}
</div>
</div>
);
} else {
const timestamp = moment.unix(props.msg.when / 1000).format('hh:mm');
)
}
</div>
);
};
return (
<div
className={'w-100 pr3 cf hide-child flex' + pending}
style={{
minHeight: 'min-content'
}}
>
<p className="child pt2 pl2 pr1 mono f9 gray2 dib">{timestamp}</p>
<div className="fr f7 clamp-message white-d pr3 lh-copy" style={{ flexGrow: 1 }}>
{this.renderContent()}
</div>
</div>
);
const renderWithSigil = (props, timestamp) => {
const paddingTop = props.paddingTop ? { 'paddingTop': '6px' } : '';
const datestamp =
'~' + moment.unix(props.msg.when / 1000).format('YYYY.M.D');
const contact = props.msg.author in props.contacts
? props.contacts[props.msg.author] : false;
let name = `~${props.msg.author}`;
let color = '#000000';
let sigilClass = 'mix-blend-diff';
if (contact) {
name = (contact.nickname.length > 0)
? contact.nickname : `~${props.msg.author}`;
color = `#${uxToHex(contact.color)}`;
sigilClass = '';
}
if (`~${props.msg.author}` === name) {
name = cite(props.msg.author);
}
return (
<div className="flex w-100">
<OverlaySigil
ship={props.msg.author}
contact={contact}
color={color}
sigilClass={sigilClass}
association={props.association}
group={props.group}
className="fl pr3 v-top bg-white bg-gray0-d"
/>
<div className="fr clamp-message white-d"
style={{ flexGrow: 1, marginTop: -8 }}>
<div className="hide-child" style={paddingTop}>
<p className={`v-mid f9 gray2 dib mr3 c-default`}>
<span
className={
'mw5 db truncate pointer ' +
(contact.nickname ? '' : 'mono')
}
onClick={() => {
writeText(props.msg.author);
}}
title={`~${props.msg.author}`}
>
{name}
</span>
</p>
<p className={`v-mid mono f9 gray2 dib`}>{timestamp}</p>
<p className={`v-mid mono f9 ml2 gray2 dib child dn-s`}>
{datestamp}
</p>
</div>
<MessageContent letter={props.msg.letter} />
</div>
</div>
);
}
}

View File

@ -0,0 +1,71 @@
import React, { Component } from 'react';
import { uxToHex } from '../../../../lib/util';
export class MetadataColor extends Component {
constructor(props) {
super(props);
this.state = {
color: props.initialValue
};
this.changeColor = this.changeColor.bind(this);
this.submitColor = this.submitColor.bind(this);
}
componentDidUpdate(prevProps) {
const { props } = this;
if (prevProps.initialValue !== props.initialValue) {
this.setState({ color: props.initialValue });
}
}
changeColor(event) {
this.setState({ color: event.target.value });
}
submitColor() {
const { props, state } = this;
let color = state.color;
if (color.startsWith('#')) {
color = state.color.substr(1);
}
const hexExp = /([0-9A-Fa-f]{6})/;
const hexTest = hexExp.exec(color);
if (!props.isDisabled && hexTest && (state.color !== props.initialValue)) {
props.setValue(color);
}
}
render() {
const { props, state } = this;
return (
<div className={'cf w-100 mb3 ' + ((props.isDisabled) ? 'o-30' : '')}>
<p className="f8 lh-copy">Change color</p>
<p className="f9 gray2 db mb4">Give this chat a color when viewing group channels</p>
<div className="relative w-100 flex"
style={{ maxWidth: '10rem' }}
>
<div className="absolute"
style={{
height: 16,
width: 16,
backgroundColor: state.color,
top: 13,
left: 11
}} />
<input
className={'pl7 f8 ba b--gray3 b--gray2-d bg-gray0-d white-d ' +
'focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3'}
value={state.color}
disabled={props.isDisabled}
onChange={this.changeColor}
onBlur={this.submitColor} />
</div>
</div>
);
}
}

View File

@ -0,0 +1,52 @@
import React, { Component } from 'react';
export class MetadataInput extends Component {
constructor(props) {
super(props);
this.state = {
value: props.initialValue
};
}
componentDidUpdate(prevProps) {
const { props } = this;
if (prevProps.initialValue !== props.initialValue) {
this.setState({ value: props.initialValue });
}
}
render() {
const {
title,
description,
isDisabled,
setValue
} = this.props;
return (
<div className={'w-100 mb3 fl ' + ((isDisabled) ? 'o-30' : '')}>
<p className="f8 lh-copy">{title}</p>
<p className="f9 gray2 db mb4">{description}</p>
<div className="relative w-100 flex" style={{ maxWidth: '29rem' }}>
<input
className={'f8 ba b--gray3 b--gray2-d bg-gray0-d white-d ' +
'focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3'}
type="text"
value={this.state.value}
disabled={isDisabled}
onChange={(e) => {
this.setState({ value: e.target.value });
}}
onBlur={() => {
if (!isDisabled) {
setValue(this.state.value || '');
}
}}
/>
</div>
</div>
);
}
}

View File

@ -0,0 +1,90 @@
import React, { Component } from 'react';
import { MetadataColor } from './metadata-color';
import { MetadataInput } from './metadata-input';
import { uxToHex } from '../../../../lib/util';
export const MetadataSettings = (props) => {
const {
isOwner,
association,
changeLoading,
api,
station
} = props;
const title =
(props.association && 'metadata' in props.association) ?
association.metadata.title : '';
const description =
(props.association && 'metadata' in props.association) ?
association.metadata.description : '';
const color =
(props.association && 'metadata' in props.association) ?
`#${uxToHex(props.association.metadata.color)}` : '';
return (
<div className="cf mt6">
<MetadataInput
title='Rename'
description='Change the name of this chat'
isDisabled={!isOwner}
initialValue={title}
setValue={(val) => {
changeLoading(false, true, 'Editing chat...', () => {
api.metadata.metadataAdd(
'chat',
association['app-path'],
association['group-path'],
val,
association.metadata.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
changeLoading(false, false, '', () => {});
});
});
}} />
<MetadataInput
title='Change description'
description='Change the description of this chat'
isDisabled={!isOwner}
initialValue={description}
setValue={(val) => {
changeLoading(false, true, 'Editing chat...', () => {
api.metadata.metadataAdd(
'chat',
association['app-path'],
association['group-path'],
association.metadata.title,
val,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
changeLoading(false, false, '', () => {});
});
});
}} />
<MetadataColor
initialValue={color}
isDisabled={!isOwner}
setValue={(val) => {
changeLoading(false, true, 'Editing chat...', () => {
props.api.metadata.metadataAdd(
'chat',
association['app-path'],
association['group-path'],
association.metadata.title,
association.metadata.description,
association.metadata['date-created'],
val
).then(() => {
changeLoading(false, false, '', () => {});
});
});
}} />
</div>
);
};

View File

@ -46,7 +46,7 @@ export class ProfileOverlay extends Component {
if (!(top || bottom)) {
bottom = `-${Math.round(OVERLAY_HEIGHT / 2)}px`;
}
const containerStyle = { top, bottom, left: '100%' };
const containerStyle = { top, bottom, left: '100%', maxWidth: '160px' };
const isOwn = window.ship === ship;
@ -79,7 +79,7 @@ export class ProfileOverlay extends Component {
</div>
<div className="pv3 pl3 pr2">
{contact && contact.nickname && (
<div className="b white-d">{contact.nickname}</div>
<div className="b white-d truncate">{contact.nickname}</div>
)}
<div className="mono gray2">{cite(`~${ship}`)}</div>
{!isOwn && (

View File

@ -9,19 +9,24 @@ export class ResubscribeElement extends Component {
}
render() {
return (
<div className="db pa3 ma3 ba b--yellow2 bg-yellow0">
<p className="lh-copy db">
Your ship has been disconnected from the chat's host.
This may be due to a bad connection, going offline, lack of permission,
or an over-the-air update.
</p>
<a onClick={this.onClickResubscribe.bind(this)}
className="db underline black pointer mt3"
>
Reconnect to this chat
</a>
</div>
);
const { props } = this;
if (props.isChatUnsynced) {
return (
<div className="db pa3 ma3 ba b--yellow2 bg-yellow0">
<p className="lh-copy db">
Your ship has been disconnected from the chat's host.
This may be due to a bad connection, going offline, lack of permission,
or an over-the-air update.
</p>
<a onClick={this.onClickResubscribe.bind(this)}
className="db underline black pointer mt3"
>
Reconnect to this chat
</a>
</div>
);
} else {
return null;
}
}
}

View File

@ -1,37 +1,41 @@
import React, { Component } from 'react';
import moment from 'moment';
export class UnreadNotice extends Component {
render() {
const { unread, unreadMsg, onRead } = this.props;
export const UnreadNotice = (props) => {
const { unreadCount, unreadMsg, dismissUnread } = props;
let datestamp = moment.unix(unreadMsg.when / 1000).format('YYYY.M.D');
const timestamp = moment.unix(unreadMsg.when / 1000).format('HH:mm');
if (!unreadMsg || (unreadCount === 0)) {
return null;
}
if (datestamp === moment().format('YYYY.M.D')) {
datestamp = null;
}
let datestamp = moment.unix(unreadMsg.when / 1000).format('YYYY.M.D');
const timestamp = moment.unix(unreadMsg.when / 1000).format('HH:mm');
return (
<div
style={{ left: '0px' }}
className="pa4 w-100 absolute z-1 unread-notice"
>
<div className="ba b--green2 green2 bg-white bg-gray0-d flex items-center pa2 f9 justify-between br1">
<p className="lh-copy db">
{unread} new messages since{' '}
{datestamp && (
<>
<span className="green3">~{datestamp}</span> at{' '}
</>
)}
<span className="green3">{timestamp}</span>
</p>
<div onClick={onRead} className="ml4 inter b--green2 pointer tr lh-copy">
Mark as Read
</div>
if (datestamp === moment().format('YYYY.M.D')) {
datestamp = null;
}
return (
<div style={{ left: '0px' }}
className="pa4 w-100 absolute z-1 unread-notice">
<div className={
"ba b--green2 green2 bg-white bg-gray0-d flex items-center " +
"pa2 f9 justify-between br1"
}>
<p className="lh-copy db">
{unreadCount} new messages since{' '}
{datestamp && (
<>
<span className="green3">~{datestamp}</span> at{' '}
</>
)}
<span className="green3">{timestamp}</span>
</p>
<div onClick={dismissUnread}
className="ml4 inter b--green2 pointer tr lh-copy">
Mark as Read
</div>
</div>
);
}
</div>
);
}

View File

@ -1,94 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { deSig } from '../../../lib/util';
import { ChatTabBar } from './lib/chat-tabbar';
import { MemberElement } from './lib/member-element';
import { InviteElement } from './lib/invite-element';
import { SidebarSwitcher } from '../../../components/SidebarSwitch';
import { GroupView } from '../../../components/Group';
import { PatpNoSig } from '../../../types/noun';
export class MemberScreen extends Component {
constructor(props) {
super(props);
this.inviteShips = this.inviteShips.bind(this);
}
inviteShips(ships) {
const { props } = this;
return props.api.chat.invite(props.station, ships.map(s => `~${s}`));
}
render() {
const { props } = this;
const isinPopout = this.props.popout ? 'popout/' : '';
let title = props.station.substr(1);
if (props.association && 'metadata' in props.association) {
title =
props.association.metadata.title !== ''
? props.association.metadata.title
: props.station.substr(1);
}
return (
<div className='h-100 w-100 overflow-x-hidden flex flex-column white-d'>
<div
className='w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8'
style={{ height: '1rem' }}
>
<Link to='/~chat/'>{'⟵ All Chats'}</Link>
</div>
<div
className={`pl4 pt2 bb b--gray4 b--gray1-d bg-gray0-d flex relative
overflow-x-scroll overflow-x-auto-l overflow-x-auto-xl flex-shrink-0`}
style={{ height: 48 }}
>
<SidebarSwitcher
sidebarShown={this.props.sidebarShown}
popout={this.props.popout}
api={this.props.api}
/>
<Link
to={'/~chat/' + isinPopout + 'room' + props.station}
className='pt2 white-d'
>
<h2
className={
'dib f9 fw4 lh-solid v-top ' +
(title === props.station.substr(1) ? 'mono' : '')
}
style={{ width: 'max-content' }}
>
{title}
</h2>
</Link>
<ChatTabBar
{...props}
station={props.station}
numPeers={5}
isOwner={deSig(props.match.params.ship) === window.ship}
popout={this.props.popout}
api={props.api}
/>
</div>
<div className='w-100 pl3 mt0 mt4-m mt4-l mt4-xl cf pr6'>
{ props.association['group-path'] && (
<GroupView
permissions
group={props.group}
resourcePath={props.association['group-path'] || ''}
associations={props.associations}
groups={props.groups}
inviteShips={this.inviteShips}
contacts={props.contacts}
/> )}
</div>
</div>
);
}
}

View File

@ -1,12 +1,15 @@
import React, { Component } from 'react';
import { deSig, uxToHex, writeText } from '../../../lib/util';
import React, { Component, Fragment } from 'react';
import { deSig } from '../../../lib/util';
import { Link } from 'react-router-dom';
import { ChatHeader } from './lib/chat-header';
import { MetadataSettings } from './lib/metadata-settings';
import { DeleteButton } from './lib/delete-button';
import { GroupifyButton } from './lib/groupify-button';
import { Spinner } from '../../../components/Spinner';
import { ChatTabBar } from './lib/chat-tabbar';
import { InviteSearch } from '../../../components/InviteSearch';
import SidebarSwitcher from '../../../components/SidebarSwitch';
import Toggle from '../../../components/toggle';
export class SettingsScreen extends Component {
constructor(props) {
@ -14,444 +17,127 @@ export class SettingsScreen extends Component {
this.state = {
isLoading: false,
title: '',
description: '',
color: '',
// groupify settings
targetGroup: null,
inclusive: false,
awaiting: false,
type: 'Editing chat...'
};
this.renderDelete = this.renderDelete.bind(this);
this.changeTargetGroup = this.changeTargetGroup.bind(this);
this.changeInclusive = this.changeInclusive.bind(this);
this.changeTitle = this.changeTitle.bind(this);
this.changeDescription = this.changeDescription.bind(this);
this.changeColor = this.changeColor.bind(this);
this.submitColor = this.submitColor.bind(this);
this.changeLoading = this.changeLoading.bind(this);
}
componentDidMount() {
const { props } = this;
if (props.association && 'metadata' in props.association) {
this.setState({
title: props.association.metadata.title,
description: props.association.metadata.description,
color: `#${uxToHex(props.association.metadata.color)}`
});
if (this.state.isLoading && (this.props.station in this.props.inbox)) {
this.setState({ isLoading: false });
}
}
componentDidUpdate(prevProps) {
const { props, state } = this;
if (Boolean(state.isLoading) && !(props.station in props.inbox)) {
if (state.isLoading && !(props.station in props.inbox)) {
this.setState({
isLoading: false
}, () => {
props.history.push('/~chat');
});
}
if ((state.title === '') && (prevProps !== props)) {
if (props.association && 'metadata' in props.association)
this.setState({
title: props.association.metadata.title,
description: props.association.metadata.description,
color: `#${uxToHex(props.association.metadata.color)}`
});
} else if (state.isLoading && (props.station in props.inbox)) {
this.setState({ isLoading: false });
}
}
changeTargetGroup(target) {
if (target.groups.length === 1) {
this.setState({ targetGroup: target.groups[0] });
} else {
this.setState({ targetGroup: null });
}
}
changeInclusive(event) {
this.setState({ inclusive: Boolean(event.target.checked) });
}
changeTitle() {
this.setState({ title: event.target.value });
}
changeDescription() {
this.setState({ description: event.target.value });
}
changeColor() {
this.setState({ color: event.target.value });
}
submitColor() {
const { props, state } = this;
let color = state.color;
if (color.startsWith('#')) {
color = state.color.substr(1);
}
const hexExp = /([0-9A-Fa-f]{6})/;
const hexTest = hexExp.exec(color);
let currentColor = '000000';
if (props.association && 'metadata' in props.association) {
currentColor = uxToHex(props.association.metadata.color);
}
if (hexTest && (hexTest[1] !== currentColor)) {
const chatOwner = (deSig(props.match.params.ship) === window.ship);
const association =
(props.association) && ('metadata' in props.association)
? props.association : {};
if (chatOwner) {
this.setState({ awaiting: true, type: 'Editing chat...' }, (() => {
props.api.metadata.metadataAdd(
'chat',
association['app-path'],
association['group-path'],
association.metadata.title,
association.metadata.description,
association.metadata['date-created'],
color
).then(() => {
this.setState({ awaiting: false });
});
}));
}
}
}
deleteChat() {
const { props } = this;
changeLoading(isLoading, awaiting, type, closure) {
this.setState({
isLoading: true,
awaiting: true,
type: (deSig(props.match.params.ship) === window.ship)
? 'Deleting chat...'
: 'Leaving chat...'
}, (() => {
props.api.chat.delete(props.station);
}));
isLoading,
awaiting,
type
}, closure);
}
groupifyChat() {
const { props, state } = this;
this.setState({
isLoading: true,
awaiting: true,
type: 'Converting chat...'
}, (() => {
props.api.chat.groupify(
props.station, state.targetGroup, state.inclusive
).then(() => this.setState({ awaiting: false }));
}));
}
renderDelete() {
const { props } = this;
const chatOwner = (deSig(props.match.params.ship) === window.ship);
const deleteButtonClasses = (chatOwner) ? 'b--red2 red2 pointer bg-gray0-d' : 'b--gray3 gray3 bg-gray0-d c-default';
const leaveButtonClasses = (!chatOwner) ? 'pointer' : 'c-default';
renderLoading() {
return (
<div>
<div className={'w-100 fl mt3 ' + ((chatOwner) ? 'o-30' : '')}>
<p className="f8 mt3 lh-copy db">Leave Chat</p>
<p className="f9 gray2 db mb4">Remove this chat from your chat list. You will need to request for access again.</p>
<a onClick={(!chatOwner) ? this.deleteChat.bind(this) : null}
className={'dib f9 black gray4-d bg-gray0-d ba pa2 b--black b--gray1-d ' + leaveButtonClasses}
>Leave this chat</a>
</div>
<div className={'w-100 fl mt3 ' + ((!chatOwner) ? 'o-30' : '')}>
<p className="f8 mt3 lh-copy db">Delete Chat</p>
<p className="f9 gray2 db mb4">Permanently delete this chat. All current members will no longer see this chat.</p>
<a onClick={(chatOwner) ? this.deleteChat.bind(this) : null}
className={'dib f9 ba pa2 ' + deleteButtonClasses}
>Delete this chat</a>
</div>
</div>
<Spinner
awaiting={this.state.awaiting}
classes="absolute right-2 bottom-2 ba pa2 b--gray1-d"
text={this.state.type}
/>
);
}
renderGroupify() {
const { props, state } = this;
renderNormal() {
const { state } = this;
const {
associations,
association,
contacts,
groups,
api,
station,
match
} = this.props;
const isOwner = deSig(match.params.ship) === window.ship;
const chatOwner = (deSig(props.match.params.ship) === window.ship);
const groupPath = props.association['group-path'];
const ownedUnmanagedVillage =
chatOwner &&
!props.contacts[groupPath];
if (!ownedUnmanagedVillage) {
return null;
} else {
let inclusiveToggle = <div />;
if (state.targetGroup) {
inclusiveToggle = (
<div className="mt4">
<Toggle
boolean={state.inclusive}
change={this.changeInclusive}
/>
<span className="dib f9 white-d inter ml3">
Add all members to group
</span>
<p className="f9 gray2 pt1" style={{ paddingLeft: 40 }}>
Add chat members to the group if they aren't in it yet
</p>
</div>
);
}
return (
<div>
<div className={'w-100 fl mt3'} style={{ maxWidth: '29rem' }}>
<p className="f8 mt3 lh-copy db">Convert Chat</p>
<p className="f9 gray2 db mb4">
Convert this chat into a group with associated chat, or select a
group to add this chat to.
</p>
<InviteSearch
groups={props.groups}
contacts={props.contacts}
associations={props.associations}
groupResults={true}
shipResults={false}
invites={{
groups: state.targetGroup ? [state.targetGroup] : [],
ships: []
}}
setInvite={this.changeTargetGroup}
/>
{inclusiveToggle}
<a onClick={this.groupifyChat.bind(this)}
className={'dib f9 black gray4-d bg-gray0-d ba pa2 mt4 b--black b--gray1-d pointer'}
>
Convert to group
</a>
</div>
</div>
);
}
}
renderMetadataSettings() {
const { props, state } = this;
const chatOwner = (deSig(props.match.params.ship) === window.ship);
const association = (props.association) && ('metadata' in props.association)
? props.association : {};
return(
<div>
<div className={'w-100 pb6 fl mt3 ' + ((chatOwner) ? '' : 'o-30')}>
<p className="f8 mt3 lh-copy">Rename</p>
<p className="f9 gray2 db mb4">Change the name of this chat</p>
<div className="relative w-100 flex"
style={{ maxWidth: '29rem' }}
>
<input
className={'f8 ba b--gray3 b--gray2-d bg-gray0-d white-d ' +
'focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3'}
value={state.title}
disabled={!chatOwner}
onChange={this.changeTitle}
onBlur={() => {
if (chatOwner) {
this.setState({ awaiting: true, type: 'Editing chat...' }, (() => {
props.api.metadata.metadataAdd(
'chat',
association['app-path'],
association['group-path'],
state.title,
association.metadata.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
this.setState({ awaiting: false });
});
}));
}
}}
/>
</div>
<p className="f8 mt3 lh-copy">Change description</p>
<p className="f9 gray2 db mb4">Change the description of this chat</p>
<div className="relative w-100 flex"
style={{ maxWidth: '29rem' }}
>
<input
className={'f8 ba b--gray3 b--gray2-d bg-gray0-d white-d ' +
'focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3'}
value={state.description}
disabled={!chatOwner}
onChange={this.changeDescription}
onBlur={() => {
if (chatOwner) {
this.setState({ awaiting: true, type: 'Editing chat...' }, (() => {
props.api.metadata.metadataAdd(
'chat',
association['app-path'],
association['group-path'],
association.metadata.title,
state.description,
association.metadata['date-created'],
uxToHex(association.metadata.color)
).then(() => {
this.setState({ awaiting: false });
});
}));
}
}}
/>
</div>
<p className="f8 mt3 lh-copy">Change color</p>
<p className="f9 gray2 db mb4">Give this chat a color when viewing group channels</p>
<div className="relative w-100 flex"
style={{ maxWidth: '10rem' }}
>
<div className="absolute"
style={{
height: 16,
width: 16,
backgroundColor: state.color,
top: 13,
left: 11
}}
/>
<input
className={'pl7 f8 ba b--gray3 b--gray2-d bg-gray0-d white-d ' +
'focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3'}
value={state.color}
disabled={!chatOwner}
onChange={this.changeColor}
onBlur={this.submitColor}
/>
</div>
</div>
</div>
return (
<Fragment>
<h2 className="f8 pb2">Chat Settings</h2>
<GroupifyButton
isOwner={isOwner}
association={association}
associations={associations}
contacts={contacts}
groups={groups}
api={api}
changeLoading={this.changeLoading} />
<DeleteButton
isOwner={isOwner}
changeLoading={this.changeLoading}
station={station}
api={api} />
<MetadataSettings
isOwner={isOwner}
changeLoading={this.changeLoading}
api={api}
association={association}
station={station} />
<Spinner
awaiting={this.state.awaiting}
classes="absolute right-2 bottom-2 ba pa2 b--gray1-d"
text={this.state.type}
/>
</Fragment>
);
}
render() {
const { props, state } = this;
const isinPopout = this.props.popout ? 'popout/' : '';
const { state } = this;
const {
api,
group,
association,
station,
popout,
sidebarShown,
match,
location
} = this.props;
const permission = Array.from(props.group.members.values());
if (state.isLoading) {
let title = props.station.substr(1);
if ((props.association) && ('metadata' in props.association)) {
title = (props.association.metadata.title !== '')
? props.association.metadata.title : props.station.substr(1);
}
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column white-d">
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: '1rem' }}
>
<Link to="/~chat/">{'⟵ All Chats'}</Link>
</div>
<div
className="pl4 pt2 bb b--gray4 b--gray2-d bg-gray0-d flex relative overflow-x-scroll overflow-x-auto-l overflow-x-auto-xl flex-shrink-0"
style={{ height: 48 }}
>
<SidebarSwitcher
sidebarShown={this.props.sidebarShown}
popout={this.props.popout}
/>
<Link to={'/~chat/' + isinPopout + 'room' + props.station}
className="pt2 white-d"
>
<h2
className={'dib f9 fw4 lh-solid v-top ' +
((title === props.station.substr(1)) ? 'mono' : '')}
style={{ width: 'max-content' }}
>
{title}
</h2>
</Link>
<ChatTabBar
{...props}
station={props.station}
numPeers={permission.length}
host={props.match.params.ship}
api={props.api}
/>
</div>
<div className="w-100 pl3 mt4 cf">
<Spinner awaiting={state.awaiting}
classes="absolute right-2 bottom-2 ba pa2 b--gray1-d"
text={state.type} />
</div>
</div>
);
}
let title = props.station.substr(1);
if ((props.association) && ('metadata' in props.association)) {
title = (props.association.metadata.title !== '')
? props.association.metadata.title : props.station.substr(1);
}
const isInPopout = popout ? "popout/" : "";
const title =
( association &&
('metadata' in association) &&
(association.metadata.title !== '')
) ? association.metadata.title : station.substr(1);
return (
<div className="h-100 w-100 overflow-x-hidden flex flex-column white-d">
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: '1rem' }}
>
<Link to="/~chat/">{'⟵ All Chats'}</Link>
</div>
<div
className="pl4 pt2 bb b--gray4 b--gray1-d flex relative overflow-x-scroll overflow-x-auto-l overflow-x-auto-xl flex-shrink-0"
style={{ height: 48 }}
>
<SidebarSwitcher
sidebarShown={this.props.sidebarShown}
popout={this.props.popout}
api={this.props.api}
/>
<Link to={'/~chat/' + isinPopout + 'room' + props.station}
className="pt2"
>
<h2
className={'dib f9 fw4 lh-solid v-top ' +
((title === props.station.substr(1)) ? 'mono' : '')}
style={{ width: 'max-content' }}
>
{title}
</h2>
</Link>
<ChatTabBar
{...props}
station={props.station}
numPeers={permission.length}
isOwner={deSig(props.match.params.ship) === window.ship}
popout={this.props.popout}
/>
</div>
<ChatHeader
match={match}
location={location}
api={api}
group={group}
association={association}
station={station}
sidebarShown={sidebarShown}
popout={popout} />
<div className="w-100 pl3 mt4 cf">
<h2 className="f8 pb2">Chat Settings</h2>
{this.renderGroupify()}
{this.renderDelete()}
{this.renderMetadataSettings()}
<Spinner awaiting={state.awaiting}
classes="absolute right-2 bottom-2 ba pa2 b--gray1-d"
text={state.type} />
{(state.isLoading) ? this.renderLoading() : this.renderNormal() }
</div>
</div>
);

View File

@ -13,8 +13,6 @@ export class Sidebar extends Component {
render() {
const { props } = this;
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
const contactAssoc =
(props.associations && 'contacts' in props.associations)
? alphabetiseAssociations(props.associations.contacts) : {};
@ -61,15 +59,6 @@ export class Sidebar extends Component {
const groupedItems = Object.keys(contactAssoc)
.filter(each => (groupedChannels[each] || []).length !== 0)
.filter((each) => {
if (selectedGroups.length === 0) {
return true;
}
const selectedPaths = selectedGroups.map((e) => {
return e[0];
});
return selectedPaths.includes(each);
})
.map((each, i) => {
const channels = groupedChannels[each] || [];
return(

View File

@ -48,7 +48,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
const invites =
(Boolean(props.invites) && '/contacts' in props.invites) ?
props.invites['/contacts'] : {};
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
const s3 = props.s3 ? props.s3 : {};
const groups = props.groups || {};
const associations = props.associations || {};
@ -62,7 +61,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
return (
<Skeleton
activeDrawer="groups"
selectedGroups={selectedGroups}
history={props.history}
api={api}
contacts={contacts}
@ -86,7 +84,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
return (
<Skeleton
history={props.history}
selectedGroups={selectedGroups}
api={api}
contacts={contacts}
groups={groups}
@ -111,7 +108,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
return (
<Skeleton
history={props.history}
selectedGroups={selectedGroups}
api={api}
contacts={contacts}
groups={groups}
@ -150,7 +146,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
return (
<Skeleton
history={props.history}
selectedGroups={selectedGroups}
api={api}
contacts={contacts}
invites={invites}
@ -198,7 +193,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
return (
<Skeleton
history={props.history}
selectedGroups={selectedGroups}
api={api}
contacts={contacts}
groups={groups}
@ -248,7 +242,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
<Skeleton
history={props.history}
api={api}
selectedGroups={selectedGroups}
contacts={contacts}
groups={groups}
invites={invites}
@ -305,7 +298,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
<Skeleton
history={props.history}
api={api}
selectedGroups={selectedGroups}
contacts={contacts}
groups={groups}
invites={invites}
@ -345,7 +337,6 @@ export default class GroupsApp extends Component<GroupsAppProps, {}> {
<Skeleton
history={props.history}
api={api}
selectedGroups={selectedGroups}
contacts={contacts}
groups={groups}
invites={invites}

View File

@ -103,6 +103,7 @@ export class JoinScreen extends Component {
spellCheck="false"
rows={1}
cols={32}
autoFocus={true}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
@ -110,7 +111,7 @@ export class JoinScreen extends Component {
}
}}
style={{
resize: 'none',
resize: 'none'
}}
onChange={this.groupChange}
value={this.state.group}

View File

@ -1,5 +1,7 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { FixedSizeList as List } from 'react-window';
import { ContactItem } from './contact-item';
import { ShareSheet } from './share-sheet';
import { Sigil } from '../../../../lib/sigil';
@ -23,6 +25,7 @@ interface ContactSidebarProps {
}
interface ContactSidebarState {
awaiting: boolean;
memberboxHeight: number;
}
@ -31,9 +34,20 @@ export class ContactSidebar extends Component<ContactSidebarProps, ContactSideba
constructor(props) {
super(props);
this.state = {
awaiting: false
awaiting: false,
memberboxHeight: 0
};
this.memberbox = this.memberbox.bind(this);
}
memberbox(element) {
if (element) {
this.setState({
memberboxHeight: element.getBoundingClientRect().height
})
}
}
render() {
const { props } = this;
@ -145,14 +159,14 @@ export class ContactSidebar extends Component<ContactSidebarProps, ContactSideba
const detailHref = `/~groups/detail${props.path}`;
return (
<div className={'bn br-m br-l br-xl b--gray4 b--gray1-d lh-copy h-100 ' +
<div ref={this.memberbox} className={'bn br-m br-l br-xl b--gray4 b--gray1-d lh-copy h-100 ' +
'flex-basis-100-s flex-basis-30-ns mw5-m mw5-l mw5-xl relative ' +
'overflow-hidden flex-shrink-0 ' + responsiveClasses}
>
<div className="pt3 pb5 pl3 f8 db dn-m dn-l dn-xl">
<Link to="/~groups/">{'⟵ All Groups'}</Link>
</div>
<div className="overflow-auto h-100">
<div className="overflow-auto h-100 flex flex-column">
<Link
to={'/~groups/add' + props.path}
className={((role === "admin" || role === "moderator")
@ -166,8 +180,20 @@ export class ContactSidebar extends Component<ContactSidebarProps, ContactSideba
>Channels</Link>
{shareSheet}
<h2 className="f9 pt4 pr4 pb2 pl4 gray2 c-default">Members</h2>
{contactItems}
{groupItems}
<List
height={this.state.memberboxHeight}
className="flex-auto"
itemCount={contactItems.length + groupItems.length}
itemSize={44}
width="100%"
>
{({ index, style }) => (<div style={style}>{
index <= (contactItems.length - 1) // If the index is within the length of contact items,
? contactItems[index] // show a contact item
: groupItems[index - contactItems.length] // Otherwise show a group item
}</div>)}
</List>
</div>
<Spinner awaiting={this.state.awaiting} text="Removing from group..." classes="pa2 ba absolute right-1 bottom-1 b--gray1-d" />
</div>

View File

@ -72,16 +72,6 @@ export class GroupSidebar extends Component {
(path in props.groups)
);
})
.filter((path) => {
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
if (selectedGroups.length === 0) {
return true;
}
const selectedPaths = selectedGroups.map(((e) => {
return e[0];
}));
return (selectedPaths.includes(path));
})
.sort((a, b) => {
let aName = a.substr(1);
let bName = b.substr(1);

View File

@ -17,7 +17,6 @@ export class Skeleton extends Component {
invites={props.invites}
activeDrawer={props.activeDrawer}
selected={props.selected}
selectedGroups={props.selectedGroups}
history={props.history}
api={props.api}
associations={props.associations}

View File

@ -1,6 +1,7 @@
import React from 'react';
import classnames from 'classnames';
import { Link } from 'react-router-dom';
import defaultApps from '../../../../lib/default-apps';
import Tile from './tile';
@ -29,7 +30,9 @@ export default class BasicTile extends React.PureComponent {
</span>
);
const routeList = ['/~chat', '/~publish', '/~link', '/~groups', '/~dojo'];
const routeList = defaultApps.map((e) => {
return `/~${e}`;
});
const tile = ( routeList.indexOf(props.linkedUrl) !== -1 ) ? (
<Link className="w-100 h-100 db pa2 no-underline" to={props.linkedUrl}>

View File

@ -41,7 +41,7 @@ export class LinksApp extends Component {
render() {
const { props } = this;
const contacts = props.contacts ? props.contacts : {};
const contacts = props.contacts ? props.contacts : {};
const groups = props.groups ? props.groups : {};
@ -51,18 +51,9 @@ export class LinksApp extends Component {
const seen = props.linksSeen ? props.linksSeen : {};
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
const selGroupPaths = selectedGroups.map(g => g[0]);
const totalUnseen = _.reduce(
links,
(acc, collection, path) => {
if(selGroupPaths.length > 0
&& !selGroupPaths.includes(associations.link?.[path]?.['group-path'])) {
return acc;
}
return acc + collection.unseenCount;
},
(acc, collection) => acc + collection.unseenCount,
0
);
@ -91,7 +82,6 @@ export class LinksApp extends Component {
groups={groups}
rightPanelHide={true}
sidebarShown={sidebarShown}
selectedGroups={selectedGroups}
links={links}
listening={listening}
api={api}
@ -109,7 +99,6 @@ export class LinksApp extends Component {
invites={invites}
groups={groups}
sidebarShown={sidebarShown}
selectedGroups={selectedGroups}
links={links}
listening={listening}
api={api}
@ -157,7 +146,6 @@ export class LinksApp extends Component {
groups={groups}
selected={resourcePath}
sidebarShown={sidebarShown}
selectedGroups={selectedGroups}
links={links}
listening={listening}
api={api}
@ -198,7 +186,6 @@ export class LinksApp extends Component {
groups={groups}
selected={resourcePath}
sidebarShown={sidebarShown}
selectedGroups={selectedGroups}
popout={popout}
links={links}
listening={listening}
@ -253,7 +240,6 @@ export class LinksApp extends Component {
groups={groups}
selected={resourcePath}
sidebarShown={sidebarShown}
selectedGroups={selectedGroups}
sidebarHideMobile={true}
popout={popout}
links={links}
@ -311,7 +297,6 @@ export class LinksApp extends Component {
groups={groups}
selected={resourcePath}
sidebarShown={sidebarShown}
selectedGroups={selectedGroups}
sidebarHideMobile={true}
popout={popout}
links={links}

View File

@ -51,24 +51,14 @@ export class ChannelsSidebar extends Component {
}
});
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
let i = -1;
const groupedItems = Object.keys(associations)
.filter((each) => {
if (selectedGroups.length === 0) {
return true;
};
const selectedPaths = selectedGroups.map((e) => {
return e[0];
});
return selectedPaths.includes(each);
})
.map((each) => {
const channels = groupedChannels[each];
if (!channels || channels.length === 0)
return;
i++;
if ((selectedGroups.length === 0) && groupedChannels['/~/'] && groupedChannels['/~/'].length !== 0) {
if (groupedChannels['/~/'] && groupedChannels['/~/'].length !== 0) {
i++;
}
@ -84,7 +74,7 @@ export class ChannelsSidebar extends Component {
/>
);
});
if ((selectedGroups.length === 0) && groupedChannels['/~/'] && groupedChannels['/~/'].length !== 0) {
if (groupedChannels['/~/'] && groupedChannels['/~/'].length !== 0) {
groupedItems.unshift(
<GroupItem
key={'/~/'}

View File

@ -31,7 +31,6 @@ export class Skeleton extends Component {
invites={linkInvites}
groups={props.groups}
selected={props.selected}
selectedGroups={props.selectedGroups}
sidebarShown={props.sidebarShown}
links={props.links}
listening={props.listening}

View File

@ -42,7 +42,6 @@ export default class PublishApp extends React.Component {
const contacts = props.contacts ? props.contacts : {};
const associations = props.associations ? props.associations : { contacts: {} };
const selectedGroups = props.selectedGroups ? props.selectedGroups : [];
const notebooks = props.notebooks ? props.notebooks : {};
@ -50,12 +49,6 @@ export default class PublishApp extends React.Component {
.values()
.map(_.values)
.flatten() // flatten into array of notebooks
.filter((each) => {
return ((selectedGroups.map((e) => {
return e[0];
}).includes(each?.['writers-group-path'])) ||
(selectedGroups.length === 0));
})
.map('num-unread')
.reduce((acc, count) => acc + count, 0)
.value();
@ -80,7 +73,6 @@ export default class PublishApp extends React.Component {
invites={invites}
notebooks={notebooks}
associations={associations}
selectedGroups={selectedGroups}
contacts={contacts}
api={api}
>
@ -111,7 +103,6 @@ export default class PublishApp extends React.Component {
invites={invites}
notebooks={notebooks}
associations={associations}
selectedGroups={selectedGroups}
contacts={contacts}
api={api}
>
@ -142,7 +133,6 @@ export default class PublishApp extends React.Component {
invites={invites}
notebooks={notebooks}
associations={associations}
selectedGroups={selectedGroups}
contacts={contacts}
api={api}
>
@ -188,7 +178,6 @@ export default class PublishApp extends React.Component {
invites={invites}
notebooks={notebooks}
associations={associations}
selectedGroups={selectedGroups}
contacts={contacts}
path={path}
api={api}
@ -215,7 +204,6 @@ export default class PublishApp extends React.Component {
notebooks={notebooks}
associations={associations}
contacts={contacts}
selectedGroups={selectedGroups}
path={path}
api={api}
>
@ -265,7 +253,6 @@ export default class PublishApp extends React.Component {
sidebarShown={sidebarShown}
invites={invites}
notebooks={notebooks}
selectedGroups={selectedGroups}
associations={associations}
contacts={contacts}
path={path}
@ -293,7 +280,6 @@ export default class PublishApp extends React.Component {
invites={invites}
notebooks={notebooks}
associations={associations}
selectedGroups={selectedGroups}
contacts={contacts}
path={path}
api={api}

View File

@ -25,6 +25,29 @@ export class NewPost extends Component {
postSubmit() {
const { state } = this;
// perf testing:
/*let closure = () => {
let x = 0;
for (var i = 0; i < 5; i++) {
x++;
let rand = Math.floor(Math.random() * 1000);
const newNote = {
'new-note': {
who: this.props.ship.slice(1),
book: this.props.book,
note: stringToSymbol(this.state.title + '-' + Date.now() + '-' + rand),
title: 'asdf-' + rand + '-' + Date.now(),
body: 'asdf-' + Date.now()
}
};
this.props.api.publishAction(newNote);
}
setTimeout(closure, 3000);
};
setTimeout(closure, 2000);*/
if (state.submit && !state.disabled) {
const newNote = {
'new-note': {

View File

@ -165,8 +165,7 @@ export class NewScreen extends Component {
</p>
<Link className="green2 absolute right-0 bottom-0 f9" to="/~groups/new">Create Group</Link>
<p className="f9 gray2 db mv1 pb4">
Selected ships will be invited to read your notebook. Selected
groups will be invited to read and write notes.
Selected ships or group will be invited to read your notebook. Additional writers can be added from the &apos;subscribers&apos; panel.
</p>
</div>
<InviteSearch

View File

@ -12,7 +12,8 @@ export class Note extends Component {
constructor(props) {
super(props);
this.state = {
deleting: false
deleting: false,
sentRead: false
};
moment.updateLocale('en', {
relativeTime: {
@ -46,16 +47,19 @@ export class Note extends Component {
}
componentDidUpdate(prevProps) {
const { props } = this;
const { props, state } = this;
if ((prevProps && prevProps.api !== props.api) || props.api) {
if (!(props.notebooks[props.ship]?.[props.book]?.notes?.[props.note]?.file)) {
props.api.publish.fetchNote(props.ship, props.book, props.note);
}
if (prevProps) {
if ((prevProps.book !== props.book) ||
(prevProps.note !== props.note) ||
(prevProps.ship !== props.ship)) {
if (prevProps && prevProps.note !== props.note) {
this.setState({ sentRead: false });
}
if (!state.sentRead &&
props.notebooks?.[props.ship]?.[props.book]?.notes?.[props.note] &&
!props.notebooks[props.ship][props.book].notes[props.note].read) {
const readAction = {
read: {
who: props.ship.slice(1),
@ -63,9 +67,10 @@ export class Note extends Component {
note: props.note
}
};
props.api.publish.publishAction(readAction);
this.setState({ sentRead: true }, () => {
props.api.publish.publishAction(readAction);
});
}
}
}
}

View File

@ -84,7 +84,7 @@ export class NotebookPosts extends Component {
' gray2 mr3'}
title={note.author}
>{name}</div>
<div className="gray2 mr3">{date}</div>
<div className={((note.read) ? "gray2 " : "green2 ") + "mr3"}>{date}</div>
<div className="gray2">{comment}</div>
</div>
</div>

View File

@ -179,7 +179,7 @@ export class Notebook extends Component {
const group = props.groups[notebook?.['writers-group-path']];
const role = roleForShip(group, window.ship);
const role = group ? roleForShip(group, window.ship) : undefined;
const subsComponent = (this.props.ship.slice(1) === window.ship) || (role === 'admin')
? (<Link to={subs} className={tabStyles.subscribers}>

View File

@ -64,23 +64,12 @@ export class Sidebar extends Component {
}
});
const selectedGroups = props.selectedGroups ? props.selectedGroups: [];
const groupedItems = Object.keys(associations)
.filter((each) => {
if (selectedGroups.length === 0) {
return true;
}
const selectedPaths = selectedGroups.map((e) => {
return e[0];
});
return (selectedPaths.includes(each));
})
.map((each, i) => {
const books = groupedNotebooks[each] || [];
if (books.length === 0)
return;
if ((selectedGroups.length === 0) &&
groupedNotebooks['/~/'] &&
if (groupedNotebooks['/~/'] &&
groupedNotebooks['/~/'].length !== 0) {
i = i + 1;
}
@ -95,8 +84,7 @@ export class Sidebar extends Component {
/>
);
});
if ((selectedGroups.length === 0) &&
groupedNotebooks['/~/'] &&
if (groupedNotebooks['/~/'] &&
groupedNotebooks['/~/'].length !== 0) {
groupedItems.unshift(
<GroupItem

View File

@ -30,7 +30,6 @@ export class Skeleton extends Component {
path={props.path}
invites={props.invites}
associations={props.associations}
selectedGroups={props.selectedGroups}
api={this.props.api}
/>
<div className={'h-100 w-100 relative white-d flex-auto ' + rightPanelHide} style={{

View File

@ -1,5 +1,7 @@
import React, { Component } from 'react';
import _, { capitalize } from 'lodash';
import { FixedSizeList as List } from 'react-window';
import { Dropdown } from '../apps/publish/components/lib/dropdown';
import { cite, deSig } from '../lib/util';
import { roleForShip, resourceFromPath } from '../lib/group';
@ -143,7 +145,7 @@ export class GroupView extends Component<
isAdmin(): boolean {
const us = `~${window.ship}`;
const role = roleForShip(this.props.group, us);
const role = roleForShip(this.props.group, window.ship);
const resource = resourceFromPath(this.props.resourcePath);
return resource.ship == us || role === 'admin';
}
@ -156,10 +158,15 @@ export class GroupView extends Component<
return options;
}
const role = roleForShip(group, ship);
const myRole = roleForShip(group, window.ship);
if (role === 'admin' || resource.ship === ship) {
return [];
}
if ('open' in group.policy) {
if (
'open' in group.policy // If blacklist, not whitelist
&& (this.isAdmin()) // And we can ban people (TODO: add || role === 'moderator')
&& ship !== window.ship // We can't ban ourselves
) {
options.unshift({ text: 'Ban', onSelect: () => this.banUser(ship) });
}
if (this.isAdmin() && !role) {
@ -199,52 +206,45 @@ export class GroupView extends Component<
});
}
renderMembers() {
memberElements() {
const { group, permissions } = this.props;
const { members } = group;
const isAdmin = this.isAdmin();
return (
<div className='flex flex-column'>
<div className='f9 gray2 mt6 mb3'>Members</div>
{Array.from(members).map((ship) => {
const role = roleForShip(group, deSig(ship));
const onRoleRemove =
role && isAdmin
? () => {
this.removeTag(ship, { tag: role });
}
: undefined;
const [present, missing] = this.getAppTags(ship);
const options = this.optionsForShip(ship, missing);
return Array.from(members).map((ship) => {
const role = roleForShip(group, deSig(ship));
const onRoleRemove =
role && isAdmin
? () => {
this.removeTag(ship, { tag: role });
}
: undefined;
const [present, missing] = this.getAppTags(ship);
const options = this.optionsForShip(ship, missing);
return (
<div key={ship} className='flex flex-column pv3'>
<GroupMember ship={ship} options={options}>
{((permissions && role) || present.length > 0) && (
<div className='flex mt1'>
{role && (
<Tag
onRemove={onRoleRemove}
description={capitalize(role)}
/>
)}
{present.map((tag, idx) => (
<Tag
key={idx}
onRemove={this.doIfAdmin(() =>
this.removeTag(ship, tag)
)}
description={tag.desc}
/>
))}
</div>
)}
</GroupMember>
return (
<GroupMember ship={ship} options={options}>
{((permissions && role) || present.length > 0) && (
<div className='flex mt1'>
{role && (
<Tag
onRemove={onRoleRemove}
description={capitalize(role)}
/>
)}
{present.map((tag, idx) => (
<Tag
key={idx}
onRemove={this.doIfAdmin(() =>
this.removeTag(ship, tag)
)}
description={tag.desc}
/>
))}
</div>
);
})}
</div>
);
)}
</GroupMember>
);
})
}
setInvites(invites: Invites) {
@ -321,6 +321,7 @@ export class GroupView extends Component<
render() {
const { group, resourcePath, className } = this.props;
const resource = resourceFromPath(resourcePath);
const memberElements = this.memberElements();
return (
<div className={className}>
@ -332,7 +333,17 @@ export class GroupView extends Component<
</div>
{'invite' in group.policy && this.renderInvites(group.policy)}
{'open' in group.policy && this.renderBanned(group.policy)}
{this.renderMembers()}
<div className='flex flex-column'>
<div className='f9 gray2 mt6 mb3'>Members</div>
<List
height={500}
itemCount={memberElements.length}
itemSize={44}
width="100%"
>
{({ index, style }) => <div key={index} style={style} className='flex flex-column pv3'>{memberElements[index]}</div>}
</List>
</div>
<Spinner
awaiting={this.state.awaiting}

View File

@ -1,249 +0,0 @@
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
export default class GroupFilter extends Component {
constructor(props) {
super(props);
this.state = {
open: false,
selected: [],
groups: [],
searchTerm: '',
results: []
};
this.toggleOpen = this.toggleOpen.bind(this);
this.handleClickOutside = this.handleClickOutside.bind(this);
this.groupIndex = this.groupIndex.bind(this);
this.search = this.search.bind(this);
this.addGroup = this.addGroup.bind(this);
this.deleteGroup = this.deleteGroup.bind(this);
}
componentDidMount() {
document.addEventListener('mousedown', this.handleClickOutside);
this.groupIndex();
const selected = localStorage.getItem('urbit-selectedGroups');
if (selected) {
this.setState({ selected: JSON.parse(selected) }, (() => {
this.props.api.local.setSelected(this.state.selected);
}));
}
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClickOutside);
}
componentDidUpdate(prevProps) {
if (prevProps !== this.props) {
this.groupIndex();
}
}
handleClickOutside(evt) {
if ((this.dropdown && !this.dropdown.contains(evt.target))
&& (this.toggleButton && !this.toggleButton.contains(evt.target))) {
this.setState({ open: false });
}
}
toggleOpen() {
this.setState({ open: !this.state.open });
}
groupIndex() {
const { props } = this;
let index = [];
const associations =
(props.associations && 'contacts' in props.associations) ?
props.associations.contacts : {};
index = Object.keys(associations).map((each) => {
const eachGroup = [];
eachGroup.push(each);
let name = each;
if (associations[each].metadata) {
name = (associations[each].metadata.title !== '')
? associations[each].metadata.title : name;
}
eachGroup.push(name);
return eachGroup;
});
this.setState({ groups: index });
}
search(evt) {
this.setState({ searchTerm: evt.target.value });
const term = evt.target.value.toLowerCase();
if (term.length < 3) {
return this.setState({ results: [] });
}
let groupMatches = [];
groupMatches = this.state.groups.filter((e) => {
return (e[0].includes(term) || e[1].includes(term));
});
this.setState({ results: groupMatches });
}
addGroup(group) {
const selected = this.state.selected;
if (!(group in selected)) {
selected.push(group);
}
this.setState({
searchTerm: '',
selected: selected,
results: []
}, (() => {
this.props.api.local.setSelected(this.state.selected);
localStorage.setItem('urbit-selectedGroups', JSON.stringify(this.state.selected));
}));
}
deleteGroup(group) {
let selected = this.state.selected;
selected = selected.filter((e) => {
return e !== group;
});
this.setState({ selected: selected }, (() => {
this.props.api.local.setSelected(this.state.selected);
localStorage.setItem('urbit-selectedGroups', JSON.stringify(this.state.selected));
}));
}
render() {
const { props, state } = this;
let currentGroup = 'All Groups';
if (state.selected.length > 0) {
const titles = state.selected.map((each) => {
return each[1];
});
currentGroup = titles.join(' + ');
}
const buttonOpened = (state.open)
? 'bg-gray5 bg-gray1-d white-d' : 'hover-bg-gray5 hover-bg-gray1-d white-d';
const dropdownClass = (state.open)
? 'absolute db z-2 bg-white bg-gray0-d white-d ba b--gray3 b--gray1-d'
: 'dn';
const inviteCount = (props.invites && Object.keys(props.invites).length > 0)
? <template className="dib fr">
<p className="dib bg-green2 bg-gray2-d white fw6 ph1 br1 v-mid" style={{ marginBottom: 2 }}>
{Object.keys(props.invites).length}
</p>
<span className="dib v-mid ml1">
<img
className="v-mid"
src="/~landscape/img/Chevron.png"
style={{ height: 16, width: 16, paddingBottom: 1 }}
/>
</span>
</template>
: <template className="dib fr">
<span className="dib v-top ml1">
<img className="v-mid"
src="/~landscape/img/Chevron.png"
style={{ height: 16, width: 16, paddingBottom: 1 }}
/>
</span>
</template>;
let selectedGroups = <div />;
let searchResults = <div />;
if (state.results.length > 0) {
const groupResults = state.results.map(((group) => {
return(
<li
key={group[0]}
className="tl list white-d f9 pv2 ph3 pointer hover-bg-gray4 hover-bg-gray1-d inter"
onClick={() => this.addGroup(group)}
>
<span className="mix-blend-diff white">{(group[1]) ? group[1] : group[0]}</span>
</li>
);
}));
searchResults = (
<div className={'tl absolute bg-white bg-gray0-d white-d pv3 z-1 w-100 ba b--gray4 b--white-d overflow-y-scroll'} style={{ maxWidth: '15.67rem', maxHeight: '8rem' }}>
<p className="f9 tl gray2 ph3 pb2">Groups</p>
{groupResults}
</div>
);
}
if (state.selected.length > 0) {
const allSelected = this.state.selected.map((each) => {
const name = each[1];
return(
<span
key={each[0]}
className={'f9 inter black pa2 bg-gray5 bg-gray1-d ' +
'ba b--gray4 b--gray2-d white-d dib mr2 mt2 c-default'}
>
{name}
<span
className="white-d ml3 mono pointer"
onClick={e => this.deleteGroup(each)}
>
x
</span>
</span>
);
});
selectedGroups = (
<div className={
'f9 gray2 bb bl br b--gray3 b--gray2-d bg-gray0-d ' +
'white-d pa3 db w-100 inter bg-gray5 lh-solid tl'
}
style={{ width: 251 }}
>
{allSelected}
</div>
);
}
return (
<div className="ml1 dib">
<div className={buttonOpened}
onClick={() => this.toggleOpen()}
ref={el => this.toggleButton = el}
>
<p className="inter dib f9 pointer pv1 ph2 mw5 truncate v-mid">{currentGroup}</p>
</div>
<div className={dropdownClass}
style={{ maxHeight: '24rem', width: 285 }}
ref={(el) => {
this.dropdown = el;
}}
>
<p className="tc bb b--gray3 b--gray1-d gray3 pv4 f9">Group Select and Filter</p>
<Link to="/~groups"
className="ma4 bg-gray5 bg-gray1-d f9 tl pa1 br1 db no-underline"
style={{ paddingLeft: '6.5px', paddingRight: '6.5px' }}
onClick={() => this.setState({ open: false })}
>
Manage all Groups
{inviteCount}
</Link>
<p className="pt4 gray3 f9 tl mh4">Filter Groups</p>
<div className="relative w-100 ph4 pt2 pb4">
<input className="ba b--gray3 white-d bg-gray0-d inter w-100 f9 pa2"
style={{ boxSizing: 'border-box' }}
placeholder="Group name..."
onChange={this.search}
value={state.searchTerm}
/>
{searchResults}
{selectedGroups}
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,261 @@
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import { Box, Row, Rule, Text } from '@tlon/indigo-react';
import index from '../lib/omnibox';
import Mousetrap from 'mousetrap';
import OmniboxInput from './OmniboxInput';
import OmniboxResult from './OmniboxResult';
import { cite } from '../lib/util';
export class Omnibox extends Component {
constructor(props) {
super(props);
this.state = {
index: new Map([]),
query: '',
results: this.initialResults(),
selected: ''
};
this.handleClickOutside = this.handleClickOutside.bind(this);
this.search = this.search.bind(this);
this.navigate = this.navigate.bind(this);
this.control - this.control.bind(this);
this.setPreviousSelected = this.setPreviousSelected.bind(this);
this.setNextSelected = this.setNextSelected.bind(this);
this.renderResults = this.renderResults.bind(this);
}
componentDidUpdate(prevProps) {
if (prevProps !== this.props) {
this.setState({ index: index(this.props.associations, this.props.apps.tiles) });
}
if (prevProps && this.props.show && prevProps.show !== this.props.show) {
Mousetrap.bind('escape', () => this.props.api.local.setOmnibox());
document.addEventListener('mousedown', this.handleClickOutside);
const touchstart = new Event('touchstart');
this.omniInput.input.dispatchEvent(touchstart);
this.omniInput.input.focus();
}
}
componentWillUpdate(prevProps) {
if (this.props.show && prevProps.show !== this.props.show) {
Mousetrap.unbind('escape');
document.removeEventListener('mousedown', this.handleClickOutside);
}
}
getSearchedCategories() {
return ['apps', 'commands', 'groups', 'subscriptions'];
}
control(evt) {
if (evt.key === 'Escape') {
if (this.state.query.length > 0) {
this.setState({ query: '', results: this.initialResults() });
} else if (this.props.show) {
this.props.api.local.setOmnibox();
}
};
if (
evt.key === 'ArrowUp' ||
(evt.shiftKey && evt.key === 'Tab')) {
evt.preventDefault();
return this.setPreviousSelected();
}
if (evt.key === 'ArrowDown' || evt.key === 'Tab') {
evt.preventDefault();
this.setNextSelected();
}
if (evt.key === 'Enter') {
evt.preventDefault();
if (this.state.selected !== '') {
this.navigate(this.state.selected);
} else {
this.navigate(Array.from(this.state.results.values()).flat()[0].link);
}
}
}
handleClickOutside(evt) {
if (this.props.show && !this.omniBox.contains(evt.target)) {
this.setState({ results: this.initialResults(), query: '' }, () => {
this.props.api.local.setOmnibox();
});
}
}
initialResults() {
return new Map(this.getSearchedCategories().map(category => [category, []]));
}
navigate(link) {
const { props } = this;
this.setState({ results: this.initialResults(), query: '' }, () => {
props.api.local.setOmnibox();
props.history.push(link);
});
}
search(event) {
const { state } = this;
let query = event.target.value;
const results = this.initialResults();
let selected = state.selected;
this.setState({ query });
// wipe results if backspacing
if (query.length === 0) {
this.setState({ results: results, selected: '' });
return;
}
// don't search for single characters
if (query.length === 1) {
return;
}
query = query.toLowerCase();
this.getSearchedCategories().map((category) => {
const categoryIndex = state.index.get(category);
results.set(category,
categoryIndex.filter((result) => {
return (
result.title.toLowerCase().includes(query) ||
result.link.toLowerCase().includes(query) ||
result.app.toLowerCase().includes(query) ||
(result.host !== null ? result.host.includes(query) : false)
);
})
);
});
const flattenedResultLinks = Array.from(results.values()).flat().map(result => result.link);
if (!flattenedResultLinks.includes(selected)) {
selected = flattenedResultLinks[0];
}
this.setState({ results, selected });
}
setPreviousSelected() {
const current = this.state.selected;
const flattenedResults = Array.from(this.state.results.values()).flat();
const totalLength = flattenedResults.length;
if (current !== '') {
const currentIndex = flattenedResults.indexOf(
...flattenedResults.filter((e) => {
return e.link === current;
})
);
if (currentIndex > 0) {
const nextLink = flattenedResults[currentIndex - 1].link;
this.setState({ selected: nextLink });
} else {
const nextLink = flattenedResults[totalLength - 1].link;
this.setState({ selected: nextLink });
}
} else {
const nextLink = flattenedResults[totalLength - 1].link;
this.setState({ selected: nextLink });
}
}
setNextSelected() {
const current = this.state.selected;
const flattenedResults = Array.from(this.state.results.values()).flat();
if (current !== '') {
const currentIndex = flattenedResults.indexOf(
...flattenedResults.filter((e) => {
return e.link === current;
})
);
if (currentIndex < flattenedResults.length - 1) {
const nextLink = flattenedResults[currentIndex + 1].link;
this.setState({ selected: nextLink });
} else {
const nextLink = flattenedResults[0].link;
this.setState({ selected: nextLink });
}
} else {
const nextLink = flattenedResults[0].link;
this.setState({ selected: nextLink });
}
}
renderResults() {
const { props, state } = this;
return <Box maxHeight="400px" overflowY="scroll" overflowX="hidden">
{this.getSearchedCategories()
.map(category => Object({ category, categoryResults: state.results.get(category) }))
.filter(category => category.categoryResults.length > 0)
.map(({ category, categoryResults }, i) => (
<Box key={i} width='max(50vw, 300px)' maxWidth='600px'>
<Rule borderTopWidth="0.5px" color="washedGray" />
<Text gray ml={2}>{category.charAt(0).toUpperCase() + category.slice(1)}</Text>
{categoryResults.map((result, i2) => (
<OmniboxResult
key={i2}
icon={result.app}
text={result.title}
subtext={cite(result.host)}
link={result.link}
navigate={() => this.navigate(result.link)}
selected={this.state.selected}
dark={props.dark} />
))}
</Box>
))
}
</Box>;
}
render() {
const { props, state } = this;
if (!state.selected && Array.from(this.state.results.values()).flat().length) {
this.setNextSelected();
}
return (
<Box
backgroundColor='scales.black30'
width='100vw'
height='100vh'
position='absolute'
top='0'
right='0'
zIndex='9'
display={props.show ? 'block' : 'none'}>
<Row justifyContent='center'>
<Box
mt='20vh'
width='max(50vw, 300px)'
maxWidth='600px'
borderRadius='2'
backgroundColor='white'
ref={(el) => {
this.omniBox = el;
}}>
<OmniboxInput
ref={(el) => {
this.omniInput = el;
}}
control={e => this.control(e)}
search={this.search}
query={state.query}
/>
{this.renderResults()}
</Box>
</Row>
</Box>
);
}
}
export default withRouter(Omnibox);

View File

@ -0,0 +1,26 @@
import React, { Component } from 'react';
export class OmniboxInput extends Component {
render() {
const { props } = this;
return (
<input
ref={(el) => {
this.input = el;
}
}
className='ba b--transparent w-100 br2 white-d bg-gray0-d inter f9 pa2'
style={{ maxWidth: 'calc(600px - 1.15rem)', boxSizing: 'border-box' }}
placeholder='Search...'
onKeyDown={props.control}
onChange={props.search}
spellCheck={false}
value={props.query}
/>
);
}
}
export default OmniboxInput;

View File

@ -0,0 +1,96 @@
import React, { Component } from 'react';
import { Row, Icon, Text } from '@tlon/indigo-react';
import defaultApps from '../lib/default-apps';
export class OmniboxResult extends Component {
constructor(props) {
super(props);
this.state = {
isSelected: false,
hovered: false
};
this.setHover = this.setHover.bind(this);
this.result = React.createRef();
}
componentDidUpdate(prevProps) {
const { props, state } = this;
if (prevProps &&
!state.hovered &&
prevProps.selected !== props.selected &&
props.selected === props.link
) {
this.result.current.scrollIntoView({ block: 'nearest' });
}
}
setHover(boolean) {
this.setState({ hovered: boolean });
}
render() {
const { icon, text, subtext, link, navigate, selected, dark } = this.props;
let invertGraphic = {};
if (icon.toLowerCase() !== 'dojo') {
invertGraphic = (!dark && this.state.hovered) ||
selected === link ||
(dark && !(this.state.hovered || selected === link))
? { filter: 'invert(1)', paddingTop: 2 }
: { filter: 'invert(0)', paddingTop: 2 };
} else {
invertGraphic =
(!dark && this.state.hovered) ||
selected === link ||
(dark && !(this.state.hovered || selected === link))
? { filter: 'invert(0)', paddingTop: 2 }
: { filter: 'invert(1)', paddingTop: 2 };
}
let graphic = <div />;
if (defaultApps.includes(icon.toLowerCase()) || icon.toLowerCase() === 'links') {
graphic = <img className="mr2 v-mid" height="12" width="12" src={`/~landscape/img/${icon.toLowerCase()}.png`} style={invertGraphic} />;
} else {
graphic = <Icon verticalAlign="middle" mr={2} size="12px" />;
}
return (
<Row
py='2'
px='2'
display='flex'
flexDirection='row'
style={{ cursor: 'pointer' }}
onMouseEnter={() => this.setHover(true)}
onMouseLeave={() => this.setHover(false)}
backgroundColor={
this.state.hovered || selected === link ? 'blue' : 'white'
}
onClick={navigate}
width="100%"
ref={this.result}
>
{this.state.hovered || selected === link ? (
<>
{graphic}
<Text color='white' mr='1' style={{ 'flex-shrink': 0 }}>
{text}
</Text>
<Text pr='2' color='white' width='100%' textAlign='right'>
{subtext}
</Text>
</>
) : (
<>
{graphic}
<Text mr='1' style={{ 'flex-shrink': 0 }}>{text}</Text>
<Text pr='2' gray width='100%' textAlign='right'>
{subtext}
</Text>
</>
)}
</Row>
);
}
}
export default OmniboxResult;

View File

@ -0,0 +1,46 @@
import React from 'react';
import { Box, Text } from '@tlon/indigo-react';
const ReconnectButton = ({ connection, subscription }) => {
const connectedStatus = connection || 'connected';
const reconnect = subscription.restart.bind(subscription);
if (connectedStatus === 'disconnected') {
return (
<>
<Box
ml={4}
px={2}
py={1}
display='inline-block'
color='red'
border={1}
lineHeight='min'
borderRadius={2}
style={{ cursor: 'pointer' }}
onClick={reconnect}>
<Text color='red'>Reconnect </Text>
</Box>
</>
);
} else if (connectedStatus === 'reconnecting') {
return (
<>
<Box
ml={4}
px={2}
py={1}
lineHeight="min"
display='inline-block'
color='yellow'
border={1}
borderRadius={2}>
<Text color='yellow'>Reconnecting</Text>
</Box>
</>
);
} else {
return null;
}
};
export default ReconnectButton;

View File

@ -1,82 +1,103 @@
import React from 'react';
import { useLocation, Link } from 'react-router-dom';
import GroupFilter from './GroupFilter';
import { Sigil } from '../lib/sigil';
const getLocationName = (basePath) => {
if (basePath === '~chat')
return 'Chat';
else if (basePath === '~dojo')
return 'Dojo';
else if (basePath === '~groups')
return 'Groups';
else if (basePath === '~link')
return 'Links';
else if (basePath === '~publish')
return 'Publish';
else
return 'Unknown';
};
import { useLocation } from 'react-router-dom';
import { Box, Text, Icon } from '@tlon/indigo-react';
import ReconnectButton from './ReconnectButton';
const StatusBar = (props) => {
const location = useLocation();
const basePath = location.pathname.split('/')[1];
const locationName = location.pathname === '/'
? 'Home'
: getLocationName(basePath);
const atHome = Boolean(location.pathname === '/');
const display = (!window.location.href.includes('popout/') &&
(locationName !== 'Unknown'))
const display = (!window.location.href.includes('popout/'))
? 'db' : 'dn';
const invites = (props.invites && props.invites['/contacts'])
? props.invites['/contacts']
: {};
const connection = props.connection || 'connected';
const reconnect = props.subscription.restart.bind(props.subscription);
const Notification = (Object.keys(invites).length > 0)
? <Icon size="22px" icon="Bullet"
fill="blue" position="absolute"
top={'-8px'} right={'7px'}
/>
: null;
const metaKey = (window.navigator.platform.includes('Mac')) ? '⌘' : 'Ctrl+';
const mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(
navigator.userAgent
);
return (
<div
className={
'bg-white bg-gray0-d w-100 justify-between relative tc pt3 ' + display
}
style={{ height: 45 }}
>
<div className="fl lh-copy absolute left-0 pl4" style={{ top: 8 }}>
<Link to="/~groups/me"
className="dib v-top" style={{ lineHeight: 0, paddingTop: 6 }}>
<Sigil
ship={'~' + window.ship}
classes="v-mid mix-blend-diff"
size={16}
color={'#000000'}
style={{ height: 45 }}>
<div className='absolute left-0 pl4' style={{ top: 10 }}>
{atHome ? null : (
<Box
style={{ cursor: 'pointer' }}
display='inline-block'
borderRadius={2}
color='washedGray'
border={1}
py={1}
px={2}
mr={2}
onClick={() => props.history.push('/')}>
<img
className='invert-d'
src='/~landscape/img/icon-home.png'
height='12'
width='12'
/>
</Box>
)}
<Box
border={1}
borderRadius={2}
color='washedGray'
display='inline-block'
style={{ cursor: 'pointer' }}
lineHeight='min'
py={1}
px={2}
onClick={() => props.api.local.setOmnibox()}>
<Text display='inline-block' style={{ transform: 'rotate(180deg)' }}>
</Text>
<Text ml={2} color='black'>
Leap
</Text>
<Text display={mobile ? 'none' : 'inline-block'} ml={4} color='gray'>
{metaKey}/
</Text>
</Box>
<ReconnectButton
connection={props.connection}
subscription={props.subscription}
/>
</div>
<div className='fl absolute relative right-0 pr4' style={{ top: 10 }}>
<Box
style={{ cursor: 'pointer' }}
display='inline-block'
borderRadius={2}
color='washedGray'
lineHeight='min'
border={1}
px={2}
py={1}
onClick={() => props.history.push('/~groups')}>
<img
className='invert-d v-mid mr1'
src='/~landscape/img/groups.png'
height='16'
width='16'
/>
</Link>
<GroupFilter invites={invites} associations={props.associations} api={props.api} />
<span className="dib f9 v-mid gray2 ml1 mr1 c-default inter">/</span>
{
location.pathname === '/'
? null
: <Link
className="dib f9 v-mid inter ml2 no-underline white-d"
to="/"
style={{ top: 14 }}
>
</Link>
}
<p className="dib f9 v-mid inter ml2 white-d">{locationName}</p>
{ connection === 'disconnected' &&
(<span
onClick={reconnect}
className="ml4 ph2 dib f9 v-mid red2 inter ba b-red2 br1 pointer"
>Reconnect </span> )
}
{ connection === 'reconnecting' &&
(<span className="ml4 ph2 dib f9 v-mid yellow2 inter ba b-yellow2 br1">Reconnecting</span> )
}
{Notification}
<Text ml={1}>Groups</Text>
</Box>
</div>
</div>
);

View File

@ -0,0 +1,3 @@
const defaultApps = ['chat', 'dojo', 'groups', 'link', 'publish'];
export default defaultApps;

View File

@ -0,0 +1,120 @@
import defaultApps from './default-apps';
const indexes = new Map([
['commands', []],
['subscriptions', []],
['groups', []],
['apps', []]
]);
// result schematic
const result = function(title, link, app, host) {
return {
'title': title,
'link': link,
'app': app,
'host': host
};
};
const commandIndex = function () {
// commands are special cased for default suite
const commands = [];
defaultApps
.filter((e) => {
return e !== 'dojo';
})
.map((e) => {
let title = e;
if (e === 'link') {
title = 'Links';
}
title = title.charAt(0).toUpperCase() + title.slice(1);
let obj = result(`${title}: Create`, `/~${e}/new`, e, null);
commands.push(obj);
if (title === 'Groups') {
obj = result(`${title}: Join Group`, `/~${e}/join`, title, null);
commands.push(obj);
}
});
return commands;
};
const appIndex = function (apps) {
// all apps are indexed from launch data
// indexed into 'apps'
const applications = [];
Object.keys(apps)
.filter((e) => {
return apps[e]?.type?.basic;
})
.map((e) => {
const obj = result(
apps[e].type.basic.title,
apps[e].type.basic.linkedUrl,
apps[e].type.basic.title,
null
);
applications.push(obj);
});
// add groups separately
applications.push(
result('Groups', '/~groups', 'groups', null)
);
return applications;
};
export default function index(associations, apps) {
// all metadata from all apps is indexed
// into subscriptions and groups
const subscriptions = [];
const groups = [];
Object.keys(associations).filter((e) => {
// skip apps with no metadata
return Object.keys(associations[e]).length > 0;
}).map((e) => {
// iterate through each app's metadata object
Object.keys(associations[e]).map((association) => {
const each = associations[e][association];
let title = each['app-path'];
if (each.metadata.title !== '') {
title = each.metadata.title;
}
let app = each['app-name'];
if (each['app-name'] === 'contacts') {
app = 'groups';
};
const shipStart = each['app-path'].substr(each['app-path'].indexOf('~'));
if (app === 'groups') {
const obj = result(
title,
`/~${app}${each['app-path']}`,
app.charAt(0).toUpperCase() + app.slice(1),
shipStart.slice(0, shipStart.indexOf('/'))
);
groups.push(obj);
} else {
const obj = result(
title,
`/~${each['app-name']}/join${each['app-path']}`,
app.charAt(0).toUpperCase() + app.slice(1),
shipStart.slice(0, shipStart.indexOf('/'))
);
subscriptions.push(obj);
}
});
});
indexes.set('commands', commandIndex());
indexes.set('subscriptions', subscriptions);
indexes.set('groups', groups);
indexes.set('apps', appIndex(apps));
return indexes;
};

View File

@ -2,24 +2,24 @@ import React, { Component } from 'react';
import { sigil, reactRenderer } from 'urbit-sigil-js';
export class Sigil extends Component {
static foregroundFromBackground(background) {
const rgb = {
r: parseInt(background.slice(1, 3), 16),
g: parseInt(background.slice(3, 5), 16),
b: parseInt(background.slice(5, 7), 16)
};
const brightness = ((299 * rgb.r) + (587 * rgb.g) + (114 * rgb.b)) / 1000;
const whiteBrightness = 255;
return ((whiteBrightness - brightness) < 50) ? 'black' : 'white';
}
render() {
const { props } = this;
const classes = props.classes || '';
const rgb = {
r: parseInt(props.color.slice(1, 3), 16),
g: parseInt(props.color.slice(3, 5), 16),
b: parseInt(props.color.slice(5, 7), 16)
};
const brightness = ((299 * rgb.r) + (587 * rgb.g) + (114 * rgb.b)) / 1000;
const whiteBrightness = 255;
let foreground = 'white';
if ((whiteBrightness - brightness) < 50) {
foreground = 'black';
}
const foreground = Sigil.foregroundFromBackground(props.color);
if (props.ship.length > 14) {
return (

View File

@ -119,6 +119,9 @@ export function writeText(str) {
// trim patps to match dojo, chat-cli
export function cite(ship) {
let patp = ship, shortened = '';
if (patp === null || patp === '') {
return null;
}
if (patp.startsWith('~')) {
patp = patp.substr(1);
}
@ -275,3 +278,38 @@ export function stringToSymbol(str) {
}
return result;
}
export function scrollIsAtTop(container) {
if (
(navigator.userAgent.includes("Safari") &&
navigator.userAgent.includes("Chrome")) ||
navigator.userAgent.includes("Firefox")
) {
return container.scrollTop === 0;
} else if (navigator.userAgent.includes("Safari")) {
return (
container.scrollHeight + Math.round(container.scrollTop) <=
container.clientHeight + 10
);
} else {
return false;
}
}
export function scrollIsAtBottom(container) {
if (
(navigator.userAgent.includes("Safari") &&
navigator.userAgent.includes("Chrome")) ||
navigator.userAgent.includes("Firefox")
) {
return (
container.scrollHeight - Math.round(container.scrollTop) <=
container.clientHeight + 10
);
} else if (navigator.userAgent.includes("Safari")) {
return container.scrollTop === 0;
} else {
return false;
}
}

View File

@ -25,7 +25,6 @@ function decodeGroup(group: Enc<Group>): Group {
tags: decodeTags(group.tags),
policy: decodePolicy(group.policy),
};
console.log(res);
return res;
}

View File

@ -50,7 +50,6 @@ export default class InviteReducer<S extends InviteState> {
accepted(json: InviteUpdate, state: S) {
const data = _.get(json, 'accepted', false);
if (data) {
console.log(data);
delete state.invites[data.path][data.uid];
}
}

View File

@ -50,7 +50,6 @@ export default class LaunchReducer<S extends LaunchState> {
changeIsShown(json: LaunchUpdate, state: S) {
const data = _.get(json, 'changeIsShown', false);
console.log(json, data);
if (data) {
let tile = state.launch.tiles[data.name];
console.log(tile);

Some files were not shown because too many files have changed in this diff Show More