Merge pull request #4356 from urbit/la/push-hook-list-resource

contact: networking updated to send out data with the appropriate resources
This commit is contained in:
L 2021-02-03 12:02:53 -06:00 committed by GitHub
commit adee5d6562
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1003 additions and 650 deletions

View File

@ -9,6 +9,7 @@
update:store
%contact-update
%contact-push-hook
%.y :: necessary to enable p2p
==
--
::
@ -30,16 +31,20 @@
++ on-arvo on-arvo:def
++ on-fail on-fail:def
++ on-agent on-agent:def
++ on-watch on-watch:def
++ on-watch
|= =path
?. ?=([%nacks ~] path)
(on-watch:def path)
?> (team:title [src our]:bowl)
`this
::
++ on-leave on-leave:def
++ resource-for-update resource-for-update:con
++ on-pull-nack
|= [=resource =tang]
^- (quip card _this)
:_ this
?~ (get-contact:con entity.resource) ~
=- [%pass /pl-nack %agent [our.bowl %contact-store] %poke %contact-update -]~
!> ^- update:store
[%remove entity.resource]
[%give %fact ~[/nacks] resource+!>(resource)]~
::
++ on-pull-kick |=(=resource `/)
--

View File

@ -1,4 +1,6 @@
/+ store=contact-store, res=resource, contact, default-agent, dbug, push-hook
/- pull-hook
/+ store=contact-store, res=resource, contact, group,
default-agent, dbug, push-hook, agentio, verb
~% %contact-push-hook-top ..part ~
|%
+$ card card:agent:gall
@ -12,21 +14,46 @@
==
::
+$ agent (push-hook:push-hook config)
::
+$ share [%share =ship]
--
::
%- agent:dbug
^- agent:gall
%+ verb |
%- (agent:push-hook config)
^- agent
|_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
con ~(. contact bowl)
grp ~(. group bowl)
io ~(. agentio bowl)
::
++ on-init
^- (quip card _this)
:_ this :_ ~
=- [%pass /us %agent [our.bowl %contact-push-hook] %poke %push-hook-action -]
!> ^- action:push-hook
[%add [our.bowl %'']]
::
++ on-init on-init:def
++ on-save !>(~)
++ on-load on-load:def
++ on-poke on-poke:def
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?. =(mark %contact-share) (on-poke:def mark vase)
=/ =share !<(share vase)
:_ this :_ ~
?: =(our.bowl src.bowl)
:: proxy poke
%+ poke:pass:io [ship.share dap.bowl]
contact-share+!>([%share our.bowl])
:: accept share
?> =(src.bowl ship.share)
%+ poke-our:pass:io %contact-pull-hook
pull-hook-action+!>([%add src.bowl [src.bowl %$]])
::
++ on-agent on-agent:def
++ on-watch on-watch:def
++ on-leave on-leave:def
@ -48,16 +75,31 @@
%set-public %.n
==
::
++ resource-for-update resource-for-update:con
::
++ initial-watch
|= [=path =resource:res]
^- vase
?> (is-allowed:con src.bowl)
|^
?> (is-allowed:con resource src.bowl)
!> ^- update:store
=/ contact=(unit contact:store) (get-contact:con our.bowl)
:+ %add
our.bowl
?^ contact u.contact
*contact:store
[%initial rolo %.n]
::
++ rolo
^- rolodex:store
=/ ugroup (scry-group:grp resource)
%- ~(gas by *rolodex:store)
?~ ugroup
=/ c=(unit contact:store) (get-contact:con our.bowl)
?~ c
[our.bowl *contact:store]~
[our.bowl u.c]~
%+ murn ~(tap in (members:grp resource))
|= s=ship
^- (unit [ship contact:store])
=/ c=(unit contact:store) (get-contact:con s)
?~(c ~ `[s u.c])
--
::
++ take-update
|= =vase

View File

@ -3,7 +3,7 @@
:: data store that holds individual contact data
::
/- store=contact-store, *resource
/+ default-agent, dbug, *migrate
/+ default-agent, dbug, *migrate, contact
|%
+$ card card:agent:gall
+$ state-4
@ -29,6 +29,7 @@
|_ =bowl:gall
+* this .
def ~(. (default-agent this %|) bowl)
con ~(. contact bowl)
::
++ on-init
=. rolodex (~(put by rolodex) our.bowl *contact:store)
@ -101,8 +102,10 @@
++ handle-initial
|= [rolo=rolodex:store is-public=?]
^- (quip card _state)
=/ our-contact (~(got by rolodex) our.bowl)
=. rolodex (~(uni by rolodex) rolo)
:_ state(rolodex rolodex, is-public is-public)
=. rolodex (~(put by rolodex) our.bowl our-contact)
:_ state(rolodex rolodex)
(send-diff [%initial rolodex is-public] %.n)
::
++ handle-add
@ -208,9 +211,22 @@
[%x %allowed-ship @ ~]
=/ =ship (slav %p i.t.t.path)
``noun+!>((~(has in allowed-ships) ship))
::
[%x %is-public ~]
``noun+!>(is-public)
::
[%x %allowed-groups ~]
``noun+!>(allowed-groups)
::
[%x %is-allowed @ @ @ @ ~]
=/ is-personal =(i.t.t.t.t.t.path 'true')
=/ =resource
?: is-personal
[our.bowl %'']
[(slav %p i.t.t.path) i.t.t.t.path]
=/ =ship (slav %p i.t.t.t.t.path)
``json+!>(`json`b+(is-allowed:con resource ship))
==
::
++ on-leave on-leave:def

View File

@ -9,6 +9,7 @@
update:store
%graph-update
%graph-push-hook
%.n
==
--
::
@ -35,7 +36,7 @@
++ on-pull-nack
|= [=resource =tang]
^- (quip card _this)
~& nacked+resource
%- (slog leaf+"nacked {<resource>}" tang)
:_ this
?. (~(has in get-keys:gra) resource) ~
=- [%pass /pull-nack %agent [our.bowl %graph-store] %poke %graph-update -]~
@ -48,4 +49,6 @@
=/ maybe-time (peek-update-log:gra resource)
?~ maybe-time `/
`/(scot %da u.maybe-time)
::
++ resource-for-update resource-for-update:gra
--

View File

@ -93,6 +93,7 @@
%tag-queries %.n
%run-updates %.n
==
++ resource-for-update resource-for-update:gra
::
++ initial-watch
|= [=path =resource:res]

View File

@ -14,6 +14,7 @@
update:store
%group-update
%group-push-hook
%.n
==
::
--
@ -28,6 +29,7 @@
+* this .
def ~(. (default-agent this %|) bowl)
dep ~(. (default:pull-hook this config) bowl)
grp ~(. grpl bowl)
::
++ on-init on-init:def
++ on-save !>(~)
@ -45,8 +47,11 @@
:_ this
=- [%pass / %agent [our.bowl %group-store] %poke -]~
group-update+!>([%remove-group resource ~])
::
++ on-pull-kick
|= =resource
^- (unit path)
`/
::
++ resource-for-update resource-for-update:grp
--

View File

@ -141,6 +141,7 @@
=(~(tap in ships.update) ~[src.bowl])
==
--
++ resource-for-update resource-for-update:grp
::
++ take-update
|= =vase

View File

@ -1,5 +1,6 @@
/- view-sur=group-view, group-store, *group, metadata=metadata-store
/+ default-agent, agentio, mdl=metadata, resource, dbug, grpl=group, verb
/+ default-agent, agentio, mdl=metadata,
resource, dbug, grpl=group, conl=contact, verb
|%
++ card card:agent:gall
+$ state-zero
@ -69,15 +70,14 @@
[cards this]
::
++ on-arvo on-arvo:def
::
++ on-leave on-leave:def
::
++ on-fail on-fail:def
--
|_ =bowl:gall
++ met ~(. mdl bowl)
++ grp ~(. grpl bowl)
++ io ~(. agentio bowl)
++ con ~(. conl bowl)
::
::
++ join
@ -86,6 +86,7 @@
++ emit-many
|= crds=(list card)
jn-core(cards (weld (flop crds) cards))
::
++ emit
|= =card
jn-core(cards [card cards])
@ -152,43 +153,65 @@
%+ poke-our:(jn-pass-io /pull-groups) %group-pull-hook
pull-hook-action+!>([%add ship rid])
(tx-progress %added)
::
::
%pull-groups
?> ?=(%poke-ack -.sign)
(ack +.sign)
::
::
%groups
?+ -.sign !!
%fact (groups-fact +.sign)
%watch-ack (ack +.sign)
%kick watch-groups
==
::
::
%pull-md
?> ?=(%poke-ack -.sign)
(ack +.sign)
::
::
%pull-co
?> ?=(%poke-ack -.sign)
(ack +.sign)
::
%share-co
?> ?=(%poke-ack -.sign)
(ack +.sign)
::
%push-co
?> ?=(%poke-ack -.sign)
(ack +.sign)
::
%md
?+ -.sign !!
%fact (md-fact +.sign)
%watch-ack (ack +.sign)
%kick watch-md
==
::
::
%pull-graphs
?> ?=(%poke-ack -.sign)
%- cleanup
?^(p.sign %strange %done)
==
::
++ groups-fact
|= =cage
?. ?=(%group-update p.cage) jn-core
=+ !<(=update:group-store q.cage)
?. ?=(%initial-group -.update) jn-core
?. =(rid resource.update) jn-core
%- emit
%+ poke-our:(jn-pass-io /pull-md) %metadata-pull-hook
pull-hook-action+!>([%add [entity .]:rid])
%- emit-many
=/ cag=^cage pull-hook-action+!>([%add [entity .]:rid])
%- zing
:~ [(poke-our:(jn-pass-io /pull-md) %metadata-pull-hook cag)]~
[(poke-our:(jn-pass-io /pull-co) %contact-pull-hook cag)]~
::
?. scry-is-public:con ~
:_ ~
%+ poke:(jn-pass-io /share-co)
[entity.rid %contact-push-hook]
[%contact-share !>([%share our.bowl])]
==
::
++ md-fact
|= [=mark =vase]

View File

@ -2,9 +2,9 @@
::
:: allow syncing group data from foreign paths to local paths
::
/- *group, invite-store, metadata=metadata-store
/- *group, invite-store, metadata=metadata-store, contact=contact-store
/+ default-agent, verb, dbug, store=group-store, grpl=group, pull-hook
/+ resource, mdl=metadata
/+ resource, mdl=metadata, agn=agentio
~% %group-hook-top ..part ~
|%
+$ card card:agent:gall
@ -15,6 +15,7 @@
update:metadata
%metadata-update
%metadata-push-hook
%.n
==
+$ state-zero
[%0 previews=(map resource group-preview:metadata)]
@ -31,20 +32,19 @@
=* state -
=> |_ =bowl:gall
++ def ~(. (default-agent state %|) bowl)
++ met ~(. mdl bowl)
++ io ~(. agn bowl)
++ get-preview
|= rid=resource
^- card
=/ =path
preview+(en-path:resource rid)
=/ =dock
[entity.rid %metadata-push-hook]
=/ =cage
metadata-hook-update+!>([%req-preview rid])
[%pass path %agent dock %poke cage]
%+ ~(poke pass:io path) dock
metadata-hook-update+!>([%req-preview rid])
::
++ watch-invites
^- card
[%pass /invites %agent [our.bowl %invite-store] %watch /updates]
(~(watch-our pass:io /invites) %invite-store /updates)
::
++ take-invites
|= =sign:agent:gall
@ -59,6 +59,68 @@
::
%kick [watch-invites^~ state]
==
::
++ watch-contacts
(~(watch-our pass:io /contacts) %contact-store /all)
::
++ take-contacts
|= =sign:agent:gall
^- (quip card _state)
?+ -.sign (on-agent:def /contacts sign)
%kick [~[watch-contacts] state]
::
%fact
:_ state
?> ?=(%contact-update p.cage.sign)
=+ !<(=update:contact q.cage.sign)
?+ -.update ~
%add
(check-contact contact.update)
::
%edit
?. ?=(%add-group -.edit-field.update) ~
%- add-missing-previews
(~(gas in *(set resource)) resource.edit-field.update ~)
::
%initial
^- (list card)
%- zing
%+ turn ~(tap by rolodex.update)
|=([ship =contact:contact] (check-contact contact))
==
==
::
++ check-contact
|= =contact:contact
^- (list card)
(add-missing-previews groups.contact)
::
++ add-missing-previews
|= groups=(set resource)
^- (list card)
=/ missing=(set resource)
(~(dif in ~(key by previews)) groups)
%+ murn ~(tap by missing)
|= group=resource
^- (unit card)
?^ (peek-metadatum:met %groups group) ~
`(get-preview group)
::
++ watch-store
(~(watch-our pass:io /store) %metadata-store /all)
::
++ take-store
|= =sign:agent:gall
^- (quip card _state)
?+ -.sign (on-agent:def /store sign)
%kick [watch-store^~ state]
::
%fact
?> ?=(%metadata-update p.cage.sign)
=+ !<(=update:metadata q.cage.sign)
?. ?=(%initial-group -.update) `state
`state(previews (~(del by previews) group.update))
==
--
|_ =bowl:gall
+* this .
@ -73,8 +135,14 @@
|= =vase
=+ !<(old=state-zero vase)
:_ this(state old)
?: (~(has by wex.bowl) [/invites our.bowl %invite-store]) ~
~[watch-invites:hc]
%- zing
:~ ?: (~(has by wex.bowl) [/invites our.bowl %invite-store]) ~
~[watch-invites:hc]
?: (~(has by wex.bowl) [/contacts our.bowl %contact-store]) ~
~[watch-contacts:hc]
?: (~(has by wex.bowl) [/store our.bowl %metadata-store]) ~
~[watch-store:hc]
==
::
++ on-poke
|= [=mark =vase]
@ -84,26 +152,24 @@
?. ?=(%preview -.hook-update)
(on-poke:def mark vase)
:_ this(previews (~(put by previews) group.hook-update +.hook-update))
=/ paths=(list path)
~[preview+(en-path:resource group.hook-update)]
:~ [%give %fact paths mark^vase]
[%give %kick paths ~]
==
=/ =path
preview+(en-path:resource group.hook-update)
(fact-kick:io path mark^vase)
::
++ on-agent
|= [=wire =sign:agent:gall]
=^ cards state
?+ wire (on-agent:def:hc wire sign)
[%invites ~] (take-invites:hc sign)
[%contacts ~] (take-contacts:hc sign)
[%store ~] (take-store:hc sign)
::
[%preview @ @ @ ~]
?. ?=(%poke-ack -.sign)
(on-agent:def:hc wire sign)
:_ state
?~ p.sign ~
:~ [%give %fact ~[wire] tang+!>(u.p.sign)]
[%give %kick ~[wire] ~]
==
(fact-kick:io wire tang+!>(u.p.sign))
==
[cards this]
::
@ -119,13 +185,14 @@
:_ this
?~ prev
(get-preview rid)^~
[%give %fact ~ metadata-hook-update+!>([%preview u.prev])]~
(fact-init:io metadata-hook-update+!>([%preview u.prev]))^~
::
++ on-leave on-leave:def
++ on-peek on-peek:def
++ on-arvo on-arvo:def
::
++ on-fail on-fail:def
++ resource-for-update resource-for-update:met
++ on-pull-nack
|= [=resource =tang]
^- (quip card _this)
@ -134,7 +201,7 @@
:_ this
%+ turn ~(tap by associations)
|= [=md-resource:metadata =association:metadata]
=- [%pass / %agent [our.bowl %metadata-store] %poke -]
%+ poke-our:pass:io %metadata-store
:- %metadata-update
!> ^- update:metadata
[%remove resource md-resource]

View File

@ -92,6 +92,7 @@
?=(%member-metadata vip.metadatum)
==
::
++ resource-for-update resource-for-update:met
++ take-update
|= =vase
^- [(list card) agent]

View File

@ -88,6 +88,18 @@
|= [paths=(list path) fac=mold]
(fact mark^!>(fac) paths)
::
++ fact-kick
|= [=path =cage]
^- (list card)
:~ (fact cage ~[path])
(kick ~[path])
==
::
++ fact-init
|= =cage
^- card
[%give %fact ~ cage]
::
++ fact
|= [=cage paths=(list path)]
^- card

View File

@ -173,4 +173,12 @@
==
--
--
::
++ share-dejs
=, dejs:format
|%
++ share
^- $-(json [%share ship])
(of share+(su ;~(pfix sig fed:ag)) ~)
--
--

View File

@ -1,6 +1,7 @@
/- store=contact-store, *resource
/+ group
/+ group, grpl=group
|_ =bowl:gall
+* grp ~(. grpl bowl)
++ scry-for
|* [=mold =path]
.^ mold
@ -11,24 +12,88 @@
(snoc `^path`path %noun)
==
::
++ resource-for-update
|= =vase
^- (list resource)
|^
=/ =update:store !<(update:store vase)
?- -.update
%initial ~
%add (rids-for-ship ship.update)
%remove (rids-for-ship ship.update)
%edit (rids-for-ship ship.update)
%allow ~
%disallow ~
%set-public ~
==
::
++ rids-for-ship
|= s=ship
^- (list resource)
:: if the ship is in any group that I am pushing updates for, push
:: it out to that resource.
::
=/ rids
%+ skim ~(tap in scry-sharing)
|= r=resource
(is-member:grp s r)
?. =(s our.bowl)
rids
(snoc rids [our.bowl %''])
--
++ scry-sharing
.^ (set resource)
%gx
(scot %p our.bowl)
%contact-push-hook
(scot %da now.bowl)
/sharing/noun
==
::
++ get-contact
|= =ship
^- (unit contact:store)
=/ upd (scry-for (unit update:store) /contact/(scot %p ship))
?~ upd ~
?> ?=(%add -.u.upd)
`contact.u.upd
=/ =rolodex:store
(scry-for rolodex:store /all)
(~(get by rolodex) ship)
::
++ scry-is-public
.^ ?
%gx
(scot %p our.bowl)
%contact-store
(scot %da now.bowl)
/is-public/noun
==
::
++ is-allowed
|= =ship
|= [rid=resource =ship]
^- ?
=/ shp (scry-for ? /allowed-ship/(scot %p ship))
?: shp %.y
=/ allowed-groups ~(tap in (scry-for (set resource) /allowed-groups))
=/ grp ~(. group bowl)
|-
?~ allowed-groups %.n
?: (~(has in (members:grp i.allowed-groups)) ship)
%.y
$(allowed-groups t.allowed-groups)
=/ allowed-groups (scry-for (set resource) /allowed-groups)
?| :: if they are requesting our personal profile, check if we are
:: either public, or if they are on the allowed-ships list.
:: this is used for direct messages and leap searches
::
?& =(rid [our.bowl %''])
?| :: if our profile is public, allow
::
scry-is-public
:: if the requester is an allowed-ship, allow
::
(scry-for ? /allowed-ship/(scot %p ship))
:: if the requester of our profile is the host of one of
:: our allowed-groups, allow
::
%+ lien ~(tap in allowed-groups)
|= res=resource
=(entity.res ship)
== ==
:: if they are requesting our contact data within a group,
:: we make sure that we are sharing that group,
:: and that they are a member of the group
::
?& (~(has in scry-sharing) rid)
(~(has in (members:grp rid)) ship)
== ==
--

View File

@ -11,6 +11,27 @@
(snoc `^path`path %noun)
==
::
++ resource-for-update
|= =vase
^- (list resource)
=/ =update:store !<(update:store vase)
?- -.q.update
%add-graph ~[resource.q.update]
%remove-graph ~[resource.q.update]
%add-nodes ~[resource.q.update]
%remove-nodes ~[resource.q.update]
%add-signatures ~[resource.uid.q.update]
%remove-signatures ~[resource.uid.q.update]
%archive-graph ~[resource.q.update]
%unarchive-graph ~
%add-tag ~
%remove-tag ~
%keys ~
%tags ~
%tag-queries ~
%run-updates ~[resource.q.update]
==
::
++ get-graph
|= res=resource
^- update:store

View File

@ -3,6 +3,15 @@
::
|_ =bowl:gall
+$ card card:agent:gall
::
++ resource-for-update
|= =vase
^- (list resource)
=/ =update:store !<(update:store vase)
?: ?=(%initial -.update)
~
~[resource.update]
::
++ scry-for
|* [=mold =path]
=. path
@ -28,6 +37,15 @@
%+ scry-for ,(unit group)
`path`groups+(en-path:resource rid)
::
++ scry-groups
.^ ,(set resource)
%gy
(scot %p our.bowl)
%group-store
(scot %da now.bowl)
/groups/noun
==
::
++ members
|= rid=resource
=; =group

View File

@ -90,10 +90,8 @@
%chat-cli
%herm
%contact-store
%contact-hook
%contact-push-hook
%contact-pull-hook
%contact-view
%metadata-store
%s3-store
%file-server
@ -109,6 +107,7 @@
%metadata-push-hook
%metadata-pull-hook
%group-view
%settings-store
==
::
++ deft-fish :: default connects
@ -257,6 +256,7 @@
=? ..on-load (lte hood-version %12)
=> (se-born | %home %contact-push-hook)
=> (se-born | %home %contact-pull-hook)
=> (se-born | %home %settings-store)
(se-born | %home %group-view)
..on-load
::

View File

@ -4,6 +4,13 @@
/+ resource
::
|_ =bowl:gall
++ resource-for-update
|= =vase
^- (list resource)
=/ =update:store !<(update:store vase)
?. ?=(?(%add %remove %initial-group) -.update) ~
~[group.update]
::
++ app-paths-from-group
|= [=app-name:store group=resource]
^- (list resource)

View File

@ -20,6 +20,22 @@
::
/- *pull-hook
/+ default-agent, resource
|%
:: JSON conversions
++ dejs
=, dejs:format
|%
++ action
%- of
:~ add+add
==
++ add
%- ot
:~ ship+(su ;~(pfix sig fed:ag))
resource+dejs:resource
==
--
--
::
::
|%
@ -30,12 +46,15 @@
:: .store-name: name of the store to send subscription updates to.
:: .update-mark: mark that updates will be tagged with
:: .push-hook-name: name of the corresponding push-hook
:: .no-validate: If true, don't validate that resource/wire/src match
:: up
::
+$ config
$: store-name=term
update=mold
update-mark=term
push-hook-name=term
no-validate=_|
==
::
:: $base-state-0: state for the pull hook
@ -106,6 +125,14 @@
++ on-pull-kick
|~ resource
*(unit path)
:: +resource-for-update: get resources from vase
::
:: This should be identical to the +resource-for-update arm in the
:: corresponding push-hook
::
++ resource-for-update
|~ vase
*(list resource)
::
:: from agent:gall
++ on-init
@ -470,24 +497,30 @@
/helper/pull-hook
wire
::
++ get-conversion
.^ tube:clay
%cc (scot %p our.bowl) %home (scot %da now.bowl)
/[update-mark.config]/resource
==
::
++ give-update
^- card
[%give %fact ~[/tracking] %pull-hook-update !>(tracking)]
::
++ check-src
|= resources=(set resource)
^- ?
%+ roll ~(tap in resources)
|= [rid=resource out=_|]
?: out %.y
?~ ship=(~(get by tracking) rid)
%.n
=(src.bowl u.ship)
::
++ update-store
|= [wire-rid=resource =vase]
^- card
=/ =wire
(make-wire /store)
=+ !<(rid=resource (get-conversion vase))
?> =(src.bowl (~(got by tracking) rid))
?> =(wire-rid rid)
=+ resources=(~(gas in *(set resource)) (resource-for-update:og vase))
?> ?| no-validate.config
?& (check-src resources)
(~(has in resources) wire-rid)
== ==
[%pass wire %agent [our.bowl store-name.config] %poke update-mark.config vase]
--
--

View File

@ -67,6 +67,16 @@
|* =config
$_ ^|
|_ bowl:gall
::
:: +resource-for-update: get affected resources from an update
::
:: Given a vase of the update, the mark of which is
:: update-mark.config, produce the affected resources, if any.
::
++ resource-for-update
|~ vase
*(list resource)
::
:: +take-update: handle update from store
::
:: Given an update from the store, do other things after proxying
@ -145,12 +155,12 @@
=* state -
^- agent:gall
=<
|_ =bowl:gall
+* this .
og ~(. push-hook bowl)
hc ~(. +> bowl)
def ~(. (default-agent this %|) bowl)
::
++ on-init
=^ cards push-hook
on-init:og
@ -165,11 +175,9 @@
|^
?- -.old
%1
=. cards
:_(cards (build-mark:hc %sing))
=^ og-cards push-hook
(on-load:og inner-state.old)
[(weld (flop cards) og-cards) this(state old)]
[(weld cards og-cards) this(state old)]
::
%0
%_ $
@ -261,6 +269,7 @@
(push-updates:hc q.cage.sign)
cards
==
::
++ on-leave
|= =path
=^ cards push-hook
@ -269,20 +278,16 @@
::
++ on-arvo
|= [=wire =sign-arvo]
?. ?=([%helper %push-hook @ *] wire)
=^ cards push-hook
(on-arvo:og wire sign-arvo)
[cards this]
?. ?=(%resource-conversion i.t.t.wire)
(on-arvo:def wire sign-arvo)
:_ this
~[(build-mark:hc %next)]
=^ cards push-hook
(on-arvo:og wire sign-arvo)
[cards this]
::
++ on-fail
|= [=term =tang]
=^ cards push-hook
(on-fail:og term tang)
[cards this]
::
++ on-peek
|= =path
^- (unit (unit cage))
@ -311,6 +316,7 @@
%remove (remove +.action)
%revoke (revoke +.action)
==
::
++ add
|= rid=resource
=. sharing
@ -322,7 +328,7 @@
=/ pax=path
[%resource (en-path:resource rid)]
=/ paths=(set path)
%- sy
%- silt
%+ turn
(incoming-subscriptions pax)
|=([ship pox=path] pox)
@ -344,6 +350,7 @@
~
`[%give %kick ~[path] `her]
--
::
++ incoming-subscriptions
|= prefix=path
^- (list (pair ship path))
@ -371,58 +378,50 @@
++ push-updates
|= =vase
^- (list card:agent:gall)
=/ rid=(unit resource)
(resource-for-update vase)
?~ rid ~
=/ rids=(list resource) (resource-for-update vase)
=| cards=(list card:agent:gall)
|-
?~ rids cards
=/ prefix=path
resource+(en-path:resource u.rid)
resource+(en-path:resource i.rids)
=/ paths=(list path)
%~ tap in
%- silt
%+ turn
(incoming-subscriptions prefix)
|=([ship pax=path] pax)
?~ paths ~
[%give %fact paths update-mark.config vase]~
?~ paths $(rids t.rids)
%_ $
rids t.rids
cards (snoc cards [%give %fact paths update-mark.config vase])
==
::
++ forward-update
|= update=vase
|= =vase
^- (list card:agent:gall)
=/ rid=resource
(need (resource-for-update update))
=/ rids=(list resource) (resource-for-update vase)
=| cards=(list card:agent:gall)
|-
?~ rids cards
=/ =path
resource+(en-path:resource rid)
resource+(en-path:resource i.rids)
=/ =wire
(make-wire resource+(en-path:resource rid))
(make-wire resource+(en-path:resource i.rids))
=/ dap=term
?:(=(our.bowl entity.rid) store-name.config dap.bowl)
[%pass wire %agent [entity.rid dap] %poke update-mark.config update]~
::
++ get-conversion
.^ tube:clay
%cc (scot %p our.bowl) %home (scot %da now.bowl)
/[update-mark.config]/resource
?:(=(our.bowl entity.i.rids) store-name.config dap.bowl)
%_ $
rids t.rids
::
cards
%+ snoc cards
[%pass wire %agent [entity.i.rids dap] %poke update-mark.config vase]
==
::
++ resource-for-update
|= update=vase
^- (unit resource)
=/ converted=(each vase (list tank))
(mule |.((get-conversion update)))
?: ?=(%| -.converted)
%- (slog p.converted)
~
[~ !<(resource p.converted)]
::
++ build-mark
|= rav=?(%sing %next)
^- card
=/ =wire
(make-wire /resource-conversion)
=/ =mood:clay
[%c da+now.bowl /[update-mark.config]/resource]
=/ =rave:clay
?:(?=(%next rav) [rav mood] [rav mood])
[%pass wire %arvo %c %warp our.bowl [%home `rave]]
|= =vase
^- (list resource)
%~ tap in
%- silt
(resource-for-update:og vase)
--
--

View File

@ -0,0 +1,15 @@
/+ *contact-store
::
|_ share=[%share =ship]
++ grad %noun
++ grow
|%
++ noun share
--
::
++ grab
|%
+$ noun [%share ship]
++ json share:share-dejs
--
--

View File

@ -6,22 +6,6 @@
|%
++ noun upd
++ json (update:enjs upd)
++ resource
|^
?- -.upd
%initial [nobody %contacts]
%add [nobody %contacts]
%remove [nobody %contacts]
%edit [nobody %contacts]
%allow !!
%disallow !!
%set-public !!
==
::
++ nobody
^- @p
(bex 128)
--
--
::
++ grab

View File

@ -7,13 +7,6 @@
|%
++ noun upd
++ json (update:enjs upd)
++ resource
?+ -.q.upd !!
?(%run-updates %add-nodes %remove-nodes %add-graph) resource.q.upd
?(%remove-graph %archive-graph %unarchive-graph) resource.q.upd
?(%add-tag %remove-tag) resource.q.upd
?(%add-signatures %remove-signatures) resource.uid.q.upd
==
++ mime [/application/x-urb-graph-update (as-octs (jam upd))]
--
::

View File

@ -4,10 +4,6 @@
++ grow
|%
++ noun upd
++ resource
?< ?=(%initial -.upd)
resource.upd
::
++ json
%+ frond:enjs:format 'groupUpdate'
(update:enjs upd)

View File

@ -4,9 +4,6 @@
++ grow
|%
++ noun update
++ resource
?> ?=(?(%add %remove %initial-group) -.update)
group.update
++ json (update:enjs:store update)
--
::

View File

@ -1,12 +1,13 @@
/- *pull-hook
|_ act=action
/+ pull-hook
|_ =action:pull-hook
++ grab
|%
++ noun action
++ noun action:pull-hook
++ json action:dejs:pull-hook
--
++ grow
|%
++ noun act
++ noun action
--
++ grad %noun
--

View File

@ -4,7 +4,9 @@
++ grow
|%
++ noun rid
++ json (enjs:resource rid)
++ json
%+ frond:enjs:format %resource
(enjs:resource rid)
--
++ grab
|%

View File

@ -45,6 +45,7 @@
[%add rid groups+rid metadatum]
;< ~ bind:m (poke-our %metadata-store %metadata-action !>(met-action))
;< ~ bind:m (poke-our %metadata-push-hook push-hook-act)
;< ~ bind:m (poke-our %contact-push-hook push-hook-act)
(pure:m !>(~))

View File

@ -26,5 +26,6 @@
;< ~ bind:m (cleanup-md:view rid)
;< ~ bind:m (poke-our %group-store %group-update !>([%remove-group rid ~]))
;< ~ bind:m (poke-our %metadata-push-hook push-hook-act)
;< ~ bind:m (poke-our %contact-push-hook push-hook-act)
;< ~ bind:m (poke-our %group-push-hook push-hook-act)
(pure:m !>(~))

View File

@ -22,6 +22,7 @@
:- %pull-hook-action
!> ^- action:pull-hook
[%remove rid]
;< ~ bind:m (poke-our %contact-pull-hook pull-hook-act)
;< ~ bind:m (poke-our %metadata-pull-hook pull-hook-act)
;< ~ bind:m (poke-our %group-pull-hook pull-hook-act)
;< ~ bind:m (poke-our %group-store %group-update !>([%remove-group rid ~]))

View File

@ -57,6 +57,7 @@ export default class BaseApi<S extends object = {}> {
}
scry<T>(app: string, path: Path): Promise<T> {
console.log(path);
return fetch(`/~/scry/${app}${path}.json`).then(r => r.json() as Promise<T>);
}

View File

@ -34,12 +34,55 @@ export default class ContactsApi extends BaseApi<StoreState> {
});
}
allowShips(ships: Patp[]) {
return this.storeAction({
allow: {
ships
}
});
}
allowGroup(ship: string, name: string) {
const group = { ship, name };
return this.storeAction({
allow: {
group
}
});
}
setPublic(setPublic: any) {
return this.storeAction({
'set-public': setPublic
});
}
share(recipient: Patp) {
return this.action(
'contact-push-hook',
'contact-share',
{ share: recipient },
);
}
fetchIsAllowed(entity, name, ship, personal) {
const isPersonal = personal ? 'true' : 'false';
return this.scry<any>(
'contact-store',
`/is-allowed/${entity}/${name}/${ship}/${isPersonal}`
);
}
retrieve(ship: string) {
const resource = { ship, name: '' };
return this.action('contact-pull-hook', 'pull-hook-action', {
add: {
resource,
ship
}
});
}
private storeAction(action: any): Promise<any> {
return this.action('contact-store', 'contact-update', action)
}

View File

@ -22,7 +22,7 @@ const result = function(title, link, app, host) {
const shipIndex = function(contacts) {
const ships = [];
Object.keys(contacts).map((e) => {
return ships.push(result(e, `/~profile/${e}`, 'profile', contacts[e]?.status));
return ships.push(result(e, `/~profile/${e}`, 'profile', contacts[e]?.status || ""));
});
return ships;
};

View File

@ -2,6 +2,7 @@ import _ from 'lodash';
import { StoreState } from '../../store/type';
import { Cage } from '~/types/cage';
import { ContactUpdate } from '~/types/contact-update';
import {resourceAsPath} from '../lib/util';
type ContactState = Pick<StoreState, 'contacts'>;
@ -14,6 +15,12 @@ export const ContactReducer = (json, state) => {
edit(data, state);
setPublic(data, state);
}
// TODO: better isolation
const res = _.get(json, 'resource', false);
if(res) {
state.nackedContacts = state.nackedContacts.add(`~${res.ship}`);
}
};
const initial = (json: ContactUpdate, state: S) => {

View File

@ -80,6 +80,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
},
isContactPublic: false,
contacts: {},
nackedContacts: new Set(),
notifications: new BigIntOrderedMap<Timebox>(),
archivedNotifications: new BigIntOrderedMap<Timebox>(),
notificationsGroupConfig: [],

View File

@ -12,7 +12,8 @@ import {
NotificationGraphConfig,
GroupNotificationsConfig,
Unreads,
JoinRequests
JoinRequests,
Patp
} from "~/types";
export interface StoreState {
@ -29,6 +30,7 @@ export interface StoreState {
// groups state
groups: Groups;
groupKeys: Set<Path>;
nackedContacts: Set<Patp>
s3: S3State;
graphs: Graphs;
graphKeys: Set<string>;

View File

@ -45,6 +45,7 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
this.subscribe('/updates', 'hark-group-hook');
this.subscribe('/all', 'settings-store');
this.subscribe('/all', 'group-view');
this.subscribe('/nacks', 'contact-pull-hook');
}
restart() {

View File

@ -167,12 +167,14 @@ class App extends React.Component {
<Omnibox
associations={state.associations}
apps={state.launch}
tiles={state.launch.tiles}
api={this.api}
contacts={state.contacts}
notifications={state.notificationsCount}
invites={state.invites}
groups={state.groups}
show={this.props.omniboxShown}
toggle={this.props.toggleOmnibox}
/>
</ErrorBoundary>
<ErrorBoundary>

View File

@ -1,4 +1,4 @@
import React, { useRef, useCallback, useEffect } from 'react';
import React, { useRef, useCallback, useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { Col } from '@tlon/indigo-react';
import _ from 'lodash';
@ -36,6 +36,7 @@ export function ChatResource(props: ChatResourceProps) {
const [,, owner, name] = station.split('/');
const ourContact = contacts?.[`~${window.ship}`];
console.log(contacts);
const chatInput = useRef<ChatInput>();
@ -89,7 +90,13 @@ export function ChatResource(props: ChatResourceProps) {
return (
<Col {...bind} height="100%" overflow="hidden" position="relative">
<ShareProfile our={ourContact} />
<ShareProfile
our={ourContact}
api={props.api}
recipient={owner}
group={group}
groupPath={groupPath}
/>
{dragging && <SubmitDragger />}
<ChatWindow
mailboxSize={5}

View File

@ -1,16 +1,111 @@
import React from 'react';
import React, {
useState,
useEffect
} from 'react';
import { Box, Row, Text, BaseImage } from '@tlon/indigo-react';
import { uxToHex } from '~/logic/lib/util';
import { Sigil } from '~/logic/lib/sigil';
export const ShareProfile = (props) => {
const image = (props?.our?.avatar)
? <BaseImage src={props.our.avatar} width='24px' height='24px' borderRadius={2} style={{ objectFit: 'cover' }} />
: <Row p={1} alignItems="center" borderRadius={2} backgroundColor={`#${uxToHex(props?.our?.color)}` || "#000000"}>
<Sigil ship={window.ship} size={16} icon color={`#${uxToHex(props?.our?.color)}` || "#000000"} />
</Row>;
const pathAsResource = (path) => {
if (!path) {
return false;
}
const pathArr = path.split('/');
if (pathArr.length !== 4) {
return false;
}
return (
return {
entity: pathArr[2],
name: pathArr[3]
};
};
export const ShareProfile = (props) => {
const { api, hideBanner, group, groupPath } = props;
const [showBanner, setShowBanner] = useState(false);
const res = pathAsResource(groupPath);
const [recipients, setRecipients] = useState([]);
useEffect(() => {
(async () => {
if (!res) { return; }
if (!group) { return; }
if (group.hidden) {
const members = _.compact(await Promise.all(
Array.from(group.members)
.map(s => {
const ship = `~${s}`;
if(s === window.ship) {
return Promise.resolve(null);
}
return props.api.contacts.fetchIsAllowed(
`~${window.ship}`,
'personal',
ship,
true
).then(isAllowed => {
return isAllowed ? null : ship;
});
})
));
if(members.length > 0) {
setShowBanner(true);
setRecipients(members);
} else {
setShowBanner(false);
}
} else {
const groupShared = await props.api.contacts.fetchIsAllowed(
res.entity,
res.name,
res.entity,
false
);
setShowBanner(!groupShared);
}
})();
}, [groupPath]);
const image = (props?.our?.avatar)
? (
<BaseImage
src={props.our.avatar}
width='24px'
height='24px'
borderRadius={2}
style={{ objectFit: 'cover' }} />
) : (
<Row
p={1}
alignItems="center"
borderRadius={2}
backgroundColor={!props.our ? `#${uxToHex(props.our.color)}` : "#000000"}>
<Sigil
ship={window.ship}
size={16}
color={`#${uxToHex(props?.our?.color)}` || "#000000"}
icon />
</Row>
);
const onClick = async () => {
if(group.hidden && recipients.length > 0) {
await api.contacts.allowShips(recipients);
await Promise.all(recipients.map(r => api.contacts.share(r)))
setShowBanner(false);
} else if (!group.hidden) {
const [,,ship,name] = groupPath.split('/');
await api.contacts.allowGroup(ship,name);
await api.contacts.share(ship);
setShowBanner(false);
}
};
return showBanner ? (
<Row
height="48px"
alignItems="center"
@ -22,9 +117,9 @@ export const ShareProfile = (props) => {
{image}
<Text verticalAlign="middle" pl={2}>Share private profile?</Text>
</Row>
<Box pr={2}>
<Box pr={2} onClick={onClick}>
<Text color="blue" bold cursor="pointer">Share</Text>
</Box>
</Row>
);
) : null;
};

View File

@ -1,167 +0,0 @@
import React from "react";
import { Sigil } from "~/logic/lib/sigil";
import * as Yup from "yup";
import { uxToHex } from "~/logic/lib/util";
import {
ManagedForm as Form,
Col,
ManagedTextInputField as Input,
Box,
Text,
Row,
BaseImage
} from "@tlon/indigo-react";
import { Formik, FormikHelpers } from "formik";
import { Contact } from "~/types/contact-update";
import { AsyncButton } from "~/views/components/AsyncButton";
import { ColorInput } from "~/views/components/ColorInput";
import GlobalApi from "~/logic/api/global";
import { ImageInput } from "~/views/components/ImageInput";
import { S3State } from "~/types";
import useLocalState from "~/logic/state/local";
interface ContactCardProps {
contact: Contact;
path: string;
api: GlobalApi;
s3: S3State;
rootIdentity: Contact;
}
const formSchema = Yup.object({
color: Yup.string(),
nickname: Yup.string(),
email: Yup.string().matches(
new RegExp(
String(
/[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*/.source
) +
/@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/
.source
),
"Not a valid email"
),
phone: Yup.string().matches(
new RegExp(
String(/^\s*(?:\+?(\d{1,3}))?/.source) +
/([-. (]*(\d{3})[-. )]*)?((\d{3})[-. ]*(\d{2,4})(?:[-.x ]*(\d+))?)\s*$/
.source
),
"Not a valid phone"
),
website: Yup.string().matches(
new RegExp(
String(/[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}/.source) +
/\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/.source
),
"Not a valid website"
),
});
const emptyContact = {
avatar: null,
color: '0',
nickname: '',
email: '',
phone: '',
website: '',
notes: ''
};
export function ContactCard(props: ContactCardProps) {
const { hideAvatars, hideNicknames } = useLocalState(({ hideAvatars, hideNicknames }) => ({
hideAvatars, hideNicknames
}));
const us = `~${window.ship}`;
const { contact, rootIdentity } = props;
const onSubmit = async (values: any, actions: FormikHelpers<Contact>) => {
try {
if(!contact) {
const [,,ship] = props.path.split('/');
values.color = uxToHex(values.color);
const sharedValues = Object.assign({}, values);
sharedValues.avatar = !values.avatar ? null : { url: values.avatar };
console.log(values);
await props.api.contacts.share(ship, props.path, us, sharedValues);
actions.setStatus({ success: null });
return;
}
await Object.keys(values).reduce((acc, key) => {
const newValue = key !== "color" ? values[key] : uxToHex(values[key]);
if (newValue !== contact[key]) {
if (key === "avatar") {
return acc.then(() =>
props.api.contacts.edit(props.path, us, {
avatar: { url: newValue },
} as any)
);
}
return acc.then(() =>
props.api.contacts.edit(props.path, us, {
[key]: newValue,
} as any)
);
}
return acc;
}, Promise.resolve());
actions.setStatus({ success: null });
} catch (e) {
console.error(e);
actions.setStatus({ error: e.message });
}
};
const hexColor = contact?.color ? `#${uxToHex(contact.color)}` : "#000000";
const image = (!hideAvatars && contact?.avatar)
? <BaseImage src={contact.avatar} width='100%' height='100%' style={{ objectFit: 'cover' }} />
: <Sigil ship={us} size={32} color={hexColor} />;
const nickname = (!hideNicknames && contact?.nickname) ? contact.nickname : "";
return (
<Box p={4} height="100%" overflowY="auto">
<Formik
validationSchema={formSchema}
initialValues={contact || rootIdentity || emptyContact}
onSubmit={onSubmit}
>
<Form
display="grid"
gridAutoRows="auto"
gridTemplateColumns="100%"
gridRowGap="5"
maxWidth="400px"
width="100%"
>
<Row
borderBottom={1}
borderBottomColor="washedGray"
pb={3}
alignItems="center"
>
<Box height='32px' width='32px'>
{image}
</Box>
<Box ml={2}>
<Text mono={!Boolean(nickname)}>{nickname}</Text>
</Box>
</Row>
<ImageInput id="avatar" label="Avatar" s3={props.s3} />
<ColorInput id="color" label="Sigil Color" />
<Input id="nickname" label="Nickname" />
<Input id="email" label="Email" />
<Input id="phone" label="Phone" />
<Input id="website" label="Website" />
<Input id="notes" label="Notes" />
<AsyncButton primary loadingText="Updating..." border>
{(contact) ? "Save" : "Share Contact"}
</AsyncButton>
</Form>
</Formik>
</Box>
);
}

View File

@ -1,4 +1,4 @@
import React from "react";
import React, {useEffect} from "react";
import { Sigil } from "~/logic/lib/sigil";
import { ViewProfile } from './ViewProfile';
import { EditProfile } from './EditProfile';
@ -24,7 +24,16 @@ export function Profile(props: any) {
if (!props.ship) {
return null;
}
const { contact, isPublic, isEdit, ship } = props;
const { contact, nackedContacts, hasLoaded, isPublic, isEdit, ship } = props;
const nacked = nackedContacts.has(ship);
useEffect(() => {
if(hasLoaded && !contact && !nacked) {
props.api.contacts.retrieve(ship);
}
}, [hasLoaded, contact])
const hexColor = contact?.color ? `#${uxToHex(contact.color)}` : "#000000";
const cover = (contact?.cover)
? <BaseImage src={contact.cover} width='100%' height='100%' style={{ objectFit: 'cover' }} />
@ -70,7 +79,13 @@ export function Profile(props: any) {
associations={props.associations}
isPublic={isPublic}/>
) : (
<ViewProfile ship={ship} contact={contact} isPublic={isPublic} />
<ViewProfile
api={props.api}
nacked={nacked}
ship={ship}
contact={contact}
isPublic={isPublic}
/>
) }
</Box>
</Center>

View File

@ -59,3 +59,4 @@ export function SetStatus(props: any) {
</Row>
);
}

View File

@ -1,4 +1,4 @@
import React from "react";
import React, {useEffect, useState} from "react";
import { Sigil } from "~/logic/lib/sigil";
import {
@ -7,17 +7,28 @@ import {
Text,
Row,
Button,
Col
Col,
LoadingSpinner
} from "@tlon/indigo-react";
import { AsyncButton } from "~/views/components/AsyncButton";
import RichText from "~/views/components/RichText";
import { useHistory } from "react-router-dom";
import {GroupSummary} from "~/views/landscape/components/GroupSummary";
import {MetadataUpdatePreview} from "~/types";
export function ViewProfile(props: any) {
const history = useHistory();
const { contact, isPublic, ship } = props;
const { api, contact, nacked, isPublic, ship } = props;
const [previews, setPreviews] = useState<MetadataUpdatePreview[]>([]);
useEffect(() => {
(async () => {
setPreviews(
await Promise.all((contact?.groups || []).map(g => api.metadata.preview(g)))
);
})();
}, [contact?.groups])
return (
<>
<Row
@ -48,7 +59,27 @@ export function ViewProfile(props: any) {
{(contact?.bio ? contact.bio : "")}
</RichText>
</Center>
</Col>
{ (contact?.groups || []).length > 0 && (
<Col gapY="3" my="3" alignItems="center">
<Text fontWeight="medium">Pinned Groups</Text>
{previews.length > 0 ? (
<LoadingSpinner />
) : (
<Row justifyContent="center" gapX="3">
{previews.map(p => (
<Box p="2" border="1" borderColor="washedGray">
<GroupSummary
metadata={p.metadata}
memberCount={p.members}
channelCount={p?.['channel-count']}
/>
</Box>
))}
</Row>
)}
</Col>
)}
{ (ship === `~${window.ship}`) ? (
<Row
pb={2}
@ -65,7 +96,7 @@ export function ViewProfile(props: any) {
</Row>
) : null
}
{ !isPublic && ship === `~${window.ship}` ? (
{ (nacked || (!isPublic && ship === `~${window.ship}`)) ? (
<Box
height="200px"
borderRadius={1}

View File

@ -45,6 +45,7 @@ export default function ProfileScreen(props: any) {
<Box>
<Profile
ship={ship}
hasLoaded={Object.keys(props.contacts).length !== 0}
associations={props.associations}
groups={props.groups}
contact={contact}
@ -52,6 +53,7 @@ export default function ProfileScreen(props: any) {
s3={props.s3}
isEdit={isEdit}
isPublic={isPublic}
nackedContacts={props.nackedContacts}
/>
</Box>
</Box>

View File

@ -1,304 +0,0 @@
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import { Box, Row, Rule, Text } from '@tlon/indigo-react';
import index from '~/logic/lib/omnibox';
import Mousetrap from 'mousetrap';
import OmniboxInput from './OmniboxInput';
import OmniboxResult from './OmniboxResult';
import { withLocalState } from '~/logic/state/local';
import defaultApps from '~/logic/lib/default-apps';
export class Omnibox extends Component {
constructor(props) {
super(props);
this.state = {
index: new Map([]),
query: '',
results: this.initialResults(),
selected: []
};
this.handleClickOutside = this.handleClickOutside.bind(this);
this.search = this.search.bind(this);
this.navigate = this.navigate.bind(this);
this.control - this.control.bind(this);
this.setPreviousSelected = this.setPreviousSelected.bind(this);
this.setNextSelected = this.setNextSelected.bind(this);
this.renderResults = this.renderResults.bind(this);
}
componentDidUpdate(prevProps, prevState) {
if (prevProps !== this.props) {
const { pathname } = this.props.location;
const selectedGroup = pathname.startsWith('/~landscape/ship/') ? '/' + pathname.split('/').slice(2,5).join('/') : null;
this.setState({ index: index(this.props.contacts, this.props.associations, this.props.apps.tiles, selectedGroup, this.props.groups) });
}
if (prevProps && (prevProps.apps !== this.props.apps) && (this.state.query === '')) {
this.setState({ results: this.initialResults() });
}
if (prevProps && this.props.show && prevProps.show !== this.props.show) {
Mousetrap.bind('escape', this.props.toggle);
document.addEventListener('mousedown', this.handleClickOutside);
const touchstart = new Event('touchstart');
this.omniInput.input.dispatchEvent(touchstart);
this.omniInput.input.focus();
}
}
componentWillUpdate(prevProps) {
if (this.props.show && prevProps.show !== this.props.show) {
Mousetrap.unbind('escape');
document.removeEventListener('mousedown', this.handleClickOutside);
}
}
getSearchedCategories() {
return ['ships', 'other', 'commands', 'groups', 'subscriptions', 'apps'];
}
control(evt) {
if (evt.key === 'Escape') {
if (this.state.query.length > 0) {
this.setState({ query: '', results: this.initialResults(), selected: [] });
} else if (this.props.show) {
this.props.toggleOmnibox();
}
};
if (
evt.key === 'ArrowUp' ||
(evt.shiftKey && evt.key === 'Tab')) {
evt.preventDefault();
return this.setPreviousSelected();
}
if (evt.key === 'ArrowDown' || evt.key === 'Tab') {
evt.preventDefault();
this.setNextSelected();
}
if (evt.key === 'Enter') {
evt.preventDefault();
if (this.state.selected.length > 0) {
this.navigate(this.state.selected[0], this.state.selected[1]);
} else if (Array.from(this.state.results.values()).flat().length === 0) {
return;
} else {
this.navigate(
Array.from(this.state.results.values()).flat()[0].app,
Array.from(this.state.results.values()).flat()[0].link);
}
}
}
handleClickOutside(evt) {
if (this.props.show && !this.omniBox.contains(evt.target)) {
this.setState({ results: this.initialResults(), query: '', selected: [] }, () => {
this.props.toggleOmnibox();
});
}
}
initialResults() {
return new Map(this.getSearchedCategories().map((category) => {
if (!this.state) {
return [category, []];
}
if (category === 'other') {
return ['other', this.state.index.get('other')];
}
return [category, []];
}));
}
navigate(app, link) {
const { props } = this;
this.setState({ results: this.initialResults(), query: '' }, () => {
props.toggleOmnibox();
if (defaultApps.includes(app.toLowerCase())
|| app === 'profile'
|| app === 'Links'
|| app === 'Terminal'
|| app === 'home'
|| app === 'inbox')
{
props.history.push(link);
} else {
window.location.href = link;
}
});
}
search(event) {
const { state } = this;
let query = event.target.value;
const results = this.initialResults();
let selected = state.selected;
this.setState({ query });
// wipe results if backspacing
if (query.length === 0) {
this.setState({ results: results, selected: [] });
return;
}
// don't search for single characters
if (query.length === 1) {
return;
}
query = query.toLowerCase();
this.getSearchedCategories().map((category) => {
const categoryIndex = state.index.get(category);
results.set(category,
categoryIndex.filter((result) => {
return (
result.title.toLowerCase().includes(query) ||
result.link.toLowerCase().includes(query) ||
result.app.toLowerCase().includes(query) ||
(result.host !== null ? result.host.toLowerCase().includes(query) : false)
);
})
);
});
const flattenedResultLinks = Array.from(results.values()).flat().map(result => [result.app, result.link]);
if (!flattenedResultLinks.includes(selected)) {
selected = flattenedResultLinks[0] || [];
}
this.setState({ results, selected });
}
setPreviousSelected() {
const current = this.state.selected;
const flattenedResults = Array.from(this.state.results.values()).flat();
const totalLength = flattenedResults.length;
if (current !== []) {
const currentIndex = flattenedResults.indexOf(
...flattenedResults.filter((e) => {
return e.link === current[1];
})
);
if (currentIndex > 0) {
const { app, link } = flattenedResults[currentIndex - 1];
this.setState({ selected: [app, link] });
} else {
const { app, link } = flattenedResults[totalLength - 1];
this.setState({ selected: [app, link] });
}
} else {
const { app, link } = flattenedResults[totalLength - 1];
this.setState({ selected: [app, link] });
}
}
setNextSelected() {
const current = this.state.selected;
const flattenedResults = Array.from(this.state.results.values()).flat();
if (current !== []) {
const currentIndex = flattenedResults.indexOf(
...flattenedResults.filter((e) => {
return e.link === current[1];
})
);
if (currentIndex < flattenedResults.length - 1) {
const { app, link } = flattenedResults[currentIndex + 1];
this.setState({ selected: [app, link] });
} else {
const { app, link } = flattenedResults[0];
this.setState({ selected: [app, link] });
}
} else {
const { app, link } = flattenedResults[0];
this.setState({ selected: [app, link] });
}
}
renderResults() {
const { props, state } = this;
return <Box
maxHeight={['200px', "400px"]}
overflowY="auto"
overflowX="hidden"
borderBottomLeftRadius='2'
borderBottomRightRadius='2'
>
{this.getSearchedCategories()
.map(category => Object({ category, categoryResults: state.results.get(category) }))
.filter(category => category.categoryResults.length > 0)
.map(({ category, categoryResults }, i) => {
const categoryTitle = (category === 'other')
? null : <Row pl='2' height='5' alignItems='center' bg='washedGray'><Text gray bold>{category.charAt(0).toUpperCase() + category.slice(1)}</Text></Row>;
const selected = this.state.selected?.length ? this.state.selected[1] : '';
return (<Box key={i} width='max(50vw, 300px)' maxWidth='600px'>
{categoryTitle}
{categoryResults.map((result, i2) => (
<OmniboxResult
key={i2}
icon={result.app}
text={result.title}
subtext={result.host}
link={result.link}
navigate={() => this.navigate(result.app, result.link)}
selected={selected}
invites={props.invites}
notifications={props.notifications}
contacts={props.contacts}
/>
))}
</Box>
);
})
}
</Box>;
}
render() {
const { props, state } = this;
if (state?.selected?.length === 0 && Array.from(this.state.results.values()).flat().length) {
this.setNextSelected();
}
return (
<Box
backgroundColor='scales.black30'
width='100%'
height='100%'
position='absolute'
top='0'
right='0'
zIndex='9'
display={props.show ? 'block' : 'none'}>
<Row justifyContent='center'>
<Box
mt={['10vh', '20vh']}
width='max(50vw, 300px)'
maxWidth='600px'
borderRadius='2'
backgroundColor='white'
ref={(el) => {
this.omniBox = el;
}}>
<OmniboxInput
ref={(el) => {
this.omniInput = el;
}}
control={e => this.control(e)}
search={this.search}
query={state.query}
/>
{this.renderResults()}
</Box>
</Row>
</Box>
);
}
}
export default withRouter(withLocalState(Omnibox, ['toggleOmnibox', 'omniboxShown']));

View File

@ -0,0 +1,298 @@
import React, { useMemo, useRef, useCallback, useEffect, useState } from 'react';
import { withRouter, useLocation, useHistory } from 'react-router-dom';
import { Box, Row, Rule, Text } from '@tlon/indigo-react';
import * as ob from 'urbit-ob';
import makeIndex from '~/logic/lib/omnibox';
import Mousetrap from 'mousetrap';
import OmniboxInput from './OmniboxInput';
import OmniboxResult from './OmniboxResult';
import { withLocalState } from '~/logic/state/local';
import { deSig } from '~/logic/lib/util';
import defaultApps from '~/logic/lib/default-apps';
import {Associations, Contacts, Groups, Tile, Invites} from '~/types';
import {useOutsideClick} from '~/logic/lib/useOutsideClick';
interface OmniboxProps {
associations: Associations;
contacts: Contacts;
groups: Groups;
tiles: {
[app: string]: Tile;
};
show: boolean;
toggle: () => void;
notifications: number;
invites: Invites;
}
const SEARCHED_CATEGORIES = ['ships', 'other', 'commands', 'groups', 'subscriptions', 'apps'];
export function Omnibox(props: OmniboxProps) {
const location = useLocation();
const history = useHistory();
const omniboxRef = useRef<HTMLDivElement | null>(null)
const inputRef = useRef<HTMLInputElement | null>(null);
const [query, setQuery] = useState('');
const [selected, setSelected] = useState<[] | [string, string]>([])
const contacts = useMemo(() => {
const maybeShip = `~${deSig(query)}`;
return ob.isValidPatp(maybeShip)
? {...props.contacts, [maybeShip]: {} }
: props.contacts;
}, [props.contacts, query]);
const index = useMemo(() => {
const selectedGroup = location.pathname.startsWith('/~landscape/ship/')
? '/' + location.pathname.split('/').slice(2,5).join('/')
: null;
return makeIndex(
contacts,
props.associations,
props.tiles,
selectedGroup,
props.groups
);
}, [location.pathname, contacts, props.associations, props.groups, props.tiles]);
const onOutsideClick = useCallback(() => {
props.show && props.toggle()
}, [props.show, props.toggle]);
useOutsideClick(omniboxRef, onOutsideClick)
// handle omnibox show
useEffect(() => {
if(!props.show) {
return;
}
Mousetrap.bind('escape', props.toggle);
const touchstart = new Event('touchstart');
inputRef?.current?.input?.dispatchEvent(touchstart);
inputRef?.current?.input?.focus();
return () => {
Mousetrap.unbind('escape');
setQuery('');
};
}, [props.show]);
const initialResults = useMemo(() => {
return new Map(SEARCHED_CATEGORIES.map((category) => {
if (category === 'other') {
return ['other', index.get('other')];
}
return [category, []];
}));
}, [index]);
const results = useMemo(() => {
if(query.length <= 1) {
return initialResults;
}
const q = query.toLowerCase();
let resultsMap = new Map();
SEARCHED_CATEGORIES.map((category) => {
const categoryIndex = index.get(category);
resultsMap.set(category,
categoryIndex.filter((result) => {
return (
result.title.toLowerCase().includes(q) ||
result.link.toLowerCase().includes(q) ||
result.app.toLowerCase().includes(q) ||
(result.host !== null ? result.host.toLowerCase().includes(q) : false)
);
})
);
});
return resultsMap;
}, [query, index]);
const navigate = useCallback((app: string, link: string) => {
props.toggle();
if (defaultApps.includes(app.toLowerCase())
|| app === 'profile'
|| app === 'Links'
|| app === 'Terminal'
|| app === 'home'
|| app === 'inbox')
{
history.push(link);
} else {
window.location.href = link;
}
}, [history, props.toggle]);
const setPreviousSelected = useCallback(() => {
const flattenedResults = Array.from(results.values()).flat();
const totalLength = flattenedResults.length;
if (selected.length) {
const currentIndex = flattenedResults.indexOf(
...flattenedResults.filter((e) => {
return e.link === selected[1];
})
);
if (currentIndex > 0) {
const { app, link } = flattenedResults[currentIndex - 1];
setSelected([app, link]);
} else {
const { app, link } = flattenedResults[totalLength - 1];
setSelected([app, link]);
}
} else {
const { app, link } = flattenedResults[totalLength - 1];
setSelected([app, link]);
}
}, [results, selected]);
const setNextSelected = useCallback(() => {
const flattenedResults = Array.from(results.values()).flat();
if (selected.length){
const currentIndex = flattenedResults.indexOf(
...flattenedResults.filter((e) => {
return e.link === selected[1];
})
);
if (currentIndex < flattenedResults.length - 1) {
const { app, link } = flattenedResults[currentIndex + 1];
setSelected([app, link]);
} else {
const { app, link } = flattenedResults[0];
setSelected([app, link]);
}
} else {
const { app, link } = flattenedResults[0];
setSelected([app, link]);
}
}, [selected, results]);
const control = useCallback((evt) => {
if (evt.key === 'Escape') {
if (query.length > 0) {
setQuery('');
return;
} else if (props.show) {
props.toggle();
return;
}
};
if (
evt.key === 'ArrowUp' ||
(evt.shiftKey && evt.key === 'Tab')) {
evt.preventDefault();
setPreviousSelected();
return;
}
if (evt.key === 'ArrowDown' || evt.key === 'Tab') {
evt.preventDefault();
setNextSelected();
return;
}
if (evt.key === 'Enter') {
evt.preventDefault();
if (selected.length) {
navigate(selected[0], selected[1]);
} else if (Array.from(results.values()).flat().length === 0) {
return;
} else {
navigate(
Array.from(results.values()).flat()[0].app,
Array.from(results.values()).flat()[0].link);
}
}
}, [
props.toggle,
selected,
navigate,
query,
props.show,
results,
setPreviousSelected,
setNextSelected
]);
useEffect(() => {
const flattenedResultLinks = Array.from(results.values())
.flat()
.map(result => [result.app, result.link]);
if (!flattenedResultLinks.includes(selected)) {
setSelected(flattenedResultLinks[0] || []);
}
}, [results]);
const search = useCallback((event) => {
setQuery(event.target.value);
}, []);
const renderResults = useCallback(() => {
return <Box
maxHeight={['200px', "400px"]}
overflowY="auto"
overflowX="hidden"
borderBottomLeftRadius='2'
borderBottomRightRadius='2'
>
{SEARCHED_CATEGORIES
.map(category => Object({ category, categoryResults: results.get(category) }))
.filter(category => category.categoryResults.length > 0)
.map(({ category, categoryResults }, i) => {
const categoryTitle = (category === 'other')
? null : <Row pl='2' height='5' alignItems='center' bg='washedGray'><Text gray bold>{category.charAt(0).toUpperCase() + category.slice(1)}</Text></Row>;
const sel = selected?.length ? selected[1] : '';
return (<Box key={i} width='max(50vw, 300px)' maxWidth='600px'>
{categoryTitle}
{categoryResults.map((result, i2) => (
<OmniboxResult
key={i2}
icon={result.app}
text={result.title}
subtext={result.host}
link={result.link}
navigate={() => navigate(result.app, result.link)}
selected={sel}
invites={props.invites}
notifications={props.notifications}
contacts={props.contacts}
/>
))}
</Box>
);
})
}
</Box>;
}, [results, navigate, selected, props.contacts, props.notifications, props.invites]);
return (
<Box
backgroundColor='scales.black30'
width='100%'
height='100%'
position='absolute'
top='0'
right='0'
zIndex='9'
display={props.show ? 'block' : 'none'}>
<Row justifyContent='center'>
<Box
mt={['10vh', '20vh']}
width='max(50vw, 300px)'
maxWidth='600px'
borderRadius='2'
backgroundColor='white'
ref={(el) => { omniboxRef.current = el; }}
>
<OmniboxInput
ref={(el) => { inputRef.current = el; }}
control={e => control(e)}
search={search}
query={query}
/>
{renderResults()}
</Box>
</Row>
</Box>
);
}
export default withLocalState(Omnibox, ['toggleOmnibox', 'omniboxShown']);