Merge branch 'release/next-userspace' into lf/graph-publish-fe

This commit is contained in:
Liam Fitzgerald 2020-10-28 16:40:02 +10:00
commit 39d538f9bc
74 changed files with 1771 additions and 1641 deletions

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:77d4ecd2ce1d5fc68297f16ac2030dfc8a072f51978363dd88a96239e4f5ac7d
size 6310667
oid sha256:38435a0a23fb4f09d55505915cd8e772b8096fd846c2c8ff3481a5b231deedf6
size 6331042

View File

@ -10,7 +10,7 @@
:: and trust it to take care of the rest.
::
/- view=chat-view, hook=chat-hook, *group,
*permission-store, *group-store, *invite-store,
*permission-store, *group-store, inv=invite-store,
sole
/+ shoe, default-agent, verb, dbug, store=chat-store,
group-store, grpl=group, resource
@ -27,7 +27,7 @@
+$ state-2
$: %2
grams=(list mail) :: all messages
known=(set [target serial]) :: known message lookup
known=(set [target serial:store]) :: known message lookup
count=@ud :: (lent grams)
bound=(map target glyph) :: bound circle glyphs
binds=(jug glyph target) :: circle glyph lookup
@ -54,7 +54,7 @@
::
+$ state-0
$: grams=(list [[=ship =path] envelope:store]) :: all messages
known=(set [[=ship =path] serial]) :: known message lookup
known=(set [[=ship =path] serial:store]) :: known message lookup
count=@ud :: (lent grams)
bound=(map [=ship =path] glyph) :: bound circle glyphs
binds=(jug glyph [=ship =path]) :: circle glyph lookup
@ -161,7 +161,7 @@
%fact
?+ p.cage.sign ~|([%chat-cli-bad-sub-mark wire p.cage.sign] !!)
%chat-update (diff-chat-update:tc wire !<(update:store q.cage.sign))
%invite-update (handle-invite-update:tc !<(invite-update q.cage.sign))
%invite-update (handle-invite-update:tc !<(update:inv q.cage.sign))
==
==
[cards this]
@ -224,9 +224,9 @@
grams ~ ::NOTE this only impacts historic message lookup in chat-cli
::
known
^- (set [target serial])
^- (set [target serial:store])
%- ~(run in known.u.old)
|= [t=[ship path] s=serial]
|= [t=[ship path] s=serial:store]
[`target`[| t] s]
::
bound
@ -324,7 +324,7 @@
:: +handle-invite-update: get new invites
::
++ handle-invite-update
|= upd=invite-update
|= upd=update:inv
^- (quip card _state)
?+ -.upd [~ state]
%invite [[(show-invite:sh-out invite.upd) ~] state]
@ -722,12 +722,11 @@
%poke
%invite-action
::
!>
^- invite-action
:^ %invite /chat
!> ^- action:inv
:^ %invite %chat
(shax (jam [our-self where] who))
^- invite
[our-self %chat-hook where who '']
^- invite:inv
[our-self %chat-hook (de-path:resource where) who '']
==
:: +set-target: set audience, update prompt
::
@ -865,7 +864,7 @@
|= =letter:store
^- (quip card _state)
~! bowl
=/ =serial (shaf %msg-uid eny.bowl)
=/ =serial:store (shaf %msg-uid eny.bowl)
:_ state
^- (list card)
%+ turn ~(tap in audience)
@ -1132,11 +1131,9 @@
:: +show-invite: print incoming invite notification
::
++ show-invite
|= invite
|= invite:inv
^- card
%- note
%+ weld "invited to: "
~(phat tr (path-to-target path))
(note "invited to: {(scow %p entity.resource)} {(trip name.resource)}")
--
::
:: +tr: render targets

View File

@ -2,7 +2,7 @@
:: mirror chat data from foreign to local based on read permissions
:: allow sending chat messages to foreign paths based on write perms
::
/- *permission-store, *invite-store, *metadata-store,
/- *permission-store, inv=invite-store, *metadata-store,
*permission-hook, *group-store, *permission-group-hook, ::TMP for upgrade
hook=chat-hook,
view=chat-view,
@ -52,7 +52,7 @@
+$ poke
$% [%chat-action action:store]
[%permission-action permission-action]
[%invite-action invite-action]
[%invite-action action:inv]
[%chat-view-action action:view]
==
::
@ -77,7 +77,7 @@
++ on-init
^- (quip card _this)
:_ this(invite-created %.y)
:~ (invite-poke:cc [%create /chat])
:~ (invite-poke:cc [%create %chat])
[%pass /invites %agent [our.bol %invite-store] %watch /invitatory/chat]
watch-groups:cc
==
@ -406,7 +406,7 @@
::
%invite-update
=^ cards state
(fact-invite-update:cc wire !<(invite-update q.cage.sign))
(fact-invite-update:cc wire !<(update:inv q.cage.sign))
[cards this]
::
%group-update
@ -719,15 +719,18 @@
==
::
++ fact-invite-update
|= [wir=wire fact=invite-update]
|= [wir=wire fact=update:inv]
^- (quip card _state)
:_ state
?+ -.fact ~
%accepted
=/ ask-history ?~((chat-scry path.invite.fact) %.y %.n)
=* shp ship.invite.fact
=* app-path path.invite.fact
~[(chat-view-poke [%join shp app-path ask-history])]
=* resource resource.invite.fact
=/ =path [(scot %p entity.resource) name.resource ~]
:_ ~
%- chat-view-poke
:^ %join ship.invite.fact
path
?=(~ (chat-scry path))
==
::
++ fact-group-update
@ -919,9 +922,9 @@
[%pass / %agent [our.bol %chat-view] %poke %chat-view-action !>(act)]
::
++ invite-poke
|= act=invite-action
|= =action:inv
^- card
[%pass / %agent [our.bol %invite-store] %poke %invite-action !>(act)]
[%pass / %agent [our.bol %invite-store] %poke %invite-action !>(action)]
::
++ sec-to-perm
|= [pax=path =kind]
@ -936,9 +939,9 @@
[%mailbox pax]
::
++ invite-scry
|= uid=serial
^- (unit invite)
%^ scry (unit invite)
|= uid=serial:inv
^- (unit invite:inv)
%^ scry (unit invite:inv)
%invite-store
/invite/chat/(scot %uv uid)
::

View File

@ -6,7 +6,7 @@
/- *permission-store,
*permission-hook,
*group,
*invite-store,
inv=invite-store,
*metadata-store,
group-hook,
*permission-group-hook,
@ -407,13 +407,14 @@
^- card
=/ managed=?
!=(ship+app-path group-path)
=/ =invite
=/ =invite:inv
:* our.bol
?:(managed %contact-hook %chat-hook)
?:(managed group-path app-path)
(de-path:resource ?:(managed group-path ship+app-path))
ship ''
==
=/ act=invite-action [%invite ?:(managed /contacts /chat) (shaf %msg-uid eny.bol) invite]
=/ act=action:inv
[%invite ?:(managed %contacts %chat) (shaf %msg-uid eny.bol) invite]
[%pass / %agent [our.bol %invite-hook] %poke %invite-action !>(act)]
::
++ chat-scry

View File

@ -4,7 +4,7 @@
/- group-hook,
*contact-hook,
*contact-view,
*invite-store,
inv=invite-store,
*metadata-hook,
*metadata-store,
*group
@ -44,7 +44,7 @@
++ on-init
^- (quip card _this)
:_ this(invite-created %.y)
:~ (invite-poke:cc [%create /contacts])
:~ (invite-poke:cc [%create %contacts])
[%pass /inv %agent [our.bol %invite-store] %watch /invitatory/contacts]
[%pass /group %agent [our.bol %group-store] %watch /groups]
==
@ -467,20 +467,10 @@
(contact-poke [%delete path])
(contact-poke [%remove path ship])
==
::
++ send-invite-poke
|= [=path =ship]
^- card
=/ =invite
:* our.bol %contact-hook
path ship ''
==
=/ act=invite-action [%invite /contacts (shaf %msg-uid eny.bol) invite]
[%pass / %agent [our.bol %invite-hook] %poke %invite-action !>(act)]
--
::
++ invite-poke
|= act=invite-action
|= act=action:inv
^- card
[%pass / %agent [our.bol %invite-store] %poke %invite-action !>(act)]
::

View File

@ -5,7 +5,7 @@
::
/-
group-hook,
*invite-store,
inv=invite-store,
*contact-hook,
*metadata-store,
*metadata-hook,
@ -161,27 +161,22 @@
%+ turn
~(tap in pending.policy.act)
|= =ship
(send-invite our.bol %contacts path ship '')
(send-invite our.bol %contacts rid ship '')
==
::
%join
=/ =path
(en-path:resource resource.act)
=/ =cage
:- %group-update
!> ^- update:group-store
[%add-members resource.act (sy our.bol ~)]
=/ =wire
[%join-group path]
[%join-group (en-path:resource resource.act)]
[%pass wire %agent [entity.resource.act %group-push-hook] %poke cage]~
::
%invite
=* rid resource.act
=/ =path
(en-path:resource rid)
=/ =group
(need (scry-group:grp rid))
:- (send-invite entity.rid %contacts path ship.act text.act)
=/ =group (need (scry-group:grp rid))
:- (send-invite entity.rid %contacts rid ship.act text.act)
?. ?=(%invite -.policy.group) ~
~[(add-pending rid ship.act)]
::
@ -276,12 +271,12 @@
[%pass / %agent [entity.rid app] %poke cage]
::
++ send-invite
|= =invite
|= =invite:inv
^- card
=/ =cage
:- %invite-action
!> ^- invite-action
[%invite /contacts (shaf %invite-uid eny.bol) invite]
!> ^- action:inv
[%invite %contacts (shaf %invite-uid eny.bol) invite]
[%pass / %agent [recipient.invite %invite-hook] %poke cage]
::
++ contact-poke

View File

@ -380,17 +380,19 @@
'connected'^b+!-.state
'expiry'^?-(-.state %& (time date.p.state), %| ~)
'next-id'^(numb next-id)
'last-ack'^(time last-ack)
'unacked'^a+(turn (sort (turn ~(tap in events) head) dor) numb)
::
:- 'subscriptions'
:- %a
%+ turn ~(tap by subscriptions)
|= [=wire [=^ship app=term =^path *]]
|= [id=@ud [=^ship app=term =^path *]]
%- pairs
:~ 'wire'^(^path wire)
:~ 'id'^(numb id)
'ship'^(^ship ship)
'app'^s+app
'path'^(^path path)
'unacked'^(numb (~(gut by unacked) id 0))
==
==
==

File diff suppressed because one or more lines are too long

View File

@ -5,7 +5,7 @@
/- glob
/+ default-agent, verb, dbug
|%
++ hash 0v1.hc18i.ms0ac.qi9qj.geopu.sutri
++ hash 0v2.1vtfh.0l23v.30s7f.n57l9.dpjvi
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
+$ all-states
$% state-0

View File

@ -1,123 +1,121 @@
:: invite-hook [landscape]:
:: invite-hook [landscape]: receive invites from any source
::
:: receive invites from any source
:: only handles %invite actions:
:: - can be poked by the host team to send an invite out to someone.
:: - can be poked by foreign ships to send an invite to us.
::
:: only handles %invite actions. accepts json, but only from the host team.
:: can be poked by the host team to send an invite out to someone.
:: can be poked by foreign ships to send an invite to us.
::
/+ *invite-json, default-agent, verb, dbug
/- *invite-store
/+ default-agent, dbug
::
|%
+$ state-0 [%0 ~]
::
+$ card card:agent:gall
--
::
=| state-0
=* state -
::
%+ verb |
%- agent:dbug
^- agent:gall
=<
|_ =bowl:gall
+* this .
do ~(. +> bowl)
def ~(. (default-agent this %|) bowl)
::
++ on-init
^- (quip card _this)
[~ this]
::
++ on-save !>(state)
++ on-load
|= old=vase
^- (quip card _this)
[~ this(state !<(state-0 old))]
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
:_ this
?+ mark (on-poke:def mark vase)
%json
:: only accept json from ourselves.
::
|_ =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-poke
|= [=mark =vase]
^- (quip card _this)
|^
:_ this
?+ mark (on-poke:def mark vase)
%invite-action
=/ act=action !<(action vase)
?+ -.act ~
%invites
?. (team:title [our src]:bowl) ~
:: outgoing. we must be inviting other ships. send them each an invite
::
?> (team:title our.bowl src.bowl)
=/ act (json-to-action !<(json vase))
?> ?=(%invite -.act)
[(invite-hook-poke:do recipient.invite.act act)]~
%+ turn ~(tap in recipients.invites.act)
|= recipient=ship
^- card
?< (team:title our.bowl recipient)
%+ invite-hook-poke recipient
:^ %invite term.act uid.act
^- invite
:* ship.invites.act
app.invites.act
resource.invites.act
recipient
text.invites.act
==
::
%invite-action
=/ act=invite-action !<(invite-action vase)
?. ?=(%invite -.act) ~
?: (team:title our.bowl src.bowl)
%invite
?: (team:title [our src]:bowl)
:: outgoing. we must be inviting another ship. send them the invite.
::
?< (team:title our.bowl recipient.invite.act)
[(invite-hook-poke:do recipient.invite.act act)]~
[(invite-hook-poke recipient.invite.act act)]~
:: else incoming. ensure invitatory exists and invite is not a duplicate.
::
?> ?=(^ (invitatory-scry:do path.act))
?> ?=(~ (invite-scry:do path.act uid.act))
[(invite-poke:do path.act act)]~
?> ?=(^ (invitatory-scry term.act))
?> ?=(~ (invite-scry term.act uid.act))
[(invite-poke term.act act)]~
==
==
::
++ invite-hook-poke
|= [=ship =action]
^- card
:* %pass
/invite-hook
%agent
[ship %invite-hook]
%poke
%invite-action
!>(action)
==
::
++ on-peek on-peek:def
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
++ invite-poke
|= [=term =action]
^- card
:* %pass
/[term]
%agent
[our.bowl %invite-store]
%poke
%invite-action
!>(action)
==
::
++ invitatory-scry
|= =term
.^ (unit invitatory)
%gx
%+ weld
/(scot %p our.bowl)/invite-store/(scot %da now.bowl)/invitatory
/[term]/noun
==
::
++ invite-scry
|= [=term uid=serial]
.^ (unit invite)
%gx
%+ weld
/(scot %p our.bowl)/invite-store/(scot %da now.bowl)/invite
/[term]/(scot %uv uid)/noun
==
--
::
|_ =bowl:gall
::
++ invite-hook-poke
|= [=ship action=invite-action]
^- card
:* %pass
/invite-hook
%agent
[ship %invite-hook]
%poke
%invite-action
!>(action)
==
::
++ invite-poke
|= [=path action=invite-action]
^- card
:* %pass
path
%agent
[our.bowl %invite-store]
%poke
%invite-action
!>(action)
==
::
++ invitatory-scry
|= pax=path
^- (unit invitatory)
=. pax
;: weld
/(scot %p our.bowl)/invite-store/(scot %da now.bowl)/invitatory
pax
/noun
==
.^((unit invitatory) %gx pax)
::
++ invite-scry
|= [pax=path uid=serial]
^- (unit invite)
=. pax
;: weld
/(scot %p our.bowl)/invite-store/(scot %da now.bowl)/invite
pax
/(scot %uv uid)/noun
==
.^((unit invite) %gx pax)
++ on-peek on-peek:def
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
--

View File

@ -1,184 +1,209 @@
:: invite-store [landscape]
/+ *invite-json, default-agent, dbug
/- store=invite-store
/+ res=resource, default-agent, dbug
|%
+$ card card:agent:gall
::
+$ versioned-state
$% state-zero
$% state-0
state-1
==
::
+$ state-zero
$: %0
=invites
+$ invitatory-0 (map serial:store invite-0)
+$ invite-0
$: =ship :: ship to subscribe to upon accepting invite
app=@tas :: app to subscribe to upon accepting invite
=path :: path to subscribe to upon accepting invite
recipient=ship :: recipient to receive invite
text=cord :: text to describe the invite
==
::
+$ state-0 [%0 invites=(map path invitatory-0)]
+$ state-1 [%1 =invites:store]
--
::
=| state-zero
=| state-1
=* state -
%- agent:dbug
^- agent:gall
=<
|_ bol=bowl:gall
+* this .
inv-core +>
ic ~(. inv-core bol)
def ~(. (default-agent this %|) bol)
++ on-init on-init:def
++ on-save !>(state)
++ on-load
|= old=vase
`this(state !<(state-zero old))
::
|_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
::
++ on-init
^- (quip card _this)
:- ~
%_ this
invites.state
%- ~(gas by *invites:store)
[%graph *invitatory:store]~
==
::
++ on-save !>(state)
++ on-load
|= old-vase=vase
=/ old !<(versioned-state old-vase)
?: ?=(%1 -.old)
`this(state old)
:- =- [%pass / %agent [our.bowl %invite-store] %poke %invite-action !>(-)]~
!> ^- action:store
[%create %graph]
%= this
state
:- %1
%- ~(gas by *invites:store)
%+ murn ~(tap by invites.old)
|= [=path =invitatory-0]
^- (unit [term invitatory:store])
?. ?=([@ ~] path) ~
:- ~
:- i.path
%- ~(gas by *invitatory:store)
%+ murn ~(tap by invitatory-0)
|= [=serial:store =invite-0]
^- (unit [serial:store invite:store])
=/ resource=(unit resource:res) (de-path-soft:res path.invite-0)
?~ resource ~
:- ~
:- serial
^- invite:store
:* ship.invite-0
app.invite-0
u.resource
recipient.invite-0
text.invite-0
==
==
::
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-leave on-leave:def
++ on-fail on-fail:def
::
++ on-watch
|= =path
^- (quip card _this)
?> (team:title our.bowl src.bowl)
=/ cards=(list card)
?+ path (on-watch:def path)
[%all ~] [%give %fact ~ %invite-update !>([%initial invites])]~
[%updates ~] ~
[%invitatory @ ~]
=/ inv=invitatory:store (~(got by invites) i.t.path)
[%give %fact ~ %invite-update !>([%invitatory inv])]~
==
[cards this]
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
|^
?> (team:title our.bowl src.bowl)
=^ cards state
?+ mark (on-poke:def mark vase)
%invite-action (poke-invite-action !<(action:store vase))
==
[cards this]
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?> (team:title our.bol src.bol)
=^ cards state
?+ mark (on-poke:def mark vase)
%json (poke-invite-action:ic (json-to-action !<(json vase)))
%invite-action (poke-invite-action:ic !<(invite-action vase))
==
[cards this]
::
++ on-watch
|= =path
^- (quip card _this)
=/ cards=(list card)
?+ path (on-watch:def path)
[%all ~] [%give %fact ~ %invite-update !>([%initial invites])]~
[%updates ~] ~
[%invitatory *]
=/ inv=invitatory (~(got by invites) t.path)
[%give %fact ~ %invite-update !>([%invitatory inv])]~
==
[cards this]
::
++ on-leave on-leave:def
++ on-peek
|= =path
^- (unit (unit cage))
?+ path (on-peek:def path)
[%x %all ~] (peek-x-all:ic t.t.path)
[%x %invitatory *] (peek-x-invitatory:ic t.t.path)
[%x %invite *] (peek-x-invite:ic t.t.path)
++ poke-invite-action
|= =action:store
^- (quip card _state)
?- -.action
%create (handle-create +.action)
%delete (handle-delete +.action)
%invite (handle-invite +.action)
%accept (handle-accept +.action)
%decline (handle-decline +.action)
%invites ~|('only send this to %invite-hook' !!)
==
::
++ on-agent on-agent:def
++ on-arvo on-arvo:def
++ on-fail on-fail:def
++ handle-create
|= =term
^- (quip card _state)
?: (~(has by invites) term)
[~ state]
:- (send-diff term [%create term])
state(invites (~(put by invites) term *invitatory:store))
::
++ handle-delete
|= =term
^- (quip card _state)
?. (~(has by invites) term)
[~ state]
:- (send-diff term [%delete term])
state(invites (~(del by invites) term))
::
++ handle-invite
|= [=term =serial:store =invite:store]
^- (quip card _state)
?. (~(has by invites) term)
[~ state]
=/ container (~(got by invites) term)
=. serial (sham eny.bowl)
=. container (~(put by container) serial invite)
:- (send-diff term [%invite term serial invite])
state(invites (~(put by invites) term container))
::
++ handle-accept
|= [=term =serial:store]
^- (quip card _state)
?. (~(has by invites) term)
[~ state]
=/ container (~(got by invites) term)
=/ invite (~(get by container) serial)
?~ invite
[~ state]
=. container (~(del by container) serial)
:- (send-diff term [%accepted term serial u.invite])
state(invites (~(put by invites) term container))
::
++ handle-decline
|= [=term =serial:store]
^- (quip card _state)
?. (~(has by invites) term)
[~ state]
=/ container (~(got by invites) term)
=/ invite (~(get by container) serial)
?~ invite
[~ state]
=. container (~(del by container) serial)
:- (send-diff term [%decline term serial])
state(invites (~(put by invites) term container))
::
++ update-subscribers
|= [=path =update:store]
^- card
[%give %fact ~[path] %invite-update !>(update)]
::
++ send-diff
|= [=term =update:store]
^- (list card)
:~ (update-subscribers /all update)
(update-subscribers /updates update)
(update-subscribers /invitatory/[term] update)
==
--
::
|_ bol=bowl:gall
::
++ peek-x-all
|= pax=path
++ on-peek
|= =path
^- (unit (unit cage))
[~ ~ %noun !>(invites)]
::
++ peek-x-invitatory
|= pax=path
^- (unit (unit cage))
?~ pax
~
=/ invitatory=(unit invitatory) (~(get by invites) pax)
[~ ~ %noun !>(invitatory)]
::
++ peek-x-invite
|= pax=path
^- (unit (unit cage))
:: /:path/:uid
=/ pas (flop pax)
?~ pas
~
=/ uid=serial (slav %uv i.pas)
=. pax (scag (dec (lent pax)) `(list @ta)`pax)
=/ invitatory=(unit invitatory) (~(get by invites) pax)
?~ invitatory
~
=/ invite=(unit invite) (~(get by u.invitatory) uid)
[~ ~ %noun !>(invite)]
::
++ poke-invite-action
|= action=invite-action
^- (quip card _state)
?> (team:title our.bol src.bol)
?- -.action
%create (handle-create action)
%delete (handle-delete action)
%invite (handle-invite action)
%accept (handle-accept action)
%decline (handle-decline action)
?+ path (on-peek:def path)
[%x %all ~]
``noun+!>(invites)
::
[%x %invitatory @ ~]
:^ ~ ~ %noun
!> ^- (unit invitatory:store)
(~(get by invites) i.t.t.path)
::
[%x %invite @ @ ~]
=* term i.t.t.path
=/ =serial:store (slav %uv i.t.t.t.path)
?. (~(has by invites) term)
~
=/ =invitatory:store (~(got by invites) term)
:^ ~ ~ %noun
!> ^- (unit invite:store)
(~(get by invitatory) serial)
==
::
++ handle-create
|= act=invite-action
^- (quip card _state)
?> ?=(%create -.act)
?: (~(has by invites) path.act)
[~ state]
:- (send-diff path.act act)
state(invites (~(put by invites) path.act *invitatory))
::
++ handle-delete
|= act=invite-action
^- (quip card _state)
?> ?=(%delete -.act)
?. (~(has by invites) path.act)
[~ state]
:- (send-diff path.act act)
state(invites (~(del by invites) path.act))
::
++ handle-invite
|= act=invite-action
^- (quip card _state)
?> ?=(%invite -.act)
?. (~(has by invites) path.act)
[~ state]
=/ container (~(got by invites) path.act)
=. uid.act (sham eny.bol)
=. container (~(put by container) uid.act invite.act)
:- (send-diff path.act act)
state(invites (~(put by invites) path.act container))
::
++ handle-accept
|= act=invite-action
^- (quip card _state)
?> ?=(%accept -.act)
?. (~(has by invites) path.act)
[~ state]
=/ container (~(got by invites) path.act)
=/ invite (~(get by container) uid.act)
?~ invite
[~ state]
=. container (~(del by container) uid.act)
:- (send-diff path.act [%accepted path.act uid.act u.invite])
state(invites (~(put by invites) path.act container))
::
++ handle-decline
|= act=invite-action
^- (quip card _state)
?> ?=(%decline -.act)
?. (~(has by invites) path.act)
[~ state]
=/ container (~(got by invites) path.act)
=/ invite (~(get by container) uid.act)
?~ invite
[~ state]
=. container (~(del by container) uid.act)
:- (send-diff path.act act)
state(invites (~(put by invites) path.act container))
::
++ update-subscribers
|= [pax=path upd=invite-update]
^- card
[%give %fact ~[pax] %invite-update !>(upd)]
::
++ send-diff
|= [pax=path upd=invite-update]
^- (list card)
:~ (update-subscribers /all upd)
(update-subscribers /updates upd)
(update-subscribers [%invitatory pax] upd)
==
::
--

