mirror of
https://github.com/urbit/shrub.git
synced 2024-12-01 14:42:02 +03:00
Merge pull request #3304 from ohAitch/patch-3
Fix {a/$foo} in type printing to [a=%foo]
This commit is contained in:
commit
22c66f9ac4
@ -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.
|
||||
|
4
.github/ISSUE_TEMPLATE/os1-bug-report.md
vendored
4
.github/ISSUE_TEMPLATE/os1-bug-report.md
vendored
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d862132e99ed393d786fb4bdb7c487bced2d4c9efc2cb7e174a73b6acca4799d
|
||||
size 6304536
|
||||
oid sha256:6cd7246753c12c7acb757e1a6ee54c177806c20a137ad8fb4300c000ac146a0f
|
||||
size 6260139
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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)])]~
|
||||
::
|
||||
|
572
pkg/arvo/app/graph-store.hoon
Normal file
572
pkg/arvo/app/graph-store.hoon
Normal 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
|
||||
--
|
@ -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
|
||||
|
BIN
pkg/arvo/app/landscape/img/groups.png
Normal file
BIN
pkg/arvo/app/landscape/img/groups.png
Normal file
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 |
@ -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>
|
||||
|
10
pkg/arvo/gen/graph-store/add-graph.hoon
Normal file
10
pkg/arvo/gen/graph-store/add-graph.hoon
Normal 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]]
|
20
pkg/arvo/gen/graph-store/add-post.hoon
Normal file
20
pkg/arvo/gen/graph-store/add-post.hoon
Normal 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 ~]]]]
|
10
pkg/arvo/gen/graph-store/add-signatures.hoon
Normal file
10
pkg/arvo/gen/graph-store/add-signatures.hoon
Normal 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]]
|
10
pkg/arvo/gen/graph-store/add-tag.hoon
Normal file
10
pkg/arvo/gen/graph-store/add-tag.hoon
Normal 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]]
|
10
pkg/arvo/gen/graph-store/archive-graph.hoon
Normal file
10
pkg/arvo/gen/graph-store/archive-graph.hoon
Normal 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]]
|
10
pkg/arvo/gen/graph-store/remove-graph.hoon
Normal file
10
pkg/arvo/gen/graph-store/remove-graph.hoon
Normal 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]]
|
10
pkg/arvo/gen/graph-store/remove-nodes.hoon
Normal file
10
pkg/arvo/gen/graph-store/remove-nodes.hoon
Normal 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]]
|
11
pkg/arvo/gen/graph-store/remove-signatures.hoon
Normal file
11
pkg/arvo/gen/graph-store/remove-signatures.hoon
Normal 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]]
|
10
pkg/arvo/gen/graph-store/remove-tag.hoon
Normal file
10
pkg/arvo/gen/graph-store/remove-tag.hoon
Normal 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]]
|
10
pkg/arvo/gen/graph-store/unarchive-graph.hoon
Normal file
10
pkg/arvo/gen/graph-store/unarchive-graph.hoon
Normal 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]]
|
411
pkg/arvo/lib/graph-store.hoon
Normal file
411
pkg/arvo/lib/graph-store.hoon
Normal 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
24
pkg/arvo/lib/graph.hoon
Normal 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])
|
||||
--
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
43
pkg/arvo/lib/signatures.hoon
Normal file
43
pkg/arvo/lib/signatures.hoon
Normal 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
|
||||
--
|
13
pkg/arvo/mar/graph/update.hoon
Normal file
13
pkg/arvo/mar/graph/update.hoon
Normal file
@ -0,0 +1,13 @@
|
||||
/+ *graph-store
|
||||
|_ upd=update
|
||||
++ grow
|
||||
|%
|
||||
++ json (update:enjs upd)
|
||||
--
|
||||
::
|
||||
++ grab
|
||||
|%
|
||||
++ noun update
|
||||
++ json update:dejs
|
||||
--
|
||||
--
|
17
pkg/arvo/mar/graph/validator/chat.hoon
Normal file
17
pkg/arvo/mar/graph/validator/chat.hoon
Normal 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
|
||||
--
|
61
pkg/arvo/sur/graph-store.hoon
Normal file
61
pkg/arvo/sur/graph-store.hoon
Normal 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
37
pkg/arvo/sur/post.hoon
Normal 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]
|
||||
==
|
||||
--
|
@ -8,5 +8,4 @@
|
||||
+$ update
|
||||
$% [%tracking tracking=(map resource ship)]
|
||||
==
|
||||
::
|
||||
--
|
||||
|
@ -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
186
pkg/interface/.eslintrc.js
Normal 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
|
||||
}
|
||||
]
|
||||
};
|
@ -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
|
||||
}
|
||||
}
|
@ -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'
|
||||
},
|
||||
|
@ -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,
|
||||
|
240
pkg/interface/package-lock.json
generated
240
pkg/interface/package-lock.json
generated
@ -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": {
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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+"
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
141
pkg/interface/src/apps/chat/components/lib/chat-editor.js
Normal file
141
pkg/interface/src/apps/chat/components/lib/chat-editor.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
58
pkg/interface/src/apps/chat/components/lib/chat-header.js
Normal file
58
pkg/interface/src/apps/chat/components/lib/chat-header.js
Normal 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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
84
pkg/interface/src/apps/chat/components/lib/chat-message.tsx
Normal file
84
pkg/interface/src/apps/chat/components/lib/chat-message.tsx
Normal 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;
|
||||
}
|
||||
};
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
194
pkg/interface/src/apps/chat/components/lib/chat-window.tsx
Normal file
194
pkg/interface/src/apps/chat/components/lib/chat-window.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
28
pkg/interface/src/apps/chat/components/lib/content/code.js
Normal file
28
pkg/interface/src/apps/chat/components/lib/content/code.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
65
pkg/interface/src/apps/chat/components/lib/content/text.js
Normal file
65
pkg/interface/src/apps/chat/components/lib/content/text.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
102
pkg/interface/src/apps/chat/components/lib/content/url.js
Normal file
102
pkg/interface/src/apps/chat/components/lib/content/url.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
51
pkg/interface/src/apps/chat/components/lib/delete-button.js
Normal file
51
pkg/interface/src/apps/chat/components/lib/delete-button.js
Normal 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>
|
||||
);
|
||||
};
|
||||
|
104
pkg/interface/src/apps/chat/components/lib/groupify-button.js
Normal file
104
pkg/interface/src/apps/chat/components/lib/groupify-button.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
71
pkg/interface/src/apps/chat/components/lib/metadata-color.js
Normal file
71
pkg/interface/src/apps/chat/components/lib/metadata-color.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
52
pkg/interface/src/apps/chat/components/lib/metadata-input.js
Normal file
52
pkg/interface/src/apps/chat/components/lib/metadata-input.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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 && (
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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(
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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}
|
||||
|
@ -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}>
|
||||
|
@ -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}
|
||||
|
@ -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={'/~/'}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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': {
|
||||
|
@ -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 'subscribers' panel.
|
||||
</p>
|
||||
</div>
|
||||
<InviteSearch
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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}>
|
||||
|
@ -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
|
||||
|
@ -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={{
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
261
pkg/interface/src/components/Omnibox.js
Normal file
261
pkg/interface/src/components/Omnibox.js
Normal 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);
|
26
pkg/interface/src/components/OmniboxInput.js
Normal file
26
pkg/interface/src/components/OmniboxInput.js
Normal 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;
|
||||
|
96
pkg/interface/src/components/OmniboxResult.js
Normal file
96
pkg/interface/src/components/OmniboxResult.js
Normal 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;
|
46
pkg/interface/src/components/ReconnectButton.js
Normal file
46
pkg/interface/src/components/ReconnectButton.js
Normal 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;
|
@ -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>
|
||||
);
|
||||
|
3
pkg/interface/src/lib/default-apps.js
Normal file
3
pkg/interface/src/lib/default-apps.js
Normal file
@ -0,0 +1,3 @@
|
||||
const defaultApps = ['chat', 'dojo', 'groups', 'link', 'publish'];
|
||||
|
||||
export default defaultApps;
|
120
pkg/interface/src/lib/omnibox.js
Normal file
120
pkg/interface/src/lib/omnibox.js
Normal 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;
|
||||
};
|
@ -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 (
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,6 @@ function decodeGroup(group: Enc<Group>): Group {
|
||||
tags: decodeTags(group.tags),
|
||||
policy: decodePolicy(group.policy),
|
||||
};
|
||||
console.log(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user