mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-14 08:34:25 +03:00
Merge branch 'release/next-userspace' into lf/graph-publish-fe
This commit is contained in:
commit
39d538f9bc
@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:77d4ecd2ce1d5fc68297f16ac2030dfc8a072f51978363dd88a96239e4f5ac7d
|
||||
size 6310667
|
||||
oid sha256:38435a0a23fb4f09d55505915cd8e772b8096fd846c2c8ff3481a5b231deedf6
|
||||
size 6331042
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
::
|
||||
|
@ -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
|
||||
|
@ -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)]
|
||||
::
|
||||
|
@ -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
|
||||
|
@ -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
@ -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
|
||||
|
@ -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
|
||||
--
|
||||
|
||||
|
@ -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)
|
||||
==
|
||||
::
|
||||
--
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
20
pkg/arvo/gen/metadata-store/remove.hoon
Normal file
20
pkg/arvo/gen/metadata-store/remove.hoon
Normal 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]
|
@ -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]
|
||||
==
|
||||
--
|
||||
--
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
--
|
||||
::
|
||||
--
|
||||
|
@ -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
|
||||
==
|
||||
--
|
||||
|
||||
|
@ -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
|
||||
::
|
||||
|
@ -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
|
||||
%_ $
|
||||
|
@ -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)
|
||||
|
@ -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 !>(~))
|
||||
==
|
||||
|
@ -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)
|
||||
|
@ -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}
|
||||
/>
|
||||
)} />
|
||||
|
180
pkg/interface/package-lock.json
generated
180
pkg/interface/package-lock.json
generated
@ -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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ const Root = styled.div`
|
||||
/* Works on Chrome/Edge/Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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'
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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';
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 })
|
||||
}
|
||||
>
|
||||
<-
|
||||
</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();
|
||||
|
@ -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
|
||||
|
@ -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) => {
|
||||
|
@ -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}
|
||||
|
@ -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} />;
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -60,6 +60,7 @@ export function MarkdownEditor(
|
||||
border={1}
|
||||
borderColor="lightGray"
|
||||
borderRadius={2}
|
||||
height={['calc(100% - 22vh)', '100%']}
|
||||
{...boxProps}
|
||||
>
|
||||
<CodeEditor
|
||||
|
@ -24,6 +24,7 @@ export const MarkdownField = ({
|
||||
return (
|
||||
<Box
|
||||
overflowY="hidden"
|
||||
height='100%'
|
||||
width="100%"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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 && (
|
||||
|
@ -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
|
||||
|
@ -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) => (
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
);
|
||||
})
|
||||
);
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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%"
|
||||
/>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -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"]}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user