View File

@ -24,6 +24,6 @@
<div id="portal-root"></div>
<script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.js"></script>
<script src="/~landscape/js/bundle/index.24abd83f9c469aff2e02.js"></script>
<script src="/~landscape/js/bundle/index.5fd962a0b23fc798e999.js"></script>
</body>
</html>

View File

@ -8,7 +8,7 @@
/- *permission-hook
/- *permission-group-hook
/- *permission-store
/- *invite-store
/- inv=invite-store
/- *metadata-store
/- *metadata-hook
/- contact-view
@ -117,7 +117,7 @@
:* %pass /permissions %agent [our.bol %permission-store] %watch
/updates
==
(invite-poke:main [%create /publish])
(invite-poke:main [%create %publish])
:* %pass /invites %agent [our.bol %invite-store] %watch
/invitatory/publish
==
@ -131,10 +131,10 @@
=+ ^- [kick-cards=(list card) old-subs=(jug @tas @p)] kick-subs
=/ inv-scry-pax
/(scot %p our.bol)/invite-store/(scot %da now.bol)/invitatory/publish/noun
=/ inv=(unit invitatory) .^((unit invitatory) %gx inv-scry-pax)
=/ invi=(unit invitatory:inv) .^((unit invitatory:inv) %gx inv-scry-pax)
=| new-state=state-two
=? tile-num.new-state ?=(^ inv)
~(wyt by u.inv)
=? tile-num.new-state ?=(^ invi)
~(wyt by u.invi)
%= $
old-state [%& %2 new-state]
::
@ -441,12 +441,12 @@
|= who=@p
^- card
=/ uid (sham %publish who book eny.bol)
=/ inv=invite
:* our.bol %publish /notebook/[book] who
=/ =invite:inv
:* our.bol %publish [our.bol book] who
(crip "invite for notebook {<our.bol>}/{(trip book)}")
==
=/ act=invite-action [%invite /publish uid inv]
[%pass /invite %agent [who %invite-hook] %poke %invite-action !>(act)]
=/ =action:inv [%invite %publish uid invite]
[%pass /invite %agent [who %invite-hook] %poke %invite-action !>(action)]
::
++ move-files
|= old-subs=(jug @tas @p)
@ -566,7 +566,7 @@
==
::
++ invite-poke
|= act=invite-action
|= act=action:inv
^- card
[%pass / %agent [our.bol %invite-store] %poke %invite-action !>(act)]
::
@ -588,11 +588,11 @@
%+ turn ~(tap in invitees)
|= who=ship
=/ uid (sham %publish who book eny.bol)
=/ inv=invite
:* our.bol %publish /(scot %p our.bol)/[book] who
=/ =invite:inv
:* our.bol %publish [our.bol book] who
(crip "invite for notebook {<our.bol>}/{(trip book)}")
==
=/ act=invite-action [%invite /publish uid inv]
=/ act=action:inv [%invite %publish uid invite]
[%pass / %agent [our.bol %invite-hook] %poke %invite-action !>(act)]
::
++ make-groups

View File

@ -0,0 +1,20 @@
:: :metadata-store|remove: remove resource from group
:: Usage:
:: :metadata-store|remove
:: <group-name> <app-name> <channel-path>
:: %urbit-community %chat /~darrux-landes/general-503
::
:: You can acquire the channel-path with
:: :metadata-store +dbug [%state '(~(got by group-indices) <group-path>)'
:: and looking for the entry with an app-path that is similar to the
:: title of the channel
::
/- *metadata-store
/+ resource
:- %say
|= $: [now=@da eny=@uvJ =beak]
[[group=term app=term =path ~] ~]
==
:- %metadata-action
^- metadata-action
[%remove (en-path:resource [p.beak group]) app path]

View File

@ -1,4 +1,5 @@
/- *invite-store
/+ resource
|%
++ slan |=(mod/@tas |=(txt/@ta (need (slaw mod txt))))
::
@ -12,9 +13,9 @@
^- json
%- pairs:enjs:format
%+ turn ~(tap by inv)
|= [=path =invitatory]
|= [=term =invitatory]
^- [cord json]
[(spat path) (invitatory-to-json invitatory)]
[term (invitatory-to-json invitatory)]
::
++ invitatory-to-json
|= =invitatory
@ -33,13 +34,13 @@
%- pairs
:~ [%ship (ship ship.invite)]
[%app [%s app.invite]]
[%path (path path.invite)]
[%resource (enjs:resource resource.invite)]
[%recipient (ship recipient.invite)]
[%text [%s text.invite]]
==
::
++ update-to-json
|= upd=invite-update
|= upd=update
=, enjs:format
^- json
%+ frond %invite-update
@ -50,15 +51,15 @@
[%initial (invites-to-json invites.upd)]
?: =(%create -.upd)
?> ?=(%create -.upd)
[%create (pairs [%path (path path.upd)]~)]
[%create (pairs [%term s+term.upd]~)]
?: =(%delete -.upd)
?> ?=(%delete -.upd)
[%delete (pairs [%path (path path.upd)]~)]
[%delete (pairs [%term s+term.upd]~)]
?: =(%accepted -.upd)
?> ?=(%accepted -.upd)
:- %accepted
%- pairs
:~ [%path (path path.upd)]
:~ [%term s+term.upd]
[%uid s+(scot %uv uid.upd)]
[%invite (invite-to-json invite.upd)]
==
@ -66,14 +67,14 @@
?> ?=(%decline -.upd)
:- %decline
%- pairs
:~ [%path (path path.upd)]
:~ [%term s+term.upd]
[%uid s+(scot %uv uid.upd)]
==
?: =(%invite -.upd)
?> ?=(%invite -.upd)
:- %invite
%- pairs
:~ [%path (path path.upd)]
:~ [%term s+term.upd]
[%uid s+(scot %uv uid.upd)]
[%invite (invite-to-json invite.upd)]
==
@ -88,53 +89,45 @@
::
++ json-to-action
|= jon=json
^- invite-action
^- action
=, dejs:format
=< (parse-json jon)
|%
++ parse-json
%- of
:~ [%create create]
[%delete delete]
:~ [%create so]
[%delete so]
[%invite invite]
[%accept accept]
[%decline decline]
==
::
++ create
(ot [%path pa]~)
::
++ delete
(ot [%path pa]~)
::
++ invite
%- ot
:~ [%path pa]
:~ [%term so]
[%uid seri]
[%invite invi]
==
::
++ accept
%- ot
:~ [%path pa]
:~ [%term so]
[%uid seri]
==
::
++ decline
%- ot
:~ [%path pa]
:~ [%term so]
[%uid seri]
==
::
++ invi
%- ot
:~ [%ship (su ;~(pfix sig fed:ag))]
[%app (se %tas)]
[%path pa]
[%app so]
[%resource dejs:resource]
[%recipient (su ;~(pfix sig fed:ag))]
[%text so]
==
--
--

View File

@ -1,6 +1,6 @@
/+ *invite-json
=, dejs:format
|_ act=invite-action
|_ act=action
++ grad %noun
++ grow
|%
@ -8,7 +8,7 @@
--
++ grab
|%
++ noun invite-action
++ noun action
++ json
|= jon=^json
(json-to-action jon)

View File

@ -1,15 +1,15 @@
/- store=invite-store
/+ *invite-json
|_ upd=invite-update
|_ =update:store
++ grad %noun
++ grow
|%
++ noun upd
++ json (update-to-json upd)
++ noun update
++ json (update-to-json update)
--
::
++ grab
|%
++ noun invite-update
++ noun update:store
--
::
--

View File

@ -1,45 +1,49 @@
/- *resource
|%
++ serial @uvH
::
+$ invite
$: =ship :: ship to subscribe to upon accepting invite
app=@tas :: app to subscribe to upon accepting invite
=path :: path to subscribe to upon accepting invite
=resource :: resource to subscribe to upon accepting invite
recipient=ship :: recipient to receive invite
text=cord :: text to describe the invite
==
::
:: +invites: each application using invites creates its own path that
+$ multi-invite
$: =ship :: ship to subscribe to upon accepting invite
app=@tas :: app to subscribe to upon accepting invite
=resource :: resource to subscribe to upon accepting invite
recipients=(set ship) :: recipient to receive invite
text=cord :: text to describe the invite
==
::
:: +invites: each application using invites creates its own resource that
:: contains a map of serial to invite. this allows it to only receive
:: invites that it is concerned with
::
+$ invites (map path invitatory) :: main data structure
+$ invites (map term invitatory) :: main data structure
::
+$ invitatory (map serial invite) :: containing or conveying an invitation
::
::
+$ invite-base
$% [%create =path] :: create a path
[%delete =path] :: delete a path
[%invite =path uid=serial =invite] :: receive an invite at path/uid
[%decline =path uid=serial] :: decline an invite at path/uid
$% [%create =term] :: create a resource
[%delete =term] :: delete a resource
[%invite =term uid=serial =invite] :: receive an invite at term/uid
[%decline =term uid=serial] :: decline an invite at term/uid
==
::
+$ invite-action
+$ action
$% invite-base
[%accept =path uid=serial] :: accept an invite at path/uid
[%accept =term uid=serial] :: accept an invite at term/uid
[%invites =term uid=serial invites=multi-invite]
==
::
+$ invite-update
+$ update
$% invite-base
[%initial =invites]
[%invitatory =invitatory] :: receive invitatory
[%accepted =path uid=serial =invite] :: an invite has been accepted
==
::
+$ invite-diff
$% [%invite-initial invites]
[%invite-update invite-update]
[%invitatory =invitatory] :: receive invitatory
[%accepted =term uid=serial =invite] :: an invite has been accepted
==
--

View File

@ -2460,7 +2460,7 @@
=/ ali-takos (reachable-takos:ze r.ali-yaki)
:: Tako worklist
::
=/ takos=(list tako) ~[r.bob-yaki]
=/ takos=(qeu tako) [r.bob-yaki ~ ~]
:: Mergebase candidates. Have proven they're common ancestors, but
:: not that they're a most recent
::
@ -2472,13 +2472,14 @@
=* outer-loop $
:: If we've finished our worklist, convert to yakis and return
::
?~ takos
?: =(~ takos)
(silt (turn ~(tap in bases) ~(got by hut.ran)))
=. done (~(put in done) i.takos)
=^ =tako takos ~(get to takos)
=. done (~(put in done) tako)
:: If this is a common ancestor, stop recursing through our
:: parentage. Check if it's comparable to any existing candidate.
::
?: (~(has in ali-takos) i.takos)
?: (~(has in ali-takos) tako)
=/ base-list ~(tap in bases)
|- ^- (set yaki)
=* bases-loop $
@ -2488,20 +2489,24 @@
:: candidate list.
::
=. bases
=/ new-reachable (reachable-takos:ze i.takos)
(~(put in (~(dif in bases) new-reachable)) i.takos)
outer-loop(takos t.takos)
=/ new-reachable (reachable-takos:ze tako)
(~(put in (~(dif in bases) new-reachable)) tako)
outer-loop
:: If it's an ancestor of another candidate, this is not most
:: recent, so skip and try next in worklist.
::
=/ base-reachable (reachable-takos:ze i.base-list)
?: (~(has in base-reachable) i.takos)
outer-loop(takos t.takos)
?: (~(has in base-reachable) tako)
outer-loop
bases-loop(base-list t.base-list)
:: Append parents to list and recurse
::
=/ bob-yaki (~(got by hut.ran) i.takos)
outer-loop(takos (weld t.takos (skip p.bob-yaki ~(has in done))))
=/ bob-yaki (~(got by hut.ran) tako)
=/ new-candidates (skip p.bob-yaki ~(has in done))
%_ outer-loop
done (~(gas in done) new-candidates)
takos (~(gas to takos) new-candidates)
==
::
:: Update mime cache
::

View File

@ -69,7 +69,7 @@
++ axle
$: :: date: date at which http-server's state was updated to this data structure
::
date=%~2020.9.30
date=%~2020.10.18
:: server-state: state of inbound requests
::
=server-state
@ -131,6 +131,12 @@
::
[%delete ~]
==
:: clog-timeout: the delay between acks after which clog-threshold kicks in
::
++ clog-timeout ~s30
:: clog-threshold: maximum per-subscription event buildup, after clog-timeout
::
++ clog-threshold 50
:: channel-timeout: the delay before a channel should be reaped
::
++ channel-timeout ~h12
@ -152,22 +158,45 @@
(can 3 a)
:: +prune-events: removes all items from the front of the queue up to :id
::
:: also produces, per request-id, the amount of events that have got acked,
:: for use with +subtract-acked-events.
::
++ prune-events
|= [q=(qeu [id=@ud lines=wall]) id=@ud]
^+ q
=| acked=(map @ud @ud)
|= [q=(qeu [id=@ud @ud channel-event]) id=@ud]
^+ [acked q]
:: if the queue is now empty, that's fine
::
?: =(~ q)
~
[acked ~]
::
=/ next=[item=[id=@ud lines=wall] _q] ~(get to q)
=/ next=[item=[id=@ud request-id=@ud channel-event] _q] ~(get to q)
:: if the head of the queue is newer than the acknowledged id, we're done
::
?: (gth id.item.next id)
q
:: otherwise, check next item
[acked q]
:: otherwise, note the ack, and check next item
::
$(q +:next)
%_ $
q +:next
::
acked
=, item.next
%+ ~(put by acked) request-id
+((~(gut by acked) request-id 0))
==
:: +subtract-acked-events: update the subscription map's pending ack counts
::
++ subtract-acked-events
|= [acked=(map @ud @ud) unacked=(map @ud @ud)]
^+ unacked
%+ roll ~(tap by acked)
|= [[rid=@ud ack=@ud] unacked=_unacked]
?~ sus=(~(get by unacked) rid)
unacked
%+ ~(put by unacked) rid
?: (lte u.sus ack) 0
(sub u.sus ack)
:: +parse-channel-request: parses a list of channel-requests
::
:: Parses a json array into a list of +channel-request. If any of the items
@ -1123,7 +1152,7 @@
%_ ..update-timeout-timer-for
session.channel-state.state
%+ ~(put by session.channel-state.state) channel-id
[[%& expiration-time duct] 0 ~ ~ ~]
[[%& expiration-time duct] 0 now ~ ~ ~ ~]
::
moves
[(set-timeout-move channel-id expiration-time) moves]
@ -1213,7 +1242,13 @@
?: =(~ queue)
events
=^ head queue ~(get to queue)
$(events [lines.p.head events])
=, p.head
::NOTE these will only fail if the mark and/or json types changed,
:: since conversion failure also gets caught during first receive.
:: we can't do anything about this, so consider it unsupported.
?~ sign=(channel-event-to-sign channel-event) $
?~ json=(sign-to-json request-id u.sign) $
$(events [(event-json-to-wall id u.json) events])
:: send the start event to the client
::
=^ http-moves state
@ -1264,7 +1299,11 @@
%+ ~(jab by session.channel-state.state) channel-id
|= =channel
^+ channel
channel(events (prune-events events.channel last-event-id))
=^ acked events.channel
(prune-events events.channel last-event-id)
=. unacked.channel
(subtract-acked-events acked unacked.channel)
channel(last-ack now)
==
:: +on-put-request: handles a PUT request
::
@ -1350,8 +1389,6 @@
%subscribe
::
=, i.requests
=/ channel-wire=wire
(channel-wire channel-id request-id)
::
=. gall-moves
:_ gall-moves
@ -1359,30 +1396,32 @@
:^ duct %pass
(subscription-wire channel-id request-id ship app)
:* %g %deal [our ship] app
`task:agent:gall`[%watch-as %json path]
`task:agent:gall`[%watch path]
==
::
=. session.channel-state.state
%+ ~(jab by session.channel-state.state) channel-id
|= =channel
channel(subscriptions (~(put by subscriptions.channel) channel-wire [ship app path duct]))
=- channel(subscriptions -)
%+ ~(put by subscriptions.channel)
request-id
[ship app path duct]
::
$(requests t.requests)
::
%unsubscribe
=/ channel-wire=wire
(channel-wire channel-id subscription-id.i.requests)
=, i.requests
::
=/ usession (~(get by session.channel-state.state) channel-id)
?~ usession
$(requests t.requests)
=/ subscriptions subscriptions:u.usession
::
?~ maybe-subscription=(~(get by subscriptions) channel-wire)
?~ maybe-subscription=(~(get by subscriptions) subscription-id)
:: the client sent us a weird request referring to a subscription
:: which isn't active.
::
~& [%missing-subscription-in-unsubscribe channel-wire]
~& [%missing-subscription-in-unsubscribe channel-id subscription-id]
$(requests t.requests)
::
=. gall-moves
@ -1398,7 +1437,10 @@
=. session.channel-state.state
%+ ~(jab by session.channel-state.state) channel-id
|= =channel
channel(subscriptions (~(del by subscriptions.channel) channel-wire))
%_ channel
subscriptions (~(del by subscriptions.channel) subscription-id)
unacked (~(del by unacked.channel) subscription-id)
==
::
$(requests t.requests)
::
@ -1410,7 +1452,7 @@
$(requests t.requests)
::
==
:: +on-gall-response: turns a gall response into an event
:: +on-gall-response: sanity-check a gall response, send as event
::
++ on-gall-response
|= [channel-id=@t request-id=@ud extra=wire =sign:agent:gall]
@ -1422,86 +1464,35 @@
:: until the source of that bug is discovered though, we keep this
:: in place to ensure a slightly tidier home.
::
?: ?& !(~(has by session.channel-state.state) channel-id)
?. ?& !(~(has by session.channel-state.state) channel-id)
?=(?(%fact %watch-ack) -.sign)
?=([@ @ ~] extra)
==
=/ =ship (slav %p i.extra)
=* app=term i.t.extra
=/ =tape
%+ weld "eyre: removing watch for "
"non-existent channel {(trip channel-id)} on {(trip app)}"
%- (slog leaf+tape ~)
:_ state
:_ ~
^- move
:^ duct %pass
(subscription-wire channel-id request-id ship app)
[%g %deal [our ship] app `task:agent:gall`[%leave ~]]
::
?- -.sign
%poke-ack
=/ =json
=, enjs:format
%- pairs :~
['response' [%s 'poke']]
['id' (numb request-id)]
?~ p.sign
['ok' [%s 'ok']]
['err' (wall (render-tang-to-wall 100 u.p.sign))]
==
::
(emit-event channel-id [(en-json:html json)]~)
::
%fact
=/ =json
=, enjs:format
%- pairs :~
['response' [%s 'diff']]
['id' (numb request-id)]
:- 'json'
?> =(%json p.cage.sign)
;;(json q.q.cage.sign)
==
::
(emit-event channel-id [(en-json:html json)]~)
::
%kick
=/ =json
=, enjs:format
%- pairs :~
['response' [%s 'quit']]
['id' (numb request-id)]
==
::
(emit-event channel-id [(en-json:html json)]~)
::
%watch-ack
=/ =json
=, enjs:format
%- pairs :~
['response' [%s 'subscribe']]
['id' (numb request-id)]
?~ p.sign
['ok' [%s 'ok']]
['err' (wall (render-tang-to-wall 100 u.p.sign))]
==
::
(emit-event channel-id [(en-json:html json)]~)
==
(emit-event channel-id request-id sign)
=/ =ship (slav %p i.extra)
=* app=term i.t.extra
=/ =tape
%+ weld "eyre: removing watch for "
"non-existent channel {(trip channel-id)} on {(trip app)}"
%- (slog leaf+tape ~)
:_ state
:_ ~
^- move
:^ duct %pass
(subscription-wire channel-id request-id ship app)
[%g %deal [our ship] app `task:agent:gall`[%leave ~]]
:: +emit-event: records an event occurred, possibly sending to client
::
:: When an event occurs, we need to record it, even if we immediately
:: send it to a connected browser so in case of disconnection, we can
:: resend it.
::
:: This function is responsible for taking the raw json lines and
:: converting them into a text/event-stream. The :event-stream-lines
:: then may get sent, and are stored for later resending until
:: acknowledged by the client.
:: This function is responsible for taking the event sign and converting
:: it into a text/event-stream. The :sign then may get sent, and is
:: stored for later resending until acknowledged by the client.
::
++ emit-event
|= [channel-id=@t json-text=wall]
|= [channel-id=@t request-id=@ud =sign:agent:gall]
^- [(list move) server-state]
::
=/ channel=(unit channel)
@ -1509,43 +1500,192 @@
?~ channel
:_ state :_ ~
[duct %pass /flog %d %flog %crud %eyre-no-channel >id=channel-id< ~]
:: attempt to convert the sign to json.
:: if conversion succeeds, we *can* send it. if the client is actually
:: connected, we *will* send it immediately.
::
=/ event-id next-id.u.channel
=/ json=(unit json)
(sign-to-json request-id sign)
=* sending &(?=([%| *] state.u.channel) ?=(^ json))
::
=/ event-stream-lines=wall
%- weld :_ [""]~
:- (weld "id: " (format-ud-as-integer event-id))
%+ turn json-text
|= =tape
(weld "data: " tape)
:: if a client is connected, send this event to them.
=/ next-id next-id.u.channel
:: if we can send it, store the event as unacked
::
=? moves ?=([%| *] state.u.channel)
=? events.u.channel ?=(^ json)
%- ~(put to events.u.channel)
[next-id request-id (sign-to-channel-event sign)]
:: if it makes sense to do so, send the event to the client
::
=? moves sending
^- (list move)
:_ moves
::NOTE assertions in this block because =* is flimsy
?> ?=([%| *] state.u.channel)
:+ p.state.u.channel %give
^- gift:able
:* %response %continue
::
^= data
%- wall-to-octs
(event-json-to-wall next-id (need json))
::
complete=%.n
==
=? next-id ?=(^ json) +(next-id)
:: update channel's unacked counts, find out if clogged
::
=^ clogged unacked.u.channel
:: poke-acks are one-offs, don't apply clog logic to them.
:: and of course don't count events we can't send as unacked.
::
?: ?| ?=(%poke-ack -.sign)
?=(~ json)
==
[| unacked.u.channel]
=/ num=@ud
(~(gut by unacked.u.channel) request-id 0)
:_ (~(put by unacked.u.channel) request-id +(num))
?& (gte num clog-threshold)
(lth (add last-ack.u.channel clog-timeout) now)
==
~? clogged [%e %clogged channel-id request-id]
:: if we're clogged, or we ran into an event we can't serialize,
:: kill this gall subscription.
::
=* kicking |(clogged ?=(~ json))
=? moves kicking
:_ moves
=+ (~(got by subscriptions.u.channel) request-id)
:^ duct %pass
(subscription-wire channel-id request-id ship app)
[%g %deal [our ship] app %leave ~]
:: update channel state to reflect the %kick
::
=? u.channel kicking
%_ u.channel
subscriptions (~(del by subscriptions.u.channel) request-id)
unacked (~(del by unacked.u.channel) request-id)
events %- ~(put to events.u.channel)
[next-id request-id (sign-to-channel-event %kick ~)]
==
:: if a client is connected, send the kick event to them
::
=? moves &(kicking ?=([%| *] state.u.channel))
:_ moves
:+ p.state.u.channel %give
^- gift:able
:* %response %continue
::
^= data
:- ~
%- as-octs:mimes:html
(crip (of-wall:format event-stream-lines))
%- wall-to-octs
%+ event-json-to-wall next-id
(need (sign-to-json request-id %kick ~))
::
complete=%.n
==
=? next-id kicking +(next-id)
::
:- moves
%_ state
session.channel-state
%+ ~(jab by session.channel-state.state) channel-id
|= =^channel
^+ channel
%+ ~(put by session.channel-state.state) channel-id
u.channel(next-id next-id)
==
:: +sign-to-channel-event: strip the vase from a sign:agent:gall
::
++ sign-to-channel-event
|= =sign:agent:gall
^- channel-event
?. ?=(%fact -.sign) sign
[%fact [p q.q]:cage.sign]
:: +channel-event-to-sign: attempt to recover a sign from a channel-event
::
++ channel-event-to-sign
|= event=channel-event
^- (unit sign:agent:gall)
?. ?=(%fact -.event) `event
:: rebuild vase for fact data
::
=* have=mark mark.event
=/ val=(unit (unit cage))
(scry [%141 %noun] ~ %cb [our %home da+now] /[have])
?. ?=([~ ~ *] val)
((slog leaf+"eyre: no mark {(trip have)}" ~) ~)
=+ !<(=dais:clay q.u.u.val)
=/ res (mule |.((vale:dais noun.event)))
?: ?=(%| -.res)
((slog leaf+"eyre: stale fact of mark {(trip have)}" ~) ~)
`[%fact have p.res]
:: +sign-to-json: render sign from request-id as json channel event
::
++ sign-to-json
|= [request-id=@ud =sign:agent:gall]
^- (unit json)
:: for facts, we try to convert the result to json
::
=/ jsyn=(unit sign:agent:gall)
?. ?=(%fact -.sign) `sign
?: ?=(%json p.cage.sign) `sign
:: find and use tube from fact mark to json
::
%_ channel
next-id +(next-id.channel)
events (~(put to events.channel) [event-id event-stream-lines])
=* have=mark p.cage.sign
=* desc=tape "from {(trip have)} to json"
=/ tube=(unit tube:clay)
=/ tuc=(unit (unit cage))
(scry [%141 %noun] ~ %cc [our %home da+now] (flop /[have]/json))
?. ?=([~ ~ *] tuc) ~
`!<(tube:clay q.u.u.tuc)
?~ tube
((slog leaf+"eyre: no tube {desc}" ~) ~)
::
=/ res (mule |.((u.tube q.cage.sign)))
?: ?=(%& -.res)
`[%fact %json p.res]
((slog leaf+"eyre: failed tube {desc}" ~) ~)
::
?~ jsyn ~
%- some
=* sign u.jsyn
=, enjs:format
%- pairs
^- (list [@t json])
:- ['id' (numb request-id)]
?- -.sign
%poke-ack
:~ ['response' [%s 'poke']]
::
?~ p.sign
['ok' [%s 'ok']]
['err' (wall (render-tang-to-wall 100 u.p.sign))]
==
::
%fact
:~ ['response' [%s 'diff']]
::
:- 'json'
~| [%unexpected-fact-mark p.cage.sign]
?> =(%json p.cage.sign)
;;(json q.q.cage.sign)
==
::
%kick
['response' [%s 'quit']]~
::
%watch-ack
:~ ['response' [%s 'subscribe']]
::
?~ p.sign
['ok' [%s 'ok']]
['err' (wall (render-tang-to-wall 100 u.p.sign))]
==
==
::
++ event-json-to-wall
|= [event-id=@ud =json]
^- wall
:~ (weld "id: " (format-ud-as-integer event-id))
(weld "data: " (en-json:html json))
""
==
::
++ on-channel-heartbeat
@ -1607,10 +1747,10 @@
:: produce a list of moves which cancels every gall subscription
::
%+ turn ~(tap by subscriptions.session)
|= [channel-wire=wire ship=@p app=term =path duc=^duct]
|= [request-id=@ud ship=@p app=term =path duc=^duct]
^- move
:^ duc %pass
(weld channel-wire /(scot %p ship)/[app])
(subscription-wire channel-id request-id ship app)
[%g %deal [our ship] app %leave ~]
--
:: +handle-gall-error: a call to +poke-http-response resulted in a %coup
@ -2339,6 +2479,34 @@
::
++ load
=> |%
+$ axle-2020-9-30
[date=%~2020.9.30 server-state=server-state-2020-9-30]
::
+$ server-state-2020-9-30
$: bindings=(list [=binding =duct =action])
=cors-registry
connections=(map duct outstanding-connection)
=authentication-state
channel-state=channel-state-2020-9-30
domains=(set turf)
=http-config
ports=[insecure=@ud secure=(unit @ud)]
outgoing-duct=duct
==
::
+$ channel-state-2020-9-30
$: session=(map @t channel-2020-9-30)
duct-to-key=(map duct @t)
==
::
+$ channel-2020-9-30
$: state=(each timer duct)
next-id=@ud
events=(qeu [id=@ud lines=wall])
subscriptions=(map wire [ship=@p app=term =path duc=duct])
heartbeat=(unit timer)
==
::
+$ axle-2020-5-29
[date=%~2020.5.29 server-state=server-state-2020-5-29]
::
@ -2346,7 +2514,7 @@
$: bindings=(list [=binding =duct =action])
connections=(map duct outstanding-connection)
=authentication-state
=channel-state
channel-state=channel-state-2020-9-30
domains=(set turf)
=http-config
ports=[insecure=@ud secure=(unit @ud)]
@ -2359,19 +2527,41 @@
$: bindings=(list [=binding =duct =action])
connections=(map duct outstanding-connection)
authentication-state=sessions=(map @uv @da)
=channel-state
channel-state=channel-state-2020-9-30
domains=(set turf)
=http-config
ports=[insecure=@ud secure=(unit @ud)]
outgoing-duct=duct
==
--
|= old=$%(axle axle-2019-10-6 axle-2020-5-29)
|= old=$%(axle axle-2019-10-6 axle-2020-5-29 axle-2020-9-30)
^+ ..^$
::
~! %loading
?- -.old
%~2020.9.30 ..^$(ax old)
%~2020.10.18 ..^$(ax old)
::
%~2020.9.30
%_ $
date.old %~2020.10.18
::
::NOTE soft-breaks the reconnect case, but is generally less disruptive
:: than wiping channels entirely.
session.channel-state.server-state.old
%- ~(run by session.channel-state.server-state.old)
|= channel-2020-9-30
^- channel
=/ subscriptions
%- ~(gas by *(map @ud [@p term path duct]))
%+ turn ~(tap by subscriptions)
|= [=wire rest=[@p term path duct]]
[(slav %ud (snag 3 wire)) rest]
:* state next-id now
*(qeu [@ud @ud channel-event])
*(map @ud @ud)
subscriptions heartbeat
==
==
::
%~2020.5.29
%_ $

View File

@ -1358,6 +1358,14 @@
::
=duct
==
:: channel-event: unacknowledged channel event, vaseless sign
::
+$ channel-event
$% $>(%poke-ack sign:agent:gall)
$>(%watch-ack sign:agent:gall)
$>(%kick sign:agent:gall)
[%fact =mark =noun]
==
:: channel: connection to the browser
::
:: Channels are the main method where a webpage communicates with Gall
@ -1384,6 +1392,11 @@
:: next-id: next sequence number to use
::
next-id=@ud
:: last-ack: time of last client ack
::
:: used for clog calculations, in combination with :unacked
::
last-ack=@da
:: events: unacknowledged events
::
:: We keep track of all events where we haven't received a
@ -1392,13 +1405,18 @@
:: channel, we send the event but we still add it to events because we
:: can't assume it got received until we get an acknowledgment.
::
events=(qeu [id=@ud lines=wall])
:: subscriptions: gall subscriptions
events=(qeu [id=@ud request-id=@ud =channel-event])
:: unacked: unacknowledged event counts by request-id
::
:: used for clog calculations, in combination with :last-ack
::
unacked=(map @ud @ud)
:: subscriptions: gall subscriptions by request-id
::
:: We maintain a list of subscriptions so if a channel times out, we
:: can cancel all the subscriptions we've made.
::
subscriptions=(map wire [ship=@p app=term =path duc=duct])
subscriptions=(map @ud [ship=@p app=term =path duc=duct])
:: heartbeat: sse heartbeat timer
::
heartbeat=(unit timer)

View File

@ -1,4 +1,9 @@
/- spider, graph=graph-store, *metadata-store, *group, group-store
/- spider,
graph=graph-store,
*metadata-store,
*group,
group-store,
inv=invite-store
/+ strandio, resource, graph-view
=>
|%
@ -27,6 +32,7 @@
=+ !<([=action:graph-view ~] arg)
?> ?=(%create -.action)
;< =bowl:spider bind:m get-bowl:strandio
::
:: Add graph to graph-store
::
?. =(our.bowl entity.rid.action)
@ -37,12 +43,14 @@
(poke-our %graph-store graph-update+!>(update))
;< ~ bind:m
(poke-our %graph-push-hook %push-hook-action !>([%add rid.action]))
::
:: Add group, if graph is unmanaged
::
;< group=resource bind:m
(handle-group rid.action associated.action)
=/ group-path=path
(en-path:resource group)
::
:: Setup metadata
::
=/ =metadata
@ -53,9 +61,30 @@
creator our.bowl
module module.action
==
=/ act=metadata-action
=/ =metadata-action
[%add group-path graph+(en-path:resource rid.action) metadata]
;< ~ bind:m (poke-our %metadata-hook %metadata-action !>(act))
;< ~ bind:m
(poke-our %metadata-hook %metadata-action !>(metadata-action))
;< ~ bind:m
(poke-our %metadata-hook %metadata-hook-action !>([%add-owned group-path]))
(pure:m !>(~))
::
:: Send invites
::
?: ?=(%group -.associated)
(pure:m !>(~))
?- -.policy.associated.action
%group (pure:m !>(~))
%invite
=/ inv-action=action:inv
:^ %invites %graph (shaf %graph-uid eny.bowl)
^- multi-invite:inv
:* our.bowl
%graph-push-hook
rid.action
pending.policy.associated.action
description.action
==
;< ~ bind:m
(poke-our %invite-hook %invite-action !>(inv-action))
(pure:m !>(~))
==

View File

@ -1308,7 +1308,7 @@
%+ expect-gall-deal
:* /channel/subscription/'0123456789abcdef'/'2'/~nul/two
[~nul ~nul] %two
%watch-as %json /one/two/three
%watch /one/two/three
==
card.i.moves
::
@ -1516,16 +1516,27 @@
==
::
++ test-prune-events
=/ q=(qeu [id=@ud lines=wall]) ~
=. q (~(put to q) [0 ~])
=. q (~(put to q) [1 ~])
=. q (~(put to q) [2 ~])
=. q (~(put to q) [3 ~])
=. q (~(put to q) [4 ~])
=/ q=(qeu [id=@ud @ud channel-event:eyre]) ~
=. q (~(put to q) [0 0 *channel-event:eyre])
=. q (~(put to q) [1 0 *channel-event:eyre])
=. q (~(put to q) [2 0 *channel-event:eyre])
=. q (~(put to q) [3 1 *channel-event:eyre])
=. q (~(put to q) [4 1 *channel-event:eyre])
::
=. q (prune-events:eyre-gate q 3)
=^ a q (prune-events:eyre-gate q 3)
::
(expect-eq !>([~ [4 ~]]) !>(~(top to q)))
%+ expect-eq
!>
:- (~(gas by *(map @ud @ud)) ~[0^3 1^1])
[~ [4 1 *channel-event:eyre]]
!>([a ~(top to q)])
::
++ test-subtract-acked-events
=/ a (~(gas by *(map @ud @ud)) ~[0^3 1^1])
=/ u (~(gas by *(map @ud @ud)) ~[0^4 2^1])
=/ e (~(gas by *(map @ud @ud)) ~[0^1 2^1])
=/ r (subtract-acked-events:eyre-gate a u)
(expect-eq !>(e) !>(r))
::
++ test-channel-sends-unacknowledged-events-on-reconnection
:: common initialization
@ -1796,6 +1807,123 @@
results9
==
::
++ test-channel-subscription-clogged
:: common initialization
::
=^ tested-elsewhere eyre-gate
(perform-init-start-channel eyre-gate *sley)
::
=/ now=@da :(add ~1111.1.2 clog-timeout:eyre-gate ~s1)
:: subscription gets a success message
::
=^ tested-elsewhere eyre-gate
%: eyre-take
eyre-gate
now
scry=scry-provides-code
^= take-args
:* wire=/channel/subscription/'0123456789abcdef'/'1'/~nul/two
duct=~[/http-put-request]
^- (hypo sign:eyre-gate)
:- *type
[%g %unto %watch-ack ~]
==
moves=~
==
:: opens the http channel
::
=^ tested-elsewhere eyre-gate
%: eyre-call
eyre-gate
now
scry=scry-provides-code
^= call-args
^- [duct * (hobo task:able:eyre-gate)]
:* duct=~[/http-get-open] ~
%request
%.n
[%ipv4 .192.168.1.1]
%'GET'
'/~/channel/0123456789abcdef'
['cookie' cookie-value]~
~
==
^= expected-moves
~ ::NOTE tested elsewher
==
:: user gets sent multiple subscription results
::
=/ max=@ud (dec clog-threshold:eyre-gate)
=/ cur=@ud 0
|- =* loop-fact $
?. =(cur max)
=^ tested-elsewhere eyre-gate
%: eyre-take
eyre-gate
now
scry=scry-provides-code
^= take-args
:* wire=/channel/subscription/'0123456789abcdef'/'1'/~nul/two
duct=~[/http-put-request]
^- (hypo sign:eyre-gate)
:- *type
[%g %unto %fact %json !>(`json`[%a [%n '1'] ~])]
==
^= moves
~ ::NOTE tested elsewhere
==
loop-fact(cur +(cur))
:: the next subscription result should trigger a clog
::
=^ results eyre-gate
%: eyre-take
eyre-gate
now
scry=scry-provides-code
^= take-args
:* wire=/channel/subscription/'0123456789abcdef'/'1'/~nul/two
duct=~[/http-put-request]
^- (hypo sign:eyre-gate)
:- *type
[%g %unto %fact %json !>(`json`[%a [%n '1'] ~])]
==
^= moves
:~ :* duct=~[/http-get-open]
%give
%response
%continue
:- ~
%- as-octt:mimes:html
"""
id: {((d-co:co 1) +(clog-threshold:eyre-gate))}
data: \{"id":1,"response":"quit"}
"""
complete=%.n
==
:* duct=~[/http-put-request] %pass
/channel/subscription/'0123456789abcdef'/'1'/~nul/two
%g %deal [~nul ~nul] %two %leave ~
==
:* duct=~[/http-get-open]
%give
%response
%continue
:- ~
%- as-octt:mimes:html
"""
id: {((d-co:co 1) clog-threshold:eyre-gate)}
data: \{"json":[1],"id":1,"response":"diff"}
"""
complete=%.n
==
==
==
results
::
++ test-born-sends-pending-cancels
::
=^ results1 eyre-gate
@ -2027,7 +2155,7 @@
::
?: ?=([%watch *] deal.expected)
?. ?=([%watch *] r.note)
[%leaf "expected %watch-as, actual {<r.note>}"]~
[%leaf "expected %watch, actual {<r.note>}"]~
:: compare the path
::
(expect-eq !>(path.deal.expected) !>(path.r.note))
@ -2201,7 +2329,7 @@
%+ expect-gall-deal
:* /channel/subscription/'0123456789abcdef'/'1'/~nul/two
[~nul ~nul] %two
%watch-as %json /one/two/three
%watch /one/two/three
==
card.i.t.moves
::
@ -2226,6 +2354,19 @@
?: &(=(%ca term) =(/hoon/handler/gen s.beam))
:+ ~ ~
vase+!>(!>(|=(* |=(* [[%404 ~] ~]))))
?: &(=(%cb term) =(/json s.beam))
:^ ~ ~ %dais
!> ^- dais:clay
|_ sam=vase
++ bunt !!
++ diff !!
++ form !!
++ join !!
++ mash !!
++ pact !!
++ vale |=(=noun !>(;;(json noun)))
++ volt !!
--
::
?> =(%j term)
?> =(~nul p.beam)

View File

@ -109,6 +109,10 @@ export class Eyre extends Component {
<td class="inter">next-id</td>
<td>{c['next-id']}</td>
</tr>
<tr>
<td class="inter">last-ack</td>
<td>{msToDa(c['last-ack'])}</td>
</tr>
<tr>
<td class="inter">unacked</td>
<td>{c.unacked.reduce((a, b) => a + b + ', ', '')}</td>
@ -117,27 +121,30 @@ export class Eyre extends Component {
</>);
const subscriptionItems = c.subscriptions.map(s => {
//NOTE jsx sorta copied from /components/subscriptions
return {key: `${s.wire} ${s.app} ${s.ship} ${s.path}`, jsx: (
return {key: `${s.id} ${s.app} ${s.ship} ${s.path}`, jsx: (
<div class="flex">
<div class="flex-auto" style={{maxWidth: '35%'}}>
{s.wire}
<div class="flex-auto" style={{maxWidth: '15%'}}>
{s.id}
</div>
<div class="flex-auto" style={{maxWidth: '15%'}}>
~{s.ship}
</div>
<div class="flex-auto" style={{maxWidth: '15%'}}>
<div class="flex-auto" style={{maxWidth: '20%'}}>
{s.app}
</div>
<div class="flex-auto" style={{maxWidth: '35%'}}>
{s.path}
</div>
<div class="flex-auto" style={{maxWidth: '15%'}}>
{s.unacked}
</div>
</div>
)};
});
return {key: c.session, jsx: (
<Summary summary={summary} details={(
<SearchableList
placeholder="wire, app, ship, path"
placeholder="id, app, ship, path"
items={subscriptionItems}
/>
)} />

View File

@ -1585,22 +1585,6 @@
}
}
},
"@react-dnd/asap": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz",
"integrity": "sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ=="
},
"@react-dnd/invariant": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz",
"integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw=="
},
"@react-dnd/shallowequal": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz",
"integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==",
"dev": true
},
"@styled-system/background": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@styled-system/background/-/background-5.1.2.tgz",
@ -3909,21 +3893,6 @@
"randombytes": "^2.0.0"
}
},
"dnd-core": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-11.1.3.tgz",
"integrity": "sha512-QugF55dNW+h+vzxVJ/LSJeTeUw9MCJ2cllhmVThVPEtF16ooBkxj0WBE5RB+AceFxMFo1rO6bJKXtqKl+JNnyA==",
"requires": {
"@react-dnd/asap": "^4.0.0",
"@react-dnd/invariant": "^2.0.0",
"redux": "^4.0.4"
}
},
"dnd-multi-backend": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/dnd-multi-backend/-/dnd-multi-backend-6.0.0.tgz",
"integrity": "sha512-qfUO4V0IACs24xfE9m9OUnwIzoL+SWzSiFbKVIHE0pFddJeZ93BZOdHS1XEYr8X3HNh+CfnfjezXgOMgjvh74g=="
},
"dns-equal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz",
@ -7872,53 +7841,6 @@
"resolved": "https://registry.npmjs.org/react-codemirror2/-/react-codemirror2-6.0.1.tgz",
"integrity": "sha512-rutEKVgvFhWcy/GeVA1hFbqrO89qLqgqdhUr7YhYgIzdyICdlRQv+ztuNvOFQMXrO0fLt0VkaYOdMdYdQgsSUA=="
},
"react-dnd": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-11.1.3.tgz",
"integrity": "sha512-8rtzzT8iwHgdSC89VktwhqdKKtfXaAyC4wiqp0SywpHG12TTLvfOoL6xNEIUWXwIEWu+CFfDn4GZJyynCEuHIQ==",
"dev": true,
"requires": {
"@react-dnd/shallowequal": "^2.0.0",
"@types/hoist-non-react-statics": "^3.3.1",
"dnd-core": "^11.1.3",
"hoist-non-react-statics": "^3.3.0"
}
},
"react-dnd-html5-backend": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-11.1.3.tgz",
"integrity": "sha512-/1FjNlJbW/ivkUxlxQd7o3trA5DE33QiRZgxent3zKme8DwF4Nbw3OFVhTRFGaYhHFNL1rZt6Rdj1D78BjnNLw==",
"requires": {
"dnd-core": "^11.1.3"
}
},
"react-dnd-multi-backend": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/react-dnd-multi-backend/-/react-dnd-multi-backend-6.0.2.tgz",
"integrity": "sha512-SwpqRv0HkJYu244FbHf9NbvGzGy14Ir9wIAhm909uvOVaHgsOq6I1THMSWSgpwUI31J3Bo5uS19tuvGpVPjzZw==",
"requires": {
"dnd-multi-backend": "^6.0.0",
"prop-types": "^15.7.2",
"react-dnd-preview": "^6.0.2"
}
},
"react-dnd-preview": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/react-dnd-preview/-/react-dnd-preview-6.0.2.tgz",
"integrity": "sha512-F2+uK4Be+q+7mZfNh9kaZols7wp1hX6G7UBTVaTpDsBpMhjFvY7/v7odxYSerSFBShh23MJl33a4XOVRFj1zoQ==",
"requires": {
"prop-types": "^15.7.2"
}
},
"react-dnd-touch-backend": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-11.1.3.tgz",
"integrity": "sha512-8lz4fxfYwUuJ6Y2seQYwh8+OfwKcbBX0CIbz7AwXfBYz54Wg2nIDU6CP8Dyybt/Wyx4D3oXmTPEaOMB62uqJvQ==",
"requires": {
"@react-dnd/invariant": "^2.0.0",
"dnd-core": "^11.1.3"
}
},
"react-dom": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz",
@ -8095,15 +8017,6 @@
"picomatch": "^2.2.1"
}
},
"redux": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
"integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==",
"requires": {
"loose-envify": "^1.4.0",
"symbol-observable": "^1.2.0"
}
},
"regenerate": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
@ -9446,11 +9359,6 @@
"xml-reader": "2.4.3"
}
},
"symbol-observable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
},
"synchronous-promise": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.13.tgz",
@ -10204,8 +10112,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
@ -10226,14 +10133,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -10248,20 +10153,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@ -10378,8 +10280,7 @@
"inherits": {
"version": "2.0.4",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@ -10391,7 +10292,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -10406,7 +10306,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -10414,14 +10313,12 @@
"minimist": {
"version": "1.2.5",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.9.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -10440,7 +10337,6 @@
"version": "0.5.3",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "^1.2.5"
}
@ -10502,8 +10398,7 @@
"npm-normalize-package-bin": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"npm-packlist": {
"version": "1.4.8",
@ -10531,8 +10426,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@ -10544,7 +10438,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -10622,8 +10515,7 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
@ -10659,7 +10551,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -10679,7 +10570,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -10723,14 +10613,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"yallist": {
"version": "3.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
}
}
},
@ -11211,8 +11099,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
@ -11233,14 +11120,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -11255,20 +11140,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@ -11385,8 +11267,7 @@
"inherits": {
"version": "2.0.4",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@ -11398,7 +11279,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -11413,7 +11293,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -11421,14 +11300,12 @@
"minimist": {
"version": "1.2.5",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.9.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -11447,7 +11324,6 @@
"version": "0.5.3",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "^1.2.5"
}
@ -11509,8 +11385,7 @@
"npm-normalize-package-bin": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"npm-packlist": {
"version": "1.4.8",
@ -11538,8 +11413,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@ -11551,7 +11425,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -11629,8 +11502,7 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
@ -11666,7 +11538,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -11686,7 +11557,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -11730,14 +11600,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"yallist": {
"version": "3.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
}
}
},

View File

@ -26,9 +26,6 @@
"prop-types": "^15.7.2",
"react": "^16.5.2",
"react-codemirror2": "^6.0.1",
"react-dnd-html5-backend": "^11.1.3",
"react-dnd-multi-backend": "^6.0.2",
"react-dnd-touch-backend": "^11.1.3",
"react-dom": "^16.8.6",
"react-helmet": "^6.1.0",
"react-markdown": "^4.3.1",
@ -73,7 +70,6 @@
"file-loader": "^6.0.0",
"html-webpack-plugin": "^4.2.0",
"moment-locales-webpack-plugin": "^1.2.0",
"react-dnd": "^11.1.3",
"react-hot-loader": "^4.12.21",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",

View File

@ -3,25 +3,25 @@ import { StoreState } from "../store/type";
import { Serial, Path } from "~/types/noun";
export default class InviteApi extends BaseApi<StoreState> {
accept(app: Path, uid: Serial) {
accept(app: string, uid: Serial) {
return this.inviteAction({
accept: {
path: app,
term: app,
uid
}
});
}
decline(app: Path, uid: Serial) {
decline(app: string, uid: Serial) {
return this.inviteAction({
decline: {
path: app,
term: app,
uid
}
});
}
private inviteAction(action) {
return this.action('invite-store', 'json', action);
return this.action('invite-store', 'invite-action', action);
}
}

View File

@ -1,9 +1,7 @@
import BaseApi from './base';
import { StoreState } from '../store/type';
export default class LaunchApi extends BaseApi<StoreState> {
add(name: string, tile = { basic : { title: '', linkedUrl: '', iconUrl: '' }}) {
return this.launchAction({ add: { name, tile } });
}
@ -12,10 +10,6 @@ export default class LaunchApi extends BaseApi<StoreState> {
return this.launchAction({ remove: name });
}
changeOrder(orderedTiles: string[] = []) {
return this.launchAction({ 'change-order': orderedTiles });
}
changeFirstTime(firstTime = true) {
return this.launchAction({ 'change-first-time': firstTime });
}
@ -31,6 +25,5 @@ export default class LaunchApi extends BaseApi<StoreState> {
private launchAction(data) {
return this.action('launch', 'launch-action', data);
}
}

View File

@ -1,5 +1,6 @@
import React, { memo } from 'react';
import { sigil, reactRenderer } from '@tlon/sigil-js';
import { Box } from '@tlon/indigo-react';
export const foregroundFromBackground = (background) => {
const rgb = {
@ -11,30 +12,42 @@ export const foregroundFromBackground = (background) => {
const whiteBrightness = 255;
return ((whiteBrightness - brightness) < 50) ? 'black' : 'white';
}
};
export const Sigil = memo(({ classes = '', color, ship, size, svgClass = '', icon = false }) => {
export const Sigil = memo(({ classes = '', color, foreground = '', ship, size, svgClass = '', icon = false, padded = false }) => {
const padding = (icon && padded) ? '2px' : '0px';
const innerSize = (icon && padded) ? (Number(size) - 4) : size;
const foregroundColor = foreground ? foreground : foregroundFromBackground(color);
return ship.length > 14
? (<div
className={'bg-black dib ' + classes}
style={{ width: size, height: size }}>
</div>)
: (<div
className={'dib ' + classes}
style={{ flexBasis: size, backgroundColor: color }}
>
? (<Box
backgroundColor='black'
borderRadius={icon ? '1' : '0'}
display='inline-block'
height={size}
width={size}
/>) : (
<Box
display='inline-block'
borderRadius={icon ? '1' : '0'}
flexBasis={size}
backgroundColor={color}
padding={padding}
className={classes}
>
{sigil({
patp: ship,
renderer: reactRenderer,
size: size,
size: innerSize,
icon,
colors: [
color,
foregroundFromBackground(color)
foregroundColor
],
class: svgClass
})}
</div>)
})
</Box>);
});
Sigil.displayName = 'Sigil';
export default Sigil;

View File

@ -7,6 +7,10 @@ export function appIsGraph(app) {
return app === 'link' || app === 'publish';
}
export function parentPath(path) {
return _.dropRight(path.split('/'), 1).join('/');
}
export function clamp(x,min,max) {
return Math.max(min, Math.min(max, x));
}

View File

@ -1,6 +1,16 @@
import _ from 'lodash';
import { OrderedMap } from "~/logic/lib/OrderedMap";
const DA_UNIX_EPOCH = 170141184475152167957503069145530368000;
const normalizeKey = (key) => {
if(key > DA_UNIX_EPOCH) {
// new links uses milliseconds since unix epoch
// old (pre-graph-store) use @da
// ported from +time:enjs:format in hoon.hoon
return Math.round((1000 * (9223372036854775 + (key - DA_UNIX_EPOCH))) / 18446744073709551616);
}
return key;
}
export const GraphReducer = (json, state) => {
const data = _.get(json, 'graph-update', false);
@ -29,6 +39,8 @@ const addGraph = (json, state) => {
// is empty
if (!node.children) {
node.children = new OrderedMap();
node.post.originalIndex = node.post.index;
node.post.index = node.post.index.split('/').map(x => x.length === 0 ? '' : normalizeKey(parseInt(x, 10))).join('/');
return node;
}
@ -41,13 +53,18 @@ const addGraph = (json, state) => {
});
if (index.length === 0) { break; }
const normalKey = normalizeKey(index[index.length - 1]);
item[1].post.originalKey = index[index.length - 1];
converted.set(
index[index.length - 1],
normalKey,
_processNode(item[1])
);
}
node.children = converted;
node.post.originalIndex = node.post.index;
node.post.index = node.post.index.split('/').map(x => x.length === 0 ? '' : normalizeKey(parseInt(x, 10))).join('/');
return node;
};
@ -69,7 +86,10 @@ const addGraph = (json, state) => {
if (index.length === 0) { break; }
let node = _processNode(item[1]);
state.graphs[resource].set(index[index.length - 1], node);
const normalKey = normalizeKey(index[index.length - 1])
node.post.originalKey = index[index.length - 1];
state.graphs[resource].set(normalKey, node);
}
state.graphKeys.add(resource);
}
@ -91,7 +111,7 @@ const mapifyChildren = (children) => {
return new OrderedMap(
children.map(([idx, node]) => {
const nd = {...node, children: mapifyChildren(node.children || []) };
return [parseInt(idx.slice(1), 10), nd];
return [normalizeKey(parseInt(idx.slice(1), 10)), nd];
}));
};
@ -99,18 +119,23 @@ const addNodes = (json, state) => {
const _addNode = (graph, index, node) => {
// set child of graph
if (index.length === 1) {
graph.set(index[0], node);
node.post.originalIndex = node.post.index;
node.post.index = node.post.index.split('/').map(x => x.length === 0 ? '' : normalizeKey(parseInt(x, 10))).join('/');
const normalKey = normalizeKey(index[0])
node.post.originalKey = index[0];
graph.set(normalKey, node);
return graph;
}
// set parent of graph
let parNode = graph.get(index[0]);
let parNode = graph.get(normalizeKey(index[0]));
if (!parNode) {
console.error('parent node does not exist, cannot add child');
return;
}
parNode.children = _addNode(parNode.children, index.slice(1), node);
graph.set(index[0], parNode);
graph.set(normalizeKey(index[0]), parNode);
return graph;
};
@ -133,6 +158,7 @@ const addNodes = (json, state) => {
item[1].children = mapifyChildren(item[1].children || []);
state.graphs[resource] = _addNode(
state.graphs[resource],
@ -148,20 +174,18 @@ const removeNodes = (json, state) => {
if (index.length === 1) {
graph.delete(index[0]);
} else {
const child = graph.get(index[0]);
const child = graph.get(normalizeKey(index[0]));
_remove(child.children, index.slice(1));
graph.set(index[0], child);
graph.set(normalizeKey(index[0]), child);
}
};
const data = _.get(json, 'remove-nodes', false);
if (data) {
console.log(data);
const { ship, name } = data.resource;
const res = `${ship}/${name}`;
if (!(res in state.graphs)) { return; }
data.indices.forEach((index) => {
console.log(index);
if (index.split('/').length === 0) { return; }
let indexArr = index.split('/').slice(1).map((ind) => {
return parseInt(ind, 10);

View File

@ -29,35 +29,35 @@ export default class InviteReducer<S extends InviteState> {
create(json: InviteUpdate, state: S) {
const data = _.get(json, 'create', false);
if (data) {
state.invites[data.path] = {};
state.invites[data] = {};
}
}
delete(json: InviteUpdate, state: S) {
const data = _.get(json, 'delete', false);
if (data) {
delete state.invites[data.path];
delete state.invites[data];
}
}
invite(json: InviteUpdate, state: S) {
const data = _.get(json, 'invite', false);
if (data) {
state.invites[data.path][data.uid] = data.invite;
state.invites[data.term][data.uid] = data.invite;
}
}
accepted(json: InviteUpdate, state: S) {
const data = _.get(json, 'accepted', false);
if (data) {
delete state.invites[data.path][data.uid];
delete state.invites[data.term][data.uid];
}
}
decline(json: InviteUpdate, state: S) {
const data = _.get(json, 'decline', false);
if (data) {
delete state.invites[data.path][data.uid];
delete state.invites[data.term][data.uid];
}
}
}

View File

@ -49,6 +49,7 @@ const Root = styled.div`
/* Works on Chrome/Edge/Safari */
*::-webkit-scrollbar {
width: 6px;
height: 6px;
}
*::-webkit-scrollbar-track {
background: transparent;

View File

@ -114,7 +114,6 @@ export function ChatResource(props: ChatResourceProps) {
group={group}
ship={owner}
station={station}
allStations={Object.keys(props.inbox)}
api={props.api}
hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars}

View File

@ -185,6 +185,8 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
size={16}
color={`#${color}`}
classes={sigilClass}
icon
padded
/>;
return (
@ -197,6 +199,7 @@ export default class ChatInput extends Component<ChatInputProps, ChatInputState>
borderTopColor='washedGray'
backgroundColor='white'
className='cf'
zIndex='0'
>
<div className="pa2 flex items-center">
{avatar}

View File

@ -1,7 +1,7 @@
import React, { Component, PureComponent } from "react";
import moment from "moment";
import _ from "lodash";
import { Box, Row, Text } from "@tlon/indigo-react";
import { Box, Row, Text, Rule } from "@tlon/indigo-react";
import { OverlaySigil } from './overlay-sigil';
import { uxToHex, cite, writeText } from '~/logic/lib/util';
@ -14,20 +14,20 @@ import RemoteContent from '~/views/components/RemoteContent';
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
export const UnreadMarker = React.forwardRef(({ dayBreak, when }, ref) => (
<div ref={ref} style={{ color: "#219dff" }} className="flex items-center f9 absolute w-100 left-0 pv0">
<hr style={{ borderColor: "#219dff" }} className="dn-s ma0 w2 bt-0" />
<p className="mh4 z-2" style={{ whiteSpace: 'normal' }}>New messages below</p>
<hr style={{ borderColor: "#219dff" }} className="ma0 flex-grow-1 bt-0" />
<Row ref={ref} color='blue' alignItems='center' fontSize='0' position='absolute' width='100%' py='2'>
<Rule borderColor='blue' display={['none', 'block']} m='0' width='2rem' />
<Text flexShrink='0' display='block' zIndex='2' mx='4' color='blue'>New messages below</Text>
<Rule borderColor='blue' flexGrow='1' m='0'/>
{dayBreak
? <p className="gray2 mh4">{moment(when).calendar()}</p>
? <Text display='block' gray mx='4'>{moment(when).calendar(null, { sameElse: DATESTAMP_FORMAT })}</Text>
: null}
<hr style={{ width: "calc(50% - 48px)" }} style={{ borderColor: "#219dff" }} className="ma0 bt-0" />
</div>
<Rule style={{ width: "calc(50% - 48px)" }} borderColor='blue' m='0' />
</Row>
));
export const DayBreak = ({ when }) => (
<div className="pv3 gray2 b--gray2 flex items-center justify-center f9 w-100">
<p>{moment(when).calendar()}</p>
<p>{moment(when).calendar(null, { sameElse: DATESTAMP_FORMAT })}</p>
</div>
);
@ -46,7 +46,6 @@ interface ChatMessageProps {
className?: string;
isPending: boolean;
style?: any;
allStations: any;
scrollWindow: HTMLDivElement;
isLastMessage?: boolean;
unreadMarkerRef: React.RefObject<HTMLDivElement>;
@ -87,7 +86,6 @@ export default class ChatMessage extends Component<ChatMessageProps> {
scrollWindow,
isLastMessage,
unreadMarkerRef,
allStations,
history,
api
} = this.props;
@ -97,7 +95,7 @@ export default class ChatMessage extends Component<ChatMessageProps> {
const containerClass = `${renderSigil
? `cf pt2 pl3 lh-copy`
: `items-center cf hide-child`} ${isPending ? 'o-40' : ''} ${className}`
: `items-top cf hide-child`} ${isPending ? 'o-40' : ''} ${className}`
const timestamp = moment.unix(msg.when / 1000).format(renderSigil ? 'hh:mm a' : 'hh:mm');
@ -118,14 +116,13 @@ export default class ChatMessage extends Component<ChatMessageProps> {
style,
containerClass,
isPending,
allStations,
history,
api,
scrollWindow
};
const unreadContainerStyle = {
height: isLastRead ? '1.66em' : '0',
height: isLastRead ? '2rem' : '0',
};
return (
@ -165,7 +162,6 @@ interface MessageProps {
containerClass: string;
isPending: boolean;
style: any;
allStations: any;
measure(element): void;
scrollWindow: HTMLDivElement;
};
@ -182,7 +178,6 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
hideAvatars,
remoteContentPolicy,
measure,
allStations,
history,
api,
scrollWindow
@ -218,7 +213,6 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
hideAvatars={hideAvatars}
hideNicknames={hideNicknames}
scrollWindow={scrollWindow}
allStations={allStations}
history={history}
api={api}
className="fl pr3 v-top bg-white bg-gray0-d pt1"
@ -255,7 +249,7 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
export const MessageWithoutSigil = ({ timestamp, msg, remoteContentPolicy, measure }) => (
<>
<p className="child pr1 mono f9 gray2 dib">{timestamp}</p>
<Text mono gray display='inline-block' pr='1' pt='2px' lineHeight='tall' className="child">{timestamp}</Text>
<Box fontSize='14px' className="clamp-message" style={{ flexGrow: 1 }}>
<MessageContent content={msg.letter} remoteContentPolicy={remoteContentPolicy} measure={measure}/>
</Box>

View File

@ -39,7 +39,6 @@ type ChatWindowProps = RouteComponentProps<{
group: Group;
ship: Patp;
station: any;
allStations: any;
api: GlobalApi;
hideNicknames: boolean;
hideAvatars: boolean;
@ -56,7 +55,7 @@ interface ChatWindowState {
export default class ChatWindow extends Component<ChatWindowProps, ChatWindowState> {
private virtualList: VirtualScroller | null;
private unreadMarkerRef: React.RefObject<HTMLDivElement>;
INITIALIZATION_MAX_TIME = 1500;
constructor(props) {
@ -68,14 +67,14 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
initialized: false,
lastRead: props.unreadCount ? props.mailboxSize - props.unreadCount : Infinity,
};
this.dismissUnread = this.dismissUnread.bind(this);
this.scrollToUnread = this.scrollToUnread.bind(this);
this.handleWindowBlur = this.handleWindowBlur.bind(this);
this.handleWindowFocus = this.handleWindowFocus.bind(this);
this.stayLockedIfActive = this.stayLockedIfActive.bind(this);
this.dismissIfLineVisible = this.dismissIfLineVisible.bind(this);
this.virtualList = null;
this.unreadMarkerRef = React.createRef();
}
@ -88,7 +87,7 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
this.setState({ initialized: true });
}, this.INITIALIZATION_MAX_TIME);
}
componentWillUnmount() {
window.removeEventListener('blur', this.handleWindowBlur);
window.removeEventListener('focus', this.handleWindowFocus);
@ -192,10 +191,10 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
}
this.setState({ fetchPending: true });
start = Math.min(mailboxSize - start, mailboxSize);
end = Math.max(mailboxSize - end, 0, start - MAX_BACKLOG_SIZE);
return api.chat
.fetchMessages(end, start, station)
.finally(() => {
@ -224,7 +223,7 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
this.dismissUnread();
}
}
render() {
const {
envelopes,
@ -243,7 +242,6 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
hideAvatars,
hideNicknames,
remoteContentPolicy,
allStations,
history
} = this.props;
@ -251,7 +249,7 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
const messages = new Map();
let lastMessage = 0;
[...envelopes]
.sort((a, b) => a.number - b.number)
.forEach(message => {
@ -267,8 +265,8 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
lastMessage = mailboxSize + index;
});
const messageProps = { association, group, contacts, hideAvatars, hideNicknames, remoteContentPolicy, unreadMarkerRef, allStations, history, api };
const messageProps = { association, group, contacts, hideAvatars, hideNicknames, remoteContentPolicy, unreadMarkerRef, history, api };
return (
<>
<UnreadNotice

View File

@ -3,7 +3,7 @@ import { UnControlled as CodeEditor } from 'react-codemirror2';
import { MOBILE_BROWSER_REGEX } from "~/logic/lib/util";
import CodeMirror from 'codemirror';
import { Row } from '@tlon/indigo-react';
import { Row, BaseInput } from '@tlon/indigo-react';
import 'codemirror/mode/markdown/markdown';
import 'codemirror/addon/display/placeholder';
@ -36,6 +36,25 @@ const MARKDOWN_CONFIG = {
}
};
// Until CodeMirror supports options.inputStyle = 'textarea' on mobile,
// we need to hack this into a regular input that has some funny behaviors
const inputProxy = (input) => new Proxy(input, {
get(target, property) {
if (property in target) {
return target[property];
}
if (property === 'setOption') {
return () => {};
}
if (property === 'getValue') {
return () => target.value;
}
if (property === 'setValue') {
return (val) => target.value = val;
}
}
});
export default class ChatEditor extends Component {
constructor(props) {
super(props);
@ -129,7 +148,11 @@ export default class ChatEditor extends Component {
'Esc': () => {
this.editor?.getInputField().blur();
}
}
},
// The below will ony work once codemirror's bug is fixed
spellcheck: !!MOBILE_BROWSER_REGEX.test(navigator.userAgent),
autocorrect: !!MOBILE_BROWSER_REGEX.test(navigator.userAgent),
autocapitalize: !!MOBILE_BROWSER_REGEX.test(navigator.userAgent)
};
return (
@ -139,22 +162,41 @@ export default class ChatEditor extends Component {
flexGrow='1'
height='100%'
maxHeight='224px'
paddingTop='8px'
width='calc(100% - 88px)'
className={inCodeMode ? 'chat code' : 'chat'}
color="black"
>
<CodeEditor
{MOBILE_BROWSER_REGEX.test(navigator.userAgent)
? <BaseInput
fontFamily={inCodeMode ? 'Source Code Pro' : 'Inter'}
fontSize="14px"
style={{ width: '100%', background: 'transparent', color: 'currentColor' }}
placeholder={inCodeMode ? "Code..." : "Message..."}
onKeyUp={event => {
if (event.key === 'Enter') {
this.submit();
} else {
this.messageChange(null, null, event.target.value);
}
}}
ref={input => {
if (!input) return;
this.editor = inputProxy(input);
}}
{...props}
/>
: <CodeEditor
value={message}
options={options}
onChange={(e, d, v) => this.messageChange(e, d, v)}
editorDidMount={(editor) => {
this.editor = editor;
if (!MOBILE_BROWSER_REGEX.test(navigator.userAgent)) {
editor.focus();
}
editor.focus();
}}
{...props}
/>
}
</Row>
);
}

View File

@ -19,7 +19,8 @@ export default class CodeContent extends Component {
overflow='auto'
maxHeight='10em'
maxWidth='100%'
backgroundColor='scales.black10'
style={{ whiteSpace: 'pre' }}
backgroundColor='washedGray'
>
{content.code.output[0].join('\n')}
</Text>
@ -36,6 +37,7 @@ export default class CodeContent extends Component {
overflow='auto'
maxHeight='10em'
maxWidth='100%'
style={{ whiteSpace: 'pre' }}
>
{content.code.expression}
</Text>

View File

@ -24,10 +24,30 @@ const DISABLED_INLINE_TOKENS = [
'reference'
];
const renderers = {
inlineCode: ({language, value}) => {
return <Text mono fontSize='14px' backgroundColor='washedGray' style={{ whiteSpace: 'preWrap'}}>{value}</Text>
},
code: ({language, value}) => {
return <Text
py='1'
className='clamp-message'
fontSize='14px'
display='block'
mono
backgroundColor='washedGray'
overflowX='scroll'
style={{ whiteSpace: 'pre'}}>
{value}
</Text>
}
};
const MessageMarkdown = React.memo(props => (
<ReactMarkdown
{...props}
unwrapDisallowed={true}
renderers={renderers}
allowNode={(node, index, parent) => {
if (
node.type === 'blockquote'

View File

@ -55,7 +55,7 @@ export class OverlaySigil extends PureComponent {
render() {
const { props, state } = this;
const { hideAvatars, allStations } = props;
const { hideAvatars } = props;
const img = (props.contact && (props.contact.avatar !== null) && !hideAvatars)
? <img src={props.contact.avatar} height={16} width={16} className="dib" />
@ -64,6 +64,8 @@ export class OverlaySigil extends PureComponent {
size={16}
color={props.color}
classes={props.sigilClass}
icon
padded
/>;
return (
@ -82,7 +84,6 @@ export class OverlaySigil extends PureComponent {
association={props.association}
group={props.group}
onDismiss={this.profileHide}
allStations={allStations}
hideAvatars={hideAvatars}
hideNicknames={props.hideNicknames}
history={props.history}

View File

@ -12,7 +12,6 @@ export class ProfileOverlay extends PureComponent {
this.popoverRef = React.createRef();
this.onDocumentClick = this.onDocumentClick.bind(this);
this.createAndRedirectToDM = this.createAndRedirectToDM.bind(this);
}
componentDidMount() {
@ -25,42 +24,6 @@ export class ProfileOverlay extends PureComponent {
document.removeEventListener('touchstart', this.onDocumentClick);
}
createAndRedirectToDM() {
const { api, ship, history, allStations } = this.props;
const station = `/~${window.ship}/dm--${ship}`;
const theirStation = `/~${ship}/dm--${window.ship}`;
if (allStations.indexOf(station) !== -1) {
history.push(`/~landscape/home/resource/chat${station}`);
return;
}
if (allStations.indexOf(theirStation) !== -1) {
history.push(`/~landscape/home/resource/chat${theirStation}`);
return;
}
const groupPath = `/ship/~${window.ship}/dm--${ship}`;
const aud = ship !== window.ship ? [`~${ship}`] : [];
const title = `${cite(window.ship)} <-> ${cite(ship)}`;
api.chat.create(
title,
'',
station,
groupPath,
{ invite: { pending: aud } },
aud,
true,
false
);
// TODO: make a pretty loading state
setTimeout(() => {
history.push(`/~landscape/home/resource/chat${station}`);
}, 5000);
}
onDocumentClick(event) {
const { popoverRef } = this;
// Do nothing if clicking ref's element or descendent elements
@ -72,7 +35,7 @@ export class ProfileOverlay extends PureComponent {
}
render() {
const { contact, ship, color, topSpace, bottomSpace, group, association, hideNicknames, hideAvatars, history } = this.props;
const { contact, ship, color, topSpace, bottomSpace, group, hideNicknames, hideAvatars, history } = this.props;
let top, bottom;
if (topSpace < OVERLAY_HEIGHT / 2) {
@ -132,7 +95,7 @@ export class ProfileOverlay extends PureComponent {
)}
<Text mono gray>{cite(`~${ship}`)}</Text>
{!isOwn && (
<Button mt={2} width="100%" style={{ cursor: 'pointer' }} onClick={this.createAndRedirectToDM}>
<Button mt={2} width="100%" style={{ cursor: 'pointer' }} onClick={() => history.push(`/~landscape/dm/${ship}`)}>
Send Message
</Button>
)}

View File

@ -225,16 +225,7 @@ blockquote {
font-size: 14px;
}
pre, code {
background-color: var(--light-gray);
}
pre code {
background-color: transparent;
white-space: pre-wrap;
}
code, .code, .chat.code .react-codemirror2 .CodeMirror * {
.chat.code .react-codemirror2 .CodeMirror * {
font-family: 'Source Code Pro';
}

View File

@ -1,24 +1,21 @@
import React from 'react';
import Helmet from 'react-helmet';
import { Link } from 'react-router-dom';
import { Box, Row, Icon, Text, Center } from '@tlon/indigo-react';
import { uxToHex, adjustHex } from "~/logic/lib/util";
import { uxToHex, adjustHex } from '~/logic/lib/util';
import './css/custom.css';
import { Sigil } from "~/logic/lib/sigil";
import { Sigil } from '~/logic/lib/sigil';
import Tiles from './components/tiles';
import Tile from './components/tiles/tile';
import Welcome from './components/welcome';
import Groups from './components/Groups';
export default class LaunchApp extends React.Component {
componentDidMount() {
// preload spinner asset
new Image().src = '/~landscape/img/Spinner.png';
}
render() {
@ -27,16 +24,15 @@ export default class LaunchApp extends React.Component {
const sigilColor = contact?.color
? `#${uxToHex(contact.color)}`
: props.dark
? "#FFFFFF"
: "#000000";
? '#FFFFFF'
: '#000000';
return (
<>
<Helmet>
<title>OS1 - Home</title>
</Helmet>
<div className="h-100 overflow-y-scroll">
<Box height='100%' overflowY='scroll'>
<Welcome firstTime={props.launch.firstTime} api={props.api} />
<Box
ml='2'
@ -80,22 +76,27 @@ export default class LaunchApp extends React.Component {
weather={props.weather}
/>
</Box>
<Groups associations={props.associations} invites={props.invites} api={props.api}/>
<Box
position="absolute"
fontFamily="mono"
left="0"
bottom="0"
color="gray"
bg="white"
ml={3}
mb={3}
borderRadius={2}
fontSize={0}
p={2}>
{props.baseHash}
</Box>
</div>
<Groups
associations={props.associations}
groups={props.groups}
invites={props.invites}
api={props.api} />
</Box>
<Box
position="absolute"
fontFamily="mono"
left="0"
bottom="0"
color="gray"
bg="white"
ml={3}
mb={3}
borderRadius={2}
fontSize={0}
p={2}
>
{props.baseHash}
</Box>
</>
);
}

View File

@ -1,8 +1,6 @@
import React from "react";
import { Box, Text } from "@tlon/indigo-react";
import { Link } from "react-router-dom";
import { useLocalStorageState } from "~/logic/lib/useLocalStorageState";
import { Associations, Association } from "~/types";
import { alphabeticalOrder } from "~/logic/lib/util";
import Tile from '../components/tiles/tile';
@ -17,19 +15,22 @@ const sortGroupsAlph = (a: Association, b: Association) =>
export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
const { associations, invites, api, ...boxProps } = props;
const incomingGroups = Object.values(invites?.['/contacts'] || {});
const incomingGroups = Object.values(invites?.['contacts'] || {});
const getKeyByValue = (object, value) => {
return Object.keys(object).find(key => object[key] === value);
}
const groups = Object.values(associations?.contacts || {})
.filter(e => e['group-path'] in props.groups)
.sort(sortGroupsAlph);
const acceptInvite = (invite) => {
const [, , ship, name] = invite.path.split('/');
const resource = { ship, name };
const resource = {
ship: `~${invite.resource.ship}`,
name: invite.resource.name
};
return api.contacts.join(resource).then(() => {
api.invite.accept('/contacts', getKeyByValue(invites['/contacts'], invite));
api.invite.accept('contacts', getKeyByValue(invites['contacts'], invite));
});
};
@ -54,10 +55,16 @@ export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
borderRadius='2'
borderColor='lightGray'
p='2'
fontSize='0'
>
fontSize='0'>
<Text display='block' pb='2' gray>You have been invited to:</Text>
<Text display='inline-block' overflow='hidden' maxWidth='100%' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }} title={invite.path.slice(6)}>{invite.path.slice(6)}</Text>
<Text
display='inline-block'
overflow='hidden'
maxWidth='100%'
style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}
title={`~${invite.resource.ship}/${invite.resource.name}`}>
{`~${invite.resource.ship}/${invite.resource.name}`}
</Text>
<Box pt='5'>
<Text
onClick={() => acceptInvite(invite)}
@ -68,7 +75,12 @@ export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
</Text>
<Text
color='red'
onClick={() => api.invite.decline('/contacts', getKeyByValue(invites['/contacts'], invite))}
onClick={() =>
api.invite.decline(
'contacts',
getKeyByValue(invites['contacts'], invite)
)
}
cursor='pointer'>
Reject
</Text>

View File

@ -1,5 +1,4 @@
import React from 'react';
import classnames from 'classnames';
import { Text, Icon } from '@tlon/indigo-react';
import Tile from './tile';
@ -23,7 +22,7 @@ export default class BasicTile extends React.PureComponent {
verticalAlign='top'
pt='5px'
pr='2px'
/>
/>
: null
}{props.title}
</Text>

View File

@ -1,38 +1,28 @@
import React from 'react';
import moment from 'moment';
import SunCalc from 'suncalc';
import styled from 'styled-components';
import Tile from './tile';
const innerSize = 124; // clock size
const VIEWBOX_SIZE = 100;
const CX = VIEWBOX_SIZE / 2;
const CY = VIEWBOX_SIZE / 2;
const RADIUS = VIEWBOX_SIZE / 2;
const CELESTIAL_BODY_SIZE = 16;
// polar to cartesian
// var ptc = function(r, theta) {
// return {
// x: r * Math.cos(theta),
// y: r * Math.sin(theta)
// }
// }
let timeTextColor = '#000000', dateTextColor = '#333333', background = '#ffffff';
const dark = window.matchMedia('(prefers-color-scheme: dark)');
if (dark.matches) {
timeTextColor = '#ffffff';
dateTextColor = '#7f7f7f';
background = '#333';
}
function darkColors(dark) {
if (dark.matches) {
timeTextColor = '#ffffff';
dateTextColor = '#7f7f7f';
background = '#ffffff';
const ApplyClockBg = styled.div`
.background {
fill: ${p => p.theme.colors.white};
}
}
dark.addListener(darkColors);
.time {
fill: ${p => p.theme.colors.black};
color: ${p => p.theme.colors.black};
}
.date {
fill: ${p => p.theme.colors.gray};
}
`;
const toRelativeTime = (date, referenceTime, unit) => moment(date)
.diff(referenceTime, unit);
@ -42,10 +32,6 @@ const minsToDegs = (mins) => {
return (mins / 1440) * 360;
};
const splitArc = (start, end) => end + ((start - end) * 0.5);
const isOdd = n => Math.abs(n % 2) == 1;
const radToDeg = rad => rad * (180 / Math.PI);
const degToRad = deg => deg * (Math.PI / 180);
@ -54,58 +40,137 @@ const convert = (date, referenceTime) => {
return minsToDegs(toRelativeTime(date, referenceTime, 'minutes'));
};
const circle = (ctx, x, y, r, from, to, fill) => {
ctx.beginPath();
ctx.arc( x, y, r, from, to );
ctx.strokeStyle = 'rgba(0,0,0,0)';
ctx.fillStyle = fill || 'rgba(0,0,0,0)';
ctx.fill();
};
const circleClip = (ctx, x, y, r, from, to, fill) => {
ctx.globalCompositeOperation = 'xor';
circle(ctx, x, y, r, from, to, fill);
ctx.globalCompositeOperation = 'source-over';
};
const circleOutline = (ctx, x, y, r, from, to, stroke, lineWidth) => {
ctx.beginPath();
ctx.arc( x, y, r, from, to );
ctx.fillStyle = 'rgba(0,0,0,0)';
ctx.lineWidth = lineWidth;
ctx.strokeStyle = stroke || 'rgba(0,0,0,0)';
if (lineWidth) {
ctx.stroke();
// https://github.com/tingletech/moon-phase
export const dFromPhase = (moonPhase) => {
let mag, sweep, d = "m50,0";
if (moonPhase <= 0.25) {
sweep = [ 1, 0 ];
mag = 20 - 20 * moonPhase * 4;
} else if (moonPhase <= 0.50) {
sweep = [ 0, 0 ];
mag = 20 * (moonPhase - 0.25) * 4;
} else if (moonPhase <= 0.75) {
sweep = [ 1, 1 ];
mag = 20 - 20 * (moonPhase - 0.50) * 4;
} else if (moonPhase <= 1) {
sweep = [ 0, 1 ];
mag = 20 * (moonPhase - 0.75) * 4;
}
};
const arc = (ctx, x, y, r, from, to, fill) => {
ctx.beginPath();
ctx.arc( x, y, r, from, to );
ctx.fillStyle = 'rgba(0,0,0,0)';
ctx.lineWidth = r * 2;
ctx.strokeStyle = fill || 'rgba(0,0,0,0)';
ctx.stroke();
};
d = d + "a" + mag + ",20 0 1," + sweep[0] + " 0,100 ";
d = d + "a20,20 0 1," + sweep[1] + " 0,-100";
return d;
}
const degArc = (ctx, x, y, r, from, to, fill) => {
ctx.beginPath();
ctx.arc( x, y, r, degToRad(from), degToRad(to));
ctx.fillStyle = 'rgba(0,0,0,0)';
ctx.lineWidth = r * 2;
ctx.strokeStyle = fill || 'rgba(0,0,0,0)';
ctx.stroke();
};
const Moon = ({ angle, ...props }) => {
const phase = SunCalc.getMoonIllumination(moment().toDate()).phase.toFixed(2);
const cx = CX + (RADIUS - 12) * Math.cos(degToRad(angle)) - (CELESTIAL_BODY_SIZE / 2);
const cy = CY + (RADIUS - 12) * Math.sin(degToRad(angle)) - (CELESTIAL_BODY_SIZE / 2);
return (
<g>
<mask id="umbra">
<rect x="-50" y="-50" height="200" width="200" fill="black" />
<path d={dFromPhase(phase)} fill="white"/>
</mask>
<use
width={CELESTIAL_BODY_SIZE}
height={CELESTIAL_BODY_SIZE}
xlinkHref="#Moon-symbol"
x={cx}
y={cy}
transform={`rotate(${angle} ${cx + (CELESTIAL_BODY_SIZE / 2)} ${cy + (CELESTIAL_BODY_SIZE / 2)})`}
/>
</g>
);
}
class Clock extends React.Component {
const Sun = ({ angle, ...props}) => (
<circle
id="sun"
cx={CX + (RADIUS - 12) * Math.cos(degToRad(angle))}
cy={CY + (RADIUS - 12) * Math.sin(degToRad(angle))}
fill="#FCC440"
stroke="rgba(0,0,0,0.1)"
r={CELESTIAL_BODY_SIZE / 2}
{...props}
></circle>
);
const SvgArc = ({ start, end, ...rest }) => {
const x1 = CX + RADIUS * Math.cos(degToRad(start));
const y1 = CY + RADIUS * Math.sin(degToRad(start));
const x2 = CX + RADIUS * Math.cos(degToRad(end));
const y2 = CY + RADIUS * Math.sin(degToRad(end));
const isLarge = Math.abs((start > 360 ? start - 360 : start) - end) > 180;
const d = [
'M', CX, CY,
'L', x1, y1,
'A', RADIUS, RADIUS, '0', '1', '1', x2, y2, 'z'
].join(' ');
return <path d={d} {...rest} />;
}
class ClockText extends React.Component {
constructor(props) {
super(props);
this.state = {
time: Date.now()
}
}
componentDidMount() {
this.interval = setInterval(() => this.setState({ time: Date.now() }), 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
const now = moment(this.state.time);
return (
<>
<use xlinkHref="#clock-center" className="background" />
<text
textAnchor="middle"
x={CX}
y={CY - 2}
fontSize="10"
fontFamily="Inter"
className="time"
>
<tspan>{now.format('h')}</tspan>
<tspan>:<animate attributeName="fill"
values="currentColor;transparent"
begin="0s"
dur="1s"
calcMode="discrete"
repeatCount="indefinite"/>
</tspan>
<tspan>{now.format('mm A')}</tspan>
</text>
<text
textAnchor="middle"
x={CX}
y={CY + 11}
fontSize="10"
fontFamily="Inter"
className="date"
>{now.format('MMM D')}<tspan style={{ fontFeatureSettings: "'sups' 1" }}>{now.format('Do').replace(now.format('D'), '')}</tspan></text>
</>
);
}
}
class Clock extends React.PureComponent {
constructor(props) {
super(props);
this.animate = this.animate.bind(this);
this.canvasRef = React.createRef();
this.canvas = null;
this.angle = 0;
this.referenceTime = moment().startOf('day').subtract(6, 'hours');
this.state = {
time: Date.now(),
lat: 0,
lon: 0,
geolocationSuccess: false,
@ -164,246 +229,88 @@ class Clock extends React.Component {
}
componentDidMount() {
this.canvas = initCanvas(
this.canvasRef,
{ x: innerSize, y: innerSize },
4
);
this.initGeolocation();
this.animate();
this.interval = setInterval(() => this.setState({ time: Date.now() }), 60000);
}
componentWillUnmount() {
if (this.animationTimer) {
window.clearTimeout(this.animationTimer);
}
}
animate() {
this.animationTimer =
window.setTimeout(() => window.requestAnimationFrame(this.animate), 1000);
const { state } = this;
const time = new Date();
const ctx = this.canvas.getContext('2d');
ctx.clearRect(0, 0, ctx.width, ctx.height);
ctx.save();
const ctr = innerSize / 2;
// Sun+moon calculations
const cx = ctr;
const cy = ctr;
this.angle = degToRad(convert(time, this.referenceTime));
const newX = cx + (ctr - 15) * Math.cos(this.angle);
const newY = cy + (ctr - 15) * Math.sin(this.angle);
// Center white circle with time and date
circle(
ctx,
ctr,
ctr,
ctr,
-1,
2 * Math.PI,
background
);
// Day
degArc(
ctx,
ctr,
ctr,
ctr / 2,
state.sunriseEnd,
state.sunset,
'rgba(33, 157, 255, .2)'
);
// Sunrise
degArc(
ctx,
ctr,
ctr,
ctr / 2,
state.sunsetStart,
state.sunriseEnd,
'#FFC700'
);
// Sunset
degArc(
ctx,
ctr,
ctr,
ctr / 2,
state.dusk,
state.dawn,
'rgba(255, 65, 54, .8)'
);
// Night
degArc(
ctx,
ctr,
ctr,
ctr / 2,
state.night,
state.nightEnd,
'rgba(0, 0, 0, .8)'
);
if (
radToDeg(this.angle) > splitArc(state.sunriseEnd, state.nightEnd)
&& radToDeg(this.angle) < splitArc(state.sunset, state.night)
) {
// Sun circle
circle(
ctx,
newX-1/2,
newY-1/2,
8,
0,
2 * Math.PI,
'#FCC440'
);
// Sun circle border
circleOutline(
ctx,
newX-1/2,
newY-1/2,
8,
0,
2 * Math.PI,
'rgba(0,0,0,0.1)',
1
);
} else {
// Moon circle
circle(
ctx,
newX-1/2,
newY-1/2,
8,
0,
2 * Math.PI,
'#FFFFFF'
);
// Moon circle outline
circleOutline(
ctx,
newX-1/2,
newY-1/2,
8,
0,
2 * Math.PI,
'#000000',
1
);
}
// Outer borders
circleOutline(
ctx,
ctr,
ctr,
ctr-1,
-1,
2 * Math.PI,
'none',
0
);
// Center white circle border
circleOutline(
ctx,
ctr,
ctr,
ctr/1.85,
-1,
2 * Math.PI,
'none',
0
);
// Inner hole
circle(
ctx,
ctr,
ctr,
ctr/1.85,
-1,
2 * Math.PI,
background
);
// Text for time and date
const timeText = isOdd(time.getSeconds())
? moment().format('h mm A')
: moment().format('h:mm A');
const dateText = moment().format('MMM Do');
ctx.textAlign = 'center';
ctx.fillStyle = timeTextColor;
ctx.font = '12px Inter';
ctx.fillText(timeText, ctr, ctr + 6 - 7);
ctx.fillStyle = dateTextColor;
ctx.font = '12px Inter';
ctx.fillText(dateText, ctr, ctr + 6 + 7);
ctx.restore();
clearInterval(this.interval);
}
render() {
const now = moment(this.state.time);
const angle = convert(now, this.referenceTime);
return (
<canvas
style={{ height: '100%', width: '100%'}}
ref={ canvasRef => this.canvasRef = canvasRef }
id="clock-canvas"
/>
<ApplyClockBg>
<svg
style={{ height: '100%', width: '100%'}}
viewBox={`0 0 ${VIEWBOX_SIZE} ${VIEWBOX_SIZE}`}
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
<symbol id="border">
<circle cx={CY} cy={CY} r={RADIUS} />
</symbol>
<symbol id="clock-center">
<circle r={VIEWBOX_SIZE / 1.85 / 2} cx={CX} cy={CY} />
</symbol>
<mask id="center-mask">
<use xlinkHref="#border" fill="white" />
<use xlinkHref="#clock-center" fill="black"/>
</mask>
<symbol id="Moon-symbol" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<g>
<path mask="url(#umbra)" d="m50,0 a20,20 0 1,1 0,100 a20,20 0 1,1 0,-100" fill="#fff" stroke="#000"/>
</g>
</symbol>
</defs>
<g mask="url(#center-mask)">
<use xlinkHref="#border" className="background" />
<SvgArc
id="day"
start={this.state.sunriseEnd}
end={this.state.sunset}
fill="rgba(33, 157, 255, .2)"
/>
<SvgArc
id="sunrise"
start={this.state.sunsetStart}
end={this.state.sunriseEnd}
fill="#FFC700"
/>
<SvgArc
id="sunset"
start={this.state.dusk}
end={this.state.dawn}
fill="rgba(255, 65, 54, .8)"
/>
<SvgArc
id="night"
start={this.state.night}
end={this.state.nightEnd}
fill="rgba(0, 0, 0, .8)"
/>
{angle > this.state.nightEnd && angle < this.state.sunset
? <Sun angle={angle} />
: <Moon angle={angle} />
}
</g>
<ClockText />
</svg>
</ApplyClockBg>
);
}
}
export default class ClockTile extends React.Component {
constructor(props) {
super(props);
}
renderWrapper(child) {
return (
<Tile p={0} border={0} bg='transparent' boxShadow='none'>
{child}
</Tile>
);
}
render() {
const data = this.props.location ? this.props.location : {};
return this.renderWrapper((
<Clock data={data} />
));
}
}
const initCanvas = (canvas, size, ratio) => {
const { x, y } = size;
// let ratio = ctx.webkitBackingStorePixelRatio < 2
// ? window.devicePixelRatio
// : 1;
// default for high print resolution.
// ratio = ratio * resMult;
canvas.width = x * ratio;
canvas.height = y * ratio;
canvas.style.width = x + 'px';
canvas.style.height = y + 'px';
canvas.getContext('2d').scale(ratio, ratio);
return canvas;
};
const ClockTile = ({ location = {} }) => (
<Tile p={0} border={0} bg='transparent' boxShadow='none'>
<Clock data={location} />
</Tile>
);
export default ClockTile;

View File

@ -1,26 +1,30 @@
import React from 'react';
import classnames from 'classnames';
import { Box, BaseImage } from '@tlon/indigo-react';
import Tile from './tile';
export default class CustomTile extends React.PureComponent {
render() {
const { props } = this;
return (
<Tile>
<div className={"w-100 h-100 relative bg-white bg-gray0-d ba " +
"b--black br2 b--gray1-d"}>
<img
className="absolute invert-d"
<Box
width='100%'
height='100%'
position='relative'
backgroundColor='white'
border='1px solid'
borderColor='washedGray'
borderRadius='2'
>
<BaseImage
position='absolute'
className="invert-d"
style={{ left: 38, top: 38 }}
src={'/~launch/img/UnknownCustomTile.png'}
width={48}
height={48} />
</div>
src='/~launch/img/UnknownCustomTile.png'
width='48px'
height='48px'
/>
</Box>
</Tile>
);
}
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import moment from 'moment';
import { Box, Icon, Text } from '@tlon/indigo-react';
import { Box, Icon, Text, BaseAnchor, BaseInput } from '@tlon/indigo-react';
import Tile from './tile';
@ -23,7 +23,7 @@ export default class WeatherTile extends React.Component {
}, (err) => {
console.log(err);
}, { maximumAge: Infinity, timeout: 10000 });
this.props.api.weather(latlng);
this.props.api.launch.weather(latlng);
this.setState({ manualEntry: !this.state.manualEntry });
});
}
@ -103,14 +103,15 @@ export default class WeatherTile extends React.Component {
let secureCheck;
let error;
if (this.state.error === true) {
error = <p className="f9 red2 pt1">Please try again.</p>;
error = <Text display='block' color='red' pt='1'>Please try again.</Text>;
}
if (location.protocol === 'https:') {
secureCheck = (
<a className="black white-d f9 pointer"
onClick={() => this.locationSubmit()}>
<Text color='black' cursor='pointer'
onClick={() => this.locationSubmit()}
>
Detect ->
</a>
</Text>
);
}
return this.renderWrapper(
@ -120,32 +121,38 @@ export default class WeatherTile extends React.Component {
justifyContent='space-between'
height='100%'
>
<a
className="f9 black white-d pointer"
<Text
color='black'
cursor='pointer'
onClick={() =>
this.setState({ manualEntry: !this.state.manualEntry })
}
>
&lt;&#45;
</a>
</Text>
{secureCheck}
<Text pb={1} mb='auto'>
Please enter your{' '}
<a
className="bb"
<BaseAnchor
borderBottom='1px solid'
color='black'
href="https://latitudeandlongitude.org/"
target="_blank"
>
latitude and longitude
</a>
</BaseAnchor>
.
</Text>
{error}
<form mt='auto' className="flex" style={{ marginBlockEnd: 0}}>
<input
<Box mt='auto' display='flex' marginBlockEnd='0'>
<BaseInput
id="gps"
size="10"
className="w-100 black white-d bg-transparent bn f9"
width='100%'
color='black'
fontSize='0'
backgroundColor='transparent'
border='0'
type="text"
placeholder="29.55, -95.08"
onKeyDown={(e) => {
@ -153,15 +160,21 @@ export default class WeatherTile extends React.Component {
e.preventDefault();
this.manualLocationSubmit(e.target.value);
}
}} />
<input
className={'bg-transparent black white-d bn pointer ' +
'f9 flex-shrink-0 pr1'}
}}
/>
<BaseInput
backgroundColor='transparent'
color='black'
cursor='pointer'
flexShrink='0'
pl='1'
fontSize='0'
border='0'
type="submit"
onClick={() => this.manualLocationSubmit()}
value="->"
/>
</form>
</Box>
</Box>
);
}
@ -216,17 +229,17 @@ export default class WeatherTile extends React.Component {
alignItems='space-between'
>
<Text color={weatherStyle.text}>
<Icon icon='Weather' color={weatherStyle.text} display='inline' style={{ position: 'relative', top: '.3em'}} />
<Icon icon='Weather' color={weatherStyle.text} display='inline' style={{ position: 'relative', top: '.3em' }} />
Weather
<a
style={{ color: weatherStyle.text }}
className='pointer'
onClick={() =>
this.setState({ manualEntry: !this.state.manualEntry })
}
>
->
</a>
<Text
color={weatherStyle.text}
cursor='pointer'
onClick={() =>
this.setState({ manualEntry: !this.state.manualEntry })
}
>
->
</Text>
</Text>
<Box
@ -257,15 +270,19 @@ export default class WeatherTile extends React.Component {
if (this.props.location) {
return this.renderWrapper((
<div
className={'pa2 w-100 h-100 ' +
'bg-white bg-gray0-d black white-d'}>
<Box
p='2'
width='100%'
height='100%'
backgroundColor='white'
color='black'
>
<Icon icon='Weather' color='black' display='inline' style={{ position: 'relative', top: '.3em' }} />
<Text>Weather</Text>
<p className="w-100 flex-col f9">
<Text pt='2' width='100%' display='flex' flexDirection='column'>
Loading, please check again later...
</p>
</div>
</Text>
</Box>
));
}
return this.renderNoData();

View File

@ -124,7 +124,7 @@ export function LinkResource(props: LinkResourceProps) {
name={name}
ship={ship}
api={api}
parentIndex={node.post.index}
parentIndex={node.post.originalIndex}
/>
</Row>
<Comments

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react';
import { Spinner } from '~/views/components/Spinner';
import { createPost } from '~/logic/api/graph';
import { deSig } from "~/logic/lib/util";
export class CommentSubmit extends Component {
@ -22,7 +23,7 @@ export class CommentSubmit extends Component {
this.setState({ disabled: true }, () => {
this.props.api.graph.addPost(
`~${this.props.ship}`,
`~${deSig(this.props.ship)}`,
this.props.name,
post
).then((r) => {

View File

@ -1,34 +1,28 @@
import React from "react";
import React from 'react';
import {
Box,
Label,
ManagedCheckboxField as Checkbox,
Button,
} from "@tlon/indigo-react";
import { Formik, Form } from "formik";
import * as Yup from "yup";
import _ from "lodash";
Button
} from '@tlon/indigo-react';
import { Formik, Form } from 'formik';
import * as Yup from 'yup';
import GlobalApi from "../../../../api/global";
import { LaunchState } from "../../../../types/launch-update";
import { DropLaunchTiles } from "./DropLaunch";
import { S3State, BackgroundConfig } from "../../../../types";
import { BackgroundPicker, BgType } from "./BackgroundPicker";
import GlobalApi from '~/logic/api/global';
import { S3State, BackgroundConfig } from '~/types';
import { BackgroundPicker, BgType } from './BackgroundPicker';
const formSchema = Yup.object().shape({
tileOrdering: Yup.array().of(Yup.string()),
bgType: Yup.string()
.oneOf(["none", "color", "url"], "invalid")
.required("Required"),
.oneOf(['none', 'color', 'url'], 'invalid')
.required('Required'),
bgUrl: Yup.string().url(),
bgColor: Yup.string().matches(/#([A-F]|[a-f]|[0-9]){6}/, "Invalid color"),
bgColor: Yup.string().matches(/#([A-F]|[a-f]|[0-9]){6}/, 'Invalid color'),
avatars: Yup.boolean(),
nicknames: Yup.boolean(),
nicknames: Yup.boolean()
});
interface FormSchema {
tileOrdering: string[];
bgType: BgType;
bgColor: string | undefined;
bgUrl: string | undefined;
@ -38,7 +32,6 @@ interface FormSchema {
interface DisplayFormProps {
api: GlobalApi;
launch: LaunchState;
dark: boolean;
background: BackgroundConfig;
hideAvatars: boolean;
@ -47,16 +40,16 @@ interface DisplayFormProps {
}
export default function DisplayForm(props: DisplayFormProps) {
const { api, launch, background, hideAvatars, hideNicknames, s3 } = props;
const { api, background, hideAvatars, hideNicknames, s3 } = props;
let bgColor, bgUrl;
if (background?.type === "url") {
if (background?.type === 'url') {
bgUrl = background.url;
}
if (background?.type === "color") {
if (background?.type === 'color') {
bgColor = background.color;
}
const bgType = background?.type || "none";
const bgType = background?.type || 'none';
return (
<Formik
@ -67,18 +60,15 @@ export default function DisplayForm(props: DisplayFormProps) {
bgColor,
bgUrl,
avatars: hideAvatars,
nicknames: hideNicknames,
tileOrdering: launch.tileOrdering,
nicknames: hideNicknames
} as FormSchema
}
onSubmit={(values, actions) => {
api.launch.changeOrder(values.tileOrdering);
const bgConfig: BackgroundConfig =
values.bgType === "color"
? { type: "color", color: values.bgColor || "" }
: values.bgType === "url"
? { type: "url", url: values.bgUrl || "" }
values.bgType === 'color'
? { type: 'color', color: values.bgColor || '' }
: values.bgType === 'url'
? { type: 'url', url: values.bgUrl || '' }
: undefined;
api.local.setBackground(bgConfig);
@ -88,7 +78,7 @@ export default function DisplayForm(props: DisplayFormProps) {
actions.setSubmitting(false);
}}
>
{(props) => (
{props => (
<Form>
<Box
display="grid"
@ -99,17 +89,6 @@ export default function DisplayForm(props: DisplayFormProps) {
<Box color="black" fontSize={1} mb={3} fontWeight={900}>
Display Preferences
</Box>
<Box mb={2}>
<Label display="block" pb={2}>
Tile Order
</Label>
<DropLaunchTiles
id="tileOrdering"
name="tileOrdering"
tiles={launch.tiles}
order={launch.tileOrdering}
/>
</Box>
<BackgroundPicker
bgType={props.values.bgType}
bgUrl={props.values.bgUrl}

View File

@ -1,127 +0,0 @@
import React, { useMemo } from "react";
import { useDrag } from "react-dnd";
import { usePreview } from "react-dnd-multi-backend";
import { capitalize } from "lodash";
import { TileTypeBasic, Tile } from "../../../../types/launch-update";
import { Box, Image as _Image, Text } from "@tlon/indigo-react";
import styled from "styled-components";
// Need to change dojo image
const Image = styled(_Image)<{ invert?: boolean }>`
${(p) =>
p.theme.colors.white !== "rgba(255,255,255,1)" ? `filter: invert(1);` : ``}
${(p) =>
!p.invert
? ``
: p.theme.colors.white !== "rgba(255,255,255,1)"
? `
filter: invert(0);
`
: `filter: invert(1);`}
`;
interface DragTileProps {
index: number;
tile: Tile;
title: string;
style?: any;
}
function DragTileBox({ title, index, tile, ...props }: any) {
const [, dragRef] = useDrag({
item: { type: "launchTile", index, tile, title },
collect: (monitor) => ({}),
});
return (
<Box
ref={dragRef}
display="flex"
alignItems="center"
justifyContent="space-around"
flexDirection="column"
border={1}
borderColor="black"
height="100%"
width="100%"
style={{ cursor: "move" }}
{...props}
></Box>
);
}
function DragTileCustom({ index, title, style }: any) {
const tile = { type: { custom: null } };
return (
<DragTileBox
bg="white"
style={style}
title={title}
tile={tile}
index={index}
>
<Text fontSize={1}>{capitalize(title)}</Text>
</DragTileBox>
);
}
function DragTileBasic(props: {
tile: TileTypeBasic;
index: number;
style: any;
}) {
const { basic: tile } = props.tile;
const isDojo = useMemo(() => tile.title === "Dojo", [tile.title]);
return (
<DragTileBox
tile={{ type: props.tile }}
index={props.index}
bg={
"white" // isDojo ? "black" : "white"
}
style={props.style}
>
<Image width="48px" height="48px" src={tile.iconUrl} invert={isDojo} />
<Text
color={
"black" // isDojo ? "white" : "black"
}
>
{tile.title}
</Text>
</DragTileBox>
);
}
export function DragTile(props: DragTileProps) {
if ("basic" in props.tile.type) {
return (
<DragTileBasic
index={props.index}
style={props.style}
tile={props.tile.type}
/>
);
} else {
return (
<DragTileCustom
style={props.style}
title={props.title}
index={props.index}
/>
);
}
}
export function DragTilePreview() {
let { display, style, item } = usePreview();
if (!display) {
return null;
}
style = { ...style, height: "96px", width: "96px", "z-index": "5" };
return <DragTile style={style} {...item} />;
}

View File

@ -1,85 +0,0 @@
import React, { useCallback, ReactNode } from "react";
import { useDrop } from "react-dnd";
import { DndProvider, usePreview } from "react-dnd-multi-backend";
import HTML5toTouch from "react-dnd-multi-backend/dist/esm/HTML5toTouch";
import { Box } from "@tlon/indigo-react";
import { DragTile, DragTilePreview } from "./DragTile";
import { useField } from "formik";
function DropLaunchTile({
children,
index,
didDrop,
}: {
index: number;
children: ReactNode;
didDrop: (item: number, location: number) => void;
}) {
const onDrop = useCallback(
(item: any, monitor: any) => {
didDrop(item.index, index);
},
[index, didDrop]
);
const { display, style, item } = usePreview();
const [{ isOver }, drop] = useDrop({
accept: "launchTile",
drop: onDrop,
collect: (monitor) => ({
isOver: !!monitor.isOver(),
}),
});
return (
<div
ref={drop}
style={{
position: "relative",
width: "100%",
height: "100%",
}}
>
{children}
</div>
);
}
export function DropLaunchTiles({ tiles, name }: any) {
const [field, meta, helpers] = useField<string[]>(name);
const { value } = meta;
const { setValue } = helpers;
const onChange = useCallback(
(x: number, y: number) => {
// swap tiles
let t = value.slice();
const c = t[x];
t[x] = t[y];
t[y] = c;
setValue(t);
},
[setValue, value]
);
return (
<DndProvider options={HTML5toTouch}>
<Box
display="grid"
gridGap={2}
gridTemplateColumns={["96px 96px", "96px 96px 96px 96px"]}
gridAutoRows="96px"
>
<DragTilePreview />
{value.map((tile, i) => (
<DropLaunchTile didDrop={onChange} key={`${i}-${tile}`} index={i}>
<DragTile title={tile} tile={tiles[tile]} index={i} />
</DropLaunchTile>
))}
</Box>
</DndProvider>
);
}

View File

@ -1,31 +1,18 @@
import React from "react";
import React from 'react';
import {
Box,
Text,
Button,
Col,
Input,
InputLabel,
Radio,
Checkbox,
} from "@tlon/indigo-react";
import * as Yup from "yup";
import { Formik, Form } from "formik";
import _ from "lodash";
import { Box } from '@tlon/indigo-react';
import GlobalApi from "../../../api/global";
import { StoreState } from "../../../store/type";
import DisplayForm from "./lib/DisplayForm";
import S3Form from "./lib/S3Form";
import SecuritySettings from "./lib/Security";
import RemoteContentForm from "./lib/RemoteContent";
import GlobalApi from '~/logic/api/global';
import { StoreState } from '~/logic/store/type';
import DisplayForm from './lib/DisplayForm';
import S3Form from './lib/S3Form';
import SecuritySettings from './lib/Security';
import RemoteContentForm from './lib/RemoteContent';
type ProfileProps = StoreState & { api: GlobalApi; ship: string };
export default function Settings({
api,
launch,
s3,
dark,
hideAvatars,
@ -45,14 +32,13 @@ export default function Settings({
>
<DisplayForm
api={api}
launch={launch}
dark={dark}
hideNicknames={hideNicknames}
hideAvatars={hideAvatars}
background={background}
s3={s3}
/>
<RemoteContentForm {...{api, remoteContentPolicy}} />
<RemoteContentForm {...{ api, remoteContentPolicy }} />
<S3Form api={api} s3={s3} />
<SecuritySettings api={api} />
</Box>

View File

@ -66,7 +66,9 @@ export function GroupifyForm(props: GroupifyFormProps) {
id="group"
label="Group"
caption="What group should this notebook be added to? If blank, a new group will be made for the notebook"
groups={props.groups}
associations={props.associations}
adminOnly
/>
<AsyncButton loadingText="Groupifying..." border>
Groupify

View File

@ -60,6 +60,7 @@ export function MarkdownEditor(
border={1}
borderColor="lightGray"
borderRadius={2}
height={['calc(100% - 22vh)', '100%']}
{...boxProps}
>
<CodeEditor

View File

@ -24,6 +24,7 @@ export const MarkdownField = ({
return (
<Box
overflowY="hidden"
height='100%'
width="100%"
display="flex"
flexDirection="column"

View File

@ -41,13 +41,13 @@ export function PostForm(props: PostFormProps) {
onSubmit={onSubmit}
validateOnBlur
>
<Form style={{ display: "contents" }}>
<Row flexDirection={["column-reverse", "row"]} mb={4} gapX={4} justifyContent='space-between'>
<Input maxWidth='40rem' flexGrow={1} placeholder="Post Title" id="title" />
<Form style={{ display: "contents"}}>
<Row flexShrink='0' flexDirection={["column-reverse", "row"]} mb={4} gapX={4} justifyContent='space-between'>
<Input maxWidth='40rem' width='100%' flexShrink={[0, 1]} placeholder="Post Title" id="title" />
<AsyncButton
ml={[0,2]}
mb={[4,0]}
flexShrink={1}
flexShrink={0}
primary
loadingText={loadingText}
>

View File

@ -22,7 +22,8 @@ interface RenderChoiceProps<C> {
}
interface DropdownSearchProps<C> {
label: string;
autoFocus?: boolean;
label?: string;
id: string;
// check if entry is exact match
isExact: (s: string) => C | undefined;
@ -51,7 +52,7 @@ interface DropdownSearchProps<C> {
export function DropdownSearch<C>(props: DropdownSearchProps<C>) {
const textarea = useRef<HTMLTextAreaElement>();
const { candidates, getKey, caption } = props;
const { candidates, getKey, caption, autoFocus } = props;
const [query, setQuery] = useState("");
@ -118,7 +119,7 @@ export function DropdownSearch<C>(props: DropdownSearchProps<C>) {
return (
<Box position="relative" zIndex={9}>
<Label htmlFor={props.id}>{props.label}</Label>
{props.label && (<Label htmlFor={props.id}>{props.label}</Label>)}
{caption ? <Label mt="2" gray>{caption}</Label> : null}
{!props.disabled && (
<Input
@ -127,6 +128,7 @@ export function DropdownSearch<C>(props: DropdownSearchProps<C>) {
value={query}
autocomplete="off"
placeholder={props.placeholder || ""}
autoFocus={autoFocus}
/>
)}
{dropdown.length !== 0 && query.length !== 0 && (

View File

@ -4,11 +4,16 @@ import _ from "lodash";
import { useField } from "formik";
import styled from "styled-components";
import { roleForShip } from "~/logic/lib/group";
import { DropdownSearch } from "./DropdownSearch";
import { Groups } from "~/types";
import { Associations, Association } from "~/types/metadata-update";
interface InviteSearchProps {
disabled?: boolean;
adminOnly: boolean;
groups: Groups;
associations: Associations;
label: string;
caption?: string;
@ -59,7 +64,18 @@ function renderCandidate(
export function GroupSearch(props: InviteSearchProps) {
const groups = useMemo(
() => Object.values(props.associations?.contacts || {}),
() => {
return props.adminOnly
? Object.values(
Object.keys(props.associations?.contacts)
.filter(e => roleForShip(props.groups[e], window.ship) === 'admin')
.reduce((obj, key) => {
obj[key] = props.associations?.contacts[key]
return obj;
}, {}) || {}
)
: Object.values(props.associations?.contacts || {});
},
[props.associations?.contacts]
);
@ -97,7 +113,7 @@ export function GroupSearch(props: InviteSearchProps) {
onSelect={onSelect}
onRemove={onRemove}
renderChoice={({ candidate, onRemove }) => (
<Box px={2} py={1} border={1} borderColor="washedGrey" color="black" fontSize={0}>
<Box cursor='default' px={2} py={1} border={1} borderColor="washedGrey" color="black" fontSize={0}>
{candidate.metadata.title}
<ClickableText ml={2} onClick={onRemove} color="black">
x

View File

@ -12,12 +12,15 @@ import { Rolodex, Groups } from "~/types";
import { HoverBox } from "./HoverBox";
interface InviteSearchProps {
autoFocus?: boolean;
disabled?: boolean;
label: string;
label?: string;
caption?: string;
id: string;
contacts: Rolodex;
groups: Groups;
hideSelection?: boolean;
maxLength?: number;
}
const ClickableText = styled(Text)`
@ -45,10 +48,11 @@ const Candidate = ({ title, detail, selected, onClick }) => (
);
export function ShipSearch(props: InviteSearchProps) {
const [{ value }, { error }, { setValue }] = useField<string[]>(props.id);
const [{ value }, { error }, { setValue, setTouched }] = useField<string[]>(props.id);
const onSelect = useCallback(
(s: string) => {
setTouched(true);
setValue([...value, s]);
},
[setValue, value]
@ -109,6 +113,8 @@ export function ShipSearch(props: InviteSearchProps) {
[nicknames]
);
const maxLength = props.maxLength
return (
<Col>
<DropdownSearch<string>
@ -123,7 +129,7 @@ export function ShipSearch(props: InviteSearchProps) {
caption={props.caption}
candidates={peers}
renderCandidate={renderCandidate}
disabled={false}
disabled={props.maxLength ? value.length >= props.maxLength : false}
search={(s: string, t: string) =>
t.toLowerCase().startsWith(s.toLowerCase())
}
@ -133,6 +139,7 @@ export function ShipSearch(props: InviteSearchProps) {
renderChoice={({ candidate, onRemove }) => null}
value={undefined}
error={error}
autoFocus={props.autoFocus}
/>
<Row minHeight="34px" flexWrap="wrap">
{value.map((s) => (

View File

@ -40,7 +40,7 @@ const StatusBar = (props) => {
</Row>
<Row justifyContent="flex-end" collapse>
<StatusBarItem onClick={() => props.history.push('/~profile')}>
<Sigil ship={props.ship} size={24} color={"#000000"} classes="dib mix-blend-diff" />
<Sigil ship={props.ship} size={16} color='black' classes='mix-blend-diff' icon />
<Text ml={2} display={["none", "inline"]} fontFamily="mono">~{props.ship}</Text>
</StatusBarItem>
</Row>

View File

@ -55,7 +55,7 @@ export class Omnibox extends Component {
}
getSearchedCategories() {
return ['commands', 'groups', 'subscriptions', 'apps', 'other'];
return ['other', 'commands', 'groups', 'subscriptions', 'apps'];
}
control(evt) {
@ -154,7 +154,7 @@ export class Omnibox extends Component {
result.title.toLowerCase().includes(query) ||
result.link.toLowerCase().includes(query) ||
result.app.toLowerCase().includes(query) ||
(result.host !== null ? result.host.includes(query) : false)
(result.host !== null ? result.host.toLowerCase().includes(query) : false)
);
})
);

View File

@ -36,7 +36,7 @@ export class OmniboxResult extends Component {
} else if (icon === 'logout') {
graphic = <Icon display="inline-block" verticalAlign="middle" icon='ArrowWest' mr='2' size='16px' color={iconFill} />;
} else if (icon === 'profile') {
graphic = <Sigil color={sigilFill} classes='dib v-mid mr2' ship={window.ship} size={16} />;
graphic = <Sigil color={sigilFill} classes='dib v-mid mr2' ship={window.ship} size={16} icon padded />;
} else if (icon === 'home') {
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Circle' mr='2' size='16px' color={iconFill} />;
} else {

View File

@ -59,9 +59,8 @@ export function GroupsPane(props: GroupsPaneProps) {
}
const popovers = (routeProps: RouteComponentProps, baseUrl: string) =>
(groupPath && (
<>
<PopoverRoutes
( <>
{groupPath && ( <PopoverRoutes
contacts={groupContacts || {}}
association={groupAssociation!}
group={group!}
@ -71,17 +70,17 @@ export function GroupsPane(props: GroupsPaneProps) {
hideNicknames={props.hideNicknames}
{...routeProps}
baseUrl={baseUrl}
/>
/>)}
<InvitePopover
api={api}
association={groupAssociation!}
baseUrl={baseUrl}
groups={props.groups}
contacts={props.contacts}
workspace={workspace}
/>
</>
)) ||
null;
)
return (
<Switch>
@ -165,7 +164,7 @@ export function GroupsPane(props: GroupsPaneProps) {
render={(routeProps) => {
const newUrl = `${baseUrl}/new`;
return (
<Skeleton recentGroups={recentGroups} {...props} baseUrl={baseUrl}>
<Skeleton mobileHide recentGroups={recentGroups} {...props} baseUrl={baseUrl}>
<NewChannel
{...routeProps}
api={api}

View File

@ -11,7 +11,7 @@ import { useOutsideClick } from "~/logic/lib/useOutsideClick";
import { FormError } from "~/views/components/FormError";
import { resourceFromPath } from "~/logic/lib/group";
import GlobalApi from "~/logic/api/global";
import { Groups, Rolodex } from "~/types";
import { Groups, Rolodex, Workspace } from "~/types";
import { ChipInput } from "~/views/components/ChipInput";
interface InvitePopoverProps {
@ -20,6 +20,7 @@ interface InvitePopoverProps {
groups: Groups;
contacts: Rolodex;
api: GlobalApi;
workspace: Workspace;
}
interface FormSchema {
@ -46,6 +47,10 @@ export function InvitePopover(props: InvitePopoverProps) {
useOutsideClick(innerRef, onOutsideClick);
const onSubmit = async ({ ships, emails }: { ships: string[] }, actions) => {
if(props.workspace.type === 'home') {
history.push(`/~landscape/dm/${ships[0]}`);
return;
}
// TODO: how to invite via email?
try {
const resource = resourceFromPath(association["group-path"]);
@ -97,13 +102,15 @@ export function InvitePopover(props: InvitePopoverProps) {
<Col gapY="3" p={3}>
<Box>
<Text>Invite to </Text>
<Text fontWeight="800">{title}</Text>
<Text fontWeight="800">{title || "DM"}</Text>
</Box>
<ShipSearch
groups={props.groups}
contacts={props.contacts}
id="ships"
label=""
maxLength={props.workspace.type === 'home' ? 1 : undefined}
autoFocus
/>
<FormError message="Failed to invite" />
{/* <ChipInput

View File

@ -1,4 +1,4 @@
import React, { useCallback } from "react";
import React, { useCallback } from 'react';
import {
Box,
ManagedTextInputField as Input,
@ -12,27 +12,26 @@ import GlobalApi from "~/logic/api/global";
import { AsyncButton } from "~/views/components/AsyncButton";
import { FormError } from "~/views/components/FormError";
import { RouteComponentProps } from "react-router-dom";
import { stringToSymbol } from "~/logic/lib/util";
import { stringToSymbol, parentPath } from "~/logic/lib/util";
import GroupSearch from "~/views/components/GroupSearch";
import { Associations } from "~/types/metadata-update";
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
import { Notebooks } from "~/types/publish-update";
import { Groups } from "~/types/group-update";
import { ShipSearch } from "~/views/components/ShipSearch";
import { Rolodex } from "~/types";
import { Rolodex, Workspace } from "~/types";
interface FormSchema {
name: string;
description: string;
ships: string[];
type: "chat" | "publish" | "links";
type: "chat" | "publish" | "link";
}
const formSchema = Yup.object({
name: Yup.string().required("Channel must have a name"),
name: Yup.string().required('Channel must have a name'),
description: Yup.string(),
ships: Yup.array(Yup.string()),
type: Yup.string().required("Must choose channel type"),
type: Yup.string().required('Must choose channel type')
});
interface NewChannelProps {
@ -41,9 +40,9 @@ interface NewChannelProps {
contacts: Rolodex;
groups: Groups;
group?: string;
workspace: Workspace;
}
export function NewChannel(props: NewChannelProps & RouteComponentProps) {
const { history, api, group, workspace } = props;
@ -54,7 +53,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
try {
const { name, description, type, ships } = values;
switch (type) {
case "chat":
case 'chat':
const appPath = `/~${window.ship}/${resId}`;
const groupPath = group || `/ship${appPath}`;
@ -63,8 +62,8 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
description,
appPath,
groupPath,
{ invite: { pending: ships.map((s) => `~${s}`) } },
ships.map((s) => `~${s}`),
{ invite: { pending: ships.map(s => `~${s}`) } },
ships.map(s => `~${s}`),
true,
false
);
@ -92,16 +91,18 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
break;
default:
console.log("fallthrough");
console.log('fallthrough');
}
if (!group) {
await waiter((p) => !!p?.groups?.[`/ship/~${window.ship}/${resId}`]);
await waiter(p => Boolean(p?.groups?.[`/ship/~${window.ship}/${resId}`]));
}
actions.setStatus({ success: null });
const resourceUrl = parentPath(location.pathname);
history.push(`${resourceUrl}/resource/${type}${type === 'link' ? '/ship' : ''}/~${window.ship}/${resId}`);
} catch (e) {
console.error(e);
actions.setStatus({ error: "Channel creation failed" });
actions.setStatus({ error: 'Channel creation failed' });
}
};
return (
@ -112,11 +113,11 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
<Formik
validationSchema={formSchema}
initialValues={{
type: "chat",
name: "",
description: "",
group: "",
ships: [],
type: 'chat',
name: '',
description: '',
group: '',
ships: []
}}
onSubmit={onSubmit}
>
@ -131,7 +132,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) {
<Box color="black" mb={2}>Channel Type</Box>
<Radio label="Chat" id="chat" name="type" />
<Radio label="Notebook" id="publish" name="type" />
<Radio label="Collection" id="links" name="type" />
<Radio label="Collection" id="link" name="type" />
</Col>
<Input
id="name"

View File

@ -3,8 +3,8 @@ import React, {
useMemo,
useCallback,
SyntheticEvent,
ChangeEvent,
} from "react";
ChangeEvent
} from 'react';
import {
Col,
Box,
@ -14,23 +14,23 @@ import {
Center,
Button,
Action,
StatelessTextInput as Input,
} from "@tlon/indigo-react";
import _ from "lodash";
import f from "lodash/fp";
import VisibilitySensor from "react-visibility-sensor";
StatelessTextInput as Input
} from '@tlon/indigo-react';
import _ from 'lodash';
import f from 'lodash/fp';
import VisibilitySensor from 'react-visibility-sensor';
import { Contact, Contacts } from "~/types/contact-update";
import { Sigil } from "~/logic/lib/sigil";
import { cite, uxToHex } from "~/logic/lib/util";
import { Group, RoleTags } from "~/types/group-update";
import { roleForShip, resourceFromPath } from "~/logic/lib/group";
import { Association } from "~/types/metadata-update";
import { useHistory, Link } from "react-router-dom";
import { Dropdown } from "~/views/components/Dropdown";
import GlobalApi from "~/logic/api/global";
import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction";
import styled from "styled-components";
import { Contact, Contacts } from '~/types/contact-update';
import { Sigil } from '~/logic/lib/sigil';
import { cite, uxToHex } from '~/logic/lib/util';
import { Group, RoleTags } from '~/types/group-update';
import { roleForShip, resourceFromPath } from '~/logic/lib/group';
import { Association } from '~/types/metadata-update';
import { useHistory, Link } from 'react-router-dom';
import { Dropdown } from '~/views/components/Dropdown';
import GlobalApi from '~/logic/api/global';
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
import styled from 'styled-components';
const TruncText = styled(Box)`
white-space: nowrap;
@ -39,7 +39,7 @@ const TruncText = styled(Box)`
`;
type Participant = Contact & { patp: string; pending: boolean };
type ParticipantsTabId = "total" | "pending" | "admin";
type ParticipantsTabId = 'total' | 'pending' | 'admin';
const searchParticipant = (search: string) => (p: Participant) => {
if (search.length == 0) {
@ -54,36 +54,36 @@ function getParticipants(cs: Contacts, group: Group) {
const contacts: Participant[] = _.map(cs, (c, patp) => ({
...c,
patp,
pending: false,
pending: false
}));
const members: Participant[] = _.map(Array.from(group.members), (m) =>
const members: Participant[] = _.map(Array.from(group.members), m =>
emptyContact(m, false)
);
const allMembers = _.unionBy(contacts, members, "patp");
const allMembers = _.unionBy(contacts, members, 'patp');
const pending: Participant[] =
"invite" in group.policy
? _.map(Array.from(group.policy.invite.pending), (m) =>
'invite' in group.policy
? _.map(Array.from(group.policy.invite.pending), m =>
emptyContact(m, true)
)
: [];
return [
_.unionBy(allMembers, pending, "patp"),
_.unionBy(allMembers, pending, 'patp'),
pending.length,
allMembers.length,
allMembers.length
] as const;
}
const emptyContact = (patp: string, pending: boolean): Participant => ({
nickname: "",
email: "",
phone: "",
color: "",
nickname: '',
email: '',
phone: '',
color: '',
avatar: null,
notes: "",
website: "",
notes: '',
website: '',
patp,
pending,
pending
});
const Tab = ({ selected, id, label, setSelected }) => (
@ -95,7 +95,7 @@ const Tab = ({ selected, id, label, setSelected }) => (
cursor="pointer"
onClick={() => setSelected(id)}
>
<Text color={selected === id ? "black" : "gray"}>{label}</Text>
<Text color={selected === id ? 'black' : 'gray'}>{label}</Text>
</Box>
);
@ -113,18 +113,18 @@ export function Participants(props: {
(p: Participant) => boolean
> = useMemo(
() => ({
total: (p) => !p.pending,
pending: (p) => p.pending,
admin: (p) => props.group.tags?.role?.admin?.has(p.patp),
total: p => !p.pending,
pending: p => p.pending,
admin: p => props.group.tags?.role?.admin?.has(p.patp)
}),
[props.group]
);
const ourRole = roleForShip(props.group, window.ship);
const [filter, setFilter] = useState<ParticipantsTabId>("total");
const [filter, setFilter] = useState<ParticipantsTabId>('total');
const [search, _setSearch] = useState("");
const [search, _setSearch] = useState('');
const setSearch = useMemo(() => _.debounce(_setSearch, 200), [_setSearch]);
const onSearchChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
@ -134,7 +134,7 @@ export function Participants(props: {
);
const adminCount = props.group.tags?.role?.admin?.size || 0;
const isInvite = "invite" in props.group.policy;
const isInvite = 'invite' in props.group.policy;
const [participants, pendingCount, memberCount] = getParticipants(
props.contacts,
@ -156,7 +156,7 @@ export function Participants(props: {
// TODO: remove when resolved
const isSafari = useMemo(() => {
const ua = window.navigator.userAgent;
return ua.includes("Safari") && !ua.includes("Chrome");
return ua.includes('Safari') && !ua.includes('Chrome');
}, []);
return (
@ -166,7 +166,7 @@ export function Participants(props: {
border={1}
borderColor="washedGray"
borderRadius={1}
position={isSafari ? "static" : "sticky"}
position={isSafari ? 'static' : 'sticky'}
top="0px"
mb={2}
px={2}
@ -192,7 +192,7 @@ export function Participants(props: {
selected={filter}
setSelected={setFilter}
id="admin"
label={`${adminCount} Admin${adminCount > 1 ? "s" : ""}`}
label={`${adminCount} Admin${adminCount > 1 ? 's' : ''}`}
/>
</Row>
</Row>
@ -210,8 +210,8 @@ export function Participants(props: {
</Row>
<Box
display="grid"
gridAutoRows={["48px 48px 1px", "48px 1px"]}
gridTemplateColumns={["48px 1fr", "48px 2fr 1fr", "48px 3fr 1fr"]}
gridAutoRows={['48px 48px 1px', '48px 1px']}
gridTemplateColumns={['48px 1fr', '48px 2fr 1fr', '48px 3fr 1fr']}
gridRowGap={2}
alignItems="center"
>
@ -224,7 +224,7 @@ export function Participants(props: {
>
{({ isVisible }) =>
isVisible ? (
cs.map((c) => (
cs.map(c => (
<Participant
api={api}
key={c.patp}
@ -261,37 +261,37 @@ function Participant(props: {
const { title } = association.metadata;
const color = uxToHex(contact.color);
const isInvite = "invite" in group.policy;
const isInvite = 'invite' in group.policy;
const role = useMemo(
() =>
contact.pending
? "pending"
: roleForShip(group, contact.patp) || "member",
? 'pending'
: roleForShip(group, contact.patp) || 'member',
[contact, group]
);
const onPromote = useCallback(async () => {
const resource = resourceFromPath(association["group-path"]);
await api.groups.addTag(resource, { tag: "admin" }, [`~${contact.patp}`]);
const resource = resourceFromPath(association['group-path']);
await api.groups.addTag(resource, { tag: 'admin' }, [`~${contact.patp}`]);
}, [api, association]);
const onDemote = useCallback(async () => {
const resource = resourceFromPath(association["group-path"]);
await api.groups.removeTag(resource, { tag: "admin" }, [
`~${contact.patp}`,
const resource = resourceFromPath(association['group-path']);
await api.groups.removeTag(resource, { tag: 'admin' }, [
`~${contact.patp}`
]);
}, [api, association]);
const onBan = useCallback(async () => {
const resource = resourceFromPath(association["group-path"]);
const resource = resourceFromPath(association['group-path']);
await api.groups.changePolicy(resource, {
open: { banShips: [`~${contact.patp}`] },
open: { banShips: [`~${contact.patp}`] }
});
}, [api, association]);
const onKick = useCallback(async () => {
const resource = resourceFromPath(association["group-path"]);
const resource = resourceFromPath(association['group-path']);
await api.groups.remove(resource, [`~${contact.patp}`]);
}, [api, association]);
@ -319,7 +319,7 @@ function Participant(props: {
</Col>
<Row
justifyContent="space-between"
gridColumn={["1 / 3", "auto"]}
gridColumn={['1 / 3', 'auto']}
alignItems="center"
>
<Col>
@ -340,7 +340,12 @@ function Participant(props: {
gapY={2}
p={2}
>
{props.role === "admin" && (
<Action bg="transparent">
<Link to={`/~landscape/dm/${contact.patp}`}>
<Text color="green">Send Message</Text>
</Link>
</Action>
{props.role === 'admin' && (
<>
{!isInvite && (
<>
@ -352,7 +357,7 @@ function Participant(props: {
</StatelessAsyncAction>
</>
)}
{role === "admin" ? (
{role === 'admin' ? (
<StatelessAsyncAction onClick={onDemote} bg="transparent">
Demote from Admin
</StatelessAsyncAction>
@ -372,7 +377,7 @@ function Participant(props: {
<Box
borderBottom={1}
borderBottomColor="washedGray"
gridColumn={["1 / 3", "1 / 4"]}
gridColumn={['1 / 3', '1 / 4']}
/>
</>
);
@ -382,7 +387,7 @@ function BlankParticipant({ length }) {
return (
<Box
gridRow={[`auto / span ${3 * length}`, `auto / span ${2 * length}`]}
gridColumn={["1 / 3", "1 / 4"]}
gridColumn={['1 / 3', '1 / 4']}
height="100%"
/>
);

View File

@ -13,6 +13,7 @@ import {
Workspace,
Groups,
Invites,
Rolodex,
} from "~/types";
import { SidebarListHeader } from "./SidebarListHeader";
import { useLocalStorageState } from "~/logic/lib/useLocalStorageState";
@ -24,6 +25,7 @@ import { roleForShip } from "~/logic/lib/group";
interface SidebarProps {
contacts: Rolodex;
children: ReactNode;
recentGroups: string[];
invites: Invites ;
@ -119,7 +121,14 @@ export function Sidebar(props: SidebarProps) {
baseUrl={props.baseUrl}
workspace={props.workspace}
/>
<SidebarListHeader initialValues={config} handleSubmit={setConfig} />
<SidebarListHeader
contacts={props.contacts}
baseUrl={props.baseUrl}
groups={props.groups}
initialValues={config}
handleSubmit={setConfig}
selected={selected || ""}
workspace={workspace} />
{sidebarInvites}
<SidebarList
config={config}

View File

@ -11,7 +11,7 @@ export class SidebarInvite extends Component<{invite: Invite, onAccept: Function
<Box width='100%' verticalAlign='middle'>
<Text display='block' pb='2' gray>You have been invited to:</Text>
<Text display='inline-block'>
{props.invite.path.substr(1)}
{`~${props.invite.resource.ship}/${props.invite.resource.name}`}
</Text>
</Box>
<Row>
@ -39,4 +39,4 @@ export class SidebarInvite extends Component<{invite: Invite, onAccept: Function
}
}
export default SidebarInvite;
export default SidebarInvite;

View File

@ -12,12 +12,22 @@ import {
import { FormikOnBlur } from "~/views/components/FormikOnBlur";
import { Dropdown } from "~/views/components/Dropdown";
import { FormikHelpers } from "formik";
import { SidebarListConfig } from "./types";
import { SidebarListConfig, Workspace } from "./types";
import { Link, useHistory } from 'react-router-dom';
import {ShipSearch} from "~/views/components/ShipSearch";
import {Groups, Rolodex} from "~/types";
export function SidebarListHeader(props: {
initialValues: SidebarListConfig;
groups: Groups;
contacts: Rolodex;
baseUrl: string;
selected: string;
workspace: Workspace;
handleSubmit: (c: SidebarListConfig) => void;
}) {
const history = useHistory();
const onSubmit = useCallback(
(values: SidebarListConfig, actions: FormikHelpers<SidebarListConfig>) => {
props.handleSubmit(values);
@ -35,12 +45,32 @@ export function SidebarListHeader(props: {
pr={2}
pl={3}
>
<Box>
<Box flexShrink='0'>
<Text>
{props.initialValues.hideUnjoined ? "Joined Channels" : "All Channels"}
</Text>
</Box>
<Box
width='100%'
textAlign='right'
mr='2'
display={(props.workspace?.type === 'home') ? 'inline-block' : 'none'}
>
<Link to={`${props.baseUrl}/invites`}>
<Text
display='inline-block'
verticalAlign='middle'
py='1px'
px='3px'
backgroundColor='washedBlue'
color='blue'
borderRadius='1'>
+ DM
</Text>
</Link>
</Box>
<Dropdown
flexShrink='0'
width="200px"
alignY="top"
alignX={["right", "left"]}

View File

@ -12,11 +12,12 @@ import { Path, AppName } from "~/types/noun";
import { LinkCollections } from "~/types/link-update";
import styled from "styled-components";
import GlobalSubscription from "~/logic/subscription/global";
import { Workspace, Groups, Graphs, Invites } from "~/types";
import { Workspace, Groups, Graphs, Invites, Rolodex } from "~/types";
import { useChat, useGraphModule } from "./Sidebar/Apps";
import { Body } from "~/views/components/Body";
interface SkeletonProps {
contacts: Rolodex;
children: ReactNode;
recentGroups: string[];
groups: Groups;
@ -59,6 +60,7 @@ export function Skeleton(props: SkeletonProps) {
>
{!props.hideSidebar && (
<Sidebar
contacts={props.contacts}
api={props.api}
recentGroups={props.recentGroups}
selected={props.selected}

View File

@ -1,20 +1,17 @@
import React, { Component } from 'react';
import { Route, Switch } from 'react-router-dom';
import { Box, Center } from '@tlon/indigo-react';
import './css/custom.css';
import { PatpNoSig, AppName } from '~/types/noun';
import { PatpNoSig } from '~/types/noun';
import GlobalApi from '~/logic/api/global';
import { StoreState } from '~/logic/store/type';
import GlobalSubscription from '~/logic/subscription/global';
import { Resource } from '~/views/components/Resource';
import { PopoverRoutes } from './components/PopoverRoutes';
import { UnjoinedResource } from '~/views/components/UnjoinedResource';
import { GroupsPane } from './components/GroupsPane';
import { Workspace } from '~/types';
import {NewGroup} from './components/NewGroup';
import {JoinGroup} from './components/JoinGroup';
import { NewGroup } from './components/NewGroup';
import { JoinGroup } from './components/JoinGroup';
import { cite } from '~/logic/lib/util';
type LandscapeProps = StoreState & {
@ -34,22 +31,44 @@ export default class Landscape extends Component<LandscapeProps, {}> {
this.props.subscription.startApp('graph');
}
createandRedirectToDM(api, ship, history, allStations) {
const station = `/~${window.ship}/dm--${ship}`;
const theirStation = `/~${ship}/dm--${window.ship}`;
if (allStations.indexOf(station) !== -1) {
history.push(`/~landscape/home/resource/chat${station}`);
return;
}
if (allStations.indexOf(theirStation) !== -1) {
history.push(`/~landscape/home/resource/chat${theirStation}`);
return;
}
const groupPath = `/ship/~${window.ship}/dm--${ship}`;
const aud = ship !== window.ship ? [`~${ship}`] : [];
const title = `${cite(window.ship)} <-> ${cite(ship)}`;
api.chat.create(
title,
'',
station,
groupPath,
{ invite: { pending: aud } },
aud,
true,
false
);
// TODO: make a pretty loading state
setTimeout(() => {
history.push(`/~landscape/home/resource/chat${station}`);
}, 5000);
}
render() {
const { props } = this;
const contacts = props.contacts || {};
const defaultContacts =
(Boolean(props.contacts) && '/~/default' in props.contacts) ?
props.contacts['/~/default'] : {};
const invites =
(Boolean(props.invites) && '/contacts' in props.invites) ?
props.invites['/contacts'] : {};
const s3 = props.s3 ? props.s3 : {};
const groups = props.groups || {};
const associations = props.associations || {};
const { api } = props;
const { api, inbox } = props;
return (
<Switch>
@ -70,7 +89,6 @@ export default class Landscape extends Component<LandscapeProps, {}> {
<Route path="/~landscape/home"
render={routeProps => {
const ws: Workspace = { type: 'home' };
return (
<GroupsPane workspace={ws} baseUrl="/~landscape/home" {...props} />
);
@ -82,21 +100,27 @@ export default class Landscape extends Component<LandscapeProps, {}> {
<NewGroup
groups={props.groups}
contacts={props.contacts}
api={props.api}
api={props.api}
{...routeProps}
/>
);
}}
/>
<Route path='/~landscape/dm/:ship?'
render={routeProps => {
const { ship } = routeProps.match.params;
return this.createandRedirectToDM(api, ship, routeProps.history, Object.keys(inbox));
}}
/>
<Route path="/~landscape/join/:ship?/:name?"
render={routeProps=> {
const { ship, name } = routeProps.match.params;
const autojoin = ship && name ? `${ship}/${name}` : null;
return (
<JoinGroup
<JoinGroup
groups={props.groups}
contacts={props.contacts}
api={props.api}
api={props.api}
autojoin={autojoin}
{...routeProps}
/>