mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-03 02:35:52 +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
|
||||
::
|
||||
:: on-init, subscribes to all groups on this ship. for every ship in a group,
|
||||
:: we subscribe to their link's local-pages and annotations
|
||||
:: at the group path (through link-proxy-hook),
|
||||
:: and forwards all entries into our link as submissions and comments.
|
||||
:: subscribes to all %link resources in the metadata-store.
|
||||
:: for all ships in groups associated with those resources, we subscribe to
|
||||
:: their link's local-pages and annotations at the resource path (through
|
||||
:: link-proxy-hook), and forward all entries into our link-store as
|
||||
:: submissions and comments.
|
||||
::
|
||||
:: if a subscription to a group member fails, we assume it's because their
|
||||
:: group definition hasn't been updated to include us yet.
|
||||
:: if a subscription to a target fails, we assume it's because their
|
||||
:: metadata+groups definition hasn't been updated to include us yet.
|
||||
:: we retry with exponential backoff, maxing out at one hour timeouts.
|
||||
:: to expede this process, we prod other potential listeners when we add
|
||||
:: them to our metadata+groups definition.
|
||||
::
|
||||
/- *link, group-store
|
||||
/+ default-agent, verb, dbug
|
||||
/- *metadata-store, *link, group-store
|
||||
/+ metadata, default-agent, verb, dbug
|
||||
::
|
||||
|%
|
||||
+$ state-0
|
||||
$: %0
|
||||
retry-timers=(map target @dr)
|
||||
:: reasoning: the resources we're subscribed to,
|
||||
:: and the groups that cause that.
|
||||
::
|
||||
:: we don't strictly need to track this in state, but doing so heavily
|
||||
:: simplifies logic and reduces the amount of big scries we do.
|
||||
:: this also gives us the option to check & restore subscriptions,
|
||||
:: should we ever need that.
|
||||
::
|
||||
reasoning=(jug [ship app-path] group-path)
|
||||
==
|
||||
::
|
||||
+$ what-target ?(%local-pages %annotations)
|
||||
@ -52,7 +64,7 @@
|
||||
++ on-init
|
||||
^- (quip card _this)
|
||||
:_ this
|
||||
[watch-groups:do]~
|
||||
~[watch-metadata:do watch-groups:do]
|
||||
::
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
@ -63,26 +75,27 @@
|
||||
++ on-agent
|
||||
|= [=wire =sign:agent:gall]
|
||||
^- (quip card _this)
|
||||
?: ?=([%groups ~] wire)
|
||||
=^ cards state
|
||||
=^ cards state
|
||||
?+ wire ~|([dap.bowl %weird-agent-wire wire] !!)
|
||||
[%metadata ~]
|
||||
(take-metadata-sign:do sign)
|
||||
::
|
||||
[%groups ~]
|
||||
(take-groups-sign:do sign)
|
||||
[cards this]
|
||||
?: ?=([%links ?(%local-pages %annotations) @ ^] wire)
|
||||
=^ cards state
|
||||
::
|
||||
[%links ?(%local-pages %annotations) @ ^]
|
||||
(take-link-sign:do (wire-to-target t.wire) sign)
|
||||
[cards this]
|
||||
?: ?=([%forward ^] wire)
|
||||
=^ cards state
|
||||
::
|
||||
[%forward ^]
|
||||
(take-forward-sign:do t.wire sign)
|
||||
[cards this]
|
||||
?: ?=([%prod *] wire)
|
||||
~| [%weird-sign -.sign]
|
||||
?> ?=(%poke-ack -.sign)
|
||||
?~ p.sign [~ this]
|
||||
%- (slog [leaf+"failed to prod" u.p.sign])
|
||||
[~ this]
|
||||
~| [dap.bowl %weird-wire wire]
|
||||
!!
|
||||
::
|
||||
[%prod *]
|
||||
?> ?=(%poke-ack -.sign)
|
||||
?~ p.sign [~ state]
|
||||
%- (slog leaf+"prod failed" u.p.sign)
|
||||
[~ state]
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
++ on-poke
|
||||
|= [=mark =vase]
|
||||
@ -122,8 +135,65 @@
|
||||
::
|
||||
::
|
||||
|_ =bowl:gall
|
||||
+* md ~(. metadata bowl)
|
||||
::
|
||||
:: groups subscription
|
||||
:: metadata subscription
|
||||
::
|
||||
++ watch-metadata
|
||||
^- card
|
||||
[%pass /metadata %agent [our.bowl %metadata-store] %watch /app-name/link]
|
||||
::
|
||||
++ take-metadata-sign
|
||||
|= =sign:agent:gall
|
||||
^- (quip card _state)
|
||||
?- -.sign
|
||||
%poke-ack ~|([dap.bowl %unexpected-poke-ack /metadata] !!)
|
||||
%kick [[watch-metadata]~ state]
|
||||
::
|
||||
%watch-ack
|
||||
?~ p.sign [~ state]
|
||||
=/ =tank
|
||||
:- %leaf
|
||||
"{(trip dap.bowl)} failed subscribe to metadata store. very wrong!"
|
||||
%- (slog tank u.p.sign)
|
||||
[~ state]
|
||||
::
|
||||
%fact
|
||||
=* mark p.cage.sign
|
||||
=* vase q.cage.sign
|
||||
?. ?=(%metadata-update mark)
|
||||
~| [dap.bowl %unexpected-mark mark]
|
||||
!!
|
||||
%- handle-metadata-update
|
||||
!<(metadata-update vase)
|
||||
==
|
||||
::
|
||||
++ handle-metadata-update
|
||||
|= upd=metadata-update
|
||||
^- (quip card _state)
|
||||
?+ -.upd [~ state]
|
||||
%associations
|
||||
=/ socs=(list [=group-path resource])
|
||||
~(tap in ~(key by associations.upd))
|
||||
=| cards=(list card)
|
||||
|- ::TODO try for +roll maybe?
|
||||
?~ socs [cards state]
|
||||
=^ caz state
|
||||
=, i.socs
|
||||
?. =(%link app-name) [~ state]
|
||||
(listen-to-group app-path group-path)
|
||||
$(socs t.socs, cards (weld cards caz))
|
||||
::
|
||||
%add
|
||||
?> =(%link app-name.resource.upd)
|
||||
(listen-to-group app-path.resource.upd group-path.upd)
|
||||
::
|
||||
%remove
|
||||
?> =(%link app-name.resource.upd)
|
||||
(leave-from-group app-path.resource.upd group-path.upd)
|
||||
==
|
||||
::
|
||||
:: groups subscriptions
|
||||
::
|
||||
++ watch-groups
|
||||
^- card
|
||||
@ -148,49 +218,98 @@
|
||||
=* mark p.cage.sign
|
||||
=* vase q.cage.sign
|
||||
?+ mark ~|([dap.bowl %unexpected-mark mark] !!)
|
||||
%group-initial (handle-group-initial !<(groups:group-store vase))
|
||||
%group-initial [~ state] ::NOTE initial handled using metadata
|
||||
%group-update (handle-group-update !<(group-update:group-store vase))
|
||||
==
|
||||
==
|
||||
::
|
||||
++ handle-group-initial
|
||||
|= =groups:group-store
|
||||
^- (quip card _state)
|
||||
=| cards=(list card)
|
||||
=/ groups=(list [=path =group:group-store])
|
||||
~(tap by groups)
|
||||
|-
|
||||
?~ groups [cards state]
|
||||
=^ caz state
|
||||
%- handle-group-update
|
||||
[%add [group path]:i.groups]
|
||||
$(cards (weld cards caz), groups t.groups)
|
||||
::
|
||||
++ handle-group-update
|
||||
|= upd=group-update:group-store
|
||||
^- (quip card _state)
|
||||
:_ state
|
||||
?+ -.upd ~
|
||||
?(%path %add %remove)
|
||||
=/ whos=(list ship) ~(tap in members.upd)
|
||||
|- ^- (list card)
|
||||
?~ whos ~
|
||||
:: no need to subscribe to ourselves
|
||||
::
|
||||
?: =(our.bowl i.whos)
|
||||
$(whos t.whos)
|
||||
?. ?=(?(%path %add %remove) -.upd)
|
||||
[~ state]
|
||||
=/ socs=(list app-path)
|
||||
(app-paths-from-group:md %link pax.upd)
|
||||
=/ whos=(list ship)
|
||||
~(tap in members.upd)
|
||||
=| cards=(list card)
|
||||
|-
|
||||
=* loop-socs $
|
||||
?~ socs [cards state]
|
||||
|-
|
||||
=* loop-whos $
|
||||
?~ whos loop-socs(socs t.socs)
|
||||
=^ caz state
|
||||
?: ?=(%remove -.upd)
|
||||
%+ weld
|
||||
$(whos t.whos)
|
||||
(end-link-subscriptions i.whos pax.upd)
|
||||
:^ (start-link-subscription %local-pages i.whos pax.upd)
|
||||
(start-link-subscription %annotations i.whos pax.upd)
|
||||
(prod-other-listener i.whos pax.upd)
|
||||
$(whos t.whos)
|
||||
==
|
||||
(leave-from-peer i.socs pax.upd i.whos)
|
||||
(listen-to-peer i.socs pax.upd i.whos)
|
||||
loop-whos(whos t.whos, cards (weld cards caz))
|
||||
::
|
||||
:: link subscriptions
|
||||
::
|
||||
++ listen-to-group
|
||||
|= [=app-path =group-path]
|
||||
^- (quip card _state)
|
||||
=/ peers=(list ship)
|
||||
~| group-path
|
||||
%~ tap in
|
||||
=- (fall - *group:group-store)
|
||||
%^ scry-for (unit group:group-store)
|
||||
%group-store
|
||||
group-path
|
||||
=| cards=(list card)
|
||||
|-
|
||||
?~ peers [cards state]
|
||||
=^ caz state
|
||||
(listen-to-peer app-path group-path i.peers)
|
||||
$(peers t.peers, cards (weld cards caz))
|
||||
::
|
||||
++ leave-from-group
|
||||
|= [=app-path =group-path]
|
||||
^- (quip card _state)
|
||||
=/ peers=(list ship)
|
||||
%~ tap in
|
||||
=- (fall - *group:group-store)
|
||||
%^ scry-for (unit group:group-store)
|
||||
%group-store
|
||||
group-path
|
||||
=| cards=(list card)
|
||||
|-
|
||||
?~ peers [cards state]
|
||||
=^ caz state
|
||||
(leave-from-peer app-path group-path i.peers)
|
||||
$(peers t.peers, cards (weld cards caz))
|
||||
::
|
||||
++ listen-to-peer
|
||||
|= [=app-path =group-path who=ship]
|
||||
^- (quip card _state)
|
||||
?: =(our.bowl who)
|
||||
[~ state]
|
||||
:_ =- state(reasoning -)
|
||||
(~(put ju reasoning) [who app-path] group-path)
|
||||
:- (prod-other-listener who app-path)
|
||||
?^ (~(get ju reasoning) [who app-path])
|
||||
~
|
||||
(start-link-subscriptions who app-path)
|
||||
::
|
||||
++ leave-from-peer
|
||||
|= [=app-path =group-path who=ship]
|
||||
^- (quip card _state)
|
||||
?: =(our.bowl who)
|
||||
[~ state]
|
||||
:_ =- state(reasoning -)
|
||||
(~(del ju reasoning) [who app-path] group-path)
|
||||
?. (~(has ju reasoning) [who app-path] group-path)
|
||||
~
|
||||
(end-link-subscriptions who app-path)
|
||||
::
|
||||
++ start-link-subscriptions
|
||||
|= [=ship =app-path]
|
||||
^- (list card)
|
||||
:~ (start-link-subscription %local-pages ship app-path)
|
||||
(start-link-subscription %annotations ship app-path)
|
||||
==
|
||||
::
|
||||
++ start-link-subscription
|
||||
|= =target
|
||||
^- card
|
||||
@ -283,7 +402,7 @@
|
||||
++ take-retry
|
||||
|= =target
|
||||
^- (list card)
|
||||
:: relevant: whether :who is still in group :where
|
||||
:: relevant: whether :who is still associated with resource :where
|
||||
::
|
||||
=; relevant=?
|
||||
?. relevant ~
|
||||
@ -291,16 +410,13 @@
|
||||
?: %- ~(has by wex.bowl)
|
||||
[[%links (target-to-wire target)] who.target %link-proxy-hook]
|
||||
|
|
||||
%. who.target
|
||||
%~ has in
|
||||
=- (fall - *group:group-store)
|
||||
.^ (unit group:group-store)
|
||||
%gx
|
||||
(scot %p our.bowl)
|
||||
%+ lien (groups-from-resource:md %link where.target)
|
||||
|= =group-path
|
||||
^- ?
|
||||
=- (~(has in (fall - *group:group-store)) who.target)
|
||||
%^ scry-for (unit group:group-store)
|
||||
%group-store
|
||||
(scot %da now.bowl)
|
||||
(snoc where.target %noun)
|
||||
==
|
||||
group-path
|
||||
::
|
||||
++ do-link-action
|
||||
|= [=wire =action]
|
||||
@ -373,4 +489,14 @@
|
||||
==
|
||||
%- (slog tank u.p.sign)
|
||||
[~ state]
|
||||
::
|
||||
++ scry-for
|
||||
|* [=mold =app-name =path]
|
||||
.^ mold
|
||||
%gx
|
||||
(scot %p our.bowl)
|
||||
app-name
|
||||
(scot %da now.bowl)
|
||||
(snoc `^path`path %noun)
|
||||
==
|
||||
--
|
||||
|
@ -4,12 +4,11 @@
|
||||
:: stores if permission conditions are met.
|
||||
:: the patterns herein should one day be generalized into a proxy-hook lib.
|
||||
::
|
||||
:: this adopts a very primitive view of groups-store as containing only
|
||||
:: groups of interesting (rather than uninteresting) ships. it sets the
|
||||
:: permission condition to be that ship must be in group matching the path
|
||||
:: it's subscribing to.
|
||||
:: we check this on-watch, but also subscribe to groups so that we can kick
|
||||
:: subscriptions if needed (eg ship removed from group).
|
||||
:: this uses metadata-store to discover resources and their associated
|
||||
:: groups. it sets the permission condition to be that a ship must be in a
|
||||
:: group associated with the resource it's subscribing to.
|
||||
:: we check this on-watch, but also subscribe to metadata & groups so that
|
||||
:: we can kick subscriptions if needed (eg ship removed from group).
|
||||
::
|
||||
:: we deduplicate incoming subscriptions on the same path, ensuring we have
|
||||
:: exactly one local subscription per unique incoming subscription path.
|
||||
@ -18,10 +17,10 @@
|
||||
:: become part of the stores standard anyway.
|
||||
::
|
||||
:: when adding support for new paths, the only things you'll likely want
|
||||
:: to touch are +permitted, +initial-response, & maybe +handle-group-update.
|
||||
:: to touch are +permitted, +initial-response, & +kick-proxies.
|
||||
::
|
||||
/- group-store
|
||||
/+ *link, default-agent, verb, dbug
|
||||
/- group-store, *metadata-store
|
||||
/+ *link, metadata, default-agent, verb, dbug
|
||||
|%
|
||||
+$ state-0
|
||||
$: %0
|
||||
@ -48,7 +47,7 @@
|
||||
++ on-init
|
||||
^- (quip card _this)
|
||||
:_ this
|
||||
[watch-groups:do]~
|
||||
~[watch-groups:do watch-metadata:do]
|
||||
::
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
@ -96,11 +95,15 @@
|
||||
--
|
||||
::
|
||||
|_ =bowl:gall
|
||||
+* md ~(. metadata bowl)
|
||||
::
|
||||
:: permissions
|
||||
::
|
||||
++ permitted
|
||||
|= [who=ship =path]
|
||||
^- ?
|
||||
:: we only expose group-specific /local-pages and /annotations,
|
||||
:: and only to ships in the relevant group.
|
||||
:: we only expose /local-pages and /annotations,
|
||||
:: to ships in the groups associated with the resource.
|
||||
:: (no url-specific annotations subscriptions, either.)
|
||||
::
|
||||
=/ target=(unit ^path)
|
||||
@ -110,12 +113,75 @@
|
||||
`t.t.path
|
||||
~
|
||||
?~ target |
|
||||
=; group
|
||||
?& ?=(^ group)
|
||||
(~(has in u.group) who)
|
||||
==
|
||||
%+ scry-for (unit group:group-store)
|
||||
[%group-store u.target]
|
||||
~? !.^(? %gu (scot %p our.bowl) %metadata-store (scot %da now.bowl) ~)
|
||||
%woah-md-s-not-booted ::TODO fallback if needed
|
||||
%+ lien (groups-from-resource:md %link u.target)
|
||||
|= =group-path
|
||||
^- ?
|
||||
=- (~(has in (fall - *group:group-store)) who)
|
||||
%^ scry-for (unit group:group-store)
|
||||
%group-store
|
||||
group-path
|
||||
::
|
||||
++ kick-revoked-permissions
|
||||
|= [=path who=(list ship)]
|
||||
^- (list card)
|
||||
%+ murn who
|
||||
|= =ship
|
||||
^- (unit card)
|
||||
:: no need to remove to ourselves
|
||||
::
|
||||
?: =(our.bowl ship) ~
|
||||
?: (permitted ship path) ~
|
||||
`(kick-proxies ship path)
|
||||
::
|
||||
:: metadata subscription
|
||||
::
|
||||
++ watch-metadata
|
||||
^- card
|
||||
[%pass /metadata %agent [our.bowl %metadata-store] %watch /app-name/link]
|
||||
::
|
||||
++ take-metadata-sign
|
||||
|= =sign:agent:gall
|
||||
^- (quip card _state)
|
||||
?- -.sign
|
||||
%poke-ack ~|([dap.bowl %unexpected-poke-ack /metadata] !!)
|
||||
%kick [[watch-metadata]~ state]
|
||||
::
|
||||
%watch-ack
|
||||
?~ p.sign [~ state]
|
||||
=/ =tank
|
||||
:- %leaf
|
||||
"{(trip dap.bowl)} failed subscribe to metadata store. very wrong!"
|
||||
%- (slog tank u.p.sign)
|
||||
[~ state]
|
||||
::
|
||||
%fact
|
||||
=* mark p.cage.sign
|
||||
=* vase q.cage.sign
|
||||
?. ?=(%metadata-update mark)
|
||||
~| [dap.bowl %unexpected-mark mark]
|
||||
!!
|
||||
%- handle-metadata-update
|
||||
!<(metadata-update vase)
|
||||
==
|
||||
::
|
||||
++ handle-metadata-update
|
||||
|= upd=metadata-update
|
||||
^- (quip card _state)
|
||||
:_ state
|
||||
?. ?=(%remove -.upd) ~
|
||||
?> =(%link app-name.resource.upd)
|
||||
:: if a group is no longer associated with a resource,
|
||||
:: we need to re-check permissions for everyone in that group.
|
||||
::
|
||||
%+ kick-revoked-permissions
|
||||
app-path.resource.upd
|
||||
%~ tap in
|
||||
=- (fall - *group:group-store)
|
||||
%^ scry-for (unit group:group-store)
|
||||
%group-store
|
||||
group-path.upd
|
||||
::
|
||||
:: groups subscription
|
||||
::TODO largely copied from link-listen-hook. maybe make a store-listener lib?
|
||||
@ -153,29 +219,26 @@
|
||||
^- (quip card _state)
|
||||
:_ state
|
||||
?. ?=(%remove -.upd) ~
|
||||
=/ whos=(list ship) ~(tap in members.upd)
|
||||
|- ^- (list card)
|
||||
?~ whos ~
|
||||
:: no need to remove to ourselves
|
||||
:: if someone was removed from a group, find all link resources associated
|
||||
:: with that group, then kick their subscriptions if they're no longer
|
||||
::
|
||||
?: =(our.bowl i.whos)
|
||||
$(whos t.whos)
|
||||
:_ $(whos t.whos)
|
||||
::NOTE this depends kind of unfortunately on the fact that we only accept
|
||||
:: subscriptions to /local-pages//* paths. it'd be more correct if we
|
||||
:: "just" looked at all paths in the map, and found the matching ones.
|
||||
::TODO what exactly did i mean by this?
|
||||
%+ kick-proxies i.whos
|
||||
:~ [%local-pages pax.upd]
|
||||
[%annotations '' pax.upd]
|
||||
==
|
||||
%- zing
|
||||
%+ turn (app-paths-from-group:md %link pax.upd)
|
||||
|= =app-path
|
||||
^- (list card)
|
||||
%+ kick-revoked-permissions
|
||||
app-path
|
||||
~(tap in members.upd)
|
||||
::
|
||||
:: proxy subscriptions
|
||||
::
|
||||
++ kick-proxies
|
||||
|= [who=ship paths=(list path)]
|
||||
|= [who=ship =path]
|
||||
^- card
|
||||
[%give %kick paths `who]
|
||||
=- [%give %kick - `who]
|
||||
:~ [%local-pages path]
|
||||
[%annotations %$ path]
|
||||
==
|
||||
::
|
||||
++ handle-proxy-sign
|
||||
|= [=wire =sign:agent:gall]
|
||||
@ -211,14 +274,10 @@
|
||||
[%give %fact ~ %link-initial !>(initial)]
|
||||
?+ path !!
|
||||
[%local-pages ^]
|
||||
:- %local-pages
|
||||
%+ scry-for (map ^path pages)
|
||||
[%link-store path]
|
||||
[%local-pages .^((map ^path pages) %gx path)]
|
||||
::
|
||||
[%annotations %$ ^]
|
||||
:- %annotations
|
||||
%+ scry-for (per-path-url notes)
|
||||
[%link-store path]
|
||||
[%annotations .^((per-path-url notes) %gx %$ t.t.path)]
|
||||
==
|
||||
::
|
||||
++ start-proxy
|
||||
@ -249,12 +308,14 @@
|
||||
::
|
||||
[(proxy-pass-link-store path %leave ~)]~
|
||||
::
|
||||
:: helpers
|
||||
::
|
||||
++ scry-for
|
||||
|* [=mold app=term =path]
|
||||
|* [=mold =app-name =path]
|
||||
.^ mold
|
||||
%gx
|
||||
(scot %p our.bowl)
|
||||
app
|
||||
app-name
|
||||
(scot %da now.bowl)
|
||||
(snoc `^path`path %noun)
|
||||
==
|
||||
|
@ -10,7 +10,11 @@
|
||||
:: /json/[n]/submission/[wood-url]/[some-group] nth matching submission
|
||||
:: /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
|
||||
@ -25,6 +29,7 @@
|
||||
=* state -
|
||||
::
|
||||
%+ verb |
|
||||
%- agent:dbug
|
||||
^- agent:gall
|
||||
=<
|
||||
|_ =bowl:gall
|
||||
@ -42,6 +47,12 @@
|
||||
::
|
||||
=+ [dap.bowl /tile '/~link/js/tile.js']
|
||||
[%pass /launch %agent [our.bowl %launch] %poke %launch-action !>(-)]
|
||||
::
|
||||
=+ [%invite-action !>([%create /link])]
|
||||
[%pass /invitatory/create %agent [our.bowl %invite-store] %poke -]
|
||||
::
|
||||
=+ /invitatory/link
|
||||
[%pass - %agent [our.bowl %invite-store] %watch -]
|
||||
==
|
||||
::
|
||||
++ on-save !>(state)
|
||||
@ -65,6 +76,9 @@
|
||||
::
|
||||
%link-action
|
||||
[(handle-action:do !<(action vase)) ~]
|
||||
::
|
||||
%link-view-action
|
||||
(handle-view-action:do !<(view-action vase))
|
||||
==
|
||||
::
|
||||
++ on-watch
|
||||
@ -104,13 +118,18 @@
|
||||
?+ -.sign (on-agent:def wire sign)
|
||||
%kick
|
||||
:_ this
|
||||
[%pass wire %agent [our.bowl %link-store] %watch wire]~
|
||||
=/ app=term
|
||||
?: ?=([%invites *] wire)
|
||||
%invite-store
|
||||
%link-store
|
||||
[%pass wire %agent [our.bowl app] %watch wire]~
|
||||
::
|
||||
%fact
|
||||
=* mark p.cage.sign
|
||||
=* vase q.cage.sign
|
||||
?+ mark (on-agent:def wire sign)
|
||||
%link-initial [~ this]
|
||||
%invite-update [(handle-invite-update:do !<(invite-update vase)) this]
|
||||
%link-initial [~ this]
|
||||
::
|
||||
%link-update
|
||||
:_ this
|
||||
@ -135,6 +154,8 @@
|
||||
--
|
||||
::
|
||||
|_ =bowl:gall
|
||||
+* md ~(. metadata bowl)
|
||||
::
|
||||
++ page-size 25
|
||||
++ get-paginated
|
||||
|* [p=(unit @ud) l=(list)]
|
||||
@ -217,10 +238,214 @@
|
||||
%- as-octs:mimes:html
|
||||
.^(@ %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
|
||||
|= =action
|
||||
^- card
|
||||
[%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
|
||||
::
|
||||
::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
|
||||
^- ^associations
|
||||
%- ~(gas by *^associations)
|
||||
%+ turn ~(tap in (~(got by group-indices) group-path))
|
||||
%+ turn ~(tap in (~(gut by group-indices) group-path ~))
|
||||
|= =resource
|
||||
:- [group-path resource]
|
||||
(~(got by associations) [group-path resource])
|
||||
|
@ -43,13 +43,15 @@
|
||||
~| path
|
||||
(woad (slav %ta i.path))
|
||||
::
|
||||
:: zip sorted a into sorted b, maintaining sort order
|
||||
::TODO stdlib
|
||||
++ merge-sorted
|
||||
:: zip sorted a into sorted b, maintaining sort order, avoiding duplicates
|
||||
::
|
||||
++ merge-sorted-unique
|
||||
|* [sort=$-([* *] ?) a=(list) b=(list)]
|
||||
|- ^- ?(_a _b)
|
||||
?~ a b
|
||||
?~ b a
|
||||
?: =(i.a i.b)
|
||||
[i.a $(a t.a, b t.b)]
|
||||
?: (sort i.a i.b)
|
||||
[i.a $(a t.a)]
|
||||
[i.b $(b t.b)]
|
||||
@ -60,7 +62,7 @@
|
||||
::TODO we would just use +cury here but it don't work
|
||||
|= [a=^pages b=^pages]
|
||||
^+ a
|
||||
%+ merge-sorted
|
||||
%+ merge-sorted-unique
|
||||
|= [a=page b=page]
|
||||
(gth time.a time.b)
|
||||
[a b]
|
||||
@ -68,7 +70,7 @@
|
||||
++ submissions
|
||||
|= [a=^submissions b=^submissions]
|
||||
^+ a
|
||||
%+ merge-sorted
|
||||
%+ merge-sorted-unique
|
||||
|= [a=submission b=submission]
|
||||
(gth time.a time.b)
|
||||
[a b]
|
||||
@ -76,7 +78,7 @@
|
||||
++ notes
|
||||
|= [a=^notes b=^notes]
|
||||
^+ a
|
||||
%+ merge-sorted
|
||||
%+ merge-sorted-unique
|
||||
|= [a=note b=note]
|
||||
(gth time.a time.b)
|
||||
[a b]
|
||||
@ -84,7 +86,7 @@
|
||||
++ comments
|
||||
|= [a=^comments b=^comments]
|
||||
^+ a
|
||||
%+ merge-sorted
|
||||
%+ merge-sorted-unique
|
||||
|= [a=comment b=comment]
|
||||
(gth time.a time.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;
|
||||
}
|
||||
|
||||
.list-ship {
|
||||
line-height: 2.2;
|
||||
}
|
||||
|
||||
.c-default {
|
||||
cursor: default;
|
||||
}
|
||||
@ -105,6 +109,23 @@ a {
|
||||
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 */
|
||||
@media all and (max-width: 34.375em) {
|
||||
.dn-s {
|
||||
@ -164,6 +185,9 @@ a {
|
||||
.b--gray0-d {
|
||||
border-color: #333;
|
||||
}
|
||||
.b--gray1-d {
|
||||
border-color: #4d4d4d;
|
||||
}
|
||||
.b--gray2-d {
|
||||
border-color: #7f7f7f;
|
||||
}
|
||||
@ -180,6 +204,9 @@ a {
|
||||
.o-60-d {
|
||||
opacity: .6;
|
||||
}
|
||||
.focus-b--white-d:focus {
|
||||
border-color: #fff;
|
||||
}
|
||||
a {
|
||||
color: #fff;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import _ from 'lodash';
|
||||
import { uuid, stringToTa } from '/lib/util';
|
||||
import { stringToTa } from '/lib/util';
|
||||
import { store } from '/store';
|
||||
import moment from 'moment';
|
||||
|
||||
@ -13,10 +13,13 @@ class UrbitApi {
|
||||
|
||||
this.invite = {
|
||||
accept: this.inviteAccept.bind(this),
|
||||
decline: this.inviteDecline.bind(this),
|
||||
invite: this.inviteInvite.bind(this)
|
||||
decline: this.inviteDecline.bind(this)
|
||||
};
|
||||
|
||||
this.groups = {
|
||||
remove: this.groupRemove.bind(this)
|
||||
}
|
||||
|
||||
this.bind = this.bind.bind(this);
|
||||
this.bindLinkView = this.bindLinkView.bind(this);
|
||||
}
|
||||
@ -61,32 +64,26 @@ class UrbitApi {
|
||||
});
|
||||
}
|
||||
|
||||
inviteAction(data) {
|
||||
this.action("invite-store", "json", data);
|
||||
groupsAction(data) {
|
||||
this.action("group-store", "group-action", data);
|
||||
}
|
||||
|
||||
inviteInvite(path, ship) {
|
||||
this.action("invite-hook", "json",
|
||||
{
|
||||
invite: {
|
||||
path: '/chat',
|
||||
invite: {
|
||||
path,
|
||||
ship: `~${window.ship}`,
|
||||
recipient: ship,
|
||||
app: 'chat-hook',
|
||||
text: `You have been invited to /${window.ship}${path}`,
|
||||
},
|
||||
uid: uuid()
|
||||
}
|
||||
groupRemove(path, members) {
|
||||
this.groupsAction({
|
||||
remove: {
|
||||
path, members
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
inviteAction(data) {
|
||||
this.action("invite-store", "json", data);
|
||||
}
|
||||
|
||||
inviteAccept(uid) {
|
||||
this.inviteAction({
|
||||
accept: {
|
||||
path: '/chat',
|
||||
path: '/link',
|
||||
uid
|
||||
}
|
||||
});
|
||||
@ -95,7 +92,7 @@ class UrbitApi {
|
||||
inviteDecline(uid) {
|
||||
this.inviteAction({
|
||||
decline: {
|
||||
path: '/chat',
|
||||
path: '/link',
|
||||
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) {
|
||||
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() {
|
||||
let sidebarBoolean = true;
|
||||
if (store.state.sidebarShown === true) {
|
||||
|
@ -2,6 +2,7 @@ import React, { Component } from 'react';
|
||||
|
||||
import { Route, Link } from 'react-router-dom';
|
||||
import { ChannelsItem } from '/components/lib/channels-item';
|
||||
import { SidebarInvite } from '/components/lib/sidebar-invite';
|
||||
|
||||
export class ChannelsSidebar extends Component {
|
||||
// drawer to the left
|
||||
@ -9,59 +10,38 @@ export class ChannelsSidebar extends Component {
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
let privateChannel =
|
||||
Object.keys(props.groups)
|
||||
.filter((path) => {
|
||||
return (path === "/~/default")
|
||||
})
|
||||
.map((path) => {
|
||||
let name = "Private"
|
||||
let selected = (props.selected === path);
|
||||
let linkCount = !!props.links[path] ? props.links[path].totalItems : 0;
|
||||
const unseenCount = !!props.links[path]
|
||||
? props.links[path].unseenCount
|
||||
: linkCount
|
||||
let sidebarInvites = Object.keys(props.invites)
|
||||
.map((uid) => {
|
||||
return (
|
||||
<ChannelsItem
|
||||
key={path}
|
||||
link={path}
|
||||
memberList={props.groups[path]}
|
||||
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}/>
|
||||
)
|
||||
<SidebarInvite
|
||||
uid={uid}
|
||||
invite={props.invites[uid]}
|
||||
api={props.api} />
|
||||
);
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
@ -70,7 +50,7 @@ export class ChannelsSidebar extends Component {
|
||||
if (this.props.popout) {
|
||||
hiddenClasses = false;
|
||||
} else {
|
||||
hiddenClasses = this.props.sidebarShown;
|
||||
hiddenClasses = this.props.sidebarShown;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -81,15 +61,15 @@ export class ChannelsSidebar extends Component {
|
||||
: "dn")}>
|
||||
<a className="db dn-m dn-l dn-xl f8 pb3 pl3" href="/">⟵ Landscape</a>
|
||||
<div className="overflow-y-scroll h-100">
|
||||
<h2 className={"f8 f9-m f9-l f9-xl " +
|
||||
"pt1 pt4-m pt4-l pt4-xl " +
|
||||
"pr4 pb3 pb3-m pb3-l pb3-xl " +
|
||||
"pl3 pl4-m pl4-l pl4-xl " +
|
||||
"black-s gray2 white-d c-default " +
|
||||
"bb b--gray4 b--gray2-d mb2 mb0-m mb0-l mb0-xl"}>
|
||||
Your Collections
|
||||
</h2>
|
||||
{privateChannel}
|
||||
<div className="w-100 bg-transparent pa4 bb b--gray4 b--gray1-d"
|
||||
style={{paddingBottom: 10, paddingTop: 10}}>
|
||||
<Link
|
||||
className="dib f9 pointer green2 gray4-d mr4"
|
||||
to={"/~link/new"}>
|
||||
New Collection
|
||||
</Link>
|
||||
</div>
|
||||
{sidebarInvites}
|
||||
{channelItems}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,13 +1,13 @@
|
||||
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 {
|
||||
render() {
|
||||
const { props } = this;
|
||||
|
||||
let selectedClass = (props.selected)
|
||||
? "bg-gray5 bg-gray1-d b--gray4 b--gray2-d"
|
||||
let selectedClass = (props.selected)
|
||||
? "bg-gray5 bg-gray1-d b--gray4 b--gray2-d"
|
||||
: "b--gray4 b--gray2-d";
|
||||
|
||||
let memberCount = props.memberList
|
||||
@ -18,7 +18,7 @@ export class ChannelsItem extends Component {
|
||||
: null;
|
||||
|
||||
return (
|
||||
<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}>
|
||||
<p className="f9 pt1">{props.name}</p>
|
||||
<p className="f9 gray2">
|
||||
|
@ -1,13 +1,13 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Route, Link } from 'react-router-dom';
|
||||
import { base64urlEncode } from '../../lib/util';
|
||||
import { makeRoutePath } from '../../lib/util';
|
||||
|
||||
export class CommentsPagination extends Component {
|
||||
render() {
|
||||
let props = this.props;
|
||||
|
||||
let prevPage = "/" + (Number(props.commentPage) - 1);
|
||||
let nextPage = "/" + (Number(props.commentPage) + 1);
|
||||
let prevPage = (Number(props.commentPage) - 1);
|
||||
let nextPage = (Number(props.commentPage) + 1);
|
||||
|
||||
let prevDisplay = ((Number(props.commentPage) > 0))
|
||||
? "dib"
|
||||
@ -17,31 +17,16 @@ export class CommentsPagination extends Component {
|
||||
? "dib"
|
||||
: "dn";
|
||||
|
||||
let encodedUrl = base64urlEncode(props.url);
|
||||
let popout = (props.popout) ? "/popout" : "";
|
||||
|
||||
return (
|
||||
<div className="w-100 relative pt4 pb6">
|
||||
<Link
|
||||
className={"pb6 absolute inter f8 left-0 " + prevDisplay}
|
||||
to={"/~link"
|
||||
+ popout
|
||||
+ props.groupPath
|
||||
+ "/" + props.linkPage
|
||||
+ "/" + props.linkIndex
|
||||
+ "/" + encodedUrl
|
||||
+ "/comments" + prevPage}>
|
||||
to={makeRoutePath(props.resourcePath, props.popout, props.linkPage, props.url, props.linkIndex, prevPage)}>
|
||||
<- Previous Page
|
||||
</Link>
|
||||
<Link
|
||||
className={"pb6 absolute inter f8 right-0 " + nextDisplay}
|
||||
to={"/~link"
|
||||
+ popout
|
||||
+ props.groupPath
|
||||
+ "/" + props.linkPage
|
||||
+ "/" + props.linkIndex
|
||||
+ "/" + encodedUrl
|
||||
+ "/comments" + nextPage}>
|
||||
to={makeRoutePath(props.resourcePath, props.popout, props.linkPage, props.url, props.linkIndex, nextPage)}>
|
||||
Next Page ->
|
||||
</Link>
|
||||
</div>
|
||||
|
@ -19,7 +19,7 @@ export class Comments extends Component {
|
||||
) {
|
||||
this.setState({requested: this.props.commentPage});
|
||||
api.getCommentsPage(
|
||||
this.props.groupPath,
|
||||
this.props.resourcePath,
|
||||
this.props.url,
|
||||
this.props.commentPage);
|
||||
}
|
||||
@ -73,8 +73,8 @@ export class Comments extends Component {
|
||||
<div>
|
||||
{commentsList}
|
||||
<CommentsPagination
|
||||
key={props.groupPath + props.commentPage}
|
||||
groupPath={props.groupPath}
|
||||
key={props.resourcePath + props.commentPage}
|
||||
resourcePath={props.resourcePath}
|
||||
popout={props.popout}
|
||||
linkPage={props.linkPage}
|
||||
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 { Route, Link } from "react-router-dom";
|
||||
import { base64urlEncode } from "../../lib/util";
|
||||
import { makeRoutePath } from "../../lib/util";
|
||||
import moment from "moment";
|
||||
|
||||
export class LinkPreview extends Component {
|
||||
@ -114,16 +114,7 @@ export class LinkPreview extends Component {
|
||||
{this.state.timeSinceLinkPost}
|
||||
</span>
|
||||
<Link
|
||||
to={
|
||||
"/~link" +
|
||||
props.groupPath +
|
||||
"/" +
|
||||
props.page +
|
||||
"/" +
|
||||
props.linkIndex +
|
||||
"/" +
|
||||
base64urlEncode(props.url)
|
||||
}
|
||||
to={makeRoutePath(props.resourcePath, props.popout, props.page, props.url, props.linkIndex)}
|
||||
className="v-top">
|
||||
<span className="f9 inter gray2">{props.comments}</span>
|
||||
</Link>
|
||||
|
@ -3,7 +3,7 @@ import moment from 'moment';
|
||||
|
||||
import { Sigil } from '/components/lib/icons/sigil';
|
||||
import { Route, Link } from 'react-router-dom';
|
||||
import { base64urlEncode } from '../../lib/util';
|
||||
import { makeRoutePath } from '../../lib/util';
|
||||
|
||||
export class LinkItem extends Component {
|
||||
constructor(props) {
|
||||
@ -34,7 +34,7 @@ export class LinkItem extends Component {
|
||||
}
|
||||
|
||||
markPostAsSeen() {
|
||||
api.seenLink(this.props.groupPath, this.props.url);
|
||||
api.seenLink(this.props.resourcePath, this.props.url);
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -58,8 +58,6 @@ export class LinkItem extends Component {
|
||||
hostname = hostname[4];
|
||||
}
|
||||
|
||||
let encodedUrl = base64urlEncode(props.url);
|
||||
|
||||
let comments = props.comments + " comment" + ((props.comments === 1) ? "" : "s");
|
||||
|
||||
let member = this.props.member || false;
|
||||
@ -90,7 +88,7 @@ export class LinkItem extends Component {
|
||||
{this.state.timeSinceLinkPost}
|
||||
</span>
|
||||
<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"
|
||||
onClick={this.markPostAsSeen}>
|
||||
<span className="f9 inter gray2">
|
||||
|
@ -20,7 +20,7 @@ export class LinkSubmit extends Component {
|
||||
? this.state.linkTitle
|
||||
: this.state.linkValue;
|
||||
api.setSpinner(true);
|
||||
api.postLink(this.props.groupPath, link, title).then(r => {
|
||||
api.postLink(this.props.resourcePath, link, title).then(r => {
|
||||
api.setSpinner(false);
|
||||
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 {
|
||||
render() {
|
||||
let props = this.props;
|
||||
|
||||
let memColor = '',
|
||||
popout = '';
|
||||
setColor = '';
|
||||
|
||||
if (props.location.pathname.includes('/members')) {
|
||||
memColor = 'black';
|
||||
if (props.location.pathname.includes('/settings')) {
|
||||
memColor = 'gray3';
|
||||
setColor = 'black white-d';
|
||||
} else if (props.location.pathname.includes('/members')) {
|
||||
memColor = 'black white-d';
|
||||
setColor = 'gray3';
|
||||
} else {
|
||||
memColor = 'gray3';
|
||||
memColor = 'gray3';
|
||||
setColor = 'gray3';
|
||||
}
|
||||
|
||||
(props.location.pathname.includes('/popout'))
|
||||
? popout = "popout/"
|
||||
: popout = "";
|
||||
|
||||
let hidePopoutIcon = (this.props.popout)
|
||||
let hidePopoutIcon = (props.popout)
|
||||
? "dn-m dn-l dn-xl"
|
||||
: "dib-m dib-l dib-xl";
|
||||
|
||||
|
||||
return (
|
||||
<div className="dib pt2 flex-shrink-0 flex-grow-1">
|
||||
{!!props.isOwner ? (
|
||||
<div className={"dib f8 pl6"}>
|
||||
<div className="dib flex-shrink-0 flex-grow-1">
|
||||
{!!props.amOwner ? (
|
||||
<div className={"dib pt2 f9 pl6 lh-solid"}>
|
||||
<Link
|
||||
className={"no-underline " + memColor}
|
||||
to={`/~link/` + popout + `members` + props.groupPath}>
|
||||
to={makeRoutePath(props.resourcePath, props.popout) + '/members'}>
|
||||
Members
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="dib" style={{ width: 0 }}></div>
|
||||
)}
|
||||
<a href={`/~link/popout` + props.groupPath} target="_blank"
|
||||
className="dib fr">
|
||||
<div className={"dib pt2 f9 pl6 pr6 lh-solid"}>
|
||||
<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
|
||||
className={`flex-shrink-0 pr4 dn` + hidePopoutIcon}
|
||||
className={`flex-shrink-0 pr3 dn ` + hidePopoutIcon}
|
||||
src="/~link/img/popout.png"
|
||||
height="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 { Route, Link } from 'react-router-dom';
|
||||
import { makeRoutePath } from '../../lib/util';
|
||||
|
||||
export class Pagination extends Component {
|
||||
render() {
|
||||
let props = this.props;
|
||||
|
||||
let prevPage = "/" + (Number(props.page) - 1);
|
||||
let nextPage = "/" + (Number(props.page) + 1);
|
||||
let prevPage = (Number(props.page) - 1);
|
||||
let nextPage = (Number(props.page) + 1);
|
||||
|
||||
let prevDisplay = ((props.currentPage > 0))
|
||||
? "dib absolute left-0"
|
||||
: "dn";
|
||||
|
||||
let nextDisplay = ((props.currentPage + 1) < props.totalPages)
|
||||
? "dib absolute right-0"
|
||||
let nextDisplay = ((props.currentPage + 1) < props.totalPages)
|
||||
? "dib absolute right-0"
|
||||
: "dn";
|
||||
|
||||
return (
|
||||
<div className="w-100 inter relative pv6">
|
||||
<div className={prevDisplay + " inter f8"}>
|
||||
<Link to={"/~link" + props.popout + props.groupPath + prevPage}>
|
||||
<Link to={makeRoutePath(props.resourcePath, props.popout, prevPage)}>
|
||||
<- Previous Page
|
||||
</Link>
|
||||
</div>
|
||||
<div className={nextDisplay + " inter f8"}>
|
||||
<Link to={"/~link" + props.popout + props.groupPath + nextPage}>
|
||||
<Link to={makeRoutePath(props.resourcePath, props.popout, nextPage)}>
|
||||
Next Page ->
|
||||
</Link>
|
||||
</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 { Route, Link } from 'react-router-dom';
|
||||
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 {
|
||||
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 (!this.state.data.title) {
|
||||
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.postComment(
|
||||
this.props.groupPath,
|
||||
this.props.resourcePath,
|
||||
url,
|
||||
this.state.comment
|
||||
).then(() => {
|
||||
@ -62,9 +63,13 @@ export class LinkDetail extends Component {
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
let popout = (props.popout) ? "/popout" : "";
|
||||
|
||||
const data = this.state.data || props.data;
|
||||
|
||||
if (!data.ship) {
|
||||
return <LoadingScreen/>;
|
||||
}
|
||||
|
||||
let ship = data.ship || "zod";
|
||||
let title = data.title || "";
|
||||
let url = data.url || "";
|
||||
@ -98,10 +103,10 @@ export class LinkDetail extends Component {
|
||||
/>
|
||||
<Link
|
||||
className="dib f9 fw4 pt2 gray2 lh-solid"
|
||||
to={"/~link" + popout + props.groupPath + "/" + props.page}>
|
||||
{"<- Collection index"}
|
||||
to={makeRoutePath(props.resourcePath, props.popout, props.page)}>
|
||||
{`<- ${props.resource.title} index`}
|
||||
</Link>
|
||||
<LinksTabBar {...props} popout={popout} groupPath={props.groupPath} />
|
||||
<LinksTabBar {...props} popout={props.popout} resourcePath={props.resourcePath} />
|
||||
</div>
|
||||
<div className="w-100 mt2 flex justify-center overflow-y-scroll ph4 pb4">
|
||||
<div className="w-100 mw7">
|
||||
@ -111,7 +116,7 @@ export class LinkDetail extends Component {
|
||||
comments={comments}
|
||||
nickname={nickname}
|
||||
ship={ship}
|
||||
groupPath={props.groupPath}
|
||||
resourcePath={props.resourcePath}
|
||||
page={props.page}
|
||||
linkIndex={props.linkIndex}
|
||||
time={this.state.data.time}
|
||||
@ -143,8 +148,8 @@ export class LinkDetail extends Component {
|
||||
</button>
|
||||
</div>
|
||||
<Comments
|
||||
groupPath={props.groupPath}
|
||||
key={props.groupPath + props.commentPage}
|
||||
resourcePath={props.resourcePath}
|
||||
key={props.resourcePath + props.commentPage}
|
||||
comments={props.comments}
|
||||
commentPage={props.commentPage}
|
||||
contacts={props.contacts}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, { Component } from 'react'
|
||||
import { LoadingScreen } from './loading';
|
||||
import { LinksTabBar } from './lib/links-tabbar';
|
||||
import { SidebarSwitcher } from '/components/lib/icons/icon-sidebar-switch.js';
|
||||
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 { Pagination } from '/components/lib/pagination.js';
|
||||
|
||||
import { getContactDetails } from '../lib/util';
|
||||
import { makeRoutePath, getContactDetails } from '../lib/util';
|
||||
|
||||
//TODO Avatar support once it's in
|
||||
export class Links extends Component {
|
||||
@ -25,17 +26,21 @@ export class Links extends Component {
|
||||
(!this.props.links[linkPage] ||
|
||||
this.props.links.local[linkPage])
|
||||
) {
|
||||
api.getPage(this.props.groupPath, this.props.page);
|
||||
api.getPage(this.props.resourcePath, this.props.page);
|
||||
}
|
||||
}
|
||||
|
||||
markAllAsSeen() {
|
||||
api.seenLink(this.props.groupPath);
|
||||
api.seenLink(this.props.resourcePath);
|
||||
}
|
||||
|
||||
render() {
|
||||
let props = this.props;
|
||||
let popout = (props.popout) ? "/popout" : "";
|
||||
|
||||
if (!props.resource.title) {
|
||||
return <LoadingScreen/>;
|
||||
}
|
||||
|
||||
let linkPage = props.page;
|
||||
|
||||
let links = !!props.links[linkPage]
|
||||
@ -77,8 +82,8 @@ export class Links extends Component {
|
||||
color={color}
|
||||
member={member}
|
||||
comments={commentCount}
|
||||
groupPath={props.groupPath}
|
||||
popout={popout}
|
||||
resourcePath={props.resourcePath}
|
||||
popout={props.popout}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@ -89,36 +94,31 @@ export class Links extends Component {
|
||||
<div
|
||||
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
|
||||
style={{ height: "1rem" }}>
|
||||
<Link to="/~link/">{"⟵ All Channels"}</Link>
|
||||
<Link to="/~link">{"⟵ All Channels"}</Link>
|
||||
</div>
|
||||
<div
|
||||
className={`pl4 pt2 flex relative overflow-x-scroll
|
||||
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 }}>
|
||||
<SidebarSwitcher
|
||||
sidebarShown={props.sidebarShown}
|
||||
popout={props.popout}/>
|
||||
<Link to={`/~link` + popout + props.groupPath} className="pt2">
|
||||
<h2
|
||||
className={`dib f9 fw4 v-top lh-solid` +
|
||||
(props.groupPath.includes("/~/")
|
||||
? ""
|
||||
: " mono")}>
|
||||
{(props.groupPath.includes("/~/"))
|
||||
? "Private"
|
||||
: props.groupPath.substr(1)}
|
||||
<Link to={makeRoutePath(props.resourcePath, props.popout, props.page)} className="pt2">
|
||||
<h2 className={`dib f9 fw4 v-top`}>
|
||||
{props.resource.title}
|
||||
</h2>
|
||||
</Link>
|
||||
<LinksTabBar
|
||||
{...props}
|
||||
popout={popout}
|
||||
groupPath={props.groupPath + "/" + props.page}/>
|
||||
popout={props.popout}
|
||||
page={props.page}
|
||||
resourcePath={props.resourcePath}/>
|
||||
</div>
|
||||
<div className="w-100 mt2 flex justify-center overflow-y-scroll ph4 pb4">
|
||||
<div className="w-100 mw7">
|
||||
<div className="flex">
|
||||
<LinkSubmit groupPath={props.groupPath}/>
|
||||
<LinkSubmit resourcePath={props.resourcePath}/>
|
||||
</div>
|
||||
<div className="pb4">
|
||||
<span
|
||||
@ -129,9 +129,9 @@ export class Links extends Component {
|
||||
{LinkList}
|
||||
<Pagination
|
||||
{...props}
|
||||
key={props.groupPath + props.page}
|
||||
popout={popout}
|
||||
groupPath={props.groupPath}
|
||||
key={props.resourcePath + props.page}
|
||||
popout={props.popout}
|
||||
resourcePath={props.resourcePath}
|
||||
currentPage={currentPage}
|
||||
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 { BrowserRouter, Route, Link } from "react-router-dom";
|
||||
import { BrowserRouter, Switch, Route, Link } from "react-router-dom";
|
||||
import classnames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
|
||||
@ -7,10 +7,17 @@ import { api } from '/api';
|
||||
import { subscription } from '/subscription';
|
||||
import { store } from '/store';
|
||||
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 { 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 {
|
||||
constructor(props) {
|
||||
@ -26,62 +33,169 @@ export class Root extends Component {
|
||||
let contacts = !!state.contacts ? state.contacts : {};
|
||||
const groups = !!state.groups ? state.groups : {};
|
||||
|
||||
const resources = !!state.resources ? state.resources : {};
|
||||
let links = !!state.links ? state.links : {};
|
||||
let comments = !!state.comments ? state.comments : {};
|
||||
const seen = !!state.seen ? state.seen : {};
|
||||
|
||||
const invites = '/link' in state.invites ?
|
||||
state.invites['/link'] : {};
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<BrowserRouter><Switch>
|
||||
<Route exact path="/~link"
|
||||
render={ (props) => {
|
||||
return (
|
||||
<Skeleton
|
||||
active="channels"
|
||||
active="collections"
|
||||
spinner={state.spinner}
|
||||
resources={resources}
|
||||
invites={invites}
|
||||
groups={groups}
|
||||
rightPanelHide={true}
|
||||
sidebarShown={true}
|
||||
sidebarShown={state.sidebarShown}
|
||||
links={links}>
|
||||
<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">
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</Skeleton>
|
||||
);
|
||||
}} />
|
||||
<Route exact path="/~link/(popout)?/:ship/:channel/:page?"
|
||||
render={ (props) => {
|
||||
// groups/contacts and link channels are the same thing in ver 1
|
||||
<Route exact path="/~link/new"
|
||||
render={(props) => {
|
||||
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 =
|
||||
`/${props.match.params.ship}/${props.match.params.channel}`;
|
||||
let contactDetails = contacts[groupPath] || {};
|
||||
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}>
|
||||
<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 popout = props.match.url.includes("/popout/");
|
||||
|
||||
let channelLinks = !!links[groupPath]
|
||||
? links[groupPath]
|
||||
let channelLinks = !!links[resourcePath]
|
||||
? links[resourcePath]
|
||||
: {local: {}};
|
||||
|
||||
let channelComments = !!comments[groupPath]
|
||||
? comments[groupPath]
|
||||
let channelComments = !!comments[resourcePath]
|
||||
? comments[resourcePath]
|
||||
: {};
|
||||
|
||||
const channelSeen = !!seen[groupPath]
|
||||
? seen[groupPath]
|
||||
const channelSeen = !!seen[resourcePath]
|
||||
? seen[resourcePath]
|
||||
: {};
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
spinner={state.spinner}
|
||||
resources={resources}
|
||||
invites={invites}
|
||||
groups={groups}
|
||||
active="links"
|
||||
selected={groupPath}
|
||||
selected={resourcePath}
|
||||
sidebarShown={state.sidebarShown}
|
||||
sidebarHideMobile={true}
|
||||
popout={popout}
|
||||
@ -93,7 +207,9 @@ export class Root extends Component {
|
||||
comments={channelComments}
|
||||
seen={channelSeen}
|
||||
page={page}
|
||||
groupPath={groupPath}
|
||||
resourcePath={resourcePath}
|
||||
resource={resource}
|
||||
amOwner={amOwner}
|
||||
popout={popout}
|
||||
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) => {
|
||||
let groupPath =
|
||||
`/${props.match.params.ship}/${props.match.params.channel}`;
|
||||
const resourcePath = '/' + props.match.params.resource;
|
||||
const resource = resources[resourcePath] || {};
|
||||
|
||||
const amOwner = amOwnerOfGroup(resource.group);
|
||||
|
||||
let popout = props.match.url.includes("/popout/");
|
||||
|
||||
let contactDetails = contacts[groupPath] || {};
|
||||
let contactDetails = contacts[resource.group] || {};
|
||||
|
||||
let index = props.match.params.index || 0;
|
||||
let page = props.match.params.page || 0;
|
||||
let url = base64urlDecode(props.match.params.encodedUrl);
|
||||
|
||||
let data = !!links[groupPath]
|
||||
? !!links[groupPath][page]
|
||||
? links[groupPath][page][index]
|
||||
let data = !!links[resourcePath]
|
||||
? !!links[resourcePath][page]
|
||||
? links[resourcePath][page][index]
|
||||
: {}
|
||||
: {};
|
||||
let coms = !comments[groupPath]
|
||||
let coms = !comments[resourcePath]
|
||||
? undefined
|
||||
: comments[groupPath][url];
|
||||
: comments[resourcePath][url];
|
||||
|
||||
let commentPage = props.match.params.commentpage || 0;
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
spinner={state.spinner}
|
||||
resources={resources}
|
||||
invites={invites}
|
||||
groups={groups}
|
||||
active="links"
|
||||
selected={groupPath}
|
||||
selected={resourcePath}
|
||||
sidebarShown={state.sidebarShown}
|
||||
sidebarHideMobile={true}
|
||||
popout={popout}
|
||||
links={links}>
|
||||
<LinkDetail
|
||||
{...props}
|
||||
resource={resource}
|
||||
page={page}
|
||||
url={url}
|
||||
linkIndex={index}
|
||||
contacts={contactDetails}
|
||||
groupPath={groupPath}
|
||||
resourcePath={resourcePath}
|
||||
groupPath={resource.group}
|
||||
amOwner={amOwner}
|
||||
popout={popout}
|
||||
sidebarShown={state.sidebarShown}
|
||||
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} />
|
||||
<div className={`cf w-100 h-100 flex ` + popoutBorder}>
|
||||
<ChannelsSidebar
|
||||
popout={popout}
|
||||
groups={this.props.groups}
|
||||
active={this.props.active}
|
||||
popout={popout}
|
||||
resources={this.props.resources}
|
||||
invites={this.props.invites}
|
||||
groups={this.props.groups}
|
||||
selected={this.props.selected}
|
||||
sidebarShown={this.props.sidebarShown}
|
||||
links={this.props.links}/>
|
||||
|
@ -1,17 +1,27 @@
|
||||
import _ from 'lodash';
|
||||
import classnames from 'classnames';
|
||||
|
||||
|
||||
export function uuid() {
|
||||
let str = "0v"
|
||||
str += Math.ceil(Math.random()*8)+"."
|
||||
for (var i = 0; i < 5; i++) {
|
||||
let _str = Math.ceil(Math.random()*10000000).toString(32);
|
||||
_str = ("00000"+_str).substr(-5,5);
|
||||
str += _str+".";
|
||||
export function makeRoutePath(
|
||||
resource, popout = false, page = 0, url = null, index = 0, compage = 0
|
||||
) {
|
||||
let route = '/~link' + (popout ? '/popout' : '') + resource;
|
||||
if (!url) {
|
||||
if (page !== 0) {
|
||||
route = route + '/' + page;
|
||||
}
|
||||
} 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) {
|
||||
|
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.decline(data, state);
|
||||
}
|
||||
data = _.get(json, 'invite-initial', false);
|
||||
if (data) {
|
||||
state.invites = data;
|
||||
}
|
||||
}
|
||||
|
||||
create(json, state) {
|
||||
@ -37,7 +41,6 @@ export class InviteUpdateReducer {
|
||||
accepted(json, state) {
|
||||
let data = _.get(json, 'accepted', false);
|
||||
if (data) {
|
||||
console.log(data);
|
||||
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 { GroupUpdateReducer } from '/reducers/group-update';
|
||||
import { ContactUpdateReducer } from '/reducers/contact-update.js';
|
||||
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 { LocalReducer } from '/reducers/local.js';
|
||||
import _ from 'lodash';
|
||||
@ -11,6 +14,8 @@ class Store {
|
||||
this.state = {
|
||||
contacts: {},
|
||||
groups: {},
|
||||
resources: {},
|
||||
invites: {},
|
||||
links: {},
|
||||
comments: {},
|
||||
seen: {},
|
||||
@ -20,8 +25,11 @@ class Store {
|
||||
};
|
||||
|
||||
this.initialReducer = new InitialReducer();
|
||||
this.groupUpdateReducer = new GroupUpdateReducer();
|
||||
this.contactUpdateReducer = new ContactUpdateReducer();
|
||||
this.permissionUpdateReducer = new PermissionUpdateReducer();
|
||||
this.metadataReducer = new MetadataReducer();
|
||||
this.inviteUpdateReducer = new InviteUpdateReducer();
|
||||
this.localReducer = new LocalReducer();
|
||||
this.linkUpdateReducer = new LinkUpdateReducer();
|
||||
this.setState = () => {};
|
||||
@ -41,8 +49,11 @@ class Store {
|
||||
|
||||
console.log('event', json);
|
||||
this.initialReducer.reduce(json, this.state);
|
||||
this.groupUpdateReducer.reduce(json, this.state);
|
||||
this.contactUpdateReducer.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.linkUpdateReducer.reduce(json, this.state);
|
||||
|
||||
|
@ -11,16 +11,25 @@ export class Subscription {
|
||||
}
|
||||
|
||||
initializeLinks() {
|
||||
// add invite, permissions flows once link stores are more than
|
||||
// group-specific
|
||||
api.bind('/all', 'PUT', api.authTokens.ship, 'group-store',
|
||||
this.handleEvent.bind(this),
|
||||
this.handleError.bind(this),
|
||||
this.handleQuitAndResubscribe.bind(this));
|
||||
this.handleEvent.bind(this),
|
||||
this.handleError.bind(this),
|
||||
this.handleQuitAndResubscribe.bind(this)
|
||||
);
|
||||
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.handleError.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
|
||||
api.getPage('', 0);
|
||||
|
Loading…
Reference in New Issue
Block a user