From 068a8c98d6c81b1c37bfe7f66205da67c92299de Mon Sep 17 00:00:00 2001 From: Fang Date: Sun, 1 Mar 2020 01:31:05 +0100 Subject: [PATCH 01/19] metadata: add helper lib for common queries --- pkg/arvo/lib/metadata.hoon | 54 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 pkg/arvo/lib/metadata.hoon diff --git a/pkg/arvo/lib/metadata.hoon b/pkg/arvo/lib/metadata.hoon new file mode 100644 index 000000000..b83909674 --- /dev/null +++ b/pkg/arvo/lib/metadata.hoon @@ -0,0 +1,54 @@ +:: metadata: helpers for getting data from the metadata-store +:: +/- *metadata-store +:: +|_ =bowl:gall +++ app-paths-from-group + |= [=app-name =group-path] + ^- (list app-path) + %+ murn + %~ tap in + =- (~(gut by -) group-path ~) + .^ (jug ^group-path resource) + %gy + (scot %p our.bowl) + %metadata-store + (scot %da now.bowl) + /group-indices + == + |= =resource + ^- (unit app-path) + ?. =(app-name.resource app-name) ~ + `app-path.resource +:: +++ groups-from-resource + |= =resource + ^- (list group-path) + =; resources + %~ tap in + %+ ~(gut by resources) + resource + *(set group-path) + .^ (jug ^resource group-path) + %gy + (scot %p our.bowl) + %metadata-store + (scot %da now.bowl) + /resource-indices + == +:: +++ check-resource-permissions + |= [=ship =resource] + ^- ? + %+ lien (groups-from-resource resource) + |= =group-path + .^ ? + %gx + (scot %p our.bowl) + %permission-store + (scot %da now.bowl) + %permitted + (scot %p ship) + (snoc group-path %noun) + == +-- \ No newline at end of file From 0658367aafad5eebf42475d12b03240aa766a147 Mon Sep 17 00:00:00 2001 From: Fang Date: Sun, 1 Mar 2020 01:36:52 +0100 Subject: [PATCH 02/19] link hooks: discover resources with metadata-store Instead of assuming groups to be singular resources, now depends on metadata-store to discover %link resources, and the groups associated with those. --- pkg/arvo/app/link-listen-hook.hoon | 264 +++++++++++++++++++++-------- pkg/arvo/app/link-proxy-hook.hoon | 147 +++++++++++----- 2 files changed, 299 insertions(+), 112 deletions(-) diff --git a/pkg/arvo/app/link-listen-hook.hoon b/pkg/arvo/app/link-listen-hook.hoon index 948aadc57..68c8c487a 100644 --- a/pkg/arvo/app/link-listen-hook.hoon +++ b/pkg/arvo/app/link-listen-hook.hoon @@ -1,21 +1,33 @@ :: link-listen-hook: get your friends' bookmarks :: -:: on-init, subscribes to all groups on this ship. for every ship in a group, -:: we subscribe to their link's local-pages and annotations -:: at the group path (through link-proxy-hook), -:: and forwards all entries into our link as submissions and comments. +:: subscribes to all %link resources in the metadata-store. +:: for all all ships in groups associated with those resources, we subscribe +:: to their link's local-pages and annotations at the resource path (through +:: link-proxy-hook), and forward all entries into our link-store as +:: submissions and comments. :: -:: if a subscription to a group member fails, we assume it's because their -:: group definition hasn't been updated to include us yet. +:: if a subscription to a target fails, we assume it's because their +:: metadata+groups definition hasn't been updated to include us yet. :: we retry with exponential backoff, maxing out at one hour timeouts. +:: to expede this process, we prod other potential listeners when we add +:: them to our metadata+groups definition. :: -/- *link, group-store -/+ default-agent, verb, dbug +/- *metadata-store, *link, group-store +/+ metadata, default-agent, verb, dbug :: |% +$ state-0 $: %0 retry-timers=(map target @dr) + :: reasoning: the resources we're subscribed to, + :: and the groups that cause that. + :: + :: we don't strictly need to track this in state, but doing so heavily + :: simplifies logic and reduces the amount of big scries we do. + :: this also gives us the option to check & restore subscriptions, + :: should we ever need that. + :: + reasoning=(jug [ship app-path] group-path) == :: +$ what-target ?(%local-pages %annotations) @@ -52,7 +64,7 @@ ++ on-init ^- (quip card _this) :_ this - [watch-groups:do]~ + ~[watch-metadata:do watch-groups:do] :: ++ on-save !>(state) ++ on-load @@ -63,26 +75,27 @@ ++ on-agent |= [=wire =sign:agent:gall] ^- (quip card _this) - ?: ?=([%groups ~] wire) - =^ cards state + =^ cards state + ?+ wire ~|([dap.bowl %weird-agent-wire wire] !!) + [%metadata ~] + (take-metadata-sign:do sign) + :: + [%groups ~] (take-groups-sign:do sign) - [cards this] - ?: ?=([%links ?(%local-pages %annotations) @ ^] wire) - =^ cards state + :: + [%links ?(%local-pages %annotations) @ ^] (take-link-sign:do (wire-to-target t.wire) sign) - [cards this] - ?: ?=([%forward ^] wire) - =^ cards state + :: + [%forward ^] (take-forward-sign:do t.wire sign) - [cards this] - ?: ?=([%prod *] wire) - ~| [%weird-sign -.sign] - ?> ?=(%poke-ack -.sign) - ?~ p.sign [~ this] - %- (slog [leaf+"failed to prod" u.p.sign]) - [~ this] - ~| [dap.bowl %weird-wire wire] - !! + :: + [%prod *] + ?> ?=(%poke-ack -.sign) + ?~ p.sign [~ state] + %- (slog leaf+"prod failed" u.p.sign) + [~ state] + == + [cards this] :: ++ on-poke |= [=mark =vase] @@ -122,8 +135,65 @@ :: :: |_ =bowl:gall ++* md ~(. metadata bowl) :: -:: groups subscription +:: metadata subscription +:: +++ watch-metadata + ^- card + [%pass /metadata %agent [our.bowl %metadata-store] %watch /app-name/link] +:: +++ take-metadata-sign + |= =sign:agent:gall + ^- (quip card _state) + ?- -.sign + %poke-ack ~|([dap.bowl %unexpected-poke-ack /metadata] !!) + %kick [[watch-metadata]~ state] + :: + %watch-ack + ?~ p.sign [~ state] + =/ =tank + :- %leaf + "{(trip dap.bowl)} failed subscribe to metadata store. very wrong!" + %- (slog tank u.p.sign) + [~ state] + :: + %fact + =* mark p.cage.sign + =* vase q.cage.sign + ?. ?=(%metadata-update mark) + ~| [dap.bowl %unexpected-mark mark] + !! + %- handle-metadata-update + !<(metadata-update vase) + == +:: +++ handle-metadata-update + |= upd=metadata-update + ^- (quip card _state) + ?+ -.upd [~ state] + %associations + =/ socs=(list [=group-path resource]) + ~(tap in ~(key by associations.upd)) + =| cards=(list card) + |- ::TODO try for +roll maybe? + ?~ socs [cards state] + =^ caz state + =, i.socs + ?. =(%link app-name) [~ state] + (listen-to-group app-path group-path) + $(socs t.socs, cards (weld cards caz)) + :: + %add + ?> =(%link app-name.resource.upd) + (listen-to-group app-path.resource.upd group-path.upd) + :: + %remove + ?> =(%link app-name.resource.upd) + (leave-from-group app-path.resource.upd group-path.upd) + == +:: +:: groups subscriptions :: ++ watch-groups ^- card @@ -148,49 +218,98 @@ =* mark p.cage.sign =* vase q.cage.sign ?+ mark ~|([dap.bowl %unexpected-mark mark] !!) - %group-initial (handle-group-initial !<(groups:group-store vase)) + %group-initial [~ state] ::NOTE initial handled using metadata %group-update (handle-group-update !<(group-update:group-store vase)) == == :: -++ handle-group-initial - |= =groups:group-store - ^- (quip card _state) - =| cards=(list card) - =/ groups=(list [=path =group:group-store]) - ~(tap by groups) - |- - ?~ groups [cards state] - =^ caz state - %- handle-group-update - [%add [group path]:i.groups] - $(cards (weld cards caz), groups t.groups) -:: ++ handle-group-update |= upd=group-update:group-store ^- (quip card _state) - :_ state - ?+ -.upd ~ - ?(%path %add %remove) - =/ whos=(list ship) ~(tap in members.upd) - |- ^- (list card) - ?~ whos ~ - :: no need to subscribe to ourselves - :: - ?: =(our.bowl i.whos) - $(whos t.whos) + ?. ?=(?(%path %add %remove) -.upd) + [~ state] + =/ socs=(list app-path) + (app-paths-from-group:md %link pax.upd) + =/ whos=(list ship) + ~(tap in members.upd) + =| cards=(list card) + |- + =* loop-socs $ + ?~ socs [cards state] + |- + =* loop-whos $ + ?~ whos loop-socs(socs t.socs) + =^ caz state ?: ?=(%remove -.upd) - %+ weld - $(whos t.whos) - (end-link-subscriptions i.whos pax.upd) - :^ (start-link-subscription %local-pages i.whos pax.upd) - (start-link-subscription %annotations i.whos pax.upd) - (prod-other-listener i.whos pax.upd) - $(whos t.whos) - == + (leave-from-peer i.socs pax.upd i.whos) + (listen-to-peer i.socs pax.upd i.whos) + loop-whos(whos t.whos, cards (weld cards caz)) :: :: link subscriptions :: +++ listen-to-group + |= [=app-path =group-path] + ^- (quip card _state) + =/ peers=(list ship) + ~| group-path + %~ tap in + =- (fall - *group:group-store) + %^ scry-for (unit group:group-store) + %group-store + group-path + =| cards=(list card) + |- + ?~ peers [cards state] + =^ caz state + (listen-to-peer app-path group-path i.peers) + $(peers t.peers, cards (weld cards caz)) +:: +++ leave-from-group + |= [=app-path =group-path] + ^- (quip card _state) + =/ peers=(list ship) + %~ tap in + =- (fall - *group:group-store) + %^ scry-for (unit group:group-store) + %group-store + group-path + =| cards=(list card) + |- + ?~ peers [cards state] + =^ caz state + (leave-from-peer app-path group-path i.peers) + $(peers t.peers, cards (weld cards caz)) +:: +++ listen-to-peer + |= [=app-path =group-path who=ship] + ^- (quip card _state) + ?: =(our.bowl who) + [~ state] + :_ =- state(reasoning -) + (~(put ju reasoning) [who app-path] group-path) + :- (prod-other-listener who app-path) + ?^ (~(get ju reasoning) [who app-path]) + ~ + (start-link-subscriptions who app-path) +:: +++ leave-from-peer + |= [=app-path =group-path who=ship] + ^- (quip card _state) + ?: =(our.bowl who) + [~ state] + :_ =- state(reasoning -) + (~(del ju reasoning) [who app-path] group-path) + ?. (~(has ju reasoning) [who app-path] group-path) + ~ + (end-link-subscriptions who app-path) +:: +++ start-link-subscriptions + |= [=ship =app-path] + ^- (list card) + :~ (start-link-subscription %local-pages ship app-path) + (start-link-subscription %annotations ship app-path) + == +:: ++ start-link-subscription |= =target ^- card @@ -283,7 +402,7 @@ ++ take-retry |= =target ^- (list card) - :: relevant: whether :who is still in group :where + :: relevant: whether :who is still associated with resource :where :: =; relevant=? ?. relevant ~ @@ -291,16 +410,13 @@ ?: %- ~(has by wex.bowl) [[%links (target-to-wire target)] who.target %link-proxy-hook] | - %. who.target - %~ has in - =- (fall - *group:group-store) - .^ (unit group:group-store) - %gx - (scot %p our.bowl) + %+ lien (groups-from-resource:md %link where.target) + |= =group-path + ^- ? + =- (~(has in (fall - *group:group-store)) who.target) + %^ scry-for (unit group:group-store) %group-store - (scot %da now.bowl) - (snoc where.target %noun) - == + group-path :: ++ do-link-action |= [=wire =action] @@ -373,4 +489,14 @@ == %- (slog tank u.p.sign) [~ state] +:: +++ scry-for + |* [=mold =app-name =path] + .^ mold + %gx + (scot %p our.bowl) + app-name + (scot %da now.bowl) + (snoc `^path`path %noun) + == -- diff --git a/pkg/arvo/app/link-proxy-hook.hoon b/pkg/arvo/app/link-proxy-hook.hoon index 49341c2a5..ac6ae9e3d 100644 --- a/pkg/arvo/app/link-proxy-hook.hoon +++ b/pkg/arvo/app/link-proxy-hook.hoon @@ -4,12 +4,11 @@ :: stores if permission conditions are met. :: the patterns herein should one day be generalized into a proxy-hook lib. :: -:: this adopts a very primitive view of groups-store as containing only -:: groups of interesting (rather than uninteresting) ships. it sets the -:: permission condition to be that ship must be in group matching the path -:: it's subscribing to. -:: we check this on-watch, but also subscribe to groups so that we can kick -:: subscriptions if needed (eg ship removed from group). +:: this uses metadata-store to discover resources and their associated +:: groups. it sets the permission condition to be that a ship must be in a +:: group associated with the resource it's subscribing to. +:: we check this on-watch, but also subscribe to metadata & groups so that +:: we can kick subscriptions if needed (eg ship removed from group). :: :: we deduplicate incoming subscriptions on the same path, ensuring we have :: exactly one local subscription per unique incoming subscription path. @@ -18,10 +17,10 @@ :: become part of the stores standard anyway. :: :: when adding support for new paths, the only things you'll likely want -:: to touch are +permitted, +initial-response, & maybe +handle-group-update. +:: to touch are +permitted, +initial-response, & +kick-proxies. :: -/- group-store -/+ *link, default-agent, verb, dbug +/- group-store, *metadata-store +/+ *link, metadata, default-agent, verb, dbug |% +$ state-0 $: %0 @@ -48,7 +47,7 @@ ++ on-init ^- (quip card _this) :_ this - [watch-groups:do]~ + ~[watch-groups:do watch-metadata:do] :: ++ on-save !>(state) ++ on-load @@ -96,11 +95,15 @@ -- :: |_ =bowl:gall ++* md ~(. metadata bowl) +:: +:: permissions +:: ++ permitted |= [who=ship =path] ^- ? - :: we only expose group-specific /local-pages and /annotations, - :: and only to ships in the relevant group. + :: we only expose /local-pages and /annotations, + :: to ships in the groups associated with the resource. :: (no url-specific annotations subscriptions, either.) :: =/ target=(unit ^path) @@ -110,12 +113,75 @@ `t.t.path ~ ?~ target | - =; group - ?& ?=(^ group) - (~(has in u.group) who) - == - %+ scry-for (unit group:group-store) - [%group-store u.target] + ~? !.^(? %gu (scot %p our.bowl) %metadata-store (scot %da now.bowl) ~) + %woah-md-s-not-booted ::TODO fallback if needed + %+ lien (groups-from-resource:md %link u.target) + |= =group-path + ^- ? + =- (~(has in (fall - *group:group-store)) who) + %^ scry-for (unit group:group-store) + %group-store + group-path +:: +++ kick-revoked-permissions + |= [=path who=(list ship)] + ^- (list card) + %+ murn who + |= =ship + ^- (unit card) + :: no need to remove to ourselves + :: + ?: =(our.bowl ship) ~ + ?: (permitted ship path) ~ + `(kick-proxies ship path) +:: +:: metadata subscription +:: +++ watch-metadata + ^- card + [%pass /metadata %agent [our.bowl %metadata-store] %watch /app-name/link] +:: +++ take-metadata-sign + |= =sign:agent:gall + ^- (quip card _state) + ?- -.sign + %poke-ack ~|([dap.bowl %unexpected-poke-ack /metadata] !!) + %kick [[watch-metadata]~ state] + :: + %watch-ack + ?~ p.sign [~ state] + =/ =tank + :- %leaf + "{(trip dap.bowl)} failed subscribe to metadata store. very wrong!" + %- (slog tank u.p.sign) + [~ state] + :: + %fact + =* mark p.cage.sign + =* vase q.cage.sign + ?. ?=(%metadata-update mark) + ~| [dap.bowl %unexpected-mark mark] + !! + %- handle-metadata-update + !<(metadata-update vase) + == +:: +++ handle-metadata-update + |= upd=metadata-update + ^- (quip card _state) + :_ state + ?. ?=(%remove -.upd) ~ + ?> =(%link app-name.resource.upd) + :: if a group is no longer associated with a resource, + :: we need to re-check permissions for everyone in that group. + :: + %+ kick-revoked-permissions + app-path.resource.upd + %~ tap in + =- (fall - *group:group-store) + %^ scry-for (unit group:group-store) + %group-store + group-path.upd :: :: groups subscription ::TODO largely copied from link-listen-hook. maybe make a store-listener lib? @@ -153,29 +219,26 @@ ^- (quip card _state) :_ state ?. ?=(%remove -.upd) ~ - =/ whos=(list ship) ~(tap in members.upd) - |- ^- (list card) - ?~ whos ~ - :: no need to remove to ourselves + :: if someone was removed from a group, find all link resources associated + :: with that group, then kick their subscriptions if they're no longer :: - ?: =(our.bowl i.whos) - $(whos t.whos) - :_ $(whos t.whos) - ::NOTE this depends kind of unfortunately on the fact that we only accept - :: subscriptions to /local-pages//* paths. it'd be more correct if we - :: "just" looked at all paths in the map, and found the matching ones. - ::TODO what exactly did i mean by this? - %+ kick-proxies i.whos - :~ [%local-pages pax.upd] - [%annotations '' pax.upd] - == + %- zing + %+ turn (app-paths-from-group:md %link pax.upd) + |= =app-path + ^- (list card) + %+ kick-revoked-permissions + app-path + ~(tap in members.upd) :: :: proxy subscriptions :: ++ kick-proxies - |= [who=ship paths=(list path)] + |= [who=ship =path] ^- card - [%give %kick paths `who] + =- [%give %kick - `who] + :~ [%local-pages path] + [%annotations %$ path] + == :: ++ handle-proxy-sign |= [=wire =sign:agent:gall] @@ -211,14 +274,10 @@ [%give %fact ~ %link-initial !>(initial)] ?+ path !! [%local-pages ^] - :- %local-pages - %+ scry-for (map ^path pages) - [%link-store path] + [%local-pages .^((map ^path pages) %gx path)] :: [%annotations %$ ^] - :- %annotations - %+ scry-for (per-path-url notes) - [%link-store path] + [%annotations .^((per-path-url notes) %gx %$ t.t.path)] == :: ++ start-proxy @@ -249,12 +308,14 @@ :: [(proxy-pass-link-store path %leave ~)]~ :: +:: helpers +:: ++ scry-for - |* [=mold app=term =path] + |* [=mold =app-name =path] .^ mold %gx (scot %p our.bowl) - app + app-name (scot %da now.bowl) (snoc `^path`path %noun) == From 115fd1c14cd4558d0fc87fc58ff57422f4d4112f Mon Sep 17 00:00:00 2001 From: Fang Date: Sun, 1 Mar 2020 01:40:06 +0100 Subject: [PATCH 03/19] metadata-store: never crash fetching group metadata In order to track & respond to all group-related metadata, hooks may want to subscribe to the metadata-store in response to group creation. In that case it is possible that there are no entries for the group-path in group-indices yet. This makes us fall back to the empty set in case group-indices has no entry for the group-path yet. --- pkg/arvo/app/metadata-store.hoon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/arvo/app/metadata-store.hoon b/pkg/arvo/app/metadata-store.hoon index ccd6614e0..81ddcdbd4 100644 --- a/pkg/arvo/app/metadata-store.hoon +++ b/pkg/arvo/app/metadata-store.hoon @@ -182,7 +182,7 @@ |= =group-path ^- ^associations %- ~(gas by *^associations) - %+ turn ~(tap in (~(got by group-indices) group-path)) + %+ turn ~(tap in (~(gut by group-indices) group-path ~)) |= =resource :- [group-path resource] (~(got by associations) [group-path resource]) From 0539f3ca1fe139d58b2666420869fe21907eb499 Mon Sep 17 00:00:00 2001 From: Fang Date: Sun, 1 Mar 2020 01:48:54 +0100 Subject: [PATCH 04/19] link fe: metadata integration - finds resources & displays details using metadata-store - supports creating new collections for groups - supports creating new collections with new unmanaged group - supports receiving invites for new (unmanaged) collections --- pkg/arvo/app/link-view.hoon | 130 +++++- pkg/arvo/mar/link/view-action.hoon | 24 ++ pkg/arvo/sur/link-view.hoon | 12 + pkg/interface/link/src/js/api.js | 11 +- .../src/js/components/lib/channel-sidebar.js | 74 ++-- .../src/js/components/lib/channels-item.js | 8 +- .../js/components/lib/comments-pagination.js | 20 +- .../link/src/js/components/lib/comments.js | 6 +- .../src/js/components/lib/invite-search.js | 369 ++++++++++++++++++ .../js/components/lib/link-detail-preview.js | 9 +- .../link/src/js/components/lib/link-item.js | 4 +- .../link/src/js/components/lib/link-submit.js | 2 +- .../src/js/components/lib/links-tabbar.js | 4 +- .../link/src/js/components/lib/pagination.js | 12 +- .../src/js/components/lib/sidebar-invite.js | 39 ++ pkg/interface/link/src/js/components/link.js | 14 +- .../link/src/js/components/links-list.js | 41 +- pkg/interface/link/src/js/components/new.js | 252 ++++++++++++ pkg/interface/link/src/js/components/root.js | 83 ++-- .../link/src/js/components/skeleton.js | 2 + .../link/src/js/reducers/invite-update.js | 5 +- .../link/src/js/reducers/link-update.js | 2 +- .../link/src/js/reducers/metadata-update.js | 47 +++ pkg/interface/link/src/js/store.js | 8 + pkg/interface/link/src/js/subscription.js | 19 +- 25 files changed, 1062 insertions(+), 135 deletions(-) create mode 100644 pkg/arvo/mar/link/view-action.hoon create mode 100644 pkg/arvo/sur/link-view.hoon create mode 100644 pkg/interface/link/src/js/components/lib/invite-search.js create mode 100644 pkg/interface/link/src/js/components/lib/sidebar-invite.js create mode 100644 pkg/interface/link/src/js/components/new.js create mode 100644 pkg/interface/link/src/js/reducers/metadata-update.js diff --git a/pkg/arvo/app/link-view.hoon b/pkg/arvo/app/link-view.hoon index 87814a424..598242c82 100644 --- a/pkg/arvo/app/link-view.hoon +++ b/pkg/arvo/app/link-view.hoon @@ -10,7 +10,10 @@ :: /json/[n]/submission/[wood-url]/[some-group] nth matching submission :: /json/seen mark-as-read updates :: -/+ *link, *server, default-agent, verb +/- *link-view, + metadata-store, *invite-store, group-store, + group-hook, permission-hook, metadata-hook +/+ *link, *server, default-agent, verb, dbug :: |% +$ state-0 @@ -25,6 +28,7 @@ =* state - :: %+ verb | +%- agent:dbug ^- agent:gall =< |_ =bowl:gall @@ -42,6 +46,12 @@ :: =+ [dap.bowl /tile '/~link/js/tile.js'] [%pass /launch %agent [our.bowl %launch] %poke %launch-action !>(-)] + :: + =+ [%invite-action !>([%create /link])] + [%pass /invitatory/create %agent [our.bowl %invite-store] %poke -] + :: + =+ /invitatory/link + [%pass - %agent [our.bowl %invite-store] %watch -] == :: ++ on-save !>(state) @@ -65,6 +75,9 @@ :: %link-action [(handle-action:do !<(action vase)) ~] + :: + %link-view-action + (handle-view-action:do !<(view-action vase)) == :: ++ on-watch @@ -104,13 +117,18 @@ ?+ -.sign (on-agent:def wire sign) %kick :_ this - [%pass wire %agent [our.bowl %link-store] %watch wire]~ + =/ app=term + ?: ?=([%invites *] wire) + %invite-store + %link-store + [%pass wire %agent [our.bowl app] %watch wire]~ :: %fact =* mark p.cage.sign =* vase q.cage.sign ?+ mark (on-agent:def wire sign) - %link-initial [~ this] + %invite-update [(handle-invite-update:do !<(invite-update vase)) this] + %link-initial [~ this] :: %link-update :_ this @@ -217,10 +235,116 @@ %- as-octs:mimes:html .^(@ %cx path) :: +++ handle-invite-update + |= upd=invite-update + ^- (list card) + ?. ?=(%accepted -.upd) ~ + ?. =(/link path.upd) ~ + |^ + :~ :: sync the group + :: + %^ do-poke %group-hook + %group-hook-action + !> ^- group-hook-action:group-hook + [%add ship path]:invite.upd + :: + :: sync the metadata + :: + %^ do-poke %metadata-hook + %metadata-hook-action + !> ^- metadata-hook-action:metadata-hook + [%add-synced ship path]:invite.upd + == + :: + ++ do-poke + |= [app=term =mark =vase] + ^- card + [%pass /create/[app]/[mark] %agent [our.bowl app] %poke mark vase] + -- +:: ++ handle-action |= =action ^- card [%pass /action %agent [our.bowl %link-store] %poke %link-action !>(action)] +:: +++ handle-view-action + |= act=view-action + ^- (list card) + ?> ?=(%create -.act) + =/ group-path=path + ?- -.members.act + %group path.members.act + %ships [~.~ (scot %p our.bowl) path.act] + == + |^ + =; group-setup=(list card) + %+ weld group-setup + :~ :: add collection to metadata-store + :: + %^ do-poke %metadata-store + %metadata-action + !> ^- metadata-action:metadata-store + :^ %add group-path + [%link path.act] + %* . *metadata:metadata-store + title title.act + description description.act + date-created now.bowl + creator our.bowl + == + == + ?: ?=(%group -.members.act) ~ + :* :: create the new group + :: + %^ do-poke %group-store + %group-action + !> ^- group-action:group-store + [%bundle group-path] + :: + :: fill the new group + :: + %^ do-poke %group-store + %group-action + !> ^- group-action:group-store + [%add (~(put in ships.members.act) our.bowl) group-path] + :: + :: make group available + :: + %^ do-poke %group-hook + %group-hook-action + !> ^- group-hook-action:group-hook + [%add our.bowl group-path] + :: + :: make a permission equivalent + :: + %^ do-poke %permission-hook + %permission-hook-action + !> ^- permission-hook-action:permission-hook + [%add-owned group-path group-path] + :: + :: send invites + :: + %+ turn ~(tap in ships.members.act) + |= =ship + ^- card + %^ do-poke %invite-hook + %invite-action + !> ^- invite-action + :^ %invite /link + (sham group-path eny.bowl) + :* our.bowl + %group-hook + group-path + ship + title.act + == + == + :: + ++ do-poke + |= [app=term =mark =vase] + ^- card + [%pass /create/[app]/[mark] %agent [our.bowl app] %poke mark vase] + -- :: +give-tile-data: total unread count as json object :: ::NOTE the full recalc of totals here probably isn't the end of the world. diff --git a/pkg/arvo/mar/link/view-action.hoon b/pkg/arvo/mar/link/view-action.hoon new file mode 100644 index 000000000..4064b5e91 --- /dev/null +++ b/pkg/arvo/mar/link/view-action.hoon @@ -0,0 +1,24 @@ +/- *link-view +=, dejs:format +|_ act=view-action +++ grab + |% + ++ noun view-action + ++ json + |^ %- of + :~ %create^(ot 'path'^pa 'title'^so 'description'^so 'members'^mems ~) + == + :: + ++ mems + %- of + :~ %group^pa + %ships^(cu sy (ar (su ;~(pfix sig fed:ag)))) + == + -- + -- +:: +++ grow + |% + ++ noun act + -- +-- diff --git a/pkg/arvo/sur/link-view.hoon b/pkg/arvo/sur/link-view.hoon new file mode 100644 index 000000000..ee239fb4a --- /dev/null +++ b/pkg/arvo/sur/link-view.hoon @@ -0,0 +1,12 @@ +:: link-view: encapsulating link management +:: +|% +++ view-action + $% $: %create + =path + title=@t + description=@t + members=$%([%group =path] [%ships ships=(set ship)]) + == + == +-- diff --git a/pkg/interface/link/src/js/api.js b/pkg/interface/link/src/js/api.js index f495f2c15..6f0269381 100644 --- a/pkg/interface/link/src/js/api.js +++ b/pkg/interface/link/src/js/api.js @@ -86,7 +86,7 @@ class UrbitApi { inviteAccept(uid) { this.inviteAction({ accept: { - path: '/chat', + path: '/link', uid } }); @@ -95,7 +95,7 @@ class UrbitApi { inviteDecline(uid) { this.inviteAction({ decline: { - path: '/chat', + path: '/link', uid } }); @@ -144,6 +144,13 @@ class UrbitApi { ); } + createCollection(path, title, description, members) { + // members is either {group:'/group-path'} or {'ships':[~zod]} + return this.action("link-view", "link-view-action", { + create: {path, title, description, members} + }) + } + linkAction(data) { return this.action("link-store", "link-action", data); } diff --git a/pkg/interface/link/src/js/components/lib/channel-sidebar.js b/pkg/interface/link/src/js/components/lib/channel-sidebar.js index eb3b6021f..dad5b7a9c 100644 --- a/pkg/interface/link/src/js/components/lib/channel-sidebar.js +++ b/pkg/interface/link/src/js/components/lib/channel-sidebar.js @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import { Route, Link } from 'react-router-dom'; import { ChannelsItem } from '/components/lib/channels-item'; +import { SidebarInvite } from '/components/lib/sidebar-invite'; export class ChannelsSidebar extends Component { // drawer to the left @@ -9,42 +10,21 @@ export class ChannelsSidebar extends Component { render() { const { props, state } = this; - let privateChannel = - Object.keys(props.groups) - .filter((path) => { - return (path === "/~/default") - }) - .map((path) => { - let name = "Private" - let selected = (props.selected === path); - let linkCount = !!props.links[path] ? props.links[path].totalItems : 0; - const unseenCount = !!props.links[path] - ? props.links[path].unseenCount - : linkCount + let sidebarInvites = Object.keys(props.invites) + .map((uid) => { return ( - - ) - }) + + ); + }); - let channelItems = - Object.keys(props.groups) - .filter((path) => { - return (!path.startsWith("/~/")) - }) - .map((path) => { - let name = path.substr(1); - let nameSeparator = name.indexOf("/"); - name = name.substr(nameSeparator + 1); - - let selected = (props.selected === path); - let linkCount = !!props.links[path] ? props.links[path].totalItems : 0; + const channelItems = + Object.keys(props.resources).map((path) => { + const meta = props.resources[path]; + const selected = (props.selected === path); + const linkCount = !!props.links[path] ? props.links[path].totalItems : 0; const unseenCount = !!props.links[path] ? props.links[path].unseenCount : linkCount @@ -53,12 +33,12 @@ export class ChannelsSidebar extends Component { - ) + name={meta.title}/> + ); }); let activeClasses = (this.props.active === "channels") ? " " : "dn-s "; @@ -70,7 +50,7 @@ export class ChannelsSidebar extends Component { if (this.props.popout) { hiddenClasses = false; } else { - hiddenClasses = this.props.sidebarShown; + hiddenClasses = this.props.sidebarShown; } return ( @@ -81,15 +61,15 @@ export class ChannelsSidebar extends Component { : "dn")}> ⟵ Landscape
-

- Your Collections -

- {privateChannel} +
+ + New Collection + +
+ {sidebarInvites} {channelItems}
diff --git a/pkg/interface/link/src/js/components/lib/channels-item.js b/pkg/interface/link/src/js/components/lib/channels-item.js index fbc354903..c1e85d521 100644 --- a/pkg/interface/link/src/js/components/lib/channels-item.js +++ b/pkg/interface/link/src/js/components/lib/channels-item.js @@ -1,13 +1,13 @@ import React, { Component } from 'react' -import { Route, Link } from 'react-router-dom'; +import { Route, Link } from 'react-router-dom'; export class ChannelsItem extends Component { render() { const { props } = this; - let selectedClass = (props.selected) - ? "bg-gray5 bg-gray1-d b--gray4 b--gray2-d" + let selectedClass = (props.selected) + ? "bg-gray5 bg-gray1-d b--gray4 b--gray2-d" : "b--gray4 b--gray2-d"; let memberCount = props.memberList @@ -18,7 +18,7 @@ export class ChannelsItem extends Component { : null; return ( - +

{props.name}

diff --git a/pkg/interface/link/src/js/components/lib/comments-pagination.js b/pkg/interface/link/src/js/components/lib/comments-pagination.js index 595089ec6..a1704072a 100644 --- a/pkg/interface/link/src/js/components/lib/comments-pagination.js +++ b/pkg/interface/link/src/js/components/lib/comments-pagination.js @@ -6,8 +6,8 @@ export class CommentsPagination extends Component { render() { let props = this.props; - let prevPage = "/" + (Number(props.commentPage) - 1); - let nextPage = "/" + (Number(props.commentPage) + 1); + let prevPage = (Number(props.commentPage) - 1); + let nextPage = (Number(props.commentPage) + 1); let prevDisplay = ((Number(props.commentPage) > 0)) ? "dib" @@ -26,22 +26,24 @@ export class CommentsPagination extends Component { className={"pb6 absolute inter f8 left-0 " + prevDisplay} to={"/~link" + popout - + props.groupPath + + "/item" + "/" + props.linkPage + "/" + props.linkIndex + + "/" + prevPage + "/" + encodedUrl - + "/comments" + prevPage}> + + props.resourcePath}> <- Previous Page + + props.resourcePath}> Next Page ->

diff --git a/pkg/interface/link/src/js/components/lib/comments.js b/pkg/interface/link/src/js/components/lib/comments.js index 309c87ddb..1b1545cb6 100644 --- a/pkg/interface/link/src/js/components/lib/comments.js +++ b/pkg/interface/link/src/js/components/lib/comments.js @@ -19,7 +19,7 @@ export class Comments extends Component { ) { this.setState({requested: this.props.commentPage}); api.getCommentsPage( - this.props.groupPath, + this.props.resourcePath, this.props.url, this.props.commentPage); } @@ -73,8 +73,8 @@ export class Comments extends Component {
{commentsList} !e.startsWith("/~/")); + + let peers = [], + peerSet = new Set(), + contacts = new Map; + Object.keys(this.props.groups).map(group => { + if (this.props.groups[group].size > 0) { + let groupEntries = this.props.groups[group].values(); + for (let member of groupEntries) { + peerSet.add(member); + } + } + if (this.props.contacts[group]) { + let groupEntries = this.props.groups[group].values(); + for (let member of groupEntries) { + if (this.props.contacts[group][member]) { + if (contacts.has(member)) { + contacts.get(member).push(this.props.contacts[group][member].nickname); + } + else { + contacts.set(member, [this.props.contacts[group][member].nickname]); + } + } + } + } + }); + peers = Array.from(peerSet); + + this.setState({ groups: groups, peers: peers, contacts: contacts }); + } + + search(event) { + let searchTerm = event.target.value.toLowerCase().replace("~", ""); + + this.setState({ searchValue: event.target.value }); + + if (searchTerm.length < 2) { + this.setState({ searchResults: { groups: [], ships: [] } }); + } + + if (searchTerm.length > 2) { + if (this.state.inviteError === true) { + this.setState({ inviteError: false }); + } + + let groupMatches = []; + if (this.props.groupResults) { + groupMatches = this.state.groups.filter(e => { + return e.includes(searchTerm); + }); + } + + let shipMatches = this.state.peers.filter(e => { + return e.includes(searchTerm) && !this.props.invites.ships.includes(e); + }); + + for (let contact of this.state.contacts.keys()) { + let thisContact = this.state.contacts.get(contact); + let match = thisContact.filter(e => { + return e.toLowerCase().includes(searchTerm); + }); + if (match.length > 0) { + if (!(contact in shipMatches)) { + shipMatches.push(contact); + } + } + } + + this.setState({ + searchResults: { groups: groupMatches, ships: shipMatches } + }); + + let isValid = true; + if (!urbitOb.isValidPatp("~" + searchTerm)) { + isValid = false; + } + + if (shipMatches.length === 0 && isValid) { + shipMatches.push(searchTerm); + this.setState({ + searchResults: { groups: groupMatches, ships: shipMatches } + }); + } + } + } + + deleteGroup() { + let { ships } = this.props.invites; + this.setState({ + searchValue: "", + searchResults: { groups: [], ships: [] } + }); + this.props.setInvite({ groups: [], ships: ships }); + } + + deleteShip(ship) { + let { groups, ships } = this.props.invites; + this.setState({ + searchValue: "", + searchResults: { groups: [], ships: [] } + }); + ships = ships.filter(e => { + return e !== ship; + }); + this.props.setInvite({ groups: groups, ships: ships }); + } + + addGroup(group) { + this.setState({ + searchValue: "", + searchResults: { groups: [], ships: [] } + }); + this.props.setInvite({ groups: [group], ships: [] }); + } + + addShip(ship) { + let { groups, ships } = this.props.invites; + this.setState({ + searchValue: "", + searchResults: { groups: [], ships: [] } + }); + if (!ships.includes(ship)) { + ships.push(ship); + } + if (groups.length > 0) { + return false; + } + this.props.setInvite({ groups: groups, ships: ships }); + } + + submitShipToAdd(ship) { + let searchTerm = ship + .toLowerCase() + .replace("~", "") + .trim(); + let isValid = true; + if (!urbitOb.isValidPatp("~" + searchTerm)) { + isValid = false; + } + if (!isValid) { + this.setState({ inviteError: true, searchValue: "" }); + } else if (isValid) { + this.addShip(searchTerm); + this.setState({ searchValue: "" }); + } + } + + render() { + const { props, state } = this; + let searchDisabled = false; + if (props.invites.groups) { + if (props.invites.groups.length > 0) { + searchDisabled = true; + } + } + + let participants =
; + let searchResults =
; + + let invErrElem = ; + if (state.inviteError) { + invErrElem = ( + + Invited ships must be validly formatted ship names. + + ); + } + + if ( + state.searchResults.groups.length > 0 || + state.searchResults.ships.length > 0 + ) { + let groupHeader = + state.searchResults.groups.length > 0 ? ( +

Groups

+ ) : ( + "" + ); + + let shipHeader = + state.searchResults.ships.length > 0 ? ( +

Ships

+ ) : ( + "" + ); + + let groupResults = state.searchResults.groups.map(group => { + return ( +
  • this.addGroup(group)}> + {group} +
  • + ); + }); + + let shipResults = state.searchResults.ships.map(ship => { + let nicknames = (this.state.contacts.has(ship)) + ? this.state.contacts.get(ship).join(", ") + : ""; + return ( +
  • this.addShip(ship)}> + + {"~" + ship} + {nicknames} +
  • + ); + }); + + searchResults = ( +
    + {groupHeader} + {groupResults} + {shipHeader} + {shipResults} +
    + ); + } + + let groupInvites = props.invites.groups || []; + let shipInvites = props.invites.ships || []; + + if (groupInvites.length > 0 || shipInvites.length > 0) { + let groups = groupInvites.map(group => { + return ( + + {group} + this.deleteGroup(group)}> + x + + + ); + }); + + let ships = shipInvites.map(ship => { + return ( + + {"~" + ship} + this.deleteShip(ship)}> + x + + + ); + }); + + participants = ( +
    + Participants + {groups} {ships} +
    + ); + } + + return ( +
    + +