mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-11 08:55:23 +03:00
Merge pull request #2378 from urbit/m/link-meta
link: metadata integration
This commit is contained in:
commit
3c039bce39
@ -1,21 +1,33 @@
|
|||||||
:: link-listen-hook: get your friends' bookmarks
|
:: link-listen-hook: get your friends' bookmarks
|
||||||
::
|
::
|
||||||
:: on-init, subscribes to all groups on this ship. for every ship in a group,
|
:: subscribes to all %link resources in the metadata-store.
|
||||||
:: we subscribe to their link's local-pages and annotations
|
:: for all ships in groups associated with those resources, we subscribe to
|
||||||
:: at the group path (through link-proxy-hook),
|
:: their link's local-pages and annotations at the resource path (through
|
||||||
:: and forwards all entries into our link as submissions and comments.
|
:: 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
|
:: if a subscription to a target fails, we assume it's because their
|
||||||
:: group definition hasn't been updated to include us yet.
|
:: metadata+groups definition hasn't been updated to include us yet.
|
||||||
:: we retry with exponential backoff, maxing out at one hour timeouts.
|
:: 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
|
/- *metadata-store, *link, group-store
|
||||||
/+ default-agent, verb, dbug
|
/+ metadata, default-agent, verb, dbug
|
||||||
::
|
::
|
||||||
|%
|
|%
|
||||||
+$ state-0
|
+$ state-0
|
||||||
$: %0
|
$: %0
|
||||||
retry-timers=(map target @dr)
|
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)
|
+$ what-target ?(%local-pages %annotations)
|
||||||
@ -52,7 +64,7 @@
|
|||||||
++ on-init
|
++ on-init
|
||||||
^- (quip card _this)
|
^- (quip card _this)
|
||||||
:_ this
|
:_ this
|
||||||
[watch-groups:do]~
|
~[watch-metadata:do watch-groups:do]
|
||||||
::
|
::
|
||||||
++ on-save !>(state)
|
++ on-save !>(state)
|
||||||
++ on-load
|
++ on-load
|
||||||
@ -63,26 +75,27 @@
|
|||||||
++ on-agent
|
++ on-agent
|
||||||
|= [=wire =sign:agent:gall]
|
|= [=wire =sign:agent:gall]
|
||||||
^- (quip card _this)
|
^- (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)
|
(take-groups-sign:do sign)
|
||||||
[cards this]
|
::
|
||||||
?: ?=([%links ?(%local-pages %annotations) @ ^] wire)
|
[%links ?(%local-pages %annotations) @ ^]
|
||||||
=^ cards state
|
|
||||||
(take-link-sign:do (wire-to-target t.wire) sign)
|
(take-link-sign:do (wire-to-target t.wire) sign)
|
||||||
[cards this]
|
::
|
||||||
?: ?=([%forward ^] wire)
|
[%forward ^]
|
||||||
=^ cards state
|
|
||||||
(take-forward-sign:do t.wire sign)
|
(take-forward-sign:do t.wire sign)
|
||||||
[cards this]
|
::
|
||||||
?: ?=([%prod *] wire)
|
[%prod *]
|
||||||
~| [%weird-sign -.sign]
|
?> ?=(%poke-ack -.sign)
|
||||||
?> ?=(%poke-ack -.sign)
|
?~ p.sign [~ state]
|
||||||
?~ p.sign [~ this]
|
%- (slog leaf+"prod failed" u.p.sign)
|
||||||
%- (slog [leaf+"failed to prod" u.p.sign])
|
[~ state]
|
||||||
[~ this]
|
==
|
||||||
~| [dap.bowl %weird-wire wire]
|
[cards this]
|
||||||
!!
|
|
||||||
::
|
::
|
||||||
++ on-poke
|
++ on-poke
|
||||||
|= [=mark =vase]
|
|= [=mark =vase]
|
||||||
@ -122,8 +135,65 @@
|
|||||||
::
|
::
|
||||||
::
|
::
|
||||||
|_ =bowl:gall
|
|_ =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
|
++ watch-groups
|
||||||
^- card
|
^- card
|
||||||
@ -148,49 +218,98 @@
|
|||||||
=* mark p.cage.sign
|
=* mark p.cage.sign
|
||||||
=* vase q.cage.sign
|
=* vase q.cage.sign
|
||||||
?+ mark ~|([dap.bowl %unexpected-mark mark] !!)
|
?+ 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))
|
%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
|
++ handle-group-update
|
||||||
|= upd=group-update:group-store
|
|= upd=group-update:group-store
|
||||||
^- (quip card _state)
|
^- (quip card _state)
|
||||||
:_ state
|
?. ?=(?(%path %add %remove) -.upd)
|
||||||
?+ -.upd ~
|
[~ state]
|
||||||
?(%path %add %remove)
|
=/ socs=(list app-path)
|
||||||
=/ whos=(list ship) ~(tap in members.upd)
|
(app-paths-from-group:md %link pax.upd)
|
||||||
|- ^- (list card)
|
=/ whos=(list ship)
|
||||||
?~ whos ~
|
~(tap in members.upd)
|
||||||
:: no need to subscribe to ourselves
|
=| cards=(list card)
|
||||||
::
|
|-
|
||||||
?: =(our.bowl i.whos)
|
=* loop-socs $
|
||||||
$(whos t.whos)
|
?~ socs [cards state]
|
||||||
|
|-
|
||||||
|
=* loop-whos $
|
||||||
|
?~ whos loop-socs(socs t.socs)
|
||||||
|
=^ caz state
|
||||||
?: ?=(%remove -.upd)
|
?: ?=(%remove -.upd)
|
||||||
%+ weld
|
(leave-from-peer i.socs pax.upd i.whos)
|
||||||
$(whos t.whos)
|
(listen-to-peer i.socs pax.upd i.whos)
|
||||||
(end-link-subscriptions i.whos pax.upd)
|
loop-whos(whos t.whos, cards (weld cards caz))
|
||||||
:^ (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)
|
|
||||||
==
|
|
||||||
::
|
::
|
||||||
:: link subscriptions
|
:: 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
|
++ start-link-subscription
|
||||||
|= =target
|
|= =target
|
||||||
^- card
|
^- card
|
||||||
@ -283,7 +402,7 @@
|
|||||||
++ take-retry
|
++ take-retry
|
||||||
|= =target
|
|= =target
|
||||||
^- (list card)
|
^- (list card)
|
||||||
:: relevant: whether :who is still in group :where
|
:: relevant: whether :who is still associated with resource :where
|
||||||
::
|
::
|
||||||
=; relevant=?
|
=; relevant=?
|
||||||
?. relevant ~
|
?. relevant ~
|
||||||
@ -291,16 +410,13 @@
|
|||||||
?: %- ~(has by wex.bowl)
|
?: %- ~(has by wex.bowl)
|
||||||
[[%links (target-to-wire target)] who.target %link-proxy-hook]
|
[[%links (target-to-wire target)] who.target %link-proxy-hook]
|
||||||
|
|
|
|
||||||
%. who.target
|
%+ lien (groups-from-resource:md %link where.target)
|
||||||
%~ has in
|
|= =group-path
|
||||||
=- (fall - *group:group-store)
|
^- ?
|
||||||
.^ (unit group:group-store)
|
=- (~(has in (fall - *group:group-store)) who.target)
|
||||||
%gx
|
%^ scry-for (unit group:group-store)
|
||||||
(scot %p our.bowl)
|
|
||||||
%group-store
|
%group-store
|
||||||
(scot %da now.bowl)
|
group-path
|
||||||
(snoc where.target %noun)
|
|
||||||
==
|
|
||||||
::
|
::
|
||||||
++ do-link-action
|
++ do-link-action
|
||||||
|= [=wire =action]
|
|= [=wire =action]
|
||||||
@ -373,4 +489,14 @@
|
|||||||
==
|
==
|
||||||
%- (slog tank u.p.sign)
|
%- (slog tank u.p.sign)
|
||||||
[~ state]
|
[~ state]
|
||||||
|
::
|
||||||
|
++ scry-for
|
||||||
|
|* [=mold =app-name =path]
|
||||||
|
.^ mold
|
||||||
|
%gx
|
||||||
|
(scot %p our.bowl)
|
||||||
|
app-name
|
||||||
|
(scot %da now.bowl)
|
||||||
|
(snoc `^path`path %noun)
|
||||||
|
==
|
||||||
--
|
--
|
||||||
|
@ -4,12 +4,11 @@
|
|||||||
:: stores if permission conditions are met.
|
:: stores if permission conditions are met.
|
||||||
:: the patterns herein should one day be generalized into a proxy-hook lib.
|
:: 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
|
:: this uses metadata-store to discover resources and their associated
|
||||||
:: groups of interesting (rather than uninteresting) ships. it sets the
|
:: groups. it sets the permission condition to be that a ship must be in a
|
||||||
:: permission condition to be that ship must be in group matching the path
|
:: group associated with the resource it's subscribing to.
|
||||||
:: it's subscribing to.
|
:: we check this on-watch, but also subscribe to metadata & groups so that
|
||||||
:: we check this on-watch, but also subscribe to groups so that we can kick
|
:: we can kick subscriptions if needed (eg ship removed from group).
|
||||||
:: subscriptions if needed (eg ship removed from group).
|
|
||||||
::
|
::
|
||||||
:: we deduplicate incoming subscriptions on the same path, ensuring we have
|
:: we deduplicate incoming subscriptions on the same path, ensuring we have
|
||||||
:: exactly one local subscription per unique incoming subscription path.
|
:: exactly one local subscription per unique incoming subscription path.
|
||||||
@ -18,10 +17,10 @@
|
|||||||
:: become part of the stores standard anyway.
|
:: become part of the stores standard anyway.
|
||||||
::
|
::
|
||||||
:: when adding support for new paths, the only things you'll likely want
|
:: 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
|
/- group-store, *metadata-store
|
||||||
/+ *link, default-agent, verb, dbug
|
/+ *link, metadata, default-agent, verb, dbug
|
||||||
|%
|
|%
|
||||||
+$ state-0
|
+$ state-0
|
||||||
$: %0
|
$: %0
|
||||||
@ -48,7 +47,7 @@
|
|||||||
++ on-init
|
++ on-init
|
||||||
^- (quip card _this)
|
^- (quip card _this)
|
||||||
:_ this
|
:_ this
|
||||||
[watch-groups:do]~
|
~[watch-groups:do watch-metadata:do]
|
||||||
::
|
::
|
||||||
++ on-save !>(state)
|
++ on-save !>(state)
|
||||||
++ on-load
|
++ on-load
|
||||||
@ -96,11 +95,15 @@
|
|||||||
--
|
--
|
||||||
::
|
::
|
||||||
|_ =bowl:gall
|
|_ =bowl:gall
|
||||||
|
+* md ~(. metadata bowl)
|
||||||
|
::
|
||||||
|
:: permissions
|
||||||
|
::
|
||||||
++ permitted
|
++ permitted
|
||||||
|= [who=ship =path]
|
|= [who=ship =path]
|
||||||
^- ?
|
^- ?
|
||||||
:: we only expose group-specific /local-pages and /annotations,
|
:: we only expose /local-pages and /annotations,
|
||||||
:: and only to ships in the relevant group.
|
:: to ships in the groups associated with the resource.
|
||||||
:: (no url-specific annotations subscriptions, either.)
|
:: (no url-specific annotations subscriptions, either.)
|
||||||
::
|
::
|
||||||
=/ target=(unit ^path)
|
=/ target=(unit ^path)
|
||||||
@ -110,12 +113,75 @@
|
|||||||
`t.t.path
|
`t.t.path
|
||||||
~
|
~
|
||||||
?~ target |
|
?~ target |
|
||||||
=; group
|
~? !.^(? %gu (scot %p our.bowl) %metadata-store (scot %da now.bowl) ~)
|
||||||
?& ?=(^ group)
|
%woah-md-s-not-booted ::TODO fallback if needed
|
||||||
(~(has in u.group) who)
|
%+ lien (groups-from-resource:md %link u.target)
|
||||||
==
|
|= =group-path
|
||||||
%+ scry-for (unit group:group-store)
|
^- ?
|
||||||
[%group-store u.target]
|
=- (~(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
|
:: groups subscription
|
||||||
::TODO largely copied from link-listen-hook. maybe make a store-listener lib?
|
::TODO largely copied from link-listen-hook. maybe make a store-listener lib?
|
||||||
@ -153,29 +219,26 @@
|
|||||||
^- (quip card _state)
|
^- (quip card _state)
|
||||||
:_ state
|
:_ state
|
||||||
?. ?=(%remove -.upd) ~
|
?. ?=(%remove -.upd) ~
|
||||||
=/ whos=(list ship) ~(tap in members.upd)
|
:: if someone was removed from a group, find all link resources associated
|
||||||
|- ^- (list card)
|
:: with that group, then kick their subscriptions if they're no longer
|
||||||
?~ whos ~
|
|
||||||
:: no need to remove to ourselves
|
|
||||||
::
|
::
|
||||||
?: =(our.bowl i.whos)
|
%- zing
|
||||||
$(whos t.whos)
|
%+ turn (app-paths-from-group:md %link pax.upd)
|
||||||
:_ $(whos t.whos)
|
|= =app-path
|
||||||
::NOTE this depends kind of unfortunately on the fact that we only accept
|
^- (list card)
|
||||||
:: subscriptions to /local-pages//* paths. it'd be more correct if we
|
%+ kick-revoked-permissions
|
||||||
:: "just" looked at all paths in the map, and found the matching ones.
|
app-path
|
||||||
::TODO what exactly did i mean by this?
|
~(tap in members.upd)
|
||||||
%+ kick-proxies i.whos
|
|
||||||
:~ [%local-pages pax.upd]
|
|
||||||
[%annotations '' pax.upd]
|
|
||||||
==
|
|
||||||
::
|
::
|
||||||
:: proxy subscriptions
|
:: proxy subscriptions
|
||||||
::
|
::
|
||||||
++ kick-proxies
|
++ kick-proxies
|
||||||
|= [who=ship paths=(list path)]
|
|= [who=ship =path]
|
||||||
^- card
|
^- card
|
||||||
[%give %kick paths `who]
|
=- [%give %kick - `who]
|
||||||
|
:~ [%local-pages path]
|
||||||
|
[%annotations %$ path]
|
||||||
|
==
|
||||||
::
|
::
|
||||||
++ handle-proxy-sign
|
++ handle-proxy-sign
|
||||||
|= [=wire =sign:agent:gall]
|
|= [=wire =sign:agent:gall]
|
||||||
@ -211,14 +274,10 @@
|
|||||||
[%give %fact ~ %link-initial !>(initial)]
|
[%give %fact ~ %link-initial !>(initial)]
|
||||||
?+ path !!
|
?+ path !!
|
||||||
[%local-pages ^]
|
[%local-pages ^]
|
||||||
:- %local-pages
|
[%local-pages .^((map ^path pages) %gx path)]
|
||||||
%+ scry-for (map ^path pages)
|
|
||||||
[%link-store path]
|
|
||||||
::
|
::
|
||||||
[%annotations %$ ^]
|
[%annotations %$ ^]
|
||||||
:- %annotations
|
[%annotations .^((per-path-url notes) %gx %$ t.t.path)]
|
||||||
%+ scry-for (per-path-url notes)
|
|
||||||
[%link-store path]
|
|
||||||
==
|
==
|
||||||
::
|
::
|
||||||
++ start-proxy
|
++ start-proxy
|
||||||
@ -249,12 +308,14 @@
|
|||||||
::
|
::
|
||||||
[(proxy-pass-link-store path %leave ~)]~
|
[(proxy-pass-link-store path %leave ~)]~
|
||||||
::
|
::
|
||||||
|
:: helpers
|
||||||
|
::
|
||||||
++ scry-for
|
++ scry-for
|
||||||
|* [=mold app=term =path]
|
|* [=mold =app-name =path]
|
||||||
.^ mold
|
.^ mold
|
||||||
%gx
|
%gx
|
||||||
(scot %p our.bowl)
|
(scot %p our.bowl)
|
||||||
app
|
app-name
|
||||||
(scot %da now.bowl)
|
(scot %da now.bowl)
|
||||||
(snoc `^path`path %noun)
|
(snoc `^path`path %noun)
|
||||||
==
|
==
|
||||||
|
@ -10,7 +10,11 @@
|
|||||||
:: /json/[n]/submission/[wood-url]/[some-group] nth matching submission
|
:: /json/[n]/submission/[wood-url]/[some-group] nth matching submission
|
||||||
:: /json/seen mark-as-read updates
|
:: /json/seen mark-as-read updates
|
||||||
::
|
::
|
||||||
/+ *link, *server, default-agent, verb
|
/- *link-view,
|
||||||
|
*invite-store, group-store,
|
||||||
|
group-hook, permission-hook, permission-group-hook,
|
||||||
|
metadata-hook, contact-view
|
||||||
|
/+ *link, metadata, *server, default-agent, verb, dbug
|
||||||
::
|
::
|
||||||
|%
|
|%
|
||||||
+$ state-0
|
+$ state-0
|
||||||
@ -25,6 +29,7 @@
|
|||||||
=* state -
|
=* state -
|
||||||
::
|
::
|
||||||
%+ verb |
|
%+ verb |
|
||||||
|
%- agent:dbug
|
||||||
^- agent:gall
|
^- agent:gall
|
||||||
=<
|
=<
|
||||||
|_ =bowl:gall
|
|_ =bowl:gall
|
||||||
@ -42,6 +47,12 @@
|
|||||||
::
|
::
|
||||||
=+ [dap.bowl /tile '/~link/js/tile.js']
|
=+ [dap.bowl /tile '/~link/js/tile.js']
|
||||||
[%pass /launch %agent [our.bowl %launch] %poke %launch-action !>(-)]
|
[%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)
|
++ on-save !>(state)
|
||||||
@ -65,6 +76,9 @@
|
|||||||
::
|
::
|
||||||
%link-action
|
%link-action
|
||||||
[(handle-action:do !<(action vase)) ~]
|
[(handle-action:do !<(action vase)) ~]
|
||||||
|
::
|
||||||
|
%link-view-action
|
||||||
|
(handle-view-action:do !<(view-action vase))
|
||||||
==
|
==
|
||||||
::
|
::
|
||||||
++ on-watch
|
++ on-watch
|
||||||
@ -104,13 +118,18 @@
|
|||||||
?+ -.sign (on-agent:def wire sign)
|
?+ -.sign (on-agent:def wire sign)
|
||||||
%kick
|
%kick
|
||||||
:_ this
|
:_ 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
|
%fact
|
||||||
=* mark p.cage.sign
|
=* mark p.cage.sign
|
||||||
=* vase q.cage.sign
|
=* vase q.cage.sign
|
||||||
?+ mark (on-agent:def wire 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
|
%link-update
|
||||||
:_ this
|
:_ this
|
||||||
@ -135,6 +154,8 @@
|
|||||||
--
|
--
|
||||||
::
|
::
|
||||||
|_ =bowl:gall
|
|_ =bowl:gall
|
||||||
|
+* md ~(. metadata bowl)
|
||||||
|
::
|
||||||
++ page-size 25
|
++ page-size 25
|
||||||
++ get-paginated
|
++ get-paginated
|
||||||
|* [p=(unit @ud) l=(list)]
|
|* [p=(unit @ud) l=(list)]
|
||||||
@ -217,10 +238,214 @@
|
|||||||
%- as-octs:mimes:html
|
%- as-octs:mimes:html
|
||||||
.^(@ %cx path)
|
.^(@ %cx path)
|
||||||
::
|
::
|
||||||
|
++ do-poke
|
||||||
|
|= [app=term =mark =vase]
|
||||||
|
^- card
|
||||||
|
[%pass /create/[app]/[mark] %agent [our.bowl app] %poke mark vase]
|
||||||
|
::
|
||||||
|
++ 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
|
||||||
|
==
|
||||||
|
::
|
||||||
++ handle-action
|
++ handle-action
|
||||||
|= =action
|
|= =action
|
||||||
^- card
|
^- card
|
||||||
[%pass /action %agent [our.bowl %link-store] %poke %link-action !>(action)]
|
[%pass /action %agent [our.bowl %link-store] %poke %link-action !>(action)]
|
||||||
|
::
|
||||||
|
++ handle-view-action
|
||||||
|
|= act=view-action
|
||||||
|
^- (list card)
|
||||||
|
?- -.act
|
||||||
|
%create (handle-create +.act)
|
||||||
|
%delete (handle-delete +.act)
|
||||||
|
%invite (handle-invite +.act)
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ handle-create
|
||||||
|
|= [=path title=@t description=@t members=create-members real-group=?]
|
||||||
|
^- (list card)
|
||||||
|
=/ group-path=^path
|
||||||
|
?- -.members
|
||||||
|
%group path.members
|
||||||
|
::
|
||||||
|
%ships
|
||||||
|
%+ weld
|
||||||
|
?:(real-group ~ [~.~]~)
|
||||||
|
[(scot %p our.bowl) path]
|
||||||
|
==
|
||||||
|
=; group-setup=(list card)
|
||||||
|
%+ weld group-setup
|
||||||
|
:~ :: add collection to metadata-store
|
||||||
|
::
|
||||||
|
%^ do-poke %metadata-hook
|
||||||
|
%metadata-action
|
||||||
|
!> ^- metadata-action:md
|
||||||
|
:^ %add group-path
|
||||||
|
[%link path]
|
||||||
|
%* . *metadata:md
|
||||||
|
title title
|
||||||
|
description description
|
||||||
|
date-created now.bowl
|
||||||
|
creator our.bowl
|
||||||
|
==
|
||||||
|
::
|
||||||
|
:: expose the metadata
|
||||||
|
::
|
||||||
|
%^ do-poke %metadata-hook
|
||||||
|
%metadata-hook-action
|
||||||
|
!> ^- metadata-hook-action:metadata-hook
|
||||||
|
[%add-owned group-path]
|
||||||
|
==
|
||||||
|
?: ?=(%group -.members) ~
|
||||||
|
:: if the group is "real", make contact-view do the heavy lifting
|
||||||
|
::
|
||||||
|
?: real-group
|
||||||
|
:_ ~
|
||||||
|
%^ do-poke %contact-view
|
||||||
|
%contact-view-action
|
||||||
|
!> ^- contact-view-action:contact-view
|
||||||
|
[%create group-path ships.members title description]
|
||||||
|
:: for "unmanaged" groups, do it ourselves
|
||||||
|
::
|
||||||
|
:* :: 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) our.bowl) group-path]
|
||||||
|
::
|
||||||
|
:: make group available
|
||||||
|
::
|
||||||
|
%^ do-poke %group-hook
|
||||||
|
%group-hook-action
|
||||||
|
!> ^- group-hook-action:group-hook
|
||||||
|
[%add our.bowl group-path]
|
||||||
|
::
|
||||||
|
:: mirror group into a permission
|
||||||
|
::
|
||||||
|
%^ do-poke %permission-group-hook
|
||||||
|
%permission-group-hook-action
|
||||||
|
!> ^- permission-group-hook-action:permission-group-hook
|
||||||
|
[%associate group-path [group-path^%white ~ ~]]
|
||||||
|
::
|
||||||
|
:: expose the permission
|
||||||
|
::
|
||||||
|
%^ 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)
|
||||||
|
|= =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
|
||||||
|
==
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ handle-delete
|
||||||
|
|= =path
|
||||||
|
^- (list card)
|
||||||
|
=/ groups=(list ^path)
|
||||||
|
(groups-from-resource:md [%link path])
|
||||||
|
%- zing
|
||||||
|
%+ turn groups
|
||||||
|
|= =group=^path
|
||||||
|
%+ snoc
|
||||||
|
^- (list card)
|
||||||
|
:: if it's a real group, we can't/shouldn't unsync it. this leaves us with
|
||||||
|
:: no way to stop propagation of collection deletion.
|
||||||
|
::
|
||||||
|
?. ?=([%'~' ^] group-path) ~
|
||||||
|
:: if it's an unmanaged group, we just stop syncing the group & metadata,
|
||||||
|
:: and clean up the group (after un-hooking it, to not push deletion).
|
||||||
|
::
|
||||||
|
:~ %^ do-poke %group-hook
|
||||||
|
%group-hook-action
|
||||||
|
!> ^- group-hook-action:group-hook
|
||||||
|
[%remove group-path]
|
||||||
|
::
|
||||||
|
%^ do-poke %metadata-hook
|
||||||
|
%metadata-hook-action
|
||||||
|
!> ^- metadata-hook-action:metadata-hook
|
||||||
|
[%remove group-path]
|
||||||
|
::
|
||||||
|
%^ do-poke %group-store
|
||||||
|
%group-action
|
||||||
|
!> ^- group-action:group-store
|
||||||
|
[%unbundle group-path]
|
||||||
|
==
|
||||||
|
:: remove collection from metadata-store
|
||||||
|
::
|
||||||
|
%^ do-poke %metadata-store
|
||||||
|
%metadata-action
|
||||||
|
!> ^- metadata-action:md
|
||||||
|
[%remove group-path [%link path]]
|
||||||
|
::
|
||||||
|
++ handle-invite
|
||||||
|
|= [=path ships=(set ship)]
|
||||||
|
^- (list card)
|
||||||
|
%- zing
|
||||||
|
%+ turn (groups-from-resource:md %link path)
|
||||||
|
|= =group=^path
|
||||||
|
^- (list card)
|
||||||
|
:- %^ do-poke %group-store
|
||||||
|
%group-action
|
||||||
|
!> ^- group-action:group-store
|
||||||
|
[%add ships group-path]
|
||||||
|
:: for managed groups, rely purely on group logic for invites
|
||||||
|
::
|
||||||
|
?. ?=([%'~' ^] group-path)
|
||||||
|
~
|
||||||
|
:: for unmanaged groups, send invites manually
|
||||||
|
::
|
||||||
|
%+ turn ~(tap in ships)
|
||||||
|
|= =ship
|
||||||
|
^- card
|
||||||
|
%^ do-poke %invite-hook
|
||||||
|
%invite-action
|
||||||
|
!> ^- invite-action
|
||||||
|
:^ %invite /link
|
||||||
|
(sham group-path eny.bowl)
|
||||||
|
:* our.bowl
|
||||||
|
%group-hook
|
||||||
|
group-path
|
||||||
|
ship
|
||||||
|
(rsh 3 1 (spat path))
|
||||||
|
==
|
||||||
:: +give-tile-data: total unread count as json object
|
:: +give-tile-data: total unread count as json object
|
||||||
::
|
::
|
||||||
::NOTE the full recalc of totals here probably isn't the end of the world.
|
::NOTE the full recalc of totals here probably isn't the end of the world.
|
||||||
|
BIN
pkg/arvo/app/link/img/search.png
Normal file
BIN
pkg/arvo/app/link/img/search.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 951 B |
@ -182,7 +182,7 @@
|
|||||||
|= =group-path
|
|= =group-path
|
||||||
^- ^associations
|
^- ^associations
|
||||||
%- ~(gas by *^associations)
|
%- ~(gas by *^associations)
|
||||||
%+ turn ~(tap in (~(got by group-indices) group-path))
|
%+ turn ~(tap in (~(gut by group-indices) group-path ~))
|
||||||
|= =resource
|
|= =resource
|
||||||
:- [group-path resource]
|
:- [group-path resource]
|
||||||
(~(got by associations) [group-path resource])
|
(~(got by associations) [group-path resource])
|
||||||
|
@ -43,13 +43,15 @@
|
|||||||
~| path
|
~| path
|
||||||
(woad (slav %ta i.path))
|
(woad (slav %ta i.path))
|
||||||
::
|
::
|
||||||
:: zip sorted a into sorted b, maintaining sort order
|
:: zip sorted a into sorted b, maintaining sort order, avoiding duplicates
|
||||||
::TODO stdlib
|
::
|
||||||
++ merge-sorted
|
++ merge-sorted-unique
|
||||||
|* [sort=$-([* *] ?) a=(list) b=(list)]
|
|* [sort=$-([* *] ?) a=(list) b=(list)]
|
||||||
|- ^- ?(_a _b)
|
|- ^- ?(_a _b)
|
||||||
?~ a b
|
?~ a b
|
||||||
?~ b a
|
?~ b a
|
||||||
|
?: =(i.a i.b)
|
||||||
|
[i.a $(a t.a, b t.b)]
|
||||||
?: (sort i.a i.b)
|
?: (sort i.a i.b)
|
||||||
[i.a $(a t.a)]
|
[i.a $(a t.a)]
|
||||||
[i.b $(b t.b)]
|
[i.b $(b t.b)]
|
||||||
@ -60,7 +62,7 @@
|
|||||||
::TODO we would just use +cury here but it don't work
|
::TODO we would just use +cury here but it don't work
|
||||||
|= [a=^pages b=^pages]
|
|= [a=^pages b=^pages]
|
||||||
^+ a
|
^+ a
|
||||||
%+ merge-sorted
|
%+ merge-sorted-unique
|
||||||
|= [a=page b=page]
|
|= [a=page b=page]
|
||||||
(gth time.a time.b)
|
(gth time.a time.b)
|
||||||
[a b]
|
[a b]
|
||||||
@ -68,7 +70,7 @@
|
|||||||
++ submissions
|
++ submissions
|
||||||
|= [a=^submissions b=^submissions]
|
|= [a=^submissions b=^submissions]
|
||||||
^+ a
|
^+ a
|
||||||
%+ merge-sorted
|
%+ merge-sorted-unique
|
||||||
|= [a=submission b=submission]
|
|= [a=submission b=submission]
|
||||||
(gth time.a time.b)
|
(gth time.a time.b)
|
||||||
[a b]
|
[a b]
|
||||||
@ -76,7 +78,7 @@
|
|||||||
++ notes
|
++ notes
|
||||||
|= [a=^notes b=^notes]
|
|= [a=^notes b=^notes]
|
||||||
^+ a
|
^+ a
|
||||||
%+ merge-sorted
|
%+ merge-sorted-unique
|
||||||
|= [a=note b=note]
|
|= [a=note b=note]
|
||||||
(gth time.a time.b)
|
(gth time.a time.b)
|
||||||
[a b]
|
[a b]
|
||||||
@ -84,7 +86,7 @@
|
|||||||
++ comments
|
++ comments
|
||||||
|= [a=^comments b=^comments]
|
|= [a=^comments b=^comments]
|
||||||
^+ a
|
^+ a
|
||||||
%+ merge-sorted
|
%+ merge-sorted-unique
|
||||||
|= [a=comment b=comment]
|
|= [a=comment b=comment]
|
||||||
(gth time.a time.b)
|
(gth time.a time.b)
|
||||||
[a b]
|
[a b]
|
||||||
|
54
pkg/arvo/lib/metadata.hoon
Normal file
54
pkg/arvo/lib/metadata.hoon
Normal file
@ -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)
|
||||||
|
==
|
||||||
|
--
|
42
pkg/arvo/mar/link/view-action.hoon
Normal file
42
pkg/arvo/mar/link/view-action.hoon
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/- *link-view
|
||||||
|
=, dejs:format
|
||||||
|
|_ act=view-action
|
||||||
|
++ grab
|
||||||
|
|%
|
||||||
|
++ noun view-action
|
||||||
|
++ json
|
||||||
|
|^ %- of
|
||||||
|
:~ %create^create
|
||||||
|
%delete^delete
|
||||||
|
%invite^invite
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ create
|
||||||
|
%- ot
|
||||||
|
:~ 'path'^pa
|
||||||
|
'title'^so
|
||||||
|
'description'^so
|
||||||
|
'members'^mems
|
||||||
|
'realGroup'^bo
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ mems
|
||||||
|
(of %group^pa %ships^ships ~)
|
||||||
|
::
|
||||||
|
++ delete
|
||||||
|
(ot 'path'^pa ~)
|
||||||
|
::
|
||||||
|
++ invite
|
||||||
|
(ot 'path'^pa 'ships'^ships ~)
|
||||||
|
::
|
||||||
|
::TODO stdlib
|
||||||
|
++ ships
|
||||||
|
(cu sy (ar (su ;~(pfix sig fed:ag))))
|
||||||
|
--
|
||||||
|
--
|
||||||
|
::
|
||||||
|
++ grow
|
||||||
|
|%
|
||||||
|
++ noun act
|
||||||
|
--
|
||||||
|
--
|
34
pkg/arvo/sur/link-view.hoon
Normal file
34
pkg/arvo/sur/link-view.hoon
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
:: link-view: encapsulating link management
|
||||||
|
::
|
||||||
|
|%
|
||||||
|
++ view-action
|
||||||
|
$% :: %create: create a new link collection
|
||||||
|
::
|
||||||
|
:: with specified metadata and group. %ships creates a new group,
|
||||||
|
:: :real-group indicates whether it's global (&) or local (|).
|
||||||
|
::
|
||||||
|
$: %create
|
||||||
|
=path
|
||||||
|
title=@t
|
||||||
|
description=@t
|
||||||
|
members=create-members
|
||||||
|
real-group=?
|
||||||
|
==
|
||||||
|
::
|
||||||
|
:: %delete: remove collection from local metadata & stop syncing
|
||||||
|
::
|
||||||
|
:: if the resource is associated with a real group, and we're the owner,
|
||||||
|
:: this deletes it for everyone
|
||||||
|
::
|
||||||
|
[%delete =path]
|
||||||
|
::
|
||||||
|
:: %invite: add to resource's group and send invite
|
||||||
|
::
|
||||||
|
[%invite =path ships=(set ship)]
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ create-members
|
||||||
|
$% [%group =path]
|
||||||
|
[%ships ships=(set ship)]
|
||||||
|
==
|
||||||
|
--
|
@ -60,6 +60,10 @@ a {
|
|||||||
font-family: "Source Code Pro", monospace;
|
font-family: "Source Code Pro", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-ship {
|
||||||
|
line-height: 2.2;
|
||||||
|
}
|
||||||
|
|
||||||
.c-default {
|
.c-default {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
@ -105,6 +109,23 @@ a {
|
|||||||
100% {transform: rotate(360deg);}
|
100% {transform: rotate(360deg);}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* toggler checkbox */
|
||||||
|
|
||||||
|
.toggle::after {
|
||||||
|
content: "";
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
background: white;
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle.checked::after {
|
||||||
|
left: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
/* responsive */
|
/* responsive */
|
||||||
@media all and (max-width: 34.375em) {
|
@media all and (max-width: 34.375em) {
|
||||||
.dn-s {
|
.dn-s {
|
||||||
@ -164,6 +185,9 @@ a {
|
|||||||
.b--gray0-d {
|
.b--gray0-d {
|
||||||
border-color: #333;
|
border-color: #333;
|
||||||
}
|
}
|
||||||
|
.b--gray1-d {
|
||||||
|
border-color: #4d4d4d;
|
||||||
|
}
|
||||||
.b--gray2-d {
|
.b--gray2-d {
|
||||||
border-color: #7f7f7f;
|
border-color: #7f7f7f;
|
||||||
}
|
}
|
||||||
@ -180,6 +204,9 @@ a {
|
|||||||
.o-60-d {
|
.o-60-d {
|
||||||
opacity: .6;
|
opacity: .6;
|
||||||
}
|
}
|
||||||
|
.focus-b--white-d:focus {
|
||||||
|
border-color: #fff;
|
||||||
|
}
|
||||||
a {
|
a {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { uuid, stringToTa } from '/lib/util';
|
import { stringToTa } from '/lib/util';
|
||||||
import { store } from '/store';
|
import { store } from '/store';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
@ -13,10 +13,13 @@ class UrbitApi {
|
|||||||
|
|
||||||
this.invite = {
|
this.invite = {
|
||||||
accept: this.inviteAccept.bind(this),
|
accept: this.inviteAccept.bind(this),
|
||||||
decline: this.inviteDecline.bind(this),
|
decline: this.inviteDecline.bind(this)
|
||||||
invite: this.inviteInvite.bind(this)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.groups = {
|
||||||
|
remove: this.groupRemove.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
this.bind = this.bind.bind(this);
|
this.bind = this.bind.bind(this);
|
||||||
this.bindLinkView = this.bindLinkView.bind(this);
|
this.bindLinkView = this.bindLinkView.bind(this);
|
||||||
}
|
}
|
||||||
@ -61,32 +64,26 @@ class UrbitApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
inviteAction(data) {
|
groupsAction(data) {
|
||||||
this.action("invite-store", "json", data);
|
this.action("group-store", "group-action", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
inviteInvite(path, ship) {
|
groupRemove(path, members) {
|
||||||
this.action("invite-hook", "json",
|
this.groupsAction({
|
||||||
{
|
remove: {
|
||||||
invite: {
|
path, members
|
||||||
path: '/chat',
|
|
||||||
invite: {
|
|
||||||
path,
|
|
||||||
ship: `~${window.ship}`,
|
|
||||||
recipient: ship,
|
|
||||||
app: 'chat-hook',
|
|
||||||
text: `You have been invited to /${window.ship}${path}`,
|
|
||||||
},
|
|
||||||
uid: uuid()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
inviteAction(data) {
|
||||||
|
this.action("invite-store", "json", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
inviteAccept(uid) {
|
inviteAccept(uid) {
|
||||||
this.inviteAction({
|
this.inviteAction({
|
||||||
accept: {
|
accept: {
|
||||||
path: '/chat',
|
path: '/link',
|
||||||
uid
|
uid
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -95,7 +92,7 @@ class UrbitApi {
|
|||||||
inviteDecline(uid) {
|
inviteDecline(uid) {
|
||||||
this.inviteAction({
|
this.inviteAction({
|
||||||
decline: {
|
decline: {
|
||||||
path: '/chat',
|
path: '/link',
|
||||||
uid
|
uid
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -144,6 +141,30 @@ class UrbitApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
linkViewAction(data) {
|
||||||
|
return this.action("link-view", "link-view-action", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
createCollection(path, title, description, members, realGroup) {
|
||||||
|
// members is either {group:'/group-path'} or {'ships':[~zod]},
|
||||||
|
// with realGroup signifying if ships should become a managed group or not.
|
||||||
|
return this.linkViewAction({
|
||||||
|
create: {path, title, description, members, realGroup}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCollection(path) {
|
||||||
|
return this.linkViewAction({
|
||||||
|
'delete': {path}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
inviteToCollection(path, ships) {
|
||||||
|
return this.linkViewAction({
|
||||||
|
'invite': {path, ships}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
linkAction(data) {
|
linkAction(data) {
|
||||||
return this.action("link-store", "link-action", data);
|
return this.action("link-store", "link-action", data);
|
||||||
}
|
}
|
||||||
@ -167,6 +188,29 @@ class UrbitApi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
metadataAction(data) {
|
||||||
|
return this.action("metadata-hook", "metadata-action", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataAdd(appPath, groupPath, title, description, dateCreated, color) {
|
||||||
|
return this.metadataAction({
|
||||||
|
add: {
|
||||||
|
'group-path': groupPath,
|
||||||
|
resource: {
|
||||||
|
'app-path': appPath,
|
||||||
|
'app-name': 'link'
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
color,
|
||||||
|
'date-created': dateCreated,
|
||||||
|
creator: `~${window.ship}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
sidebarToggle() {
|
sidebarToggle() {
|
||||||
let sidebarBoolean = true;
|
let sidebarBoolean = true;
|
||||||
if (store.state.sidebarShown === true) {
|
if (store.state.sidebarShown === true) {
|
||||||
|
@ -2,6 +2,7 @@ import React, { Component } from 'react';
|
|||||||
|
|
||||||
import { Route, Link } from 'react-router-dom';
|
import { Route, Link } from 'react-router-dom';
|
||||||
import { ChannelsItem } from '/components/lib/channels-item';
|
import { ChannelsItem } from '/components/lib/channels-item';
|
||||||
|
import { SidebarInvite } from '/components/lib/sidebar-invite';
|
||||||
|
|
||||||
export class ChannelsSidebar extends Component {
|
export class ChannelsSidebar extends Component {
|
||||||
// drawer to the left
|
// drawer to the left
|
||||||
@ -9,59 +10,38 @@ export class ChannelsSidebar extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
|
|
||||||
let privateChannel =
|
let sidebarInvites = Object.keys(props.invites)
|
||||||
Object.keys(props.groups)
|
.map((uid) => {
|
||||||
.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
|
|
||||||
return (
|
return (
|
||||||
<ChannelsItem
|
<SidebarInvite
|
||||||
key={path}
|
uid={uid}
|
||||||
link={path}
|
invite={props.invites[uid]}
|
||||||
memberList={props.groups[path]}
|
api={props.api} />
|
||||||
selected={selected}
|
);
|
||||||
linkCount={linkCount}
|
|
||||||
unseenCount={unseenCount}
|
|
||||||
name={name}/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
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 unseenCount = !!props.links[path]
|
|
||||||
? props.links[path].unseenCount
|
|
||||||
: linkCount
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ChannelsItem
|
|
||||||
key={path}
|
|
||||||
link={path}
|
|
||||||
memberList={props.groups[path]}
|
|
||||||
selected={selected}
|
|
||||||
linkCount={linkCount}
|
|
||||||
unseenCount={unseenCount}
|
|
||||||
name={name}/>
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let activeClasses = (this.props.active === "channels") ? " " : "dn-s ";
|
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
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChannelsItem
|
||||||
|
key={path}
|
||||||
|
link={path}
|
||||||
|
memberList={props.groups[meta.group]}
|
||||||
|
selected={selected}
|
||||||
|
linkCount={linkCount}
|
||||||
|
unseenCount={unseenCount}
|
||||||
|
name={meta.title}/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let activeClasses = (this.props.active === "collections") ? " " : "dn-s ";
|
||||||
|
|
||||||
let hiddenClasses = true;
|
let hiddenClasses = true;
|
||||||
|
|
||||||
@ -70,7 +50,7 @@ export class ChannelsSidebar extends Component {
|
|||||||
if (this.props.popout) {
|
if (this.props.popout) {
|
||||||
hiddenClasses = false;
|
hiddenClasses = false;
|
||||||
} else {
|
} else {
|
||||||
hiddenClasses = this.props.sidebarShown;
|
hiddenClasses = this.props.sidebarShown;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -81,15 +61,15 @@ export class ChannelsSidebar extends Component {
|
|||||||
: "dn")}>
|
: "dn")}>
|
||||||
<a className="db dn-m dn-l dn-xl f8 pb3 pl3" href="/">⟵ Landscape</a>
|
<a className="db dn-m dn-l dn-xl f8 pb3 pl3" href="/">⟵ Landscape</a>
|
||||||
<div className="overflow-y-scroll h-100">
|
<div className="overflow-y-scroll h-100">
|
||||||
<h2 className={"f8 f9-m f9-l f9-xl " +
|
<div className="w-100 bg-transparent pa4 bb b--gray4 b--gray1-d"
|
||||||
"pt1 pt4-m pt4-l pt4-xl " +
|
style={{paddingBottom: 10, paddingTop: 10}}>
|
||||||
"pr4 pb3 pb3-m pb3-l pb3-xl " +
|
<Link
|
||||||
"pl3 pl4-m pl4-l pl4-xl " +
|
className="dib f9 pointer green2 gray4-d mr4"
|
||||||
"black-s gray2 white-d c-default " +
|
to={"/~link/new"}>
|
||||||
"bb b--gray4 b--gray2-d mb2 mb0-m mb0-l mb0-xl"}>
|
New Collection
|
||||||
Your Collections
|
</Link>
|
||||||
</h2>
|
</div>
|
||||||
{privateChannel}
|
{sidebarInvites}
|
||||||
{channelItems}
|
{channelItems}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
|
import { Route, Link } from 'react-router-dom';
|
||||||
import { Route, Link } from 'react-router-dom';
|
import { makeRoutePath } from '../../lib/util';
|
||||||
|
|
||||||
export class ChannelsItem extends Component {
|
export class ChannelsItem extends Component {
|
||||||
render() {
|
render() {
|
||||||
const { props } = this;
|
const { props } = this;
|
||||||
|
|
||||||
let selectedClass = (props.selected)
|
let selectedClass = (props.selected)
|
||||||
? "bg-gray5 bg-gray1-d b--gray4 b--gray2-d"
|
? "bg-gray5 bg-gray1-d b--gray4 b--gray2-d"
|
||||||
: "b--gray4 b--gray2-d";
|
: "b--gray4 b--gray2-d";
|
||||||
|
|
||||||
let memberCount = props.memberList
|
let memberCount = props.memberList
|
||||||
@ -18,7 +18,7 @@ export class ChannelsItem extends Component {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={"/~link" + props.link}>
|
<Link to={makeRoutePath(props.link)}>
|
||||||
<div className={"w-100 v-mid f9 pl4 bb z1 pa3 pt4 pb4 b--gray4 b--gray1-d gray3-d pointer " + selectedClass}>
|
<div className={"w-100 v-mid f9 pl4 bb z1 pa3 pt4 pb4 b--gray4 b--gray1-d gray3-d pointer " + selectedClass}>
|
||||||
<p className="f9 pt1">{props.name}</p>
|
<p className="f9 pt1">{props.name}</p>
|
||||||
<p className="f9 gray2">
|
<p className="f9 gray2">
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Route, Link } from 'react-router-dom';
|
import { Route, Link } from 'react-router-dom';
|
||||||
import { base64urlEncode } from '../../lib/util';
|
import { makeRoutePath } from '../../lib/util';
|
||||||
|
|
||||||
export class CommentsPagination extends Component {
|
export class CommentsPagination extends Component {
|
||||||
render() {
|
render() {
|
||||||
let props = this.props;
|
let props = this.props;
|
||||||
|
|
||||||
let prevPage = "/" + (Number(props.commentPage) - 1);
|
let prevPage = (Number(props.commentPage) - 1);
|
||||||
let nextPage = "/" + (Number(props.commentPage) + 1);
|
let nextPage = (Number(props.commentPage) + 1);
|
||||||
|
|
||||||
let prevDisplay = ((Number(props.commentPage) > 0))
|
let prevDisplay = ((Number(props.commentPage) > 0))
|
||||||
? "dib"
|
? "dib"
|
||||||
@ -17,31 +17,16 @@ export class CommentsPagination extends Component {
|
|||||||
? "dib"
|
? "dib"
|
||||||
: "dn";
|
: "dn";
|
||||||
|
|
||||||
let encodedUrl = base64urlEncode(props.url);
|
|
||||||
let popout = (props.popout) ? "/popout" : "";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-100 relative pt4 pb6">
|
<div className="w-100 relative pt4 pb6">
|
||||||
<Link
|
<Link
|
||||||
className={"pb6 absolute inter f8 left-0 " + prevDisplay}
|
className={"pb6 absolute inter f8 left-0 " + prevDisplay}
|
||||||
to={"/~link"
|
to={makeRoutePath(props.resourcePath, props.popout, props.linkPage, props.url, props.linkIndex, prevPage)}>
|
||||||
+ popout
|
|
||||||
+ props.groupPath
|
|
||||||
+ "/" + props.linkPage
|
|
||||||
+ "/" + props.linkIndex
|
|
||||||
+ "/" + encodedUrl
|
|
||||||
+ "/comments" + prevPage}>
|
|
||||||
<- Previous Page
|
<- Previous Page
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
className={"pb6 absolute inter f8 right-0 " + nextDisplay}
|
className={"pb6 absolute inter f8 right-0 " + nextDisplay}
|
||||||
to={"/~link"
|
to={makeRoutePath(props.resourcePath, props.popout, props.linkPage, props.url, props.linkIndex, nextPage)}>
|
||||||
+ popout
|
|
||||||
+ props.groupPath
|
|
||||||
+ "/" + props.linkPage
|
|
||||||
+ "/" + props.linkIndex
|
|
||||||
+ "/" + encodedUrl
|
|
||||||
+ "/comments" + nextPage}>
|
|
||||||
Next Page ->
|
Next Page ->
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,7 +19,7 @@ export class Comments extends Component {
|
|||||||
) {
|
) {
|
||||||
this.setState({requested: this.props.commentPage});
|
this.setState({requested: this.props.commentPage});
|
||||||
api.getCommentsPage(
|
api.getCommentsPage(
|
||||||
this.props.groupPath,
|
this.props.resourcePath,
|
||||||
this.props.url,
|
this.props.url,
|
||||||
this.props.commentPage);
|
this.props.commentPage);
|
||||||
}
|
}
|
||||||
@ -73,8 +73,8 @@ export class Comments extends Component {
|
|||||||
<div>
|
<div>
|
||||||
{commentsList}
|
{commentsList}
|
||||||
<CommentsPagination
|
<CommentsPagination
|
||||||
key={props.groupPath + props.commentPage}
|
key={props.resourcePath + props.commentPage}
|
||||||
groupPath={props.groupPath}
|
resourcePath={props.resourcePath}
|
||||||
popout={props.popout}
|
popout={props.popout}
|
||||||
linkPage={props.linkPage}
|
linkPage={props.linkPage}
|
||||||
linkIndex={props.linkIndex}
|
linkIndex={props.linkIndex}
|
||||||
|
74
pkg/interface/link/src/js/components/lib/invite-element.js
Normal file
74
pkg/interface/link/src/js/components/lib/invite-element.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { InviteSearch } from './invite-search';
|
||||||
|
|
||||||
|
export class InviteElement extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
members: [],
|
||||||
|
error: false,
|
||||||
|
success: false
|
||||||
|
};
|
||||||
|
this.setInvite = this.setInvite.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
modifyMembers() {
|
||||||
|
const { props, state } = this;
|
||||||
|
|
||||||
|
let aud = state.members.map(mem => `~${mem}`);
|
||||||
|
|
||||||
|
if (state.members.length === 0) {
|
||||||
|
this.setState({
|
||||||
|
error: true,
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.setSpinner(true);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
error: false,
|
||||||
|
success: true,
|
||||||
|
members: []
|
||||||
|
}, () => {
|
||||||
|
api.inviteToCollection(props.resourcePath, aud).then(() => {
|
||||||
|
api.setSpinner(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setInvite(invite) {
|
||||||
|
this.setState({members: invite.ships});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { props, state } = this;
|
||||||
|
|
||||||
|
let modifyButtonClasses = "mt4 db f9 ba pa2 white-d bg-gray0-d b--black b--gray2-d pointer";
|
||||||
|
if (state.error) {
|
||||||
|
modifyButtonClasses = modifyButtonClasses + ' gray3';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<InviteSearch
|
||||||
|
groups={{}}
|
||||||
|
contacts={props.contacts}
|
||||||
|
groupResults={false}
|
||||||
|
invites={{
|
||||||
|
groups: [],
|
||||||
|
ships: this.state.members
|
||||||
|
}}
|
||||||
|
setInvite={this.setInvite}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={this.modifyMembers.bind(this)}
|
||||||
|
className={modifyButtonClasses}>
|
||||||
|
Invite
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
369
pkg/interface/link/src/js/components/lib/invite-search.js
Normal file
369
pkg/interface/link/src/js/components/lib/invite-search.js
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
import React, { Component } from "react";
|
||||||
|
import urbitOb from "urbit-ob";
|
||||||
|
import { Sigil } from "../lib/icons/sigil";
|
||||||
|
|
||||||
|
export class InviteSearch extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
groups: [],
|
||||||
|
peers: [],
|
||||||
|
contacts: new Map,
|
||||||
|
searchValue: "",
|
||||||
|
searchResults: {
|
||||||
|
groups: [],
|
||||||
|
ships: []
|
||||||
|
},
|
||||||
|
inviteError: false
|
||||||
|
};
|
||||||
|
this.search = this.search.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.peerUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (prevProps !== this.props) {
|
||||||
|
this.peerUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
peerUpdate() {
|
||||||
|
let groups = Array.from(Object.keys(this.props.groups));
|
||||||
|
groups = groups.filter(e => !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 = <div />;
|
||||||
|
let searchResults = <div />;
|
||||||
|
|
||||||
|
let invErrElem = <span />;
|
||||||
|
if (state.inviteError) {
|
||||||
|
invErrElem = (
|
||||||
|
<span className="f9 inter red2 db pt2">
|
||||||
|
Invited ships must be validly formatted ship names.
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
state.searchResults.groups.length > 0 ||
|
||||||
|
state.searchResults.ships.length > 0
|
||||||
|
) {
|
||||||
|
let groupHeader =
|
||||||
|
state.searchResults.groups.length > 0 ? (
|
||||||
|
<p className="f9 gray2 ph3 pb2">Groups</p>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
let shipHeader =
|
||||||
|
state.searchResults.ships.length > 0 ? (
|
||||||
|
<p className="f9 gray2 pv2 ph3">Ships</p>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
let groupResults = state.searchResults.groups.map(group => {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={group}
|
||||||
|
className={
|
||||||
|
"list mono white-d f8 pv2 ph3 pointer" +
|
||||||
|
" hover-bg-gray4 hover-black-d"
|
||||||
|
}
|
||||||
|
onClick={e => this.addGroup(group)}>
|
||||||
|
<span className="mix-blend-diff black">{group}</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let shipResults = state.searchResults.ships.map(ship => {
|
||||||
|
let nicknames = (this.state.contacts.has(ship))
|
||||||
|
? this.state.contacts.get(ship).join(", ")
|
||||||
|
: "";
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={ship}
|
||||||
|
className={
|
||||||
|
"list mono white-d f8 pv1 ph3 pointer" +
|
||||||
|
" hover-bg-gray4 hover-black-d relative"
|
||||||
|
}
|
||||||
|
onClick={e => this.addShip(ship)}>
|
||||||
|
<Sigil
|
||||||
|
ship={"~" + ship}
|
||||||
|
size={24}
|
||||||
|
color="#000000"
|
||||||
|
classes="mix-blend-diff v-mid"
|
||||||
|
/>
|
||||||
|
<span className="v-mid ml2 mw5 truncate dib">{"~" + ship}</span>
|
||||||
|
<span className="absolute right-1 di truncate mw4 inter f9 pt1">{nicknames}</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
searchResults = (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"absolute bg-white bg-gray0-d white-d" +
|
||||||
|
" pv3 z-1 w-100 mt1 ba b--white-d overflow-y-scroll mh-16"
|
||||||
|
}>
|
||||||
|
{groupHeader}
|
||||||
|
{groupResults}
|
||||||
|
{shipHeader}
|
||||||
|
{shipResults}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let groupInvites = props.invites.groups || [];
|
||||||
|
let shipInvites = props.invites.ships || [];
|
||||||
|
|
||||||
|
if (groupInvites.length > 0 || shipInvites.length > 0) {
|
||||||
|
let groups = groupInvites.map(group => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={group}
|
||||||
|
className={
|
||||||
|
"f9 mono black pa2 bg-gray5 bg-gray1-d" +
|
||||||
|
" ba b--gray4 b--gray2-d white-d dib mr2 mt2 c-default"
|
||||||
|
}>
|
||||||
|
{group}
|
||||||
|
<span
|
||||||
|
className="white-d ml3 mono pointer"
|
||||||
|
onClick={e => this.deleteGroup(group)}>
|
||||||
|
x
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let ships = shipInvites.map(ship => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={ship}
|
||||||
|
className={
|
||||||
|
"f9 mono black pa2 bg-gray5 bg-gray1-d" +
|
||||||
|
" ba b--gray4 b--gray2-d white-d dib mr2 mt2 c-default"
|
||||||
|
}>
|
||||||
|
{"~" + ship}
|
||||||
|
<span
|
||||||
|
className="white-d ml3 mono pointer"
|
||||||
|
onClick={e => this.deleteShip(ship)}>
|
||||||
|
x
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
participants = (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"f9 gray2 bb bl br b--gray3 b--gray2-d bg-gray0-d " +
|
||||||
|
"white-d pa3 db w-100 inter"
|
||||||
|
}>
|
||||||
|
<span className="db gray2">Participants</span>
|
||||||
|
{groups} {ships}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<img
|
||||||
|
src="/~link/img/search.png"
|
||||||
|
className="absolute invert-d"
|
||||||
|
style={{
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
top: 14,
|
||||||
|
left: 12
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
ref={e => {
|
||||||
|
this.textarea = e;
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
"f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 w-100" +
|
||||||
|
" db focus-b--black focus-b--white-d"
|
||||||
|
}
|
||||||
|
placeholder="Search for ships or existing groups"
|
||||||
|
disabled={searchDisabled}
|
||||||
|
rows={1}
|
||||||
|
spellCheck={false}
|
||||||
|
style={{
|
||||||
|
resize: "none",
|
||||||
|
paddingLeft: 36
|
||||||
|
}}
|
||||||
|
onKeyPress={e => {
|
||||||
|
if (e.key === "Enter" || e.key === ",") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.submitShipToAdd(this.state.searchValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={this.search}
|
||||||
|
value={state.searchValue}
|
||||||
|
/>
|
||||||
|
{searchResults}
|
||||||
|
{participants}
|
||||||
|
{invErrElem}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InviteSearch;
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Route, Link } from "react-router-dom";
|
import { Route, Link } from "react-router-dom";
|
||||||
import { base64urlEncode } from "../../lib/util";
|
import { makeRoutePath } from "../../lib/util";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
||||||
export class LinkPreview extends Component {
|
export class LinkPreview extends Component {
|
||||||
@ -114,16 +114,7 @@ export class LinkPreview extends Component {
|
|||||||
{this.state.timeSinceLinkPost}
|
{this.state.timeSinceLinkPost}
|
||||||
</span>
|
</span>
|
||||||
<Link
|
<Link
|
||||||
to={
|
to={makeRoutePath(props.resourcePath, props.popout, props.page, props.url, props.linkIndex)}
|
||||||
"/~link" +
|
|
||||||
props.groupPath +
|
|
||||||
"/" +
|
|
||||||
props.page +
|
|
||||||
"/" +
|
|
||||||
props.linkIndex +
|
|
||||||
"/" +
|
|
||||||
base64urlEncode(props.url)
|
|
||||||
}
|
|
||||||
className="v-top">
|
className="v-top">
|
||||||
<span className="f9 inter gray2">{props.comments}</span>
|
<span className="f9 inter gray2">{props.comments}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -3,7 +3,7 @@ import moment from 'moment';
|
|||||||
|
|
||||||
import { Sigil } from '/components/lib/icons/sigil';
|
import { Sigil } from '/components/lib/icons/sigil';
|
||||||
import { Route, Link } from 'react-router-dom';
|
import { Route, Link } from 'react-router-dom';
|
||||||
import { base64urlEncode } from '../../lib/util';
|
import { makeRoutePath } from '../../lib/util';
|
||||||
|
|
||||||
export class LinkItem extends Component {
|
export class LinkItem extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -34,7 +34,7 @@ export class LinkItem extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
markPostAsSeen() {
|
markPostAsSeen() {
|
||||||
api.seenLink(this.props.groupPath, this.props.url);
|
api.seenLink(this.props.resourcePath, this.props.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -58,8 +58,6 @@ export class LinkItem extends Component {
|
|||||||
hostname = hostname[4];
|
hostname = hostname[4];
|
||||||
}
|
}
|
||||||
|
|
||||||
let encodedUrl = base64urlEncode(props.url);
|
|
||||||
|
|
||||||
let comments = props.comments + " comment" + ((props.comments === 1) ? "" : "s");
|
let comments = props.comments + " comment" + ((props.comments === 1) ? "" : "s");
|
||||||
|
|
||||||
let member = this.props.member || false;
|
let member = this.props.member || false;
|
||||||
@ -90,7 +88,7 @@ export class LinkItem extends Component {
|
|||||||
{this.state.timeSinceLinkPost}
|
{this.state.timeSinceLinkPost}
|
||||||
</span>
|
</span>
|
||||||
<Link to=
|
<Link to=
|
||||||
{"/~link" + props.popout + props.groupPath + "/" + props.page + "/" + props.linkIndex + "/" + encodedUrl}
|
{makeRoutePath(props.resourcePath, props.popout, props.page, props.url, props.linkIndex)}
|
||||||
className="v-top"
|
className="v-top"
|
||||||
onClick={this.markPostAsSeen}>
|
onClick={this.markPostAsSeen}>
|
||||||
<span className="f9 inter gray2">
|
<span className="f9 inter gray2">
|
||||||
|
@ -20,7 +20,7 @@ export class LinkSubmit extends Component {
|
|||||||
? this.state.linkTitle
|
? this.state.linkTitle
|
||||||
: this.state.linkValue;
|
: this.state.linkValue;
|
||||||
api.setSpinner(true);
|
api.setSpinner(true);
|
||||||
api.postLink(this.props.groupPath, link, title).then(r => {
|
api.postLink(this.props.resourcePath, link, title).then(r => {
|
||||||
api.setSpinner(false);
|
api.setSpinner(false);
|
||||||
this.setState({ linkValue: "", linkTitle: "" });
|
this.setState({ linkValue: "", linkTitle: "" });
|
||||||
});
|
});
|
||||||
|
@ -1,44 +1,54 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { makeRoutePath } from '../../lib/util';
|
||||||
|
|
||||||
export class LinksTabBar extends Component {
|
export class LinksTabBar extends Component {
|
||||||
render() {
|
render() {
|
||||||
let props = this.props;
|
let props = this.props;
|
||||||
|
|
||||||
let memColor = '',
|
let memColor = '',
|
||||||
popout = '';
|
setColor = '';
|
||||||
|
|
||||||
if (props.location.pathname.includes('/members')) {
|
if (props.location.pathname.includes('/settings')) {
|
||||||
memColor = 'black';
|
memColor = 'gray3';
|
||||||
|
setColor = 'black white-d';
|
||||||
|
} else if (props.location.pathname.includes('/members')) {
|
||||||
|
memColor = 'black white-d';
|
||||||
|
setColor = 'gray3';
|
||||||
} else {
|
} else {
|
||||||
memColor = 'gray3';
|
memColor = 'gray3';
|
||||||
|
setColor = 'gray3';
|
||||||
}
|
}
|
||||||
|
|
||||||
(props.location.pathname.includes('/popout'))
|
let hidePopoutIcon = (props.popout)
|
||||||
? popout = "popout/"
|
|
||||||
: popout = "";
|
|
||||||
|
|
||||||
let hidePopoutIcon = (this.props.popout)
|
|
||||||
? "dn-m dn-l dn-xl"
|
? "dn-m dn-l dn-xl"
|
||||||
: "dib-m dib-l dib-xl";
|
: "dib-m dib-l dib-xl";
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dib pt2 flex-shrink-0 flex-grow-1">
|
<div className="dib flex-shrink-0 flex-grow-1">
|
||||||
{!!props.isOwner ? (
|
{!!props.amOwner ? (
|
||||||
<div className={"dib f8 pl6"}>
|
<div className={"dib pt2 f9 pl6 lh-solid"}>
|
||||||
<Link
|
<Link
|
||||||
className={"no-underline " + memColor}
|
className={"no-underline " + memColor}
|
||||||
to={`/~link/` + popout + `members` + props.groupPath}>
|
to={makeRoutePath(props.resourcePath, props.popout) + '/members'}>
|
||||||
Members
|
Members
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="dib" style={{ width: 0 }}></div>
|
<div className="dib" style={{ width: 0 }}></div>
|
||||||
)}
|
)}
|
||||||
<a href={`/~link/popout` + props.groupPath} target="_blank"
|
<div className={"dib pt2 f9 pl6 pr6 lh-solid"}>
|
||||||
className="dib fr">
|
<Link
|
||||||
|
className={"no-underline " + setColor}
|
||||||
|
to={makeRoutePath(props.resourcePath, props.popout) + '/settings'}>
|
||||||
|
Settings
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<a href={makeRoutePath(props.resourcePath, true, props.page)}
|
||||||
|
target="_blank"
|
||||||
|
className="dib fr pt2 pr1">
|
||||||
<img
|
<img
|
||||||
className={`flex-shrink-0 pr4 dn` + hidePopoutIcon}
|
className={`flex-shrink-0 pr3 dn ` + hidePopoutIcon}
|
||||||
src="/~link/img/popout.png"
|
src="/~link/img/popout.png"
|
||||||
height="16"
|
height="16"
|
||||||
width="16"/>
|
width="16"/>
|
||||||
|
52
pkg/interface/link/src/js/components/lib/member-element.js
Normal file
52
pkg/interface/link/src/js/components/lib/member-element.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { Sigil } from '/components/lib/icons/sigil';
|
||||||
|
import { uxToHex } from '/lib/util';
|
||||||
|
|
||||||
|
|
||||||
|
export class MemberElement extends Component {
|
||||||
|
|
||||||
|
onRemove() {
|
||||||
|
const { props } = this;
|
||||||
|
//TODO don't really need to use link-view here, but should we anyway?
|
||||||
|
api.groups.remove(props.groupPath, [`~${props.ship}`]);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { props } = this;
|
||||||
|
|
||||||
|
let actionElem;
|
||||||
|
if (props.ship === props.owner) {
|
||||||
|
actionElem = (
|
||||||
|
<p className="w-20 dib list-ship black white-d f8 c-default">
|
||||||
|
Host
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
} else if (props.amOwner && window.ship !== props.ship) {
|
||||||
|
actionElem = (
|
||||||
|
<a onClick={this.onRemove.bind(this)}
|
||||||
|
className="w-20 dib list-ship black white-d f8 pointer">
|
||||||
|
Ban
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
actionElem = (
|
||||||
|
<span></span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = !!props.contact
|
||||||
|
? `${props.contact.nickname} (~${props.ship})` : `~${props.ship}`;
|
||||||
|
let color = !!props.contact ? uxToHex(props.contact.color) : '000000';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex mb2">
|
||||||
|
<Sigil ship={props.ship} size={32} color={`#${color}`} />
|
||||||
|
<p className={
|
||||||
|
"w-70 mono list-ship dib v-mid black white-d ml2 nowrap f8"
|
||||||
|
}>{name}</p>
|
||||||
|
{actionElem}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,30 +1,31 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Route, Link } from 'react-router-dom';
|
import { Route, Link } from 'react-router-dom';
|
||||||
|
import { makeRoutePath } from '../../lib/util';
|
||||||
|
|
||||||
export class Pagination extends Component {
|
export class Pagination extends Component {
|
||||||
render() {
|
render() {
|
||||||
let props = this.props;
|
let props = this.props;
|
||||||
|
|
||||||
let prevPage = "/" + (Number(props.page) - 1);
|
let prevPage = (Number(props.page) - 1);
|
||||||
let nextPage = "/" + (Number(props.page) + 1);
|
let nextPage = (Number(props.page) + 1);
|
||||||
|
|
||||||
let prevDisplay = ((props.currentPage > 0))
|
let prevDisplay = ((props.currentPage > 0))
|
||||||
? "dib absolute left-0"
|
? "dib absolute left-0"
|
||||||
: "dn";
|
: "dn";
|
||||||
|
|
||||||
let nextDisplay = ((props.currentPage + 1) < props.totalPages)
|
let nextDisplay = ((props.currentPage + 1) < props.totalPages)
|
||||||
? "dib absolute right-0"
|
? "dib absolute right-0"
|
||||||
: "dn";
|
: "dn";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-100 inter relative pv6">
|
<div className="w-100 inter relative pv6">
|
||||||
<div className={prevDisplay + " inter f8"}>
|
<div className={prevDisplay + " inter f8"}>
|
||||||
<Link to={"/~link" + props.popout + props.groupPath + prevPage}>
|
<Link to={makeRoutePath(props.resourcePath, props.popout, prevPage)}>
|
||||||
<- Previous Page
|
<- Previous Page
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className={nextDisplay + " inter f8"}>
|
<div className={nextDisplay + " inter f8"}>
|
||||||
<Link to={"/~link" + props.popout + props.groupPath + nextPage}>
|
<Link to={makeRoutePath(props.resourcePath, props.popout, nextPage)}>
|
||||||
Next Page ->
|
Next Page ->
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
39
pkg/interface/link/src/js/components/lib/sidebar-invite.js
Normal file
39
pkg/interface/link/src/js/components/lib/sidebar-invite.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export class SidebarInvite extends Component {
|
||||||
|
|
||||||
|
onAccept() {
|
||||||
|
api.invite.accept(this.props.uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDecline() {
|
||||||
|
api.invite.decline(this.props.uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { props } = this;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-100 bg-transparent pa4 bb b--gray4 b--gray1-d'>
|
||||||
|
<div className='w-100 v-mid'>
|
||||||
|
<p className="dib f8 mono gray4-d">
|
||||||
|
{props.invite.text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
className="dib pointer pa2 f9 bg-green2 white mt4"
|
||||||
|
onClick={this.onAccept.bind(this)}>
|
||||||
|
Accept Invite
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="dib pointer ml4 pa2 f9 bg-black bg-gray0-d white mt4"
|
||||||
|
onClick={this.onDecline.bind(this)}>
|
||||||
|
Decline
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,8 @@ import { SidebarSwitcher } from '/components/lib/icons/icon-sidebar-switch.js';
|
|||||||
import { api } from '../api';
|
import { api } from '../api';
|
||||||
import { Route, Link } from 'react-router-dom';
|
import { Route, Link } from 'react-router-dom';
|
||||||
import { Comments } from './lib/comments';
|
import { Comments } from './lib/comments';
|
||||||
import { getContactDetails } from '../lib/util';
|
import { LoadingScreen } from './loading';
|
||||||
|
import { makeRoutePath, getContactDetails } from '../lib/util';
|
||||||
|
|
||||||
export class LinkDetail extends Component {
|
export class LinkDetail extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -29,7 +30,7 @@ export class LinkDetail extends Component {
|
|||||||
// if we have no preloaded data, and we aren't expecting it, get it
|
// if we have no preloaded data, and we aren't expecting it, get it
|
||||||
if (!this.state.data.title) {
|
if (!this.state.data.title) {
|
||||||
api.getSubmission(
|
api.getSubmission(
|
||||||
this.props.groupPath, this.props.url, this.updateData.bind(this)
|
this.props.resourcePath, this.props.url, this.updateData.bind(this)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,7 +47,7 @@ export class LinkDetail extends Component {
|
|||||||
api.setSpinner(true);
|
api.setSpinner(true);
|
||||||
|
|
||||||
api.postComment(
|
api.postComment(
|
||||||
this.props.groupPath,
|
this.props.resourcePath,
|
||||||
url,
|
url,
|
||||||
this.state.comment
|
this.state.comment
|
||||||
).then(() => {
|
).then(() => {
|
||||||
@ -62,9 +63,13 @@ export class LinkDetail extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
let props = this.props;
|
let props = this.props;
|
||||||
let popout = (props.popout) ? "/popout" : "";
|
|
||||||
|
|
||||||
const data = this.state.data || props.data;
|
const data = this.state.data || props.data;
|
||||||
|
|
||||||
|
if (!data.ship) {
|
||||||
|
return <LoadingScreen/>;
|
||||||
|
}
|
||||||
|
|
||||||
let ship = data.ship || "zod";
|
let ship = data.ship || "zod";
|
||||||
let title = data.title || "";
|
let title = data.title || "";
|
||||||
let url = data.url || "";
|
let url = data.url || "";
|
||||||
@ -98,10 +103,10 @@ export class LinkDetail extends Component {
|
|||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
className="dib f9 fw4 pt2 gray2 lh-solid"
|
className="dib f9 fw4 pt2 gray2 lh-solid"
|
||||||
to={"/~link" + popout + props.groupPath + "/" + props.page}>
|
to={makeRoutePath(props.resourcePath, props.popout, props.page)}>
|
||||||
{"<- Collection index"}
|
{`<- ${props.resource.title} index`}
|
||||||
</Link>
|
</Link>
|
||||||
<LinksTabBar {...props} popout={popout} groupPath={props.groupPath} />
|
<LinksTabBar {...props} popout={props.popout} resourcePath={props.resourcePath} />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-100 mt2 flex justify-center overflow-y-scroll ph4 pb4">
|
<div className="w-100 mt2 flex justify-center overflow-y-scroll ph4 pb4">
|
||||||
<div className="w-100 mw7">
|
<div className="w-100 mw7">
|
||||||
@ -111,7 +116,7 @@ export class LinkDetail extends Component {
|
|||||||
comments={comments}
|
comments={comments}
|
||||||
nickname={nickname}
|
nickname={nickname}
|
||||||
ship={ship}
|
ship={ship}
|
||||||
groupPath={props.groupPath}
|
resourcePath={props.resourcePath}
|
||||||
page={props.page}
|
page={props.page}
|
||||||
linkIndex={props.linkIndex}
|
linkIndex={props.linkIndex}
|
||||||
time={this.state.data.time}
|
time={this.state.data.time}
|
||||||
@ -143,8 +148,8 @@ export class LinkDetail extends Component {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Comments
|
<Comments
|
||||||
groupPath={props.groupPath}
|
resourcePath={props.resourcePath}
|
||||||
key={props.groupPath + props.commentPage}
|
key={props.resourcePath + props.commentPage}
|
||||||
comments={props.comments}
|
comments={props.comments}
|
||||||
commentPage={props.commentPage}
|
commentPage={props.commentPage}
|
||||||
contacts={props.contacts}
|
contacts={props.contacts}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import React, { Component } from 'react'
|
import React, { Component } from 'react'
|
||||||
|
import { LoadingScreen } from './loading';
|
||||||
import { LinksTabBar } from './lib/links-tabbar';
|
import { LinksTabBar } from './lib/links-tabbar';
|
||||||
import { SidebarSwitcher } from '/components/lib/icons/icon-sidebar-switch.js';
|
import { SidebarSwitcher } from '/components/lib/icons/icon-sidebar-switch.js';
|
||||||
import { Route, Link } from "react-router-dom";
|
import { Route, Link } from "react-router-dom";
|
||||||
@ -6,7 +7,7 @@ import { LinkItem } from '/components/lib/link-item.js';
|
|||||||
import { LinkSubmit } from '/components/lib/link-submit.js';
|
import { LinkSubmit } from '/components/lib/link-submit.js';
|
||||||
import { Pagination } from '/components/lib/pagination.js';
|
import { Pagination } from '/components/lib/pagination.js';
|
||||||
|
|
||||||
import { getContactDetails } from '../lib/util';
|
import { makeRoutePath, getContactDetails } from '../lib/util';
|
||||||
|
|
||||||
//TODO Avatar support once it's in
|
//TODO Avatar support once it's in
|
||||||
export class Links extends Component {
|
export class Links extends Component {
|
||||||
@ -25,17 +26,21 @@ export class Links extends Component {
|
|||||||
(!this.props.links[linkPage] ||
|
(!this.props.links[linkPage] ||
|
||||||
this.props.links.local[linkPage])
|
this.props.links.local[linkPage])
|
||||||
) {
|
) {
|
||||||
api.getPage(this.props.groupPath, this.props.page);
|
api.getPage(this.props.resourcePath, this.props.page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
markAllAsSeen() {
|
markAllAsSeen() {
|
||||||
api.seenLink(this.props.groupPath);
|
api.seenLink(this.props.resourcePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let props = this.props;
|
let props = this.props;
|
||||||
let popout = (props.popout) ? "/popout" : "";
|
|
||||||
|
if (!props.resource.title) {
|
||||||
|
return <LoadingScreen/>;
|
||||||
|
}
|
||||||
|
|
||||||
let linkPage = props.page;
|
let linkPage = props.page;
|
||||||
|
|
||||||
let links = !!props.links[linkPage]
|
let links = !!props.links[linkPage]
|
||||||
@ -77,8 +82,8 @@ export class Links extends Component {
|
|||||||
color={color}
|
color={color}
|
||||||
member={member}
|
member={member}
|
||||||
comments={commentCount}
|
comments={commentCount}
|
||||||
groupPath={props.groupPath}
|
resourcePath={props.resourcePath}
|
||||||
popout={popout}
|
popout={props.popout}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -89,36 +94,31 @@ export class Links extends Component {
|
|||||||
<div
|
<div
|
||||||
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
|
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
|
||||||
style={{ height: "1rem" }}>
|
style={{ height: "1rem" }}>
|
||||||
<Link to="/~link/">{"⟵ All Channels"}</Link>
|
<Link to="/~link">{"⟵ All Channels"}</Link>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`pl4 pt2 flex relative overflow-x-scroll
|
className={`pl4 pt2 flex relative overflow-x-scroll
|
||||||
overflow-x-auto-l overflow-x-auto-xl flex-shrink-0
|
overflow-x-auto-l overflow-x-auto-xl flex-shrink-0
|
||||||
bb bn-m bn-l bn-xl b--gray4 b--gray1-d bg-gray0-d`}
|
bb b--gray4 b--gray1-d bg-gray0-d`}
|
||||||
style={{ height: 48 }}>
|
style={{ height: 48 }}>
|
||||||
<SidebarSwitcher
|
<SidebarSwitcher
|
||||||
sidebarShown={props.sidebarShown}
|
sidebarShown={props.sidebarShown}
|
||||||
popout={props.popout}/>
|
popout={props.popout}/>
|
||||||
<Link to={`/~link` + popout + props.groupPath} className="pt2">
|
<Link to={makeRoutePath(props.resourcePath, props.popout, props.page)} className="pt2">
|
||||||
<h2
|
<h2 className={`dib f9 fw4 v-top`}>
|
||||||
className={`dib f9 fw4 v-top lh-solid` +
|
{props.resource.title}
|
||||||
(props.groupPath.includes("/~/")
|
|
||||||
? ""
|
|
||||||
: " mono")}>
|
|
||||||
{(props.groupPath.includes("/~/"))
|
|
||||||
? "Private"
|
|
||||||
: props.groupPath.substr(1)}
|
|
||||||
</h2>
|
</h2>
|
||||||
</Link>
|
</Link>
|
||||||
<LinksTabBar
|
<LinksTabBar
|
||||||
{...props}
|
{...props}
|
||||||
popout={popout}
|
popout={props.popout}
|
||||||
groupPath={props.groupPath + "/" + props.page}/>
|
page={props.page}
|
||||||
|
resourcePath={props.resourcePath}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-100 mt2 flex justify-center overflow-y-scroll ph4 pb4">
|
<div className="w-100 mt2 flex justify-center overflow-y-scroll ph4 pb4">
|
||||||
<div className="w-100 mw7">
|
<div className="w-100 mw7">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<LinkSubmit groupPath={props.groupPath}/>
|
<LinkSubmit resourcePath={props.resourcePath}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="pb4">
|
<div className="pb4">
|
||||||
<span
|
<span
|
||||||
@ -129,9 +129,9 @@ export class Links extends Component {
|
|||||||
{LinkList}
|
{LinkList}
|
||||||
<Pagination
|
<Pagination
|
||||||
{...props}
|
{...props}
|
||||||
key={props.groupPath + props.page}
|
key={props.resourcePath + props.page}
|
||||||
popout={popout}
|
popout={props.popout}
|
||||||
groupPath={props.groupPath}
|
resourcePath={props.resourcePath}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
/>
|
/>
|
||||||
|
15
pkg/interface/link/src/js/components/loading.js
Normal file
15
pkg/interface/link/src/js/components/loading.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
export class LoadingScreen extends Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="h-100 w-100 overflow-x-hidden flex flex-column bg-white bg-gray0-d dn db-ns">
|
||||||
|
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
|
||||||
|
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
|
||||||
|
Loading...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
106
pkg/interface/link/src/js/components/member.js
Normal file
106
pkg/interface/link/src/js/components/member.js
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { store } from '/store';
|
||||||
|
|
||||||
|
import urbitOb from 'urbit-ob';
|
||||||
|
import { LoadingScreen } from './loading';
|
||||||
|
import { LinksTabBar } from '/components/lib/links-tabbar';
|
||||||
|
import { MemberElement } from '/components/lib/member-element';
|
||||||
|
import { InviteElement } from '/components/lib/invite-element';
|
||||||
|
import { SidebarSwitcher } from '/components/lib/icons/icon-sidebar-switch.js';
|
||||||
|
import { makeRoutePath } from '/lib/util';
|
||||||
|
|
||||||
|
export class MemberScreen extends Component {
|
||||||
|
render() {
|
||||||
|
const { props, state } = this;
|
||||||
|
|
||||||
|
if (!props.groupPath) {
|
||||||
|
return <LoadingScreen/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isManaged = ('/~/' !== props.groupPath.slice(0,3));
|
||||||
|
|
||||||
|
let members = Array.from(props.group).map((mem) => {
|
||||||
|
let contact = (mem in props.contactDetails)
|
||||||
|
? props.contactDetails[mem] : false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MemberElement
|
||||||
|
key={mem}
|
||||||
|
amOwner={props.amOwner}
|
||||||
|
contact={contact}
|
||||||
|
ship={mem}
|
||||||
|
groupPath={props.groupPath}
|
||||||
|
resourcePath={props.resourcePath}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-100 w-100 overflow-x-hidden flex flex-column white-d">
|
||||||
|
<div
|
||||||
|
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
|
||||||
|
style={{ height: "1rem" }}>
|
||||||
|
<Link to="/~link">{"⟵ All Collections"}</Link>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`pl4 pt2 bb b--gray4 b--gray1-d bg-gray0-d flex relative
|
||||||
|
overflow-x-scroll overflow-x-auto-l overflow-x-auto-xl flex-shrink-0`}
|
||||||
|
style={{ height: 48 }}>
|
||||||
|
<SidebarSwitcher
|
||||||
|
sidebarShown={this.props.sidebarShown}
|
||||||
|
popout={this.props.popout}
|
||||||
|
/>
|
||||||
|
<Link to={makeRoutePath(props.resourcePath, props.popout)}
|
||||||
|
className="pt2 white-d">
|
||||||
|
<h2
|
||||||
|
className="dib f9 fw4 v-top"
|
||||||
|
style={{ width: "max-content" }}>
|
||||||
|
{props.resource.title}
|
||||||
|
</h2>
|
||||||
|
</Link>
|
||||||
|
<LinksTabBar
|
||||||
|
{...props}
|
||||||
|
groupPath={props.groupPath}
|
||||||
|
resourcePath={props.resourcePath}
|
||||||
|
amOwner={props.amOwner}
|
||||||
|
popout={props.popout}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-100 pl3 mt0 mt4-m mt4-l mt4-xl cf pr6">
|
||||||
|
<div className="w-100 w-50-l w-50-xl fl pa2 pr3 pt3 pt0-l pt0-xl">
|
||||||
|
<p className="f8 pb2">Members</p>
|
||||||
|
<p className="f9 gray2 mb3">
|
||||||
|
{ 'Everyone with permission to use this collection.' +
|
||||||
|
((isManaged && props.amOwner)
|
||||||
|
? ' Removing someone removes them from the group.'
|
||||||
|
: '')
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
{members}
|
||||||
|
</div>
|
||||||
|
{ !props.amOwner ? null : (
|
||||||
|
<div className="w-100 w-50-l w-50-xl fl pa2 pr3 pt3 pt0-l pt0-xl">
|
||||||
|
<p className="f8 pb2">Modify Permissions</p>
|
||||||
|
<p className="f9 gray2 mb3">
|
||||||
|
{ 'Invite someone to this collection.' +
|
||||||
|
(isManaged
|
||||||
|
? ' Adding someone adds them to the group.'
|
||||||
|
: '')
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
<InviteElement
|
||||||
|
groupPath={props.groupPath}
|
||||||
|
resourcePath={props.resourcePath}
|
||||||
|
permissions={props.permission}
|
||||||
|
contacts={props.contacts}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
239
pkg/interface/link/src/js/components/new.js
Normal file
239
pkg/interface/link/src/js/components/new.js
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { InviteSearch } from './lib/invite-search';
|
||||||
|
import { Route, Link } from 'react-router-dom';
|
||||||
|
import { makeRoutePath, isPatTa, deSig } from '/lib/util';
|
||||||
|
import urbitOb from 'urbit-ob';
|
||||||
|
|
||||||
|
export class NewScreen extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
idName: '',
|
||||||
|
groups: [],
|
||||||
|
ships: [],
|
||||||
|
idError: false,
|
||||||
|
inviteError: false,
|
||||||
|
createGroup: true
|
||||||
|
};
|
||||||
|
|
||||||
|
this.titleChange = this.titleChange.bind(this);
|
||||||
|
this.descriptionChange = this.descriptionChange.bind(this);
|
||||||
|
this.setInvite = this.setInvite.bind(this);
|
||||||
|
this.createGroupChange = this.createGroupChange.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
const { props, state } = this;
|
||||||
|
|
||||||
|
if (prevProps !== props) {
|
||||||
|
let target = `/${state.idName}`;
|
||||||
|
if (target in props.resources) {
|
||||||
|
props.history.push(makeRoutePath(target));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
titleChange(event) {
|
||||||
|
let asciiSafe = event.target.value.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9-]/g, "-");
|
||||||
|
this.setState({
|
||||||
|
idName: asciiSafe + '-' + Math.floor(Math.random()*10000), // uniqueness
|
||||||
|
title: event.target.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptionChange(event) {
|
||||||
|
this.setState({
|
||||||
|
description: event.target.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setInvite(value) {
|
||||||
|
this.setState({
|
||||||
|
groups: value.groups,
|
||||||
|
ships: value.ships
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createGroupChange(event) {
|
||||||
|
this.setState({
|
||||||
|
createGroup: !!event.target.checked,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onClickCreate() {
|
||||||
|
const { props, state } = this;
|
||||||
|
|
||||||
|
if (!state.title) {
|
||||||
|
this.setState({
|
||||||
|
idError: true,
|
||||||
|
inviteError: false
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let appPath = `/${state.idName}`;
|
||||||
|
|
||||||
|
if (appPath in props.resources) {
|
||||||
|
this.setState({
|
||||||
|
inviteError: false,
|
||||||
|
idError: true,
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isValid = true;
|
||||||
|
let aud = state.ships.map(mem => `~${deSig(mem.trim())}`);
|
||||||
|
aud.forEach((mem) => {
|
||||||
|
if (!urbitOb.isValidPatp(mem)) {
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
this.setState({
|
||||||
|
inviteError: true,
|
||||||
|
idError: false,
|
||||||
|
success: false
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = state.groups.length === 0
|
||||||
|
? {ships: aud}
|
||||||
|
: {group: state.groups[0]};
|
||||||
|
|
||||||
|
if (this.textarea) {
|
||||||
|
this.textarea.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
error: false,
|
||||||
|
success: true,
|
||||||
|
group: [],
|
||||||
|
ships: []
|
||||||
|
}, () => {
|
||||||
|
api.setSpinner(true);
|
||||||
|
let submit = api.createCollection(
|
||||||
|
appPath,
|
||||||
|
state.title,
|
||||||
|
state.description,
|
||||||
|
target,
|
||||||
|
state.createGroup
|
||||||
|
);
|
||||||
|
submit.then(() => {
|
||||||
|
api.setSpinner(false);
|
||||||
|
props.history.push(makeRoutePath(appPath));
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { props, state } = this;
|
||||||
|
|
||||||
|
let createGroupClasses = state.createGroup
|
||||||
|
? "relative checked bg-green2 br3 h1 toggle v-mid z-0"
|
||||||
|
: "relative bg-gray4 bg-gray1-d br3 h1 toggle v-mid z-0";
|
||||||
|
|
||||||
|
let createClasses = !!state.idName
|
||||||
|
? "pointer db f9 mt7 green2 bg-gray0-d ba pv3 ph4 b--green2"
|
||||||
|
: "pointer db f9 mt7 gray2 ba bg-gray0-d pa2 pv3 ph4 b--gray3";
|
||||||
|
|
||||||
|
let idClasses =
|
||||||
|
"f7 ba b--gray3 b--gray2-d bg-gray0-d white-d pa3 db w-100 " +
|
||||||
|
"focus-b--black focus-b--white-d ";
|
||||||
|
|
||||||
|
let idErrElem = (<span />);
|
||||||
|
if (state.idError) {
|
||||||
|
idErrElem = (
|
||||||
|
<span className="f9 inter red2 db pt2">
|
||||||
|
Collection must have a valid name.
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let createGroupToggle = <div/>
|
||||||
|
if (state.groups.length === 0) {
|
||||||
|
createGroupToggle = (
|
||||||
|
<div className="mt7">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
style={{ WebkitAppearance: "none", width: 28 }}
|
||||||
|
className={createGroupClasses}
|
||||||
|
onChange={this.createGroupChange}
|
||||||
|
/>
|
||||||
|
<span className="dib f9 white-d inter ml3">Create Group</span>
|
||||||
|
<p className="f9 gray2 pt1" style={{ paddingLeft: 40 }}>
|
||||||
|
Participants will share this group across applications
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"h-100 w-100 mw6 pa3 pt4 overflow-x-hidden " +
|
||||||
|
"bg-gray0-d white-d flex flex-column"
|
||||||
|
}>
|
||||||
|
<div className="w-100 dn-m dn-l dn-xl inter pt1 pb6 f8">
|
||||||
|
<Link to="/~link">{"⟵ All Collections"}</Link>
|
||||||
|
</div>
|
||||||
|
<h2 className="mb3 f8">New Collection</h2>
|
||||||
|
<div className="w-100">
|
||||||
|
<p className="f8 mt3 lh-copy db">Name</p>
|
||||||
|
<textarea
|
||||||
|
className={idClasses}
|
||||||
|
placeholder="Cool Collection"
|
||||||
|
rows={1}
|
||||||
|
style={{
|
||||||
|
resize: "none"
|
||||||
|
}}
|
||||||
|
onChange={this.titleChange}
|
||||||
|
/>
|
||||||
|
{idErrElem}
|
||||||
|
<p className="f8 mt3 lh-copy db">
|
||||||
|
Description
|
||||||
|
<span className="gray3"> (Optional)</span>
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
className={idClasses}
|
||||||
|
placeholder="The hippest links"
|
||||||
|
rows={1}
|
||||||
|
style={{
|
||||||
|
resize: "none"
|
||||||
|
}}
|
||||||
|
onChange={this.descriptionChange}
|
||||||
|
/>
|
||||||
|
<p className="f8 mt4 lh-copy db">
|
||||||
|
Invite
|
||||||
|
<span className="gray3"> (Optional)</span>
|
||||||
|
</p>
|
||||||
|
<p className="f9 gray2 db mb2 pt1">
|
||||||
|
Selected entities will be able to post to the collection
|
||||||
|
</p>
|
||||||
|
<InviteSearch
|
||||||
|
groups={props.groups}
|
||||||
|
contacts={props.contacts}
|
||||||
|
groupResults={true}
|
||||||
|
invites={{
|
||||||
|
groups: state.groups,
|
||||||
|
ships: state.ships
|
||||||
|
}}
|
||||||
|
setInvite={this.setInvite}
|
||||||
|
/>
|
||||||
|
{createGroupToggle}
|
||||||
|
<button
|
||||||
|
onClick={this.onClickCreate.bind(this)}
|
||||||
|
className={createClasses}>
|
||||||
|
Create Collection
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { BrowserRouter, Route, Link } from "react-router-dom";
|
import { BrowserRouter, Switch, Route, Link } from "react-router-dom";
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
@ -7,10 +7,17 @@ import { api } from '/api';
|
|||||||
import { subscription } from '/subscription';
|
import { subscription } from '/subscription';
|
||||||
import { store } from '/store';
|
import { store } from '/store';
|
||||||
import { Skeleton } from '/components/skeleton';
|
import { Skeleton } from '/components/skeleton';
|
||||||
|
import { NewScreen } from '/components/new';
|
||||||
|
import { MemberScreen } from '/components/member';
|
||||||
|
import { SettingsScreen } from '/components/settings';
|
||||||
import { Links } from '/components/links-list';
|
import { Links } from '/components/links-list';
|
||||||
import { LinkDetail } from '/components/link';
|
import { LinkDetail } from '/components/link';
|
||||||
import { base64urlDecode } from '../lib/util';
|
import { makeRoutePath, amOwnerOfGroup, base64urlDecode } from '../lib/util';
|
||||||
|
|
||||||
|
//NOTE route paths make the assumption that a resource identifier is always
|
||||||
|
// just a single /path element. technically, backend supports /longer/paths
|
||||||
|
// but no tlon-sanctioned frontend creates those right now, so we're opting
|
||||||
|
// out of supporting them completely for the time being.
|
||||||
|
|
||||||
export class Root extends Component {
|
export class Root extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -26,62 +33,169 @@ export class Root extends Component {
|
|||||||
let contacts = !!state.contacts ? state.contacts : {};
|
let contacts = !!state.contacts ? state.contacts : {};
|
||||||
const groups = !!state.groups ? state.groups : {};
|
const groups = !!state.groups ? state.groups : {};
|
||||||
|
|
||||||
|
const resources = !!state.resources ? state.resources : {};
|
||||||
let links = !!state.links ? state.links : {};
|
let links = !!state.links ? state.links : {};
|
||||||
let comments = !!state.comments ? state.comments : {};
|
let comments = !!state.comments ? state.comments : {};
|
||||||
const seen = !!state.seen ? state.seen : {};
|
const seen = !!state.seen ? state.seen : {};
|
||||||
|
|
||||||
|
const invites = '/link' in state.invites ?
|
||||||
|
state.invites['/link'] : {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter><Switch>
|
||||||
<Route exact path="/~link"
|
<Route exact path="/~link"
|
||||||
render={ (props) => {
|
render={ (props) => {
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
active="channels"
|
active="collections"
|
||||||
spinner={state.spinner}
|
spinner={state.spinner}
|
||||||
|
resources={resources}
|
||||||
|
invites={invites}
|
||||||
groups={groups}
|
groups={groups}
|
||||||
rightPanelHide={true}
|
rightPanelHide={true}
|
||||||
sidebarShown={true}
|
sidebarShown={state.sidebarShown}
|
||||||
links={links}>
|
links={links}>
|
||||||
<div className="h-100 w-100 overflow-x-hidden flex flex-column bg-white bg-gray0-d dn db-ns">
|
<div className="h-100 w-100 overflow-x-hidden flex flex-column bg-white bg-gray0-d dn db-ns">
|
||||||
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
|
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
|
||||||
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
|
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
|
||||||
Collections are shared across groups. To create a new collection, <a className="black white-d" href="/~contacts">create a group</a>.
|
Select or create a collection to begin.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
);
|
);
|
||||||
}} />
|
}} />
|
||||||
<Route exact path="/~link/(popout)?/:ship/:channel/:page?"
|
<Route exact path="/~link/new"
|
||||||
render={ (props) => {
|
render={(props) => {
|
||||||
// groups/contacts and link channels are the same thing in ver 1
|
return (
|
||||||
|
<Skeleton
|
||||||
|
spinner={state.spinner}
|
||||||
|
resources={resources}
|
||||||
|
invites={invites}
|
||||||
|
groups={groups}
|
||||||
|
rightPanelHide={true}
|
||||||
|
sidebarShown={state.sidebarShown}
|
||||||
|
links={links}>
|
||||||
|
<NewScreen
|
||||||
|
resources={resources}
|
||||||
|
groups={groups}
|
||||||
|
contacts={contacts}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Skeleton>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Route exact path="/~link/join/:resource"
|
||||||
|
render={ (props) => {
|
||||||
|
const resourcePath = '/' + props.match.params.resource;
|
||||||
|
props.history.push(makeRoutePath(resourcePath));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Route exact path="/~link/(popout)?/:resource/members"
|
||||||
|
render={(props) => {
|
||||||
|
const popout = props.match.url.includes("/popout/");
|
||||||
|
const resourcePath = '/' + props.match.params.resource;
|
||||||
|
const resource = resources[resourcePath] || {};
|
||||||
|
|
||||||
let groupPath =
|
const contactDetails = contacts[resource.group] || {};
|
||||||
`/${props.match.params.ship}/${props.match.params.channel}`;
|
const group = groups[resource.group] || new Set([]);
|
||||||
let contactDetails = contacts[groupPath] || {};
|
const amOwner = amOwnerOfGroup(resource.group);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Skeleton
|
||||||
|
spinner={state.spinner}
|
||||||
|
resources={resources}
|
||||||
|
invites={invites}
|
||||||
|
groups={groups}
|
||||||
|
selected={resourcePath}
|
||||||
|
rightPanelHide={true}
|
||||||
|
sidebarShown={state.sidebarShown}
|
||||||
|
links={links}>
|
||||||
|
<MemberScreen
|
||||||
|
sidebarShown={state.sidebarShown}
|
||||||
|
resource={resource}
|
||||||
|
contacts={contacts}
|
||||||
|
contactDetails={contactDetails}
|
||||||
|
groupPath={resource.group}
|
||||||
|
group={group}
|
||||||
|
amOwner={amOwner}
|
||||||
|
resourcePath={resourcePath}
|
||||||
|
popout={popout}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Skeleton>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Route exact path="/~link/(popout)?/:resource/settings"
|
||||||
|
render={ (props) => {
|
||||||
|
const popout = props.match.url.includes("/popout/");
|
||||||
|
const resourcePath = '/' + props.match.params.resource;
|
||||||
|
const resource = resources[resourcePath] || false;
|
||||||
|
|
||||||
|
const contactDetails = contacts[resource.group] || {};
|
||||||
|
const group = groups[resource.group] || new Set([]);
|
||||||
|
const amOwner = amOwnerOfGroup(resource.group);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Skeleton
|
||||||
|
spinner={state.spinner}
|
||||||
|
resources={resources}
|
||||||
|
invites={invites}
|
||||||
|
groups={groups}
|
||||||
|
selected={resourcePath}
|
||||||
|
rightPanelHide={true}
|
||||||
|
sidebarShown={state.sidebarShown}
|
||||||
|
links={links}>
|
||||||
|
<SettingsScreen
|
||||||
|
sidebarShown={state.sidebarShown}
|
||||||
|
resource={resource}
|
||||||
|
contacts={contacts}
|
||||||
|
contactDetails={contactDetails}
|
||||||
|
groupPath={resource.group}
|
||||||
|
group={group}
|
||||||
|
amOwner={amOwner}
|
||||||
|
resourcePath={resourcePath}
|
||||||
|
popout={popout}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Skeleton>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Route exact path="/~link/(popout)?/:resource/:page?"
|
||||||
|
render={ (props) => {
|
||||||
|
const resourcePath = '/' + props.match.params.resource;
|
||||||
|
const resource = resources[resourcePath] || {};
|
||||||
|
|
||||||
|
const amOwner = amOwnerOfGroup(resource.group);
|
||||||
|
|
||||||
|
let contactDetails = contacts[resource.group] || {};
|
||||||
|
|
||||||
let page = props.match.params.page || 0;
|
let page = props.match.params.page || 0;
|
||||||
|
|
||||||
let popout = props.match.url.includes("/popout/");
|
let popout = props.match.url.includes("/popout/");
|
||||||
|
|
||||||
let channelLinks = !!links[groupPath]
|
let channelLinks = !!links[resourcePath]
|
||||||
? links[groupPath]
|
? links[resourcePath]
|
||||||
: {local: {}};
|
: {local: {}};
|
||||||
|
|
||||||
let channelComments = !!comments[groupPath]
|
let channelComments = !!comments[resourcePath]
|
||||||
? comments[groupPath]
|
? comments[resourcePath]
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
const channelSeen = !!seen[groupPath]
|
const channelSeen = !!seen[resourcePath]
|
||||||
? seen[groupPath]
|
? seen[resourcePath]
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
spinner={state.spinner}
|
spinner={state.spinner}
|
||||||
|
resources={resources}
|
||||||
|
invites={invites}
|
||||||
groups={groups}
|
groups={groups}
|
||||||
active="links"
|
selected={resourcePath}
|
||||||
selected={groupPath}
|
|
||||||
sidebarShown={state.sidebarShown}
|
sidebarShown={state.sidebarShown}
|
||||||
sidebarHideMobile={true}
|
sidebarHideMobile={true}
|
||||||
popout={popout}
|
popout={popout}
|
||||||
@ -93,7 +207,9 @@ export class Root extends Component {
|
|||||||
comments={channelComments}
|
comments={channelComments}
|
||||||
seen={channelSeen}
|
seen={channelSeen}
|
||||||
page={page}
|
page={page}
|
||||||
groupPath={groupPath}
|
resourcePath={resourcePath}
|
||||||
|
resource={resource}
|
||||||
|
amOwner={amOwner}
|
||||||
popout={popout}
|
popout={popout}
|
||||||
sidebarShown={state.sidebarShown}
|
sidebarShown={state.sidebarShown}
|
||||||
/>
|
/>
|
||||||
@ -101,47 +217,53 @@ export class Root extends Component {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Route exact path="/~link/(popout)?/:ship/:channel/:page/:index/:encodedUrl/(comments)?/:commentpage?"
|
<Route exact path="/~link/(popout)?/:resource/:page/:index/:encodedUrl/:commentpage?"
|
||||||
render={ (props) => {
|
render={ (props) => {
|
||||||
let groupPath =
|
const resourcePath = '/' + props.match.params.resource;
|
||||||
`/${props.match.params.ship}/${props.match.params.channel}`;
|
const resource = resources[resourcePath] || {};
|
||||||
|
|
||||||
|
const amOwner = amOwnerOfGroup(resource.group);
|
||||||
|
|
||||||
let popout = props.match.url.includes("/popout/");
|
let popout = props.match.url.includes("/popout/");
|
||||||
|
|
||||||
let contactDetails = contacts[groupPath] || {};
|
let contactDetails = contacts[resource.group] || {};
|
||||||
|
|
||||||
let index = props.match.params.index || 0;
|
let index = props.match.params.index || 0;
|
||||||
let page = props.match.params.page || 0;
|
let page = props.match.params.page || 0;
|
||||||
let url = base64urlDecode(props.match.params.encodedUrl);
|
let url = base64urlDecode(props.match.params.encodedUrl);
|
||||||
|
|
||||||
let data = !!links[groupPath]
|
let data = !!links[resourcePath]
|
||||||
? !!links[groupPath][page]
|
? !!links[resourcePath][page]
|
||||||
? links[groupPath][page][index]
|
? links[resourcePath][page][index]
|
||||||
: {}
|
: {}
|
||||||
: {};
|
: {};
|
||||||
let coms = !comments[groupPath]
|
let coms = !comments[resourcePath]
|
||||||
? undefined
|
? undefined
|
||||||
: comments[groupPath][url];
|
: comments[resourcePath][url];
|
||||||
|
|
||||||
let commentPage = props.match.params.commentpage || 0;
|
let commentPage = props.match.params.commentpage || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
spinner={state.spinner}
|
spinner={state.spinner}
|
||||||
|
resources={resources}
|
||||||
|
invites={invites}
|
||||||
groups={groups}
|
groups={groups}
|
||||||
active="links"
|
selected={resourcePath}
|
||||||
selected={groupPath}
|
|
||||||
sidebarShown={state.sidebarShown}
|
sidebarShown={state.sidebarShown}
|
||||||
sidebarHideMobile={true}
|
sidebarHideMobile={true}
|
||||||
popout={popout}
|
popout={popout}
|
||||||
links={links}>
|
links={links}>
|
||||||
<LinkDetail
|
<LinkDetail
|
||||||
{...props}
|
{...props}
|
||||||
|
resource={resource}
|
||||||
page={page}
|
page={page}
|
||||||
url={url}
|
url={url}
|
||||||
linkIndex={index}
|
linkIndex={index}
|
||||||
contacts={contactDetails}
|
contacts={contactDetails}
|
||||||
groupPath={groupPath}
|
resourcePath={resourcePath}
|
||||||
|
groupPath={resource.group}
|
||||||
|
amOwner={amOwner}
|
||||||
popout={popout}
|
popout={popout}
|
||||||
sidebarShown={state.sidebarShown}
|
sidebarShown={state.sidebarShown}
|
||||||
data={data}
|
data={data}
|
||||||
@ -152,7 +274,7 @@ export class Root extends Component {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</BrowserRouter>
|
</Switch></BrowserRouter>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
299
pkg/interface/link/src/js/components/settings.js
Normal file
299
pkg/interface/link/src/js/components/settings.js
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { deSig, uxToHex } from '/lib/util';
|
||||||
|
import { Route, Link } from "react-router-dom";
|
||||||
|
|
||||||
|
import { LoadingScreen } from './loading';
|
||||||
|
import { LinksTabBar } from '/components/lib/links-tabbar';
|
||||||
|
import SidebarSwitcher from './lib/icons/icon-sidebar-switch';
|
||||||
|
import { makeRoutePath } from '../lib/util';
|
||||||
|
|
||||||
|
export class SettingsScreen extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isLoading: false,
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
color: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
this.changeTitle = this.changeTitle.bind(this);
|
||||||
|
this.changeDescription = this.changeDescription.bind(this);
|
||||||
|
this.changeColor = this.changeColor.bind(this);
|
||||||
|
this.renderDelete = this.renderDelete.bind(this);
|
||||||
|
this.renderMetadataSettings = this.renderMetadataSettings.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (this.props.resource) {
|
||||||
|
this.setState({
|
||||||
|
title: this.props.resource.title,
|
||||||
|
description: this.props.resource.description,
|
||||||
|
color: uxToHex(this.props.resource.color || '0x0')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
const { props, state } = this;
|
||||||
|
if (!!state.isLoading && !props.resource) {
|
||||||
|
this.setState({
|
||||||
|
isLoading: false
|
||||||
|
}, () => {
|
||||||
|
api.setSpinner(false);
|
||||||
|
props.history.push('/~link');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.resource && (prevProps !== props)) {
|
||||||
|
this.setState({
|
||||||
|
title: props.resource.title,
|
||||||
|
description: props.resource.description,
|
||||||
|
color: uxToHex(props.resource.color || '0x0')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changeTitle() {
|
||||||
|
this.setState({title: event.target.value})
|
||||||
|
}
|
||||||
|
|
||||||
|
changeDescription() {
|
||||||
|
this.setState({description: event.target.value});
|
||||||
|
}
|
||||||
|
|
||||||
|
changeColor() {
|
||||||
|
this.setState({color: event.target.value});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteCollection() {
|
||||||
|
const { props, state } = this;
|
||||||
|
|
||||||
|
api.deleteCollection(props.resourcePath);
|
||||||
|
api.setSpinner(true);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
isLoading: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderDelete() {
|
||||||
|
const { props, state } = this;
|
||||||
|
|
||||||
|
const isManaged = ('/~/' !== props.groupPath.slice(0,3));
|
||||||
|
|
||||||
|
let deleteButtonClasses = (props.amOwner) ? 'b--red2 red2 pointer bg-gray0-d' : 'b--grey3 grey3 bg-gray0-d c-default';
|
||||||
|
let leaveButtonClasses = (!props.amOwner) ? "pointer" : "c-default";
|
||||||
|
|
||||||
|
let deleteClasses = 'dib f9 black gray4-d bg-gray0-d ba pa2 b--black b--gray0-d pointer';
|
||||||
|
let deleteText = 'Remove this collection from your collection list.';
|
||||||
|
let deleteAction = 'Remove';
|
||||||
|
if (props.amOwner && isManaged) {
|
||||||
|
deleteText = 'Delete this collection. (All group members will no longer see this chat.)';
|
||||||
|
deleteAction = 'Delete';
|
||||||
|
deleteClasses = 'dib f9 ba pa2 b--red2 red2 pointer bg-gray0-d';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-100 fl mt3">
|
||||||
|
<p className="f8 mt3 lh-copy db">Delete Collection</p>
|
||||||
|
<p className="f9 gray2 db mb4">{deleteText}</p>
|
||||||
|
<a onClick={this.deleteCollection.bind(this)}
|
||||||
|
className={deleteClasses}>{deleteAction + ' collection'}</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMetadataSettings() {
|
||||||
|
const { props, state } = this;
|
||||||
|
const { resource } = props;
|
||||||
|
|
||||||
|
return(
|
||||||
|
<div>
|
||||||
|
<div className={"w-100 pb6 fl mt3 " + ((props.amOwner) ? '' : 'o-30')}>
|
||||||
|
<p className="f8 mt3 lh-copy">Rename</p>
|
||||||
|
<p className="f9 gray2 db mb4">Change the name of this collection</p>
|
||||||
|
<div className="relative w-100 flex"
|
||||||
|
style={{maxWidth: "29rem"}}>
|
||||||
|
<input
|
||||||
|
className={"f8 ba b--gray3 b--gray2-d bg-gray0-d white-d " +
|
||||||
|
"focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3"}
|
||||||
|
value={this.state.title}
|
||||||
|
disabled={!props.amOwner}
|
||||||
|
onChange={this.changeTitle}
|
||||||
|
/>
|
||||||
|
<span className={"f8 absolute pa3 inter " +
|
||||||
|
((props.amOwner) ? "pointer" : "")}
|
||||||
|
style={{ right: 12, top: 1 }}
|
||||||
|
ref="rename"
|
||||||
|
onClick={() => {
|
||||||
|
if (props.amOwner) {
|
||||||
|
api.setSpinner(true);
|
||||||
|
api.metadataAdd(
|
||||||
|
props.resourcePath,
|
||||||
|
props.groupPath,
|
||||||
|
state.title,
|
||||||
|
props.resource.description,
|
||||||
|
props.resource['date-created'],
|
||||||
|
uxToHex(props.resource.color)
|
||||||
|
).then(() => {
|
||||||
|
api.setSpinner(false);
|
||||||
|
this.refs.rename.innerText = "Saved";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
Save
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="f8 mt3 lh-copy">Change description</p>
|
||||||
|
<p className="f9 gray2 db mb4">
|
||||||
|
Change the description of this collection
|
||||||
|
</p>
|
||||||
|
<div className="relative w-100 flex"
|
||||||
|
style={{ maxWidth: "29rem" }}>
|
||||||
|
<input
|
||||||
|
className={"f8 ba b--gray3 b--gray2-d bg-gray0-d white-d " +
|
||||||
|
"focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3"}
|
||||||
|
value={this.state.description}
|
||||||
|
disabled={!props.amOwner}
|
||||||
|
onChange={this.changeDescription}
|
||||||
|
/>
|
||||||
|
<span className={"f8 absolute pa3 inter " +
|
||||||
|
((props.amOwner) ? "pointer" : "")}
|
||||||
|
style={{ right: 12, top: 1 }}
|
||||||
|
ref="description"
|
||||||
|
onClick={() => {
|
||||||
|
if (props.amOwner) {
|
||||||
|
api.setSpinner(true);
|
||||||
|
api.metadataAdd(
|
||||||
|
props.resourcePath,
|
||||||
|
props.groupPath,
|
||||||
|
props.resource.title,
|
||||||
|
state.description,
|
||||||
|
props.resource['date-created'],
|
||||||
|
uxToHex(props.resource.color)
|
||||||
|
).then(() => {
|
||||||
|
api.setSpinner(false);
|
||||||
|
this.refs.description.innerText = "Saved";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
Save
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="f8 mt3 lh-copy">Change color</p>
|
||||||
|
<p className="f9 gray2 db mb4">Give this collection a color when viewing group channels</p>
|
||||||
|
<div className="relative w-100 flex"
|
||||||
|
style={{ maxWidth: "20rem" }}>
|
||||||
|
<input
|
||||||
|
className={"f8 ba b--gray3 b--gray2-d bg-gray0-d white-d " +
|
||||||
|
"focus-b--black focus-b--white-d pa3 db w-100 flex-auto mr3"}
|
||||||
|
value={this.state.color}
|
||||||
|
disabled={!props.amOwner}
|
||||||
|
onChange={this.changeColor}
|
||||||
|
/>
|
||||||
|
<span className={"f8 absolute pa3 inter " +
|
||||||
|
((props.amOwner) ? "pointer" : "")}
|
||||||
|
style={{ right: 12, top: 1 }}
|
||||||
|
ref="color"
|
||||||
|
onClick={() => {
|
||||||
|
if (props.amOwner && state.color.match(/[0-9A-F]{6}/i)) {
|
||||||
|
api.setSpinner(true);
|
||||||
|
api.metadataAdd(
|
||||||
|
props.resourcePath,
|
||||||
|
props.groupPath,
|
||||||
|
props.resource.title,
|
||||||
|
props.resource.description,
|
||||||
|
props.resource['date-created'],
|
||||||
|
state.color
|
||||||
|
).then(() => {
|
||||||
|
api.setSpinner(false);
|
||||||
|
this.refs.color.innerText = "Saved";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
Save
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { props, state } = this;
|
||||||
|
const isinPopout = this.props.popout ? "popout/" : "";
|
||||||
|
|
||||||
|
let writeGroup = Array.from(props.group.values());
|
||||||
|
|
||||||
|
if (props.groupPath === undefined) {
|
||||||
|
return <LoadingScreen/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!state.isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="h-100 w-100 overflow-x-hidden flex flex-column white-d">
|
||||||
|
<div
|
||||||
|
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
|
||||||
|
style={{ height: "1rem" }}>
|
||||||
|
<Link to="/~link">{"⟵ All Collections"}</Link>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="pl4 pt2 bb b--gray4 b--gray2-d bg-gray0-d flex relative overflow-x-scroll overflow-x-auto-l overflow-x-auto-xl flex-shrink-0"
|
||||||
|
style={{ height: 48 }}>
|
||||||
|
<SidebarSwitcher
|
||||||
|
sidebarShown={this.props.sidebarShown}
|
||||||
|
popout={this.props.popout}
|
||||||
|
/>
|
||||||
|
<Link to={makeRoutePath(props.resourcePath, props.popout)}
|
||||||
|
className="pt2 white-d">
|
||||||
|
<h2
|
||||||
|
className="dib f9 fw4 v-top"
|
||||||
|
style={{ width: "max-content" }}>
|
||||||
|
{props.resource.title}
|
||||||
|
</h2>
|
||||||
|
</Link>
|
||||||
|
<LinksTabBar {...props}/>
|
||||||
|
</div>
|
||||||
|
<div className="w-100 pl3 mt4 cf">
|
||||||
|
<h2 className="f8 pb2">Removing...</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-100 w-100 overflow-x-hidden flex flex-column white-d">
|
||||||
|
<div
|
||||||
|
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
|
||||||
|
style={{ height: "1rem" }}>
|
||||||
|
<Link to="/~link">{"⟵ All Collections"}</Link>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="pl4 pt2 bb b--gray4 b--gray1-d flex relative overflow-x-scroll overflow-x-auto-l overflow-x-auto-xl flex-shrink-0"
|
||||||
|
style={{ height: 48 }}>
|
||||||
|
<SidebarSwitcher
|
||||||
|
sidebarShown={this.props.sidebarShown}
|
||||||
|
popout={this.props.popout}
|
||||||
|
/>
|
||||||
|
<Link to={makeRoutePath(props.resourcePath, props.popout)}
|
||||||
|
className="pt2">
|
||||||
|
<h2
|
||||||
|
className="dib f9 fw4 v-top"
|
||||||
|
style={{ width: "max-content" }}>
|
||||||
|
{props.resource.title}
|
||||||
|
</h2>
|
||||||
|
</Link>
|
||||||
|
<LinksTabBar {...props}/>
|
||||||
|
</div>
|
||||||
|
<div className="w-100 pl3 mt4 cf">
|
||||||
|
<h2 className="f8 pb2">Collection Settings</h2>
|
||||||
|
{this.renderDelete()}
|
||||||
|
{this.renderMetadataSettings()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -24,9 +24,11 @@ export class Skeleton extends Component {
|
|||||||
<HeaderBar spinner={this.props.spinner} />
|
<HeaderBar spinner={this.props.spinner} />
|
||||||
<div className={`cf w-100 h-100 flex ` + popoutBorder}>
|
<div className={`cf w-100 h-100 flex ` + popoutBorder}>
|
||||||
<ChannelsSidebar
|
<ChannelsSidebar
|
||||||
popout={popout}
|
|
||||||
groups={this.props.groups}
|
|
||||||
active={this.props.active}
|
active={this.props.active}
|
||||||
|
popout={popout}
|
||||||
|
resources={this.props.resources}
|
||||||
|
invites={this.props.invites}
|
||||||
|
groups={this.props.groups}
|
||||||
selected={this.props.selected}
|
selected={this.props.selected}
|
||||||
sidebarShown={this.props.sidebarShown}
|
sidebarShown={this.props.sidebarShown}
|
||||||
links={this.props.links}/>
|
links={this.props.links}/>
|
||||||
|
@ -1,17 +1,27 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
export function makeRoutePath(
|
||||||
export function uuid() {
|
resource, popout = false, page = 0, url = null, index = 0, compage = 0
|
||||||
let str = "0v"
|
) {
|
||||||
str += Math.ceil(Math.random()*8)+"."
|
let route = '/~link' + (popout ? '/popout' : '') + resource;
|
||||||
for (var i = 0; i < 5; i++) {
|
if (!url) {
|
||||||
let _str = Math.ceil(Math.random()*10000000).toString(32);
|
if (page !== 0) {
|
||||||
_str = ("00000"+_str).substr(-5,5);
|
route = route + '/' + page;
|
||||||
str += _str+".";
|
}
|
||||||
|
} else {
|
||||||
|
route = `${route}/${page}/${index}/${base64urlEncode(url)}`;
|
||||||
|
if (compage !== 0) {
|
||||||
|
route = route + '/' + compage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return route;
|
||||||
|
}
|
||||||
|
|
||||||
return str.slice(0,-1);
|
export function amOwnerOfGroup(groupPath) {
|
||||||
|
if (!groupPath) return false;
|
||||||
|
const groupOwner = /(\/~)?\/~([a-z-]{3,})\/.*/.exec(groupPath)[2];
|
||||||
|
return (window.ship === groupOwner);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getContactDetails(contact) {
|
export function getContactDetails(contact) {
|
||||||
|
63
pkg/interface/link/src/js/reducers/group-update.js
Normal file
63
pkg/interface/link/src/js/reducers/group-update.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export class GroupUpdateReducer {
|
||||||
|
reduce(json, state) {
|
||||||
|
let data = _.get(json, 'group-update', false);
|
||||||
|
if (data) {
|
||||||
|
this.add(data, state);
|
||||||
|
this.remove(data, state);
|
||||||
|
this.bundle(data, state);
|
||||||
|
this.unbundle(data, state);
|
||||||
|
this.keys(data, state);
|
||||||
|
this.path(data, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add(json, state) {
|
||||||
|
let data = _.get(json, 'add', false);
|
||||||
|
if (data) {
|
||||||
|
for (let member of data.members) {
|
||||||
|
state.groups[data.path].add(member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(json, state) {
|
||||||
|
let data = _.get(json, 'remove', false);
|
||||||
|
if (data) {
|
||||||
|
for (let member of data.members) {
|
||||||
|
state.groups[data.path].delete(member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle(json, state) {
|
||||||
|
|
||||||
|
let data = _.get(json, 'bundle', false);
|
||||||
|
if (data) {
|
||||||
|
state.groups[data.path] = new Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unbundle(json, state) {
|
||||||
|
let data = _.get(json, 'unbundle', false);
|
||||||
|
if (data) {
|
||||||
|
delete state.groups[data.path];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys(json, state) {
|
||||||
|
let data = _.get(json, 'keys', false);
|
||||||
|
if (data) {
|
||||||
|
state.groupKeys = new Set(data.keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path(json, state) {
|
||||||
|
let data = _.get(json, 'path', false);
|
||||||
|
if (data) {
|
||||||
|
state.groups[data.path] = new Set([data.members]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -11,6 +11,10 @@ export class InviteUpdateReducer {
|
|||||||
this.accepted(data, state);
|
this.accepted(data, state);
|
||||||
this.decline(data, state);
|
this.decline(data, state);
|
||||||
}
|
}
|
||||||
|
data = _.get(json, 'invite-initial', false);
|
||||||
|
if (data) {
|
||||||
|
state.invites = data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
create(json, state) {
|
create(json, state) {
|
||||||
@ -37,7 +41,6 @@ export class InviteUpdateReducer {
|
|||||||
accepted(json, state) {
|
accepted(json, state) {
|
||||||
let data = _.get(json, 'accepted', false);
|
let data = _.get(json, 'accepted', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
console.log(data);
|
|
||||||
delete state.invites[data.path][data.uid];
|
delete state.invites[data.path][data.uid];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
69
pkg/interface/link/src/js/reducers/metadata-update.js
Normal file
69
pkg/interface/link/src/js/reducers/metadata-update.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export class MetadataReducer {
|
||||||
|
reduce(json, state) {
|
||||||
|
let data = _.get(json, 'metadata-update', false);
|
||||||
|
if (data) {
|
||||||
|
this.associations(data, state);
|
||||||
|
this.add(data, state);
|
||||||
|
this.remove(data, state);
|
||||||
|
this.update(data, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
associations(json, state) {
|
||||||
|
let data = _.get(json, 'associations', false);
|
||||||
|
if (data) {
|
||||||
|
let metadata = new Map;
|
||||||
|
Object.keys(data).map((key) => {
|
||||||
|
let assoc = data[key];
|
||||||
|
if (assoc['app-name'] !== 'link') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.resources[assoc['app-path']]) {
|
||||||
|
console.error('beware! overwriting previous data', data['app-path']);
|
||||||
|
}
|
||||||
|
state.resources[assoc['app-path']] = {
|
||||||
|
group: assoc['group-path'],
|
||||||
|
...assoc.metadata
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add(json, state) {
|
||||||
|
let data = _.get(json, 'add', false);
|
||||||
|
if (data) {
|
||||||
|
if (state.resources[data['app-path']]) {
|
||||||
|
console.error('beware! overwriting previous data', data['app-path']);
|
||||||
|
}
|
||||||
|
this.update({'update-metadata': data}, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(json, state) {
|
||||||
|
let data = _.get(json, 'remove', false);
|
||||||
|
if (data) {
|
||||||
|
if (data['app-name'] !== 'link') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const have = state.resources[data['app-path']];
|
||||||
|
if (have && have.group === data['group-path']) {
|
||||||
|
delete state.resources[data['app-path']];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update(json, state) {
|
||||||
|
let data = _.get(json, 'update-metadata', false);
|
||||||
|
if (data) {
|
||||||
|
if (data['app-name'] !== 'link') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.resources[data['app-path']] = {
|
||||||
|
group: data['group-path'],
|
||||||
|
...data.metadata
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,9 @@
|
|||||||
import { InitialReducer } from '/reducers/initial';
|
import { InitialReducer } from '/reducers/initial';
|
||||||
|
import { GroupUpdateReducer } from '/reducers/group-update';
|
||||||
import { ContactUpdateReducer } from '/reducers/contact-update.js';
|
import { ContactUpdateReducer } from '/reducers/contact-update.js';
|
||||||
import { PermissionUpdateReducer } from '/reducers/permission-update';
|
import { PermissionUpdateReducer } from '/reducers/permission-update';
|
||||||
|
import { MetadataReducer } from '/reducers/metadata-update.js';
|
||||||
|
import { InviteUpdateReducer } from '/reducers/invite-update';
|
||||||
import { LinkUpdateReducer } from '/reducers/link-update';
|
import { LinkUpdateReducer } from '/reducers/link-update';
|
||||||
import { LocalReducer } from '/reducers/local.js';
|
import { LocalReducer } from '/reducers/local.js';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@ -11,6 +14,8 @@ class Store {
|
|||||||
this.state = {
|
this.state = {
|
||||||
contacts: {},
|
contacts: {},
|
||||||
groups: {},
|
groups: {},
|
||||||
|
resources: {},
|
||||||
|
invites: {},
|
||||||
links: {},
|
links: {},
|
||||||
comments: {},
|
comments: {},
|
||||||
seen: {},
|
seen: {},
|
||||||
@ -20,8 +25,11 @@ class Store {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.initialReducer = new InitialReducer();
|
this.initialReducer = new InitialReducer();
|
||||||
|
this.groupUpdateReducer = new GroupUpdateReducer();
|
||||||
this.contactUpdateReducer = new ContactUpdateReducer();
|
this.contactUpdateReducer = new ContactUpdateReducer();
|
||||||
this.permissionUpdateReducer = new PermissionUpdateReducer();
|
this.permissionUpdateReducer = new PermissionUpdateReducer();
|
||||||
|
this.metadataReducer = new MetadataReducer();
|
||||||
|
this.inviteUpdateReducer = new InviteUpdateReducer();
|
||||||
this.localReducer = new LocalReducer();
|
this.localReducer = new LocalReducer();
|
||||||
this.linkUpdateReducer = new LinkUpdateReducer();
|
this.linkUpdateReducer = new LinkUpdateReducer();
|
||||||
this.setState = () => {};
|
this.setState = () => {};
|
||||||
@ -41,8 +49,11 @@ class Store {
|
|||||||
|
|
||||||
console.log('event', json);
|
console.log('event', json);
|
||||||
this.initialReducer.reduce(json, this.state);
|
this.initialReducer.reduce(json, this.state);
|
||||||
|
this.groupUpdateReducer.reduce(json, this.state);
|
||||||
this.contactUpdateReducer.reduce(json, this.state);
|
this.contactUpdateReducer.reduce(json, this.state);
|
||||||
this.permissionUpdateReducer.reduce(json, this.state);
|
this.permissionUpdateReducer.reduce(json, this.state);
|
||||||
|
this.metadataReducer.reduce(json, this.state);
|
||||||
|
this.inviteUpdateReducer.reduce(json, this.state);
|
||||||
this.localReducer.reduce(json, this.state);
|
this.localReducer.reduce(json, this.state);
|
||||||
this.linkUpdateReducer.reduce(json, this.state);
|
this.linkUpdateReducer.reduce(json, this.state);
|
||||||
|
|
||||||
|
@ -11,16 +11,25 @@ export class Subscription {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initializeLinks() {
|
initializeLinks() {
|
||||||
// add invite, permissions flows once link stores are more than
|
|
||||||
// group-specific
|
|
||||||
api.bind('/all', 'PUT', api.authTokens.ship, 'group-store',
|
api.bind('/all', 'PUT', api.authTokens.ship, 'group-store',
|
||||||
this.handleEvent.bind(this),
|
this.handleEvent.bind(this),
|
||||||
this.handleError.bind(this),
|
this.handleError.bind(this),
|
||||||
this.handleQuitAndResubscribe.bind(this));
|
this.handleQuitAndResubscribe.bind(this)
|
||||||
|
);
|
||||||
api.bind('/primary', 'PUT', api.authTokens.ship, 'contact-view',
|
api.bind('/primary', 'PUT', api.authTokens.ship, 'contact-view',
|
||||||
|
this.handleEvent.bind(this),
|
||||||
|
this.handleError.bind(this),
|
||||||
|
this.handleQuitAndResubscribe.bind(this)
|
||||||
|
);
|
||||||
|
api.bind('/primary', 'PUT', api.authTokens.ship, 'invite-view',
|
||||||
this.handleEvent.bind(this),
|
this.handleEvent.bind(this),
|
||||||
this.handleError.bind(this),
|
this.handleError.bind(this),
|
||||||
this.handleQuitAndResubscribe.bind(this));
|
this.handleQuitAndResubscribe.bind(this));
|
||||||
|
api.bind('/app-name/link', 'PUT', api.authTokens.ship, 'metadata-store',
|
||||||
|
this.handleEvent.bind(this),
|
||||||
|
this.handleError.bind(this),
|
||||||
|
this.handleQuitAndResubscribe.bind(this)
|
||||||
|
);
|
||||||
|
|
||||||
// open a subscription for all submissions
|
// open a subscription for all submissions
|
||||||
api.getPage('', 0);
|
api.getPage('', 0);
|
||||||
|
Loading…
Reference in New Issue
Block a user