From 9c0ad13abd1b9be613ff6076a9a6c5e54ebcd62c Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Tue, 12 Jan 2021 11:11:20 +1000 Subject: [PATCH 01/27] group-store: add resource field to app-tag Adds a resource field to the app-tag type, as apps will generally want to use tags to identify a specific resource --- pkg/arvo/app/group-store.hoon | 125 ++++++++++------------------------ pkg/arvo/lib/group-store.hoon | 3 + pkg/arvo/sur/group-store.hoon | 19 ------ pkg/arvo/sur/group.hoon | 18 ++++- 4 files changed, 55 insertions(+), 110 deletions(-) diff --git a/pkg/arvo/app/group-store.hoon b/pkg/arvo/app/group-store.hoon index 43bcb67f0..af040db66 100644 --- a/pkg/arvo/app/group-store.hoon +++ b/pkg/arvo/app/group-store.hoon @@ -37,26 +37,24 @@ +$ versioned-state $% state-zero state-one + state-two == :: +$ state-zero - $: %0 - =groups:state-zero:store - == -:: + [%0 *] :: +$ state-one $: %1 - =groups + =groups:groups-state-one == :: -+$ diff - $% [%group-update update:store] - [%group-initial groups] ++$ state-two + $: %2 + =groups == -- :: -=| state-one +=| state-two =* state - :: %- agent:dbug @@ -74,90 +72,37 @@ ++ on-load |= =old=vase =/ old !<(versioned-state old-vase) - ?: ?=(%1 -.old) - `this(state old) |^ - :- :~ [%pass / %agent [our.bowl dap.bowl] %poke %noun !>(%perm-upgrade)] - kick-all - == - =* paths ~(key by groups.old) - =/ [unmanaged=(list path) managed=(list path)] - (skid ~(tap in paths) |=(=path =('~' (snag 0 path)))) - =. groups (all-unmanaged unmanaged) - =. groups (all-managed managed) - this - :: - ++ all-managed - |= paths=(list path) - ^+ groups - ?~ paths - groups - =/ [rid=resource =group] - (migrate-group i.paths) - %= $ - paths t.paths - :: - groups - (~(put by groups) rid group) + ?- -.old + %2 `this(state old) + :: + %1 + %_ $ + -.old %2 + groups.old (groups-1-to-2 groups.old) == + :: + %0 $(old *state-two) + == :: - ++ all-unmanaged - |= paths=(list path) - ^+ groups - ?~ paths - groups - ?: |(=(/~/default i.paths) =(4 (lent i.paths))) - $(paths t.paths) - =/ [=resource =group] - (migrate-unmanaged i.paths) - %= $ - paths t.paths - :: - groups - (~(put by groups) resource group) - == - ++ kick-all - ^- card - :+ %give %kick - :_ ~ - %~ tap by - %+ roll ~(val by sup.bowl) - |= [[=ship pax=path] paths=(set path)] - (~(put in paths) pax) - :: - ++ migrate-unmanaged - |= pax=path - ^- [resource group] - =/ members=(set ship) - (~(got by groups.old) pax) - =| =invite:policy - ?> ?=(^ pax) - =/ rid=resource - (resource-from-old-path t.pax) + ++ groups-1-to-2 + |= =groups:groups-state-one + ^+ ^groups + %- ~(run by groups) + |= =group:groups-state-one =/ =tags - (~(put ju *tags) %admin entity.rid) - :- rid - [members tags invite %.y] - :: - ++ resource-from-old-path - |= pax=path - ^- resource - ?> ?=([@ @ *] pax) - =/ ship - (slav %p i.pax) - [ship i.t.pax] - :: - ++ migrate-group - |= pax=path - =/ members - (~(got by groups.old) pax) - =| =invite:policy - =/ rid=resource - (resource-from-old-path pax) - =/ =tags - (~(put ju *tags) %admin entity.rid) - [rid members tags invite %.n] + (tags-1-to-2 tags.group) + [members.group tags [policy hidden]:group] :: + ++ tags-1-to-2 + |= =tags:groups-state-one + ^- ^tags + %- ~(gas by *^tags) + %+ murn + ~(tap by tags) + |= [=tag:groups-state-one ships=(set ship)] + ?^ tag ~ + `[tag ships] -- :: ++ on-poke @@ -273,8 +218,8 @@ |= arc=* ^- (quip card _state) |^ - =/ sty=state-one - [%1 (remake-groups ;;((tree [resource tree-group]) +.arc))] + =/ sty=state-two + [%2 (remake-groups ;;((tree [resource tree-group]) +.arc))] :_ sty %+ roll ~(tap by groups.sty) |= [[rid=resource grp=group] out=(list card)] diff --git a/pkg/arvo/lib/group-store.hoon b/pkg/arvo/lib/group-store.hoon index da60c2926..27348b053 100644 --- a/pkg/arvo/lib/group-store.hoon +++ b/pkg/arvo/lib/group-store.hoon @@ -167,6 +167,7 @@ %+ turn ~(tap in sit) item + :: ++ tag |= =^tag ^- json @@ -175,6 +176,7 @@ %- pairs :~ app+s+app.tag tag+s+tag.tag + resource+s+(enjs-path:resource resource.tag) == :: ++ policy @@ -366,6 +368,7 @@ %. json %- ot :~ app+so + resource+dejs-path:resource tag+so == diff --git a/pkg/arvo/sur/group-store.hoon b/pkg/arvo/sur/group-store.hoon index 6905704a9..5a3931f95 100644 --- a/pkg/arvo/sur/group-store.hoon +++ b/pkg/arvo/sur/group-store.hoon @@ -2,25 +2,6 @@ ^? |% :: -++ state-zero - |% - +$ group (set ship) - :: - +$ group-action - $% [%add members=group pax=path] :: add member to group - [%remove members=group pax=path] :: remove member from group - [%bundle pax=path] :: create group at path - [%unbundle pax=path] :: delete group at path - == - :: - +$ group-update - $% [%keys keys=(set path)] :: keys have changed - [%path members=group pax=path] - group-action - == -:: - +$ groups (map path group) - -- :: $action: request to change group-store state :: :: %add-group: add a group diff --git a/pkg/arvo/sur/group.hoon b/pkg/arvo/sur/group.hoon index 837276d6b..c5806493d 100644 --- a/pkg/arvo/sur/group.hoon +++ b/pkg/arvo/sur/group.hoon @@ -2,6 +2,22 @@ :: ^? |% +:: +++ groups-state-one + |% + +$ groups (map resource group) + :: + +$ tag $@(group-tag [app=term tag=term]) + :: + +$ tags (jug tag ship) + :: + +$ group + $: members=(set ship) + =tags + =policy + hidden=? + == + -- :: $groups: a mapping from group-ids to groups :: +$ groups (map resource group) @@ -16,7 +32,7 @@ :: Tags may be used and recognised differently across apps. :: for example, you could use tags like `%author`, `%bot`, `%flagged`... :: -+$ tag $@(group-tag [app=term tag=term]) ++$ tag $@(group-tag [app=term =resource tag=term]) :: $role-tag: a kind of $group-tag that identifies a privileged user :: :: These roles are From ad05ddcc345d1dd2b5416ac837b597cc6cf28d8c Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Tue, 26 Jan 2021 12:42:44 +1000 Subject: [PATCH 02/27] graph-hooks: %add-nodes permissioning --- pkg/arvo/app/graph-push-hook.hoon | 192 ++++++++++++++++----- pkg/arvo/app/metadata-hook.hoon | 2 + pkg/arvo/lib/graph.hoon | 4 + pkg/arvo/lib/group-store.hoon | 43 ++--- pkg/arvo/lib/group.hoon | 8 + pkg/arvo/lib/metadata-json.hoon | 2 +- pkg/arvo/mar/graph/permissions/add.hoon | 12 ++ pkg/arvo/mar/graph/permissions/remove.hoon | 12 ++ pkg/arvo/mar/graph/validator/chat.hoon | 1 + pkg/arvo/mar/graph/validator/publish.hoon | 23 ++- pkg/arvo/sur/graph-store.hoon | 13 ++ 11 files changed, 236 insertions(+), 76 deletions(-) create mode 100644 pkg/arvo/mar/graph/permissions/add.hoon create mode 100644 pkg/arvo/mar/graph/permissions/remove.hoon diff --git a/pkg/arvo/app/graph-push-hook.hoon b/pkg/arvo/app/graph-push-hook.hoon index c862a53c0..93477e7ad 100644 --- a/pkg/arvo/app/graph-push-hook.hoon +++ b/pkg/arvo/app/graph-push-hook.hoon @@ -1,3 +1,4 @@ +/- *group /+ store=graph-store /+ metadata /+ res=resource @@ -20,77 +21,76 @@ :: +$ agent (push-hook:push-hook config) :: -++ is-allowed - |= [=resource:res =bowl:gall requires-admin=?] - ^- ? - =/ grp ~(. group bowl) - =/ met ~(. metadata bowl) - =/ group=(unit resource:res) - (peek-group:met %graph resource) - ?~ group %.n - ?: requires-admin - (is-admin:grp src.bowl u.group) - ?| (is-member:grp src.bowl u.group) - (is-admin:grp src.bowl u.group) - == -:: -++ is-allowed-remove - |= [=resource:res indices=(set index:store) =bowl:gall] - ^- ? - =/ gra ~(. graph bowl) - ?. (is-allowed resource bowl %.n) - %.n - %+ levy - ~(tap in indices) - |= =index:store - ^- ? - =/ =node:store - (got-node:gra resource index) - ?| =(author.post.node src.bowl) - (is-allowed resource bowl %.y) - == ++$ state-null ~ ++$ state-zero [%0 marks=(set mark)] ++$ versioned-state + $@ state-null + state-zero -- :: +=| state-zero +=* state - %- agent:dbug ^- agent:gall %- (agent:push-hook config) ^- agent +=< |_ =bowl:gall +* this . def ~(. (default-agent this %|) bowl) grp ~(. group bowl) gra ~(. graph bowl) + hc ~(. +> bowl) :: ++ on-init on-init:def -++ on-save !>(~) -++ on-load on-load:def +++ on-save !>(state) +++ on-load + |= =vase + =+ !<(old=versioned-state vase) + =? old ?=(~ old) + [%0 ~] + ?> ?=(%0 -.old) + `this(state old) +:: ++ on-poke on-poke:def ++ on-agent on-agent:def ++ on-watch on-watch:def ++ on-leave on-leave:def ++ on-peek on-peek:def -++ on-arvo on-arvo:def +++ on-arvo + |= [=wire =sign-arvo] + ^- (quip card _this) + ?+ wire (on-arvo:def wire sign-arvo) + :: + [%perms @ @ ~] + ?> ?=(?(%add %remove) i.t.t.wire) + =* mark i.t.wire + :_ this + (build-permissions mark i.t.t.wire %next)^~ + == +:: ++ on-fail on-fail:def :: ++ should-proxy-update |= =vase ^- ? =/ =update:store !<(update:store vase) + =* rid resource.q.update ?- -.q.update - %add-graph (is-allowed resource.q.update bowl %.y) - %remove-graph (is-allowed resource.q.update bowl %.y) - %add-nodes (is-allowed resource.q.update bowl %.n) - %remove-nodes (is-allowed-remove resource.q.update indices.q.update bowl) - %add-signatures (is-allowed resource.uid.q.update bowl %.n) - %remove-signatures (is-allowed resource.uid.q.update bowl %.y) - %archive-graph (is-allowed resource.q.update bowl %.y) + %add-graph %.n + %remove-graph %.n + %add-nodes (is-allowed-add:hc resource.q.update nodes.q.update) + %remove-nodes (is-allowed-remove:hc resource.q.update indices.q.update bowl) + %add-signatures %.n + %remove-signatures %.n + %archive-graph %.n %unarchive-graph %.n %add-tag %.n %remove-tag %.n %keys %.n %tags %.n %tag-queries %.n - %run-updates (is-allowed resource.q.update bowl %.y) + %run-updates %.n == :: ++ initial-watch @@ -115,6 +115,15 @@ ^- [(list card) agent] =/ =update:store !<(update:store vase) ?+ -.q.update [~ this] + %add-graph + ?~ mark.q.update `this + =* mark u.mark.q.update + ?: (~(has in marks) mark) `this + :_ this(marks (~(put in marks) mark)) + :~ (build-permissions:hc mark %add %sing) + (build-permissions:hc mark %remove %sing) + == + :: %remove-graph :_ this [%give %kick ~[resource+(en-path:res resource.q.update)] ~]~ @@ -124,3 +133,106 @@ [%give %kick ~[resource+(en-path:res resource.q.update)] ~]~ == -- +|_ =bowl:gall ++* grp ~(. group bowl) + met ~(. metadata bowl) + gra ~(. graph bowl) +++ scry + |= [care=@t desk=@t =path] + %+ weld + /[care]/(scot %p our.bowl)/[desk]/(scot %da now.bowl) + path +:: +++ scry-mark + |= =resource:res + .^ (unit mark) + (scry %gx %graph-store /graph-mark/(scot %p entity.resource)/[name.resource]/noun) + == + +++ add-mark + |= [=resource:res =indexed-post:store vip=vip-metadata:met] + ^- permissions:store + =- (check vip) + !< check=$-(vip-metadata:met permissions:store) + %. !>(indexed-post) + =/ mark=(unit mark) + (get-mark:gra resource) + ?~ mark |=(=vase !>([%no %no %no])) + .^(tube:clay (scry %cc %home /[u.mark]/graph-add-permissions)) +:: +++ get-permission + |= [=permissions:store role=(unit role-tag) writers=(set ship)] + ^- permission-level:store + ?: ?=(?([~ %admin] [~ %moderator]) role) + admin.permissions + ?: =(~ writers) + writer.permissions + ?: (~(has in writers) src.bowl) + writer.permissions + reader.permissions +:: +++ is-allowed + |= * + %.y +:: +++ is-allowed-add + |= [=resource:res nodes=(map index:store node:store)] + ^- ? + =/ assoc=(unit association:met) + (peek-association:met %graph resource) + ?~ assoc %.n + =/ role=(unit (unit role-tag)) + (role-for-ship:grp group.u.assoc src.bowl) + =/ writers=(set ship) + (get-tagged-ships:grp group.u.assoc [%graph resource %writers]) + ?~ role %.n + =/ is-admin=? + ?=(?([~ %admin] [~ %moderator]) u.role) + %+ levy ~(tap by nodes) + |= [=index:store =node:store] + =/ =permissions:store + %^ add-mark resource + [(snag (dec (lent index)) index) post.node] + vip.metadata.u.assoc + =/ =permission-level:store + (get-permission permissions u.role writers) + ~& permission-level + ?- permission-level + %yes %.y + %no %.n + :: + %self + =/ parent-index=index:store + (scag (dec (lent index)) index) + =/ parent-node=node:store + (got-node:gra resource parent-index) + =(author.post.node src.bowl) + == +:: +++ is-allowed-remove + |= [=resource:res indices=(set index:store) =bowl:gall] + ^- ? + =/ gra ~(. graph bowl) + ?. (is-allowed resource bowl %.n) + %.n + %+ levy + ~(tap in indices) + |= =index:store + ^- ? + =/ =node:store + (got-node:gra resource index) + ?| =(author.post.node src.bowl) + (is-allowed resource bowl %.y) + == +:: +++ build-permissions + |= [=mark kind=?(%add %remove) mode=?(%sing %next)] + ^- card + =/ =wire /perms/[mark]/[kind] + =/ perm-mark=@t + (cat 3 %graph-permissions kind) + =/ =mood:clay [%c da+now.bowl /[mark]/[perm-mark]] + =/ =rave:clay ?:(?=(%sing mode) [mode mood] [mode mood]) + [%pass wire %arvo %c %warp our.bowl %home `rave] +-- + diff --git a/pkg/arvo/app/metadata-hook.hoon b/pkg/arvo/app/metadata-hook.hoon index 8d0d99e33..edd91bac2 100644 --- a/pkg/arvo/app/metadata-hook.hoon +++ b/pkg/arvo/app/metadata-hook.hoon @@ -39,6 +39,8 @@ ++ on-save !>(state) ++ on-load |= =vase + ?: =(1 1) + `this =+ !<(old=versioned-state vase) |^ ?: ?=(%2 -.old) diff --git a/pkg/arvo/lib/graph.hoon b/pkg/arvo/lib/graph.hoon index 6d78a206f..9285ef663 100644 --- a/pkg/arvo/lib/graph.hoon +++ b/pkg/arvo/lib/graph.hoon @@ -104,4 +104,8 @@ index (snoc index atom) nodes (tap:orm:store p.children.node) == +:: +++ get-mark + |= res=resource + (scry-for ,(unit mark) /graph-mark/(scot %p entity.res)/[name.res]) -- diff --git a/pkg/arvo/lib/group-store.hoon b/pkg/arvo/lib/group-store.hoon index 27348b053..dedc9dd4d 100644 --- a/pkg/arvo/lib/group-store.hoon +++ b/pkg/arvo/lib/group-store.hoon @@ -127,38 +127,17 @@ ++ tags |= =^tags ^- json - |^ - :- %o - (~(uni by app) group) - ++ group - ^- (map @t json) - %- malt - %+ murn - ~(tap by tags) - |= [=^tag ships=(^set ^ship)] - ^- (unit [@t json]) - ?^ tag - ~ - `[tag (set ship ships)] - ++ app - ^- (map @t json) - =| app-tags=(map @t json) - =/ tags ~(tap by tags) - |- - ?~ tags - app-tags - =* tag i.tags - ?@ p.tag - $(tags t.tags) - =/ app=json - (~(gut by app-tags) app.p.tag [%o ~]) - ?> ?=(%o -.app) - =. p.app - (~(put by p.app) tag.p.tag (set ship q.tag)) - =. app-tags - (~(put by app-tags) app.p.tag app) - $(tags t.tags) - -- + %- pairs + %+ turn ~(tap by tags) + |= [=^tag ships=(^set ^ship)] + ^- [@t json] + :_ (set ship ships) + ?@ tag tag + ;: (cury cat 3) + app.tag '\\' + tag.tag '\\' + (enjs-path:resource resource.tag) + == :: ++ set |* [item=$-(* json) sit=(^set)] diff --git a/pkg/arvo/lib/group.hoon b/pkg/arvo/lib/group.hoon index ecacecd4c..221030071 100644 --- a/pkg/arvo/lib/group.hoon +++ b/pkg/arvo/lib/group.hoon @@ -99,6 +99,14 @@ (en-path:resource rid) ship :: +++ get-tagged-ships + |= [rid=resource =tag] + ^- (set ship) + =/ grp=(unit group) + (scry-group rid) + ?~ grp ~ + (~(get ju tags.u.grp) tag) +:: ++ is-managed-path |= =path ^- ? diff --git a/pkg/arvo/lib/metadata-json.hoon b/pkg/arvo/lib/metadata-json.hoon index fa6352358..6604dd4ae 100644 --- a/pkg/arvo/lib/metadata-json.hoon +++ b/pkg/arvo/lib/metadata-json.hoon @@ -56,9 +56,9 @@ ++ vip %- su ;~ pose - (tag %$) (tag %reader-comments) (tag %member-metadata) + (tag %$) == :: ++ metadata diff --git a/pkg/arvo/mar/graph/permissions/add.hoon b/pkg/arvo/mar/graph/permissions/add.hoon new file mode 100644 index 000000000..614530dc1 --- /dev/null +++ b/pkg/arvo/mar/graph/permissions/add.hoon @@ -0,0 +1,12 @@ +/- *graph-store +|_ per=permissions +++ grad %noun +++ grow + |% + ++ noun per + -- +++ grab + |% + ++ noun permissions + -- +-- diff --git a/pkg/arvo/mar/graph/permissions/remove.hoon b/pkg/arvo/mar/graph/permissions/remove.hoon new file mode 100644 index 000000000..614530dc1 --- /dev/null +++ b/pkg/arvo/mar/graph/permissions/remove.hoon @@ -0,0 +1,12 @@ +/- *graph-store +|_ per=permissions +++ grad %noun +++ grow + |% + ++ noun per + -- +++ grab + |% + ++ noun permissions + -- +-- diff --git a/pkg/arvo/mar/graph/validator/chat.hoon b/pkg/arvo/mar/graph/validator/chat.hoon index 236c6ac45..16cc0f572 100644 --- a/pkg/arvo/mar/graph/validator/chat.hoon +++ b/pkg/arvo/mar/graph/validator/chat.hoon @@ -3,6 +3,7 @@ ++ grow |% ++ noun i + :: ?(%no %writer) ++ notification-kind :: ?+ index.p.i ~ diff --git a/pkg/arvo/mar/graph/validator/publish.hoon b/pkg/arvo/mar/graph/validator/publish.hoon index d55bbed2c..65d8b72e0 100644 --- a/pkg/arvo/mar/graph/validator/publish.hoon +++ b/pkg/arvo/mar/graph/validator/publish.hoon @@ -1,10 +1,27 @@ -/- *post +/- *post, met=metadata-store |_ i=indexed-post ++ grow |% ++ noun i + ++ graph-add-permissions + |= vip=vip-metadata:met + ?+ index.p.i !! + [@ ~] [%yes %yes %no] :: new note + [@ %1 @ @ ~] [%self %self %no] + [@ %2 @ ~] [%yes %yes ?:(?=(%reader-comments vip) %yes %no)] + [@ %2 @ @ ~] [%self %self %self] + == + :: + ++ graph-remove-permissions + |= vip=vip-metadata:met + ?+ index.p.i !! + [@ ~] [%yes %self %self] + [@ %1 @ @ ~] [%self %self %self] + [@ %2 @ ~] [%yes %self ?:(?=(%reader-comments vip) %self %no)] + [@ %2 @ @ ~] [%yes %self %self] + == :: +notification-kind - :: Ignore all containers, only notify on content + :: ignore all containers, only notify on content :: ++ notification-kind ?+ index.p.i ~ @@ -16,7 +33,7 @@ -- ++ grab |% - :: +noun: Validate publish post + :: +noun: validate publish post :: ++ noun |= p=* diff --git a/pkg/arvo/sur/graph-store.hoon b/pkg/arvo/sur/graph-store.hoon index 6780e4210..845885ed1 100644 --- a/pkg/arvo/sur/graph-store.hoon +++ b/pkg/arvo/sur/graph-store.hoon @@ -1,5 +1,17 @@ /- *post |% +:: ++$ permissions + [admin=permission-level writer=permission-level reader=permission-level] +:: +:: $permission-level: levels of permissions in increasing order +:: +:: %no: May not add/remove node +:: %self: May only nodes beneath nodes that were added by +:: the same pilot, may remove nodes that the pilot 'owns' +:: %yes: May add a node or remove node ++$ permission-level + ?(%no %self %yes) +$ graph ((mop atom node) gth) +$ marked-graph [p=graph q=(unit mark)] :: @@ -11,6 +23,7 @@ +$ update-log ((mop time logged-update) gth) +$ update-logs (map resource update-log) :: +:: +$ internal-graph $~ [%empty ~] $% [%graph p=graph] From 4b6637ec9a1cc6b76a940d040c40a58faeeb3d09 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Tue, 26 Jan 2021 14:47:35 +1000 Subject: [PATCH 03/27] graph-hooks: %remove-nodes permissioning --- pkg/arvo/app/graph-push-hook.hoon | 94 +++++++++++++++-------- pkg/arvo/app/metadata-push-hook.hoon | 12 ++- pkg/arvo/app/metadata-store.hoon | 2 - pkg/arvo/mar/graph/validator/publish.hoon | 6 +- pkg/arvo/mar/metadata/update.hoon | 2 +- pkg/arvo/ted/graph/create.hoon | 3 +- 6 files changed, 77 insertions(+), 42 deletions(-) diff --git a/pkg/arvo/app/graph-push-hook.hoon b/pkg/arvo/app/graph-push-hook.hoon index 93477e7ad..5b73f31e6 100644 --- a/pkg/arvo/app/graph-push-hook.hoon +++ b/pkg/arvo/app/graph-push-hook.hoon @@ -80,7 +80,7 @@ %add-graph %.n %remove-graph %.n %add-nodes (is-allowed-add:hc resource.q.update nodes.q.update) - %remove-nodes (is-allowed-remove:hc resource.q.update indices.q.update bowl) + %remove-nodes (is-allowed-remove:hc resource.q.update indices.q.update) %add-signatures %.n %remove-signatures %.n %archive-graph %.n @@ -148,22 +148,34 @@ .^ (unit mark) (scry %gx %graph-store /graph-mark/(scot %p entity.resource)/[name.resource]/noun) == - -++ add-mark - |= [=resource:res =indexed-post:store vip=vip-metadata:met] +:: +++ perm-mark-name + |= perm=@t + ^- @t + (cat 3 'graph-permissions-' perm) +:: +++ perm-mark + |= [=resource:res perm=@t vip=vip-metadata:met =indexed-post:store] ^- permissions:store =- (check vip) !< check=$-(vip-metadata:met permissions:store) %. !>(indexed-post) - =/ mark=(unit mark) - (get-mark:gra resource) - ?~ mark |=(=vase !>([%no %no %no])) - .^(tube:clay (scry %cc %home /[u.mark]/graph-add-permissions)) + =/ mark (get-mark:gra resource) + ?~ mark |=(=vase !>([%no %no %no])) + .^(tube:clay (scry %cc %home /[u.mark]/(perm-mark-name perm))) +:: +++ add-mark + |= [=resource:res vip=vip-metadata:met =indexed-post:store] + (perm-mark resource %add vip indexed-post) +:: +++ remove-mark + |= [=resource:res vip=vip-metadata:met =indexed-post:store] + (perm-mark resource %remove vip indexed-post) :: ++ get-permission - |= [=permissions:store role=(unit role-tag) writers=(set ship)] + |= [=permissions:store is-admin=? writers=(set ship)] ^- permission-level:store - ?: ?=(?([~ %admin] [~ %moderator]) role) + ?: is-admin admin.permissions ?: =(~ writers) writer.permissions @@ -175,27 +187,41 @@ |= * %.y :: -++ is-allowed-add - |= [=resource:res nodes=(map index:store node:store)] - ^- ? +++ get-roles-writers-variation + |= =resource:res + ^- (unit [is-admin=? writers=(set ship) vip=vip-metadata:met]) =/ assoc=(unit association:met) (peek-association:met %graph resource) - ?~ assoc %.n + ?~ assoc ~ =/ role=(unit (unit role-tag)) (role-for-ship:grp group.u.assoc src.bowl) =/ writers=(set ship) (get-tagged-ships:grp group.u.assoc [%graph resource %writers]) - ?~ role %.n + ?~ role ~ =/ is-admin=? ?=(?([~ %admin] [~ %moderator]) u.role) + `[is-admin writers vip.metadata.u.assoc] +:: +++ node-to-indexed-post + |= =node:store + ^- indexed-post:store + =* index index.post.node + [(snag (dec (lent index)) index) post.node] +:: +++ is-allowed-add + |= [=resource:res nodes=(map index:store node:store)] + ^- ? + %- (bond |.(%.n)) + %+ biff (get-roles-writers-variation resource) + |= [is-admin=? writers=(set ship) vip=vip-metadata:met] + %- some %+ levy ~(tap by nodes) |= [=index:store =node:store] =/ =permissions:store - %^ add-mark resource - [(snag (dec (lent index)) index) post.node] - vip.metadata.u.assoc + %^ add-mark resource vip + (node-to-indexed-post node) =/ =permission-level:store - (get-permission permissions u.role writers) + (get-permission permissions is-admin writers) ~& permission-level ?- permission-level %yes %.y @@ -206,32 +232,36 @@ (scag (dec (lent index)) index) =/ parent-node=node:store (got-node:gra resource parent-index) - =(author.post.node src.bowl) + =(author.post.parent-node src.bowl) == :: ++ is-allowed-remove - |= [=resource:res indices=(set index:store) =bowl:gall] + |= [=resource:res indices=(set index:store)] ^- ? - =/ gra ~(. graph bowl) - ?. (is-allowed resource bowl %.n) - %.n - %+ levy - ~(tap in indices) + %- (bond |.(%.n)) + %+ biff (get-roles-writers-variation) + |= [is-admin=? writers=(set ship) vip=vip-metadata:met] + %- some + %+ levy ~(tap by indices) |= =index:store - ^- ? =/ =node:store (got-node:gra resource index) - ?| =(author.post.node src.bowl) - (is-allowed resource bowl %.y) + =/ =permissions:store + %^ remove-mark resource vip + (node-to-indexed-post node) + =/ =permission-level:store + (get-permission permissions is-admin writers) + ?- permission-level + %yes %.y + %no %.n + %self =(author.post.node src.bowl) == :: ++ build-permissions |= [=mark kind=?(%add %remove) mode=?(%sing %next)] ^- card =/ =wire /perms/[mark]/[kind] - =/ perm-mark=@t - (cat 3 %graph-permissions kind) - =/ =mood:clay [%c da+now.bowl /[mark]/[perm-mark]] + =/ =mood:clay [%c da+now.bowl /[mark]/(perm-mark-name kind)] =/ =rave:clay ?:(?=(%sing mode) [mode mood] [mode mood]) [%pass wire %arvo %c %warp our.bowl %home `rave] -- diff --git a/pkg/arvo/app/metadata-push-hook.hoon b/pkg/arvo/app/metadata-push-hook.hoon index 5758a282f..823b88b0a 100644 --- a/pkg/arvo/app/metadata-push-hook.hoon +++ b/pkg/arvo/app/metadata-push-hook.hoon @@ -76,13 +76,19 @@ ++ should-proxy-update |= =vase =+ !<(upd=metadata-update vase) - ?. ?=(?(%add %remove %update) -.upd) + ?. ?=(?(%add %remove) -.upd) %.n =/ role=(unit (unit role-tag)) (role-for-ship:grp group.upd src.bowl) + =/ =metadata + (need (peek-metadata:met %contacts group.upd)) ?~ role %.n - ?~ u.role %.n - ?=(?(%admin %moderator) u.u.role) + ?^ u.role + ?=(?(%admin %moderator) u.u.role) + ?. ?=(%add -.upd) %.n + ?& =(src.bowl entity.resource.resource.upd) + ?=(%member-metadata vip.metadata) + == :: ++ take-update |= =vase diff --git a/pkg/arvo/app/metadata-store.hoon b/pkg/arvo/app/metadata-store.hoon index 8cdd26b64..123260b44 100644 --- a/pkg/arvo/app/metadata-store.hoon +++ b/pkg/arvo/app/metadata-store.hoon @@ -354,8 +354,6 @@ |= [group=resource =md-resource =metadata] ^- (quip card _state) :- %+ send-diff app-name.md-resource - ?: (~(has by resource-indices) md-resource) - [%updated-metadata group md-resource metadata metadata] [%add group md-resource metadata] %= state associations diff --git a/pkg/arvo/mar/graph/validator/publish.hoon b/pkg/arvo/mar/graph/validator/publish.hoon index 65d8b72e0..597a955a9 100644 --- a/pkg/arvo/mar/graph/validator/publish.hoon +++ b/pkg/arvo/mar/graph/validator/publish.hoon @@ -3,16 +3,16 @@ ++ grow |% ++ noun i - ++ graph-add-permissions + ++ graph-permissions-add |= vip=vip-metadata:met ?+ index.p.i !! [@ ~] [%yes %yes %no] :: new note - [@ %1 @ @ ~] [%self %self %no] + [@ %1 @ ~] [%self %self %no] [@ %2 @ ~] [%yes %yes ?:(?=(%reader-comments vip) %yes %no)] [@ %2 @ @ ~] [%self %self %self] == :: - ++ graph-remove-permissions + ++ graph-permissions-remove |= vip=vip-metadata:met ?+ index.p.i !! [@ ~] [%yes %self %self] diff --git a/pkg/arvo/mar/metadata/update.hoon b/pkg/arvo/mar/metadata/update.hoon index 2bbf57bcf..f7aec7b26 100644 --- a/pkg/arvo/mar/metadata/update.hoon +++ b/pkg/arvo/mar/metadata/update.hoon @@ -6,7 +6,7 @@ |% ++ noun upd ++ resource - ?> ?=(?(%add %remove %initial-group) -.upd) + ?> ?=(?(%add %remove %initial-group %updated-metadata) -.upd) group.upd ++ json (update-to-json upd) -- diff --git a/pkg/arvo/ted/graph/create.hoon b/pkg/arvo/ted/graph/create.hoon index a883d822c..b1993ed6c 100644 --- a/pkg/arvo/ted/graph/create.hoon +++ b/pkg/arvo/ted/graph/create.hoon @@ -64,11 +64,12 @@ date-created now.bowl creator our.bowl module module.action + preview %.n == =/ =metadata-action [%add group graph+rid.action metadata] ;< ~ bind:m - (poke-our %metadata-store %metadata-action !>(metadata-action)) + (poke-our %metadata-push-hook %metadata-update !>(metadata-action)) ;< ~ bind:m (poke-our %metadata-push-hook %push-hook-action !>([%add group])) :: From 955ea5450234c43b4659afaa1efdd397b1785dd9 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Tue, 26 Jan 2021 14:50:33 +1000 Subject: [PATCH 04/27] graph: add permissioning for chat --- pkg/arvo/mar/graph/validator/chat.hoon | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/pkg/arvo/mar/graph/validator/chat.hoon b/pkg/arvo/mar/graph/validator/chat.hoon index 16cc0f572..344b3822d 100644 --- a/pkg/arvo/mar/graph/validator/chat.hoon +++ b/pkg/arvo/mar/graph/validator/chat.hoon @@ -1,11 +1,22 @@ -/- *post +/- *post, met=metadata-store |_ i=indexed-post ++ grow |% ++ noun i - :: ?(%no %writer) + :: + ++ graph-permissions-add + |= vip=vip-metadata:met + ?+ index.p.i !! + [@ ~] [%yes %yes %no] + == + :: + ++ graph-permissions-remove + |= vip=vip-metadata:met + ?+ index.p.i !! + [@ ~] [%self %self %no] + == + :: ++ notification-kind - :: ?+ index.p.i ~ [@ ~] `[%message 0 %count %.n] == From d0fb69847b68c22fcde23353de0f31ec8efef389 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Tue, 26 Jan 2021 14:54:29 +1000 Subject: [PATCH 05/27] graph: add permissioning for links --- pkg/arvo/mar/graph/validator/link.hoon | 23 ++++++++++++++++++++++- pkg/arvo/mar/graph/validator/publish.hoon | 4 ++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/pkg/arvo/mar/graph/validator/link.hoon b/pkg/arvo/mar/graph/validator/link.hoon index 1efcf6b11..143a99258 100644 --- a/pkg/arvo/mar/graph/validator/link.hoon +++ b/pkg/arvo/mar/graph/validator/link.hoon @@ -1,8 +1,29 @@ -/- *post +/- *post, met=metadata-store |_ i=indexed-post ++ grow |% ++ noun i + :: + ++ graph-permissions-add + |= vip=vip-metadata:met + =/ reader + ?=(%reader-comments vip) + ?+ index.p.i !! + [@ ~] [%yes %yes %no] + [@ @ ~] [%yes %yes ?:(reader %yes %no)] + [@ @ @ ~] [%self %self %self] + == + :: + ++ graph-permissions-remove + |= vip=vip-metadata:met + =/ reader + ?=(%reader-comments vip) + ?+ index.p.i !! + [@ ~] [%yes %self %self] + [@ @ ~] [%yes %self %self] + [@ @ @ ~] [%yes %self %self] + == + :: ++ notification-kind ?+ index.p.i ~ [@ ~] `[%link 0 %each %.y] diff --git a/pkg/arvo/mar/graph/validator/publish.hoon b/pkg/arvo/mar/graph/validator/publish.hoon index 597a955a9..62fc8c466 100644 --- a/pkg/arvo/mar/graph/validator/publish.hoon +++ b/pkg/arvo/mar/graph/validator/publish.hoon @@ -16,8 +16,8 @@ |= vip=vip-metadata:met ?+ index.p.i !! [@ ~] [%yes %self %self] - [@ %1 @ @ ~] [%self %self %self] - [@ %2 @ ~] [%yes %self ?:(?=(%reader-comments vip) %self %no)] + [@ %1 @ @ ~] [%yes %self %self] + [@ %2 @ ~] [%yes %self %self] [@ %2 @ @ ~] [%yes %self %self] == :: +notification-kind From a100ac99bb00d212ee4461350c2b22f9625f3b58 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Thu, 28 Jan 2021 10:34:23 +1000 Subject: [PATCH 06/27] interface: update state management for permissions --- pkg/interface/src/logic/api/metadata.ts | 4 +- pkg/interface/src/logic/lib/group.ts | 51 +++++++++++++++---- pkg/interface/src/logic/lib/util.ts | 6 +++ .../src/logic/reducers/group-update.ts | 22 +++----- pkg/interface/src/types/group-update.ts | 1 + pkg/interface/src/types/metadata-update.ts | 4 +- pkg/interface/src/types/util.ts | 4 +- 7 files changed, 64 insertions(+), 28 deletions(-) diff --git a/pkg/interface/src/logic/api/metadata.ts b/pkg/interface/src/logic/api/metadata.ts index 464e2f205..44243429e 100644 --- a/pkg/interface/src/logic/api/metadata.ts +++ b/pkg/interface/src/logic/api/metadata.ts @@ -23,9 +23,9 @@ export default class MetadataApi extends BaseApi { 'date-created': dateCreated, creator, 'module': moduleName, - preview: false, picture: '', - permissions: '' + preview: false, + vip: '' } } }); diff --git a/pkg/interface/src/logic/lib/group.ts b/pkg/interface/src/logic/lib/group.ts index 7d554cc1b..0bc3274f7 100644 --- a/pkg/interface/src/logic/lib/group.ts +++ b/pkg/interface/src/logic/lib/group.ts @@ -1,8 +1,12 @@ -import { roleTags, RoleTags, Group, Resource } from '~/types/group-update'; -import { PatpNoSig, Path } from '~/types/noun'; +import _ from "lodash"; +import { roleTags, RoleTags, Group, Resource } from "~/types/group-update"; +import { PatpNoSig, Path } from "~/types/noun"; +import {deSig} from "./util"; - -export function roleForShip(group: Group, ship: PatpNoSig): RoleTags | undefined { +export function roleForShip( + group: Group, + ship: PatpNoSig +): RoleTags | undefined { return roleTags.reduce((currRole, role) => { const roleShips = group?.tags?.role?.[role]; return roleShips && roleShips.has(ship) ? role : currRole; @@ -10,11 +14,40 @@ export function roleForShip(group: Group, ship: PatpNoSig): RoleTags | undefined } export function resourceFromPath(path: Path): Resource { - const [, , ship, name] = path.split('/'); - return { ship, name } -} - -export function makeResource(ship: string, name:string) { + const [, , ship, name] = path.split("/"); return { ship, name }; } +export function makeResource(ship: string, name: string) { + return { ship, name }; +} + +export function isWriter(group: Group, resource: string) { + const writers: Set | undefined = _.get( + group.tags, + ["graph", resource, "writers"], + undefined + ); + const admins = group.tags?.role?.admin ?? new Set(); + if (_.isUndefined(writers)) { + return true; + } else { + return writers.has(window.ship) || admins.has(window.ship); + } +} + +export function isChannelAdmin(group: Group, resource: string, ship: string = `~${window.ship}`) { + const role = roleForShip(group, ship.slice(1)); + + return ( + isHost(resource, ship) || + role === "admin" || + role === "moderator" + ); +} + +export function isHost(resource: string, ship: string = `~${window.ship}`) { + const [, , host] = resource.split("/"); + + return ship === host; +} diff --git a/pkg/interface/src/logic/lib/util.ts b/pkg/interface/src/logic/lib/util.ts index 8d431824f..34a534c48 100644 --- a/pkg/interface/src/logic/lib/util.ts +++ b/pkg/interface/src/logic/lib/util.ts @@ -23,6 +23,12 @@ export const getModuleIcon = (mod: string) => { return _.capitalize(mod); } +export function wait(ms: number) { + return new Promise((resolve, reject) => { + setTimeout(resolve, ms); + }); +} + export function appIsGraph(app: string) { return app === 'publish' || app == 'link'; } diff --git a/pkg/interface/src/logic/reducers/group-update.ts b/pkg/interface/src/logic/reducers/group-update.ts index 0b0ad8ec0..8de063f89 100644 --- a/pkg/interface/src/logic/reducers/group-update.ts +++ b/pkg/interface/src/logic/reducers/group-update.ts @@ -41,22 +41,16 @@ function decodePolicy(policy: Enc): GroupPolicy { } function decodeTags(tags: Enc): Tags { + console.log(tags); return _.reduce( tags, - (acc, tag, key): Tags => { - if (Array.isArray(tag)) { - acc.role[key] = new Set(tag); + (acc, ships, key): Tags => { + if (key.search(/\\/) === -1) { + acc.role[key] = new Set(ships); return acc; } else { - const app = _.reduce( - tag, - (inner, t, k) => { - inner[k] = new Set(t); - return inner; - }, - {} - ); - acc[key] = app; + const [app, tag, resource] = key.split('\\'); + _.set(acc, [app, resource, tag], new Set(ships)); return acc; } }, @@ -143,7 +137,7 @@ export default class GroupReducer { const resourcePath = resourceAsPath(resource); const tags = state.groups[resourcePath].tags; const tagAccessors = - 'app' in tag ? [tag.app,tag.tag] : ['role', tag.tag]; + 'app' in tag ? [tag.app,tag.resource, tag.tag] : ['role', tag.tag]; const tagged = _.get(tags, tagAccessors, new Set()); for (const ship of ships) { tagged.add(ship); @@ -158,7 +152,7 @@ export default class GroupReducer { const resourcePath = resourceAsPath(resource); const tags = state.groups[resourcePath].tags; const tagAccessors = - 'app' in tag ? [tag.app,tag.tag] : ['role', tag.tag]; + 'app' in tag ? [tag.app, tag.resource, tag.tag] : ['role', tag.tag]; const tagged = _.get(tags, tagAccessors, new Set()); if (!tagged) { diff --git a/pkg/interface/src/types/group-update.ts b/pkg/interface/src/types/group-update.ts index 8640f3e02..ad7c35004 100644 --- a/pkg/interface/src/types/group-update.ts +++ b/pkg/interface/src/types/group-update.ts @@ -8,6 +8,7 @@ interface RoleTag { interface AppTag { app: string; + resource: string; tag: string; } diff --git a/pkg/interface/src/types/metadata-update.ts b/pkg/interface/src/types/metadata-update.ts index ae00a180f..640bd5a9b 100644 --- a/pkg/interface/src/types/metadata-update.ts +++ b/pkg/interface/src/types/metadata-update.ts @@ -62,7 +62,7 @@ export interface Metadata { module: string; picture: string; preview: boolean; - permissions: Permissions; + vip: PermVariation; } -export type Permissions = '' | 'reader-comments'; +export type PermVariation = '' | 'reader-comments' | 'member-metadata'; diff --git a/pkg/interface/src/types/util.ts b/pkg/interface/src/types/util.ts index c2e281d81..435410fec 100644 --- a/pkg/interface/src/types/util.ts +++ b/pkg/interface/src/types/util.ts @@ -1,2 +1,4 @@ - +import { Icon } from "@tlon/indigo-react"; export type PropFunc any> = Parameters[0]; + +export type IconRef = PropFunc['icon']; From 514f6cfbf03533f67d1c705f3e14a2acbf319521 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Thu, 28 Jan 2021 10:35:46 +1000 Subject: [PATCH 07/27] interface: refactor modal components --- pkg/interface/src/logic/lib/useModal.tsx | 61 +++++-------------- .../src/logic/lib/useOutsideClick.ts | 11 +++- .../src/views/components/ModalOverlay.tsx | 29 +++++++++ 3 files changed, 55 insertions(+), 46 deletions(-) create mode 100644 pkg/interface/src/views/components/ModalOverlay.tsx diff --git a/pkg/interface/src/logic/lib/useModal.tsx b/pkg/interface/src/logic/lib/useModal.tsx index 3aa6f21d9..982ed5fbd 100644 --- a/pkg/interface/src/logic/lib/useModal.tsx +++ b/pkg/interface/src/logic/lib/useModal.tsx @@ -5,9 +5,12 @@ import React, { SyntheticEvent, useMemo, useEffect, + useRef, } from "react"; import { Box } from "@tlon/indigo-react"; +import { useOutsideClick } from "./useOutsideClick"; +import { ModalOverlay } from "~/views/components/ModalOverlay"; type ModalFunc = (dismiss: () => void) => JSX.Element; interface UseModalProps { @@ -19,11 +22,8 @@ interface UseModalResult { showModal: () => void; } -const stopPropagation = (e: SyntheticEvent) => { - e.stopPropagation(); -}; - export function useModal(props: UseModalProps): UseModalResult { + const innerRef = useRef(); const [modalShown, setModalShown] = useState(false); const dismiss = useCallback(() => { @@ -44,54 +44,25 @@ export function useModal(props: UseModalProps): UseModalResult { [modalShown, props.modal, dismiss] ); - const handleKeyDown = useCallback( - (event) => { - if (event.key === "Escape") { - dismiss(); - } - }, - [dismiss] - ); - - useEffect(() => { - window.addEventListener("keydown", handleKeyDown); - - return () => { - window.removeEventListener("keydown", handleKeyDown); - }; - }, [modalShown]); + useOutsideClick(innerRef, dismiss); const modal = useMemo( () => !inner ? null : ( - - - {inner} - - + {inner} + ), [inner, dismiss] ); diff --git a/pkg/interface/src/logic/lib/useOutsideClick.ts b/pkg/interface/src/logic/lib/useOutsideClick.ts index 412e3ee5c..f8b7c4138 100644 --- a/pkg/interface/src/logic/lib/useOutsideClick.ts +++ b/pkg/interface/src/logic/lib/useOutsideClick.ts @@ -1,7 +1,7 @@ import { useEffect, RefObject } from "react"; export function useOutsideClick( - ref: RefObject, + ref: RefObject, onClick: () => void ) { useEffect(() => { @@ -14,10 +14,19 @@ export function useOutsideClick( onClick(); } } + + function handleKeyDown(ev) { + if(ev.key === "Escape") { + onClick(); + + } + } document.addEventListener("mousedown", handleClick); + document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("mousedown", handleClick); + document.removeEventListener("keydown", handleKeyDown); }; }, [ref.current, onClick]); } diff --git a/pkg/interface/src/views/components/ModalOverlay.tsx b/pkg/interface/src/views/components/ModalOverlay.tsx new file mode 100644 index 000000000..ad41cd2f0 --- /dev/null +++ b/pkg/interface/src/views/components/ModalOverlay.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Box } from "@tlon/indigo-react"; +import { PropFunc } from "~/types/util"; + +interface ModalOverlayProps { + spacing: PropFunc["m"]; +} +export const ModalOverlay = React.forwardRef( + (props: ModalOverlayProps & PropFunc, ref) => { + const { spacing, ...rest } = props; + return ( + + + + ); + } +); From dce85246b0bae5332bfbf12d7e18e91152b7e462 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Thu, 28 Jan 2021 10:36:19 +1000 Subject: [PATCH 08/27] chat: hide input if unable to write --- pkg/interface/src/views/apps/chat/ChatResource.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/interface/src/views/apps/chat/ChatResource.tsx b/pkg/interface/src/views/apps/chat/ChatResource.tsx index a8914b5d5..ec6cf36fa 100644 --- a/pkg/interface/src/views/apps/chat/ChatResource.tsx +++ b/pkg/interface/src/views/apps/chat/ChatResource.tsx @@ -13,6 +13,7 @@ import SubmitDragger from '~/views/components/SubmitDragger'; import { useLocalStorageState } from '~/logic/lib/useLocalStorageState'; import { Loading } from '~/views/components/Loading'; import useS3 from '~/logic/lib/useS3'; +import {isWriter} from '~/logic/lib/group'; type ChatResourceProps = StoreState & { association: Association; @@ -37,6 +38,8 @@ export function ChatResource(props: ChatResourceProps) { const chatInput = useRef(); + const canWrite = isWriter(group, station); + useEffect(() => { const count = Math.min(50, unreadCount + 15); props.api.graph.getNewest(owner, name, count); @@ -107,6 +110,7 @@ export function ChatResource(props: ChatResourceProps) { location={props.location} scrollTo={scrollTo ? parseInt(scrollTo, 10) : undefined} /> + { canWrite && ( + /> )} ); } From fdf80d8edafbe63a99b285b3a850058e90c19127 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Thu, 28 Jan 2021 10:38:00 +1000 Subject: [PATCH 09/27] links: hide relevant inputs if we are missing permissions --- pkg/interface/src/views/apps/links/LinkResource.tsx | 1 - pkg/interface/src/views/apps/links/LinkWindow.tsx | 6 ++++-- pkg/interface/src/views/components/Comments.tsx | 8 +++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pkg/interface/src/views/apps/links/LinkResource.tsx b/pkg/interface/src/views/apps/links/LinkResource.tsx index f33990fd7..64a728cce 100644 --- a/pkg/interface/src/views/apps/links/LinkResource.tsx +++ b/pkg/interface/src/views/apps/links/LinkResource.tsx @@ -61,7 +61,6 @@ export function LinkResource(props: LinkResourceProps) { return
; } - return ( diff --git a/pkg/interface/src/views/apps/links/LinkWindow.tsx b/pkg/interface/src/views/apps/links/LinkWindow.tsx index 91708d72c..5d54372f1 100644 --- a/pkg/interface/src/views/apps/links/LinkWindow.tsx +++ b/pkg/interface/src/views/apps/links/LinkWindow.tsx @@ -15,6 +15,7 @@ import GlobalApi from "~/logic/api/global"; import VirtualScroller from "~/views/components/VirtualScroller"; import { LinkItem } from "./components/LinkItem"; import LinkSubmit from "./components/LinkSubmit"; +import {isWriter} from "~/logic/lib/group"; interface LinkWindowProps { association: Association; @@ -50,6 +51,7 @@ export function LinkWindow(props: LinkWindowProps) { const first = graph.peekLargest()?.[0]; const [,,ship, name] = association.resource.split('/'); + const canWrite = isWriter(props.group, association.resource) const style = useMemo(() => ({ @@ -79,10 +81,10 @@ export function LinkWindow(props: LinkWindowProps) { measure, key: index.toString() }; - if(index.eq(first ?? bigInt.zero)) { + if(canWrite && index.eq(first ?? bigInt.zero)) { return ( <> - + diff --git a/pkg/interface/src/views/components/Comments.tsx b/pkg/interface/src/views/components/Comments.tsx index 717f84fec..9c37751e6 100644 --- a/pkg/interface/src/views/components/Comments.tsx +++ b/pkg/interface/src/views/components/Comments.tsx @@ -11,6 +11,7 @@ import { createPost, createBlankNodeWithChildPost } from '~/logic/api/graph'; import { getLatestCommentRevision } from '~/logic/lib/publish'; import { scanForMentions } from '~/logic/lib/graph'; import { getUnreadCount } from '~/logic/lib/hark'; +import {isWriter} from '~/logic/lib/group'; interface CommentsProps { comments: GraphNode; @@ -92,18 +93,19 @@ export function Comments(props: CommentsProps) { useEffect(() => { - console.log(`dismissing ${association?.resource}`); return () => { api.hark.markCountAsRead(association, parentIndex, 'comment') }; }, [comments.post.index]) - const readCount = children.length - getUnreadCount(props?.unreads, association.resource, parentIndex) + const readCount = children.length - getUnreadCount(props?.unreads, association.resource, parentIndex); + + const canComment = isWriter(group, association.resource) || association.metadata.vip === 'reader-comments'; return ( - {( !props.editCommentId ? : null )} + {( !props.editCommentId && canComment ? : null )} {( !!props.editCommentId ? ( Date: Thu, 28 Jan 2021 10:39:32 +1000 Subject: [PATCH 10/27] publish: hide relevant inputs if we are missing permissions --- pkg/interface/package-lock.json | 1296 ----------------- .../views/apps/publish/components/Note.tsx | 2 +- .../apps/publish/components/Notebook.tsx | 24 +- .../views/apps/publish/components/Writers.js | 13 +- .../apps/publish/components/new-post.tsx | 4 +- 5 files changed, 35 insertions(+), 1304 deletions(-) diff --git a/pkg/interface/package-lock.json b/pkg/interface/package-lock.json index 24a6e024a..cdadefa5f 100644 --- a/pkg/interface/package-lock.json +++ b/pkg/interface/package-lock.json @@ -11769,563 +11769,6 @@ "path-dirname": "^1.0.0" }, "dependencies": { -<<<<<<< HEAD - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "3.2.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.6.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.9.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "0.5.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.3.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.14.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4.4.2" - } - }, - "nopt": { - "version": "4.0.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "2.3.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.7.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.1", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.13", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.1.1", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { -======= "is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", @@ -12333,7 +11776,6 @@ "dev": true, "optional": true, "requires": { ->>>>>>> origin/release/next-userspace "is-extglob": "^2.1.0" } } @@ -12511,37 +11953,6 @@ "which": "^1.2.9" } }, -<<<<<<< HEAD - "emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", - "dev": true - }, - "enhanced-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", - "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.4.0", - "tapable": "^1.0.0" - } - }, - "loader-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.2.3.tgz", - "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^2.0.0", - "json5": "^1.0.1" - } - }, -======= ->>>>>>> origin/release/next-userspace "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", @@ -12569,15 +11980,6 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, -<<<<<<< HEAD - "v8-compile-cache": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", - "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==", - "dev": true - }, -======= ->>>>>>> origin/release/next-userspace "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -12590,15 +11992,9 @@ } }, "webpack-dev-middleware": { -<<<<<<< HEAD - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.2.tgz", - "integrity": "sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==", -======= "version": "3.7.3", "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz", "integrity": "sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==", ->>>>>>> origin/release/next-userspace "dev": true, "requires": { "memory-fs": "^0.4.1", @@ -12609,29 +12005,17 @@ }, "dependencies": { "mime": { -<<<<<<< HEAD - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.5.tgz", - "integrity": "sha512-3hQhEUF027BuxZjQA3s7rIv/7VCQPa27hN9u9g87sEkWaKwQPuXOkVKtOeiyUrnWqTDiOs8Ed2rwg733mB0R5w==", -======= "version": "2.5.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.5.0.tgz", "integrity": "sha512-ft3WayFSFUVBuJj7BMLKAQcSlItKtfjsKDDsii3rqFDAZ7t11zRe8ASw/GlmivGwVUYtwkQrxiGGpL6gFvB0ag==", ->>>>>>> origin/release/next-userspace "dev": true } } }, "webpack-dev-server": { -<<<<<<< HEAD - "version": "3.10.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.10.3.tgz", - "integrity": "sha512-e4nWev8YzEVNdOMcNzNeCN947sWJNd43E5XvsJzbAL08kGc2frm1tQ32hTJslRS+H65LCb/AaUCYU7fjHCpDeQ==", -======= "version": "3.11.2", "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.2.tgz", "integrity": "sha512-A80BkuHRQfCiNtGBS1EMf2ChTUs0x+B3wGDFmOeT4rmJOHhHTCH2naNxIHhmkr0/UillP4U3yeIyv1pNp+QDLQ==", ->>>>>>> origin/release/next-userspace "dev": true, "requires": { "ansi-html": "0.0.7", @@ -12642,30 +12026,13 @@ "debug": "^4.1.1", "del": "^4.1.1", "express": "^4.17.1", -<<<<<<< HEAD - "html-entities": "^1.2.1", -======= "html-entities": "^1.3.1", ->>>>>>> origin/release/next-userspace "http-proxy-middleware": "0.19.1", "import-local": "^2.0.0", "internal-ip": "^4.3.0", "ip": "^1.1.5", "is-absolute-url": "^3.0.3", "killable": "^1.0.1", -<<<<<<< HEAD - "loglevel": "^1.6.6", - "opn": "^5.5.0", - "p-retry": "^3.0.1", - "portfinder": "^1.0.25", - "schema-utils": "^1.0.0", - "selfsigned": "^1.10.7", - "semver": "^6.3.0", - "serve-index": "^1.9.1", - "sockjs": "0.3.19", - "sockjs-client": "1.4.0", - "spdy": "^4.0.1", -======= "loglevel": "^1.6.8", "opn": "^5.5.0", "p-retry": "^3.0.1", @@ -12677,18 +12044,13 @@ "sockjs": "^0.3.21", "sockjs-client": "^1.5.0", "spdy": "^4.0.2", ->>>>>>> origin/release/next-userspace "strip-ansi": "^3.0.1", "supports-color": "^6.1.0", "url": "^0.11.0", "webpack-dev-middleware": "^3.7.2", "webpack-log": "^2.0.0", "ws": "^6.2.1", -<<<<<<< HEAD - "yargs": "12.0.5" -======= "yargs": "^13.3.2" ->>>>>>> origin/release/next-userspace }, "dependencies": { "ansi-regex": { @@ -12711,7 +12073,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", -<<<<<<< HEAD "dev": true, "requires": { "remove-trailing-separator": "^1.0.1" @@ -12763,663 +12124,6 @@ "upath": "^1.1.1" } }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "fsevents": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.12.tgz", - "integrity": "sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==", - "dev": true, - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1", - "node-pre-gyp": "*" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "3.2.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.6.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "bundled": true, - "dev": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true - }, - "minipass": { - "version": "2.9.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "0.5.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "ms": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "needle": { - "version": "2.3.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.14.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4.4.2" - } - }, - "nopt": { - "version": "4.0.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "2.3.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.7.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.1", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.13", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, -======= ->>>>>>> origin/release/next-userspace - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } -<<<<<<< HEAD - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "yallist": { - "version": "3.1.1", - "bundled": true, - "dev": true, - "optional": true -======= ->>>>>>> origin/release/next-userspace - } - } - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - } - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", diff --git a/pkg/interface/src/views/apps/publish/components/Note.tsx b/pkg/interface/src/views/apps/publish/components/Note.tsx index 3c71ecd9e..abffefaba 100644 --- a/pkg/interface/src/views/apps/publish/components/Note.tsx +++ b/pkg/interface/src/views/apps/publish/components/Note.tsx @@ -52,7 +52,7 @@ export function Note(props: NoteProps & RouteComponentProps) { let adminLinks: JSX.Element | null = null; - if (window.ship === note?.post?.author) { + if (true || window.ship === note?.post?.author) { adminLinks = ( diff --git a/pkg/interface/src/views/apps/publish/components/Notebook.tsx b/pkg/interface/src/views/apps/publish/components/Notebook.tsx index de960f3d2..206349723 100644 --- a/pkg/interface/src/views/apps/publish/components/Notebook.tsx +++ b/pkg/interface/src/views/apps/publish/components/Notebook.tsx @@ -1,9 +1,10 @@ import React from "react"; -import { RouteComponentProps } from "react-router-dom"; +import { RouteComponentProps, Link } from "react-router-dom"; import { NotebookPosts } from "./NotebookPosts"; -import { Col } from "@tlon/indigo-react"; +import { Col, Box, Text, Button, Row } from "@tlon/indigo-react"; import GlobalApi from "~/logic/api/global"; import { Contacts, Rolodex, Groups, Associations, Graph, Association, Unreads } from "~/types"; +import { useShowNickname } from "~/logic/lib/util"; interface NotebookProps { api: GlobalApi; @@ -35,8 +36,27 @@ export function Notebook(props: NotebookProps & RouteComponentProps) { return null; // Waiting on groups to populate } + + const relativePath = (p: string) => props.baseUrl + p; + + const contact = notebookContacts?.[ship]; + const isOwn = `~${window.ship}` === ship; + console.log(association.resource); + + const showNickname = useShowNickname(contact); + return ( + + + {association.metadata?.title} + by + + {showNickname ? contact?.nickname : ship} + + + + `~${e}`); await api.groups.addTag( resource, - { app: 'publish', tag: `writers-${name}` }, + { app: 'graph', resource: association.resource, tag: `writers` }, ships ); actions.resetForm(); @@ -28,7 +28,8 @@ export class Writers extends Component { actions.setStatus({ error: e.message }); } }; - const writers = Array.from(groups?.[association?.group]?.tags.publish?.[`writers-${name}`] || new Set()).map(e => cite(`~${e}`)).join(', '); + const writers = Array.from(groups?.[association?.group]?.tags.graph[association.resource]?.writers || []).map(s => `~${s}`).join(', '); + return ( @@ -51,10 +52,14 @@ export class Writers extends Component { - {writers.length > 0 && <> + {writers.length > 0 ? <> Current writers: {writers} - } + : + + All group members can write to this channel + + } ); } diff --git a/pkg/interface/src/views/apps/publish/components/new-post.tsx b/pkg/interface/src/views/apps/publish/components/new-post.tsx index d44fa2885..2b0dd366a 100644 --- a/pkg/interface/src/views/apps/publish/components/new-post.tsx +++ b/pkg/interface/src/views/apps/publish/components/new-post.tsx @@ -32,7 +32,9 @@ export default function NewPost(props: NewPostProps & RouteComponentProps) { try { const [noteId, nodes] = newPost(title, body) await api.graph.addNodes(ship, book, nodes) - await waiter(p => p.graph.has(noteId)); + await waiter(p => + p.graph.has(noteId) && !p.graph.get(noteId)?.post?.pending + ); history.push(`${props.baseUrl}/note/${noteId}`); } catch (e) { console.error(e); From 2c2bc0195dd7b5d0a47642da9c87a788eb1bd626 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Thu, 28 Jan 2021 10:42:54 +1000 Subject: [PATCH 11/27] interface: refactor common components, add FormSubmit, IconRadio --- .../src/views/components/AsyncButton.tsx | 7 +- .../src/views/components/FormSubmit.tsx | 41 +++++ .../src/views/components/HoverBox.tsx | 11 +- .../src/views/components/IconRadio.tsx | 149 ++++++++++++++++++ .../src/views/components/ShipSearch.tsx | 6 +- .../landscape/components/SidebarItem.tsx | 44 ++++++ 6 files changed, 252 insertions(+), 6 deletions(-) create mode 100644 pkg/interface/src/views/components/FormSubmit.tsx create mode 100644 pkg/interface/src/views/components/IconRadio.tsx create mode 100644 pkg/interface/src/views/landscape/components/SidebarItem.tsx diff --git a/pkg/interface/src/views/components/AsyncButton.tsx b/pkg/interface/src/views/components/AsyncButton.tsx index f62288ee5..a181ef784 100644 --- a/pkg/interface/src/views/components/AsyncButton.tsx +++ b/pkg/interface/src/views/components/AsyncButton.tsx @@ -4,11 +4,12 @@ import { Button, LoadingSpinner } from "@tlon/indigo-react"; import { useFormikContext } from "formik"; -export function AsyncButton({ +export function AsyncButton({ children, + onSuccess = () => {}, ...rest }: Parameters[0]) { - const { isSubmitting, status, isValid } = useFormikContext(); + const { isSubmitting, status, isValid, setStatus } = useFormikContext(); const [success, setSuccess] = useState(); useEffect(() => { @@ -16,6 +17,7 @@ export function AsyncButton({ let done = false; if ("success" in s) { setSuccess(true); + onSuccess(); done = true; } else if ("error" in s) { setSuccess(false); @@ -25,6 +27,7 @@ export function AsyncButton({ setTimeout(() => { setSuccess(undefined); }, 1500); + done = false; } }, [status]); diff --git a/pkg/interface/src/views/components/FormSubmit.tsx b/pkg/interface/src/views/components/FormSubmit.tsx new file mode 100644 index 000000000..ae481de85 --- /dev/null +++ b/pkg/interface/src/views/components/FormSubmit.tsx @@ -0,0 +1,41 @@ +import React, { useCallback, ReactNode } from "react"; +import { useFormikContext } from "formik"; +import { Row, Button } from "@tlon/indigo-react"; +import { AsyncButton } from "./AsyncButton"; + +interface FormSubmitProps { + children?: ReactNode; +} + +export function FormSubmit(props: FormSubmitProps) { + const { children } = props; + const { initialValues, values, dirty, resetForm, isSubmitting } = useFormikContext(); + + const handleSuccess = useCallback(() => { + resetForm({ errors: {}, touched: {}, values, status: {} }); + }, [resetForm, values]); + + const handleRevert = useCallback(() => { + resetForm({ errors: {}, touched: {}, values: initialValues, status: {} }); + }, [resetForm, initialValues]); + + + return ( + + {dirty && !isSubmitting && ( + + )} + + {children} + + + ); +} diff --git a/pkg/interface/src/views/components/HoverBox.tsx b/pkg/interface/src/views/components/HoverBox.tsx index 4b6488b6a..5b2959eaf 100644 --- a/pkg/interface/src/views/components/HoverBox.tsx +++ b/pkg/interface/src/views/components/HoverBox.tsx @@ -2,6 +2,7 @@ import React from "react"; import { Link } from "react-router-dom"; import styled from "styled-components"; import { Box } from "@tlon/indigo-react"; +import { PropFunc } from "~/types/util"; interface HoverBoxProps { selected: boolean; bg: string; @@ -16,7 +17,15 @@ export const HoverBox = styled(Box)` } `; -export const HoverBoxLink = ({ to, children, ...rest }) => ( +interface HoverBoxLinkProps { + to: string; +} + +export const HoverBoxLink = ({ + to, + children, + ...rest +}: HoverBoxLinkProps & PropFunc) => ( {children} diff --git a/pkg/interface/src/views/components/IconRadio.tsx b/pkg/interface/src/views/components/IconRadio.tsx new file mode 100644 index 000000000..912329e8f --- /dev/null +++ b/pkg/interface/src/views/components/IconRadio.tsx @@ -0,0 +1,149 @@ +import React, { useCallback, useMemo } from "react"; +import styled from "styled-components"; +import { + Icon, + Box, + Row, + BaseLabel, + Indicator, + Col, + Label, +} from "@tlon/indigo-react"; +import { useField } from "formik"; + +type IconRadioProps = Parameters[0] & { + id: string; + icon: string; + name: string; + disabled?: boolean; + caption?: string; + label: string; +}; + +// Hide this input completely +const HiddenInput = styled.input` + position: absolute; + opacity: 0; + height: 0; + width: 0; + margin: 0px; +`; + +type IconIndicatorProps = Parameters & { + disabled?: boolean; + selected?: boolean; + hasError?: boolean; +}; + +// stolen from indigo +// TODO: indigo should probably export this +const indicator = { + state: { + on: { + //"*": { fill: "white" }, + backgroundColor: "blue", + borderColor: "blue", + }, + off: { + //"*": { fill: "transparent" }, + backgroundColor: "white", + borderColor: "lightGray", + }, + onError: { + //"*": { fill: "white" }, + backgroundColor: "red", + borderColor: "red", + }, + offError: { + // "*": { fill: "transparent" }, + backgroundColor: "washedRed", + borderColor: "red", + }, + offDisabled: { + //"*": { fill: "transparent" }, + backgroundColor: "washedGray", + borderColor: "lightGray", + }, + onDisabled: { + //"*": { fill: "lightGray" }, + backgroundColor: "washedGray", + borderColor: "lightGray", + }, + }, +}; + +const IconIndicator = ({ disabled, selected, hasError, children, ...rest }) => { + const style = useMemo(() => { + if (selected && disabled) return indicator.state.onDisabled; + if (selected && hasError) return indicator.state.onError; + if (selected) return indicator.state.on; + if (disabled) return indicator.state.offDisabled; + if (hasError) return indicator.state.offError; + return indicator.state.off; + }, [selected, disabled, hasError]); + + return ( + + {children} + + ); +}; + +export function IconRadio(props: IconRadioProps) { + const { id, icon, disabled, caption, label, name, ...rest } = props; + const [field, meta, { setTouched }] = useField({ + name, + id, + value: id, + type: "radio", + }); + + const onChange = useCallback( + (e: React.ChangeEvent) => { + setTouched(true); + field.onChange(e); + }, + [field.onChange, setTouched] + ); + + return ( + + + + + + + + {caption ? ( + + ) : null} + + + + + ); +} diff --git a/pkg/interface/src/views/components/ShipSearch.tsx b/pkg/interface/src/views/components/ShipSearch.tsx index f26ed4253..fbfddfd46 100644 --- a/pkg/interface/src/views/components/ShipSearch.tsx +++ b/pkg/interface/src/views/components/ShipSearch.tsx @@ -47,7 +47,7 @@ const Candidate = ({ title, detail, selected, onClick }) => ( export function ShipSearch(props: InviteSearchProps) { const { id, label, caption } = props; - const [{}, meta, { setValue, setTouched, setError: _setError }] = useField({ + const [{ value }, meta, { setValue, setTouched, setError: _setError }] = useField({ name: id, multiple: true }); @@ -56,7 +56,7 @@ export function ShipSearch(props: InviteSearchProps) { const { error, touched } = meta; - const [selected, setSelected] = useState([] as string[]); + const [selected, setSelected] = useState(value); const [inputShip, setInputShip] = useState(undefined as string | undefined); const [inputTouched, setInputTouched] = useState(false); @@ -92,7 +92,7 @@ export function ShipSearch(props: InviteSearchProps) { (s: string) => { setTouched(true); checkInput(true, undefined); - s = `${deSig(s)}`; + s = `~${deSig(s)}`; setSelected(v => _.uniq([...v, s])) }, [setTouched, checkInput, setSelected] diff --git a/pkg/interface/src/views/landscape/components/SidebarItem.tsx b/pkg/interface/src/views/landscape/components/SidebarItem.tsx new file mode 100644 index 000000000..798d31e3e --- /dev/null +++ b/pkg/interface/src/views/landscape/components/SidebarItem.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { Row, Icon, Text } from "@tlon/indigo-react"; + +import { IconRef, PropFunc } from "~/types/util"; +import { HoverBoxLink } from "~/views/components/HoverBox"; + +interface SidebarItemProps { + selected?: boolean; + icon: IconRef; + text: string; + to: string; + color?: string; + children?: JSX.Element; +} + +export const SidebarItem = ({ + icon, + text, + to, + selected = false, + color = "black", + children, + ...rest +}: SidebarItemProps & PropFunc) => { + return ( + + + + {text} + + {children} + + ); +}; From e972010e2ddec8a159b93c88de897ffed712098f Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Thu, 28 Jan 2021 10:54:38 +1000 Subject: [PATCH 12/27] channels: refactor settings, remove dropdown hamburger --- .../landscape/components/ChannelMenu.tsx | 127 ---------- .../ChannelPermissions.tsx | 220 ++++++++++++++++++ .../ChannelPopoverRoutes/Details.tsx | 83 +++++++ .../ChannelPopoverRoutes/Notifications.tsx | 44 ++++ .../ChannelPopoverRoutes/Sidebar.tsx | 66 ++++++ .../components/ChannelPopoverRoutes/index.tsx | 98 ++++++++ .../landscape/components/ChannelSettings.tsx | 110 --------- .../views/landscape/components/Resource.tsx | 60 ++--- .../landscape/components/ResourceSkeleton.tsx | 119 +++++----- 9 files changed, 588 insertions(+), 339 deletions(-) delete mode 100644 pkg/interface/src/views/landscape/components/ChannelMenu.tsx create mode 100644 pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/ChannelPermissions.tsx create mode 100644 pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/Details.tsx create mode 100644 pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/Notifications.tsx create mode 100644 pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/Sidebar.tsx create mode 100644 pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/index.tsx delete mode 100644 pkg/interface/src/views/landscape/components/ChannelSettings.tsx diff --git a/pkg/interface/src/views/landscape/components/ChannelMenu.tsx b/pkg/interface/src/views/landscape/components/ChannelMenu.tsx deleted file mode 100644 index fc1120656..000000000 --- a/pkg/interface/src/views/landscape/components/ChannelMenu.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import React, { useCallback } from "react"; -import { Link, useHistory } from "react-router-dom"; - -import { Icon, Row, Col, Button, Text, Box, Action } from "@tlon/indigo-react"; -import { Dropdown } from "~/views/components/Dropdown"; -import { Association, NotificationGraphConfig } from "~/types"; -import GlobalApi from "~/logic/api/global"; -import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction"; -import { appIsGraph } from "~/logic/lib/util"; - -const ChannelMenuItem = ({ - icon, - color = undefined as string | undefined, - children, - bottom = false, -}) => ( - - - {children} - -); - -interface ChannelMenuProps { - association: Association; - api: GlobalApi; - graphNotificationConfig: NotificationGraphConfig; - chatNotificationConfig: string[]; -} - -export function ChannelMenu(props: ChannelMenuProps) { - const { association, api } = props; - const history = useHistory(); - const { metadata } = association; - const app = metadata.module || association["app-name"]; - const workspace = history.location.pathname.startsWith("/~landscape/home") - ? "/home" - : association?.group; - const baseUrl = `/~landscape${workspace}/resource/${app}${association.resource}`; - const rid = association.resource; - - const [,, ship, name] = rid.split("/"); - - const isOurs = ship.slice(1) === window.ship; - - const isMuted = - props.graphNotificationConfig.watching.findIndex( - (a) => a.graph === rid && a.index === "/" - ) === -1; - - const onChangeMute = async () => { - const func = isMuted ? "listenGraph" : "ignoreGraph"; - await api.hark[func](rid, "/"); - }; - const onUnsubscribe = useCallback(async () => { - await api.graph.leaveGraph(ship, name); - history.push(`/~landscape${workspace}`); - }, [api, association]); - - const onDelete = useCallback(async () => { - if (confirm('Are you sure you want to delete this channel?')) { - await api.graph.deleteGraph(name); - history.push(`/~landscape${workspace}`); - } - }, [api, association]); - - return ( - - - - {isMuted ? "Unmute" : "Mute"} this channel - - - {isOurs ? ( - <> - - - Delete Channel - - - - - - Channel Settings - - - - - ) : ( - - - Unsubscribe from Channel - - - )} - - } - alignX="right" - alignY="top" - dropWidth="250px" - > - - - ); -} diff --git a/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/ChannelPermissions.tsx b/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/ChannelPermissions.tsx new file mode 100644 index 000000000..3d576bde0 --- /dev/null +++ b/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/ChannelPermissions.tsx @@ -0,0 +1,220 @@ +import React from "react"; +import _ from "lodash"; +import * as Yup from "yup"; +import { + Label, + ManagedTextInputField as Input, + ManagedToggleSwitchField as Checkbox, + Box, + Col, + Text, + Row, + ManagedRadioButtonField as Radio, +} from "@tlon/indigo-react"; +import { Formik, Form, FormikHelpers } from "formik"; +import { PermVariation, Association, Group, Groups, Rolodex } from "~/types"; +import { ShipSearch } from "~/views/components/ShipSearch"; +import GlobalApi from "~/logic/api/global"; +import { resourceFromPath } from "~/logic/lib/group"; +import { AsyncButton } from "~/views/components/AsyncButton"; +import { FormSubmit } from "~/views/components/FormSubmit"; + +function PermissionsSummary(props: { + writersSize: number; + vip: PermVariation; +}) { + const { writersSize, vip } = props; + + const description = + writersSize === 0 + ? "Currently, all members of the group can write to this channel" + : `Currently, only ${writersSize} ship${ + writersSize > 1 ? "s" : "" + } can write to this channel`; + + const vipDescription = + vip === "reader-comments" && writersSize !== 0 + ? ". All ships may comment" + : ""; + + return ( + + + {description} + {vipDescription} + + + ); +} + +interface GraphPermissionsProps { + association: Association; + group: Group; + groups: Groups; + contacts: Rolodex; + api: GlobalApi; +} + +interface FormSchema { + writePerms: "self" | "everyone" | "subset"; + writers: string[]; + readerComments: boolean; +} + +const formSchema = (members: string[]) => { + return Yup.object({ + writePerms: Yup.string(), + writers: Yup.array(Yup.string().oneOf(members, "${value} is not in group")), + readerComments: Yup.boolean(), + }); +}; + +export function GraphPermissions(props: GraphPermissionsProps) { + const { api, group, association } = props; + + const writers = _.get( + group.tags, + ["graph", association.resource, "writers"], + new Set() + ); + + let [, , hostShip] = association.resource.split("/"); + hostShip = hostShip.slice(1); + + const writePerms = + writers.size === 0 + ? ("everyone" as const) + : writers.size === 1 && writers.has(hostShip) + ? ("self" as const) + : ("subset" as const); + + const readerComments = association.metadata.vip === "reader-comments"; + + const initialValues = { + writePerms, + writers: Array.from(writers) + .filter((x) => x !== hostShip) + .map((s) => `~${s}`), + readerComments: association.metadata.vip === "reader-comments", + }; + + const onSubmit = async (values: FormSchema, actions) => { + const resource = resourceFromPath(association.group); + const tag = { + app: "graph", + resource: association.resource, + tag: "writers", + }; + const allWriters = Array.from(writers).map(w => `~${w}`); + if (values.readerComments !== readerComments) { + await api.metadata.update(association, { + vip: values.readerComments ? "reader-comments" : "", + }); + } + + if (values.writePerms === "everyone") { + if (writePerms === "everyone") { + actions.setStatus({ success: null }); + return; + } + await api.groups.removeTag( + resource, + tag, + allWriters + ); + } else if (values.writePerms === "self") { + if (writePerms === "self") { + actions.setStatus({ success: null }); + return; + } + await api.groups.removeTag( + resource, + tag, + allWriters + ); + await api.groups.addTag(resource, tag, [`~${hostShip}`]); + actions.setStatus({ success: null }); + } else if (values.writePerms === "subset") { + const toRemove = _.difference(allWriters, values.writers) + + const toAdd = [..._.difference(values.writers, allWriters), `~${hostShip}`]; + + toRemove.length > 0 && await api.groups.removeTag(resource, tag, toRemove); + toAdd.length > 0 && await api.groups.addTag(resource, tag, toAdd); + + actions.setStatus({ success: null }); + } + }; + + const schema = formSchema(Array.from(group.members).map(m => `~${m}`)); + + return ( + + {({ values }) => ( +
+ + + + Permissions + + + Add or remove read/write privileges to this channel. Group + admins can always write to a channel + + + + + + + + + + + + {values.writePerms === "subset" && ( + + )} + + {association.metadata.module !== "chat" && ( + + )} + + Update Permissions + + + + )} +
+ ); +} diff --git a/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/Details.tsx b/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/Details.tsx new file mode 100644 index 000000000..20d0d70da --- /dev/null +++ b/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/Details.tsx @@ -0,0 +1,83 @@ +import React from "react"; +import { + Box, + ManagedTextInputField as Input, + ManagedCheckboxField as Checkbox, + Col, + Label, + Text, + Row, +} from "@tlon/indigo-react"; +import { Formik, Form } from "formik"; + +import { FormError } from "~/views/components/FormError"; +import { ColorInput } from "~/views/components/ColorInput"; +import { AsyncButton } from "~/views/components/AsyncButton"; +import { uxToHex, wait } from "~/logic/lib/util"; +import GlobalApi from "~/logic/api/global"; +import { Association } from "~/types"; +import { FormSubmit } from "~/views/components/FormSubmit"; + +interface FormSchema { + title: string; + description: string; + color: string; +} + +interface ChannelDetailsProps { + api: GlobalApi; + association: Association; +} + +export function ChannelDetails(props: ChannelDetailsProps) { + const { association, api } = props; + const { metadata } = association; + const initialValues: FormSchema = { + title: metadata?.title || "", + description: metadata?.description || "", + color: metadata?.color || "0x0", + }; + + const onSubmit = async (values: FormSchema, actions) => { + const { title, description } = values; + const color = uxToHex(values.color); + await api.metadata.update(association, { title, color, description }); + actions.setStatus({ success: null }); + }; + + return ( + +
+ + + + Channel Details + + + + + + + + Update Details + + + + +
+ ); +} diff --git a/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/Notifications.tsx b/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/Notifications.tsx new file mode 100644 index 000000000..aa245cefe --- /dev/null +++ b/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/Notifications.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { Col, Text, BaseLabel, Label } from "@tlon/indigo-react"; +import GlobalApi from "~/logic/api/global"; +import { Association, NotificationGraphConfig } from "~/types"; +import { StatelessAsyncToggle } from "~/views/components/StatelessAsyncToggle"; + +interface ChannelNotificationsProps { + api: GlobalApi; + association: Association; + notificationsGraphConfig: NotificationGraphConfig; +} + +export function ChannelNotifications(props: ChannelNotificationsProps) { + const { api, association } = props; + const rid = association.resource; + + const isMuted = + props.notificationsGraphConfig.watching.findIndex( + (a) => a.graph === rid && a.index === "/" + ) === -1; + + const onChangeMute = async () => { + const func = isMuted ? "listenGraph" : "ignoreGraph"; + await api.hark[func](rid, "/"); + }; + + return ( + + + Channel Notifications + + + + + + + + + + ); +} diff --git a/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/Sidebar.tsx b/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/Sidebar.tsx new file mode 100644 index 000000000..ccacbd355 --- /dev/null +++ b/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/Sidebar.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import { Text, Col } from "@tlon/indigo-react"; +import { SidebarItem } from "../SidebarItem"; +import { isChannelAdmin } from "~/logic/lib/group"; + +export function ChannelPopoverRoutesSidebar(props: { + baseUrl: string; + isOwner: boolean; + isAdmin: boolean; +}) { + const { baseUrl, isAdmin, isOwner } = props; + + const relativePath = (p: string) => `${baseUrl}${p}`; + + return ( + + + Channel Settings + + + Preferences + + + {!isOwner && ( + + )} + {isAdmin && ( + <> + + Administration + + + + + + )} + + ); +} diff --git a/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/index.tsx b/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/index.tsx new file mode 100644 index 000000000..c440a4bd3 --- /dev/null +++ b/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/index.tsx @@ -0,0 +1,98 @@ +import React, { useRef } from "react"; +import { ModalOverlay } from "~/views/components/ModalOverlay"; +import { Col, Box, Text, Row } from "@tlon/indigo-react"; +import { ChannelPopoverRoutesSidebar } from "./Sidebar"; +import { ChannelDetails } from "./Details"; +import { GraphPermissions } from "./ChannelPermissions"; +import { + Association, + Groups, + Group, + Rolodex, + NotificationGraphConfig, +} from "~/types"; +import GlobalApi from "~/logic/api/global"; +import { useHashLink } from "~/logic/lib/useHashLink"; +import { useOutsideClick } from "~/logic/lib/useOutsideClick"; +import { useHistory } from "react-router-dom"; +import { ChannelNotifications } from "./Notifications"; +import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton"; +import { wait } from "~/logic/lib/util"; +import { isChannelAdmin, isHost } from "~/logic/lib/group"; + +interface ChannelPopoverRoutesProps { + baseUrl: string; + association: Association; + group: Group; + groups: Groups; + contacts: Rolodex; + api: GlobalApi; + notificationsGraphConfig: NotificationGraphConfig; +} + +export function ChannelPopoverRoutes(props: ChannelPopoverRoutesProps) { + const { association, group } = props; + useHashLink(); + const overlayRef = useRef(); + const history = useHistory(); + + useOutsideClick(overlayRef, () => { + history.push(props.baseUrl); + }); + + const handleUnsubscribe = async () => { + await wait(150); + }; + + const canAdmin = isChannelAdmin(group, association.resource); + const isOwner = isHost(association.resource); + return ( + + + + + + {!isOwner && ( + + + Unsubscribe from Channel + + + Unsubscribing from a channel will revoke your ability to read + its contents. Any permissions set by the channel host will still + apply once you have left. + + + + Unsubscribe from {props.association.metadata.title} + + + + )} + {canAdmin && ( + <> + + + + )} + + + + ); +} diff --git a/pkg/interface/src/views/landscape/components/ChannelSettings.tsx b/pkg/interface/src/views/landscape/components/ChannelSettings.tsx deleted file mode 100644 index 39a571f4a..000000000 --- a/pkg/interface/src/views/landscape/components/ChannelSettings.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { useEffect } from "react"; -import { AsyncButton } from "~/views/components/AsyncButton"; -import * as Yup from "yup"; -import { - Box, - ManagedTextInputField as Input, - Col, - Label, - Text, -} from "@tlon/indigo-react"; -import { Formik, Form, useFormikContext, FormikHelpers } from "formik"; -import GlobalApi from "~/logic/api/global"; -import { uxToHex } from "~/logic/lib/util"; -import { FormError } from "~/views/components/FormError"; -import { ColorInput } from "~/views/components/ColorInput"; -import { Association, Groups, Associations } from "~/types"; -import Writers from '~/views/apps/publish/components/Writers'; -import GroupifyForm from "./GroupifyForm"; - -interface FormSchema { - title: string; - description: string; - color: string; -} - -interface ChannelSettingsProps { - association: Association; - groups: Groups; - associations: Associations; - api: GlobalApi; -} - -export function ChannelSettings(props: ChannelSettingsProps) { - const { api, association } = props; - const { metadata } = association; - const initialValues: FormSchema = { - title: metadata?.title || "", - description: metadata?.description || "", - color: metadata?.color || "0x0", - }; - - const onSubmit = async ( - values: FormSchema, - actions: FormikHelpers - ) => { - try { - const app = association["app-name"]; - const resource = association.resource; - const group = association.group; - const date = metadata["date-created"]; - const { title, description, color } = values; - await api.metadata.metadataAdd( - app, - resource, - group, - title, - description, - date, - uxToHex(color), - metadata.module - ); - actions.setStatus({ success: null }); - } catch (e) { - console.log(e); - actions.setStatus({ error: e.message }); - } - }; - - return ( - - -
- - - Channel Settings - - - - - - - Save - - - - -
- - {(metadata?.module === 'publish') && (<> - - - )} - - - ); -} diff --git a/pkg/interface/src/views/landscape/components/Resource.tsx b/pkg/interface/src/views/landscape/components/Resource.tsx index ddfd316c5..d1f4d671e 100644 --- a/pkg/interface/src/views/landscape/components/Resource.tsx +++ b/pkg/interface/src/views/landscape/components/Resource.tsx @@ -13,6 +13,7 @@ import GlobalApi from "~/logic/api/global"; import { RouteComponentProps, Route, Switch } from "react-router-dom"; import { ChannelSettings } from "./ChannelSettings"; import { ResourceSkeleton } from "./ResourceSkeleton"; +import {ChannelPopoverRoutes} from "./ChannelPopoverRoutes"; const TruncatedBox = styled(Box)` white-space: nowrap; @@ -27,14 +28,14 @@ type ResourceProps = StoreState & { } & RouteComponentProps; export function Resource(props: ResourceProps) { - const { association, api, notificationsGraphConfig } = props; + const { association, api, notificationsGraphConfig, groups } = props; const app = association.metadata.module || association["app-name"]; const rid = association.resource; const selectedGroup = association.group; const relativePath = (p: string) => `${props.baseUrl}/resource/${app}${rid}${p}`; - const skelProps = { api, association }; + const skelProps = { api, association, groups }; let title = props.association.metadata.title; if ('workspace' in props) { if ('group' in props.workspace && props.workspace.group in props.associations.contacts) { @@ -46,48 +47,35 @@ export function Resource(props: ResourceProps) { {props.notificationsCount ? `(${String(props.notificationsCount)}) ` : ''}{ title } + + {app === "chat" ? ( + + ) : app === "publish" ? ( + + ) : ( + + )} + { return ( - - - + ); }} /> - ( - - {app === "chat" ? ( - - ) : app === "publish" ? ( - - ) : ( - - )} - - )} - /> ); diff --git a/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx b/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx index 36c668e39..f4caf0bdc 100644 --- a/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx +++ b/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx @@ -1,5 +1,5 @@ import React, { ReactNode } from "react"; -import { Row, Box, Col, Text } from "@tlon/indigo-react"; +import { Row, Icon, Box, Col, Text } from "@tlon/indigo-react"; import styled from "styled-components"; import { Link } from "react-router-dom"; @@ -13,7 +13,8 @@ import GlobalApi from "~/logic/api/global"; import { RouteComponentProps, Route, Switch } from "react-router-dom"; import { ChannelSettings } from "./ChannelSettings"; import { ChannelMenu } from "./ChannelMenu"; -import { NotificationGraphConfig } from "~/types"; +import { NotificationGraphConfig, Groups } from "~/types"; +import {isWriter} from "~/logic/lib/group"; const TruncatedBox = styled(Box)` white-space: pre; @@ -22,22 +23,23 @@ const TruncatedBox = styled(Box)` `; type ResourceSkeletonProps = { + groups: Groups; association: Association; - notificationsGraphConfig: NotificationGraphConfig; api: GlobalApi; baseUrl: string; children: ReactNode; - atRoot?: boolean; title?: string; groupTags?: any; }; export function ResourceSkeleton(props: ResourceSkeletonProps) { - const { association, api, baseUrl, children, atRoot, groupTags } = props; + const { association, api, baseUrl, children, atRoot, groups } = props; const app = association?.metadata?.module || association["app-name"]; const rid = association.resource; + const group = groups[association.group]; const workspace = - baseUrl === "/~landscape/home" ? "/home" : association.group; + group?.hidden ? "/home" : association.group; + const title = props.title || association?.metadata?.title; const [, , ship, resource] = rid.split("/"); @@ -45,10 +47,10 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) { const resourcePath = (p: string) => baseUrl + `/resource/${app}/ship/${ship}/${resource}` + p; const isOwn = `~${window.ship}` === ship; - let isWriter = (app === 'publish') ? true : false; + let canWrite = (app === 'publish') ? true : false; - if (groupTags?.publish?.[`writers-${resource}`]) { - isWriter = isOwn || groupTags?.publish?.[`writers-${resource}`]?.has(window.ship); + if (!isWriter(group, association.resource)) { + canWrite = isOwn; } return ( @@ -63,64 +65,49 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) { borderBottom={1} borderBottomColor="washedGray" > - {atRoot ? ( - + {"<- Back"} + + + + {title} + + + + - {"<- Back"} - - ) : ( - - - Go back to channel - - - )} - - {atRoot && ( - <> - - - {title} - - - - - {association?.metadata?.description} - - - - {isWriter && ( - - + New Post - - )} - - - )} + {association?.metadata?.description} + + + + {canWrite && ( + + + New Post + + )} + + + {children} From 61ba382957f21d250c2e118d1289e9a1251944de Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Thu, 28 Jan 2021 10:55:35 +1000 Subject: [PATCH 13/27] interface: surface member-metadata functionality --- .../landscape/components/GroupSettings/Admin.tsx | 14 +++++++++++++- .../views/landscape/components/Sidebar/Sidebar.tsx | 1 + .../components/Sidebar/SidebarListHeader.tsx | 8 ++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/pkg/interface/src/views/landscape/components/GroupSettings/Admin.tsx b/pkg/interface/src/views/landscape/components/GroupSettings/Admin.tsx index 86970fbad..34fc6f9ab 100644 --- a/pkg/interface/src/views/landscape/components/GroupSettings/Admin.tsx +++ b/pkg/interface/src/views/landscape/components/GroupSettings/Admin.tsx @@ -31,6 +31,7 @@ interface FormSchema { color: string; isPrivate: boolean; picture: string; + adminMetadata: boolean; } const formSchema = Yup.object({ @@ -38,6 +39,7 @@ const formSchema = Yup.object({ description: Yup.string(), color: Yup.string(), isPrivate: Yup.boolean(), + adminMetadata: Yup.boolean() }); interface GroupAdminSettingsProps { @@ -58,6 +60,7 @@ export function GroupAdminSettings(props: GroupAdminSettingsProps) { color: metadata?.color, picture: metadata?.picture, isPrivate: currentPrivate, + adminMetadata: metadata.vip !== 'member-metadata' }; const onSubmit = async ( @@ -65,13 +68,15 @@ export function GroupAdminSettings(props: GroupAdminSettingsProps) { actions: FormikHelpers ) => { try { - const { title, description, picture, color, isPrivate } = values; + const { title, description, picture, color, isPrivate, adminMetadata } = values; const uxColor = uxToHex(color); + const vip = adminMetadata ? '' : 'member-metadata'; await props.api.metadata.update(props.association, { title, description, picture, color: uxColor, + vip }); if (isPrivate !== currentPrivate) { const resource = resourceFromPath(props.association.group); @@ -135,6 +140,13 @@ export function GroupAdminSettings(props: GroupAdminSettingsProps) { caption="If enabled, users must be invited to join the group" disabled={disabled} /> + + Date: Thu, 28 Jan 2021 14:27:38 +1000 Subject: [PATCH 14/27] ShipSearch: refactor to prevent unnecessary rerenders --- .../src/views/components/DropdownSearch.tsx | 4 +- .../src/views/components/ShipSearch.tsx | 311 ++++++++++-------- 2 files changed, 168 insertions(+), 147 deletions(-) diff --git a/pkg/interface/src/views/components/DropdownSearch.tsx b/pkg/interface/src/views/components/DropdownSearch.tsx index 6a12e5acd..5860af19e 100644 --- a/pkg/interface/src/views/components/DropdownSearch.tsx +++ b/pkg/interface/src/views/components/DropdownSearch.tsx @@ -37,6 +37,7 @@ interface DropdownSearchExtraProps { placeholder?: string; onChange?: (e: ChangeEvent) => void; onBlur?: (e: any) => void; + onFocus?: (e: FocusEvent) => void; } type DropdownSearchProps = PropFunc & @@ -53,6 +54,7 @@ export function DropdownSearch(props: DropdownSearchProps) { renderCandidate, disabled, placeholder, + onFocus = () => {}, onChange = () => {}, onBlur = () => {}, ...rest @@ -101,7 +103,7 @@ export function DropdownSearch(props: DropdownSearchProps) { return () => { mousetrap.unbind(["down", "tab"]); mousetrap.unbind(["up", "shift+tab"]); - mousetrap.unbind("enter", onEnter); + mousetrap.unbind("enter"); }; }, [textarea.current, next, back, onEnter]); diff --git a/pkg/interface/src/views/components/ShipSearch.tsx b/pkg/interface/src/views/components/ShipSearch.tsx index fbfddfd46..4cda31169 100644 --- a/pkg/interface/src/views/components/ShipSearch.tsx +++ b/pkg/interface/src/views/components/ShipSearch.tsx @@ -1,8 +1,25 @@ -import React, { useMemo, useCallback, ChangeEvent, useState, SyntheticEvent, useEffect } from "react"; -import { Box, Label, Icon, Text, Row, Col, ErrorLabel } from "@tlon/indigo-react"; +import React, { + useMemo, + useCallback, + ChangeEvent, + useState, + SyntheticEvent, + useEffect, + useRef, +} from "react"; +import { + Box, + Label, + Icon, + Text, + Row, + Col, + ErrorLabel, +} from "@tlon/indigo-react"; import _ from "lodash"; import ob from "urbit-ob"; -import { useField } from "formik"; +import * as Yup from "yup"; +import { useField, FieldArray, useFormikContext } from "formik"; import styled from "styled-components"; import { DropdownSearch } from "./DropdownSearch"; @@ -13,18 +30,47 @@ import { HoverBox } from "./HoverBox"; const INVALID_SHIP_ERR = "Invalid ship"; -interface InviteSearchProps { +interface InviteSearchProps { autoFocus?: boolean; disabled?: boolean; label?: string; caption?: string; - id: string; + id: I; contacts: Rolodex; groups: Groups; hideSelection?: boolean; maxLength?: number; } +const getNicknameForShips = (groups: Groups, contacts: Rolodex) => { + const peerSet = new Set(); + const nicknames = new Map(); + _.forEach(groups, (group, path) => { + if (group.members.size > 0) { + const groupEntries = group.members.values(); + for (const member of groupEntries) { + peerSet.add(member); + } + } + + const groupContacts = contacts[path]; + + if (groupContacts) { + const groupEntries = group.members.values(); + for (const member of groupEntries) { + if (groupContacts[member]) { + if (nicknames.has(member)) { + nicknames.get(member)?.push(groupContacts[member].nickname); + } else { + nicknames.set(member, [groupContacts[member].nickname]); + } + } + } + } + }); + return [Array.from(peerSet), nicknames] as const; +}; + const Candidate = ({ title, detail, selected, onClick }) => ( ( ); -export function ShipSearch(props: InviteSearchProps) { +type Value = { + [k in I]: string[]; +}; + +const shipItemSchema = Yup.string().test( + "is-patp", + "${value} is not a valid @p", + ob.isValidPatp +); + +export const shipSearchSchema = Yup.array(shipItemSchema).compact(); + +export const shipSearchSchemaInGroup = (members: string[]) => + Yup.array(shipItemSchema.oneOf(members, "${value} not a member of this group")).compact(); + +export function ShipSearch>( + props: InviteSearchProps +) { const { id, label, caption } = props; - const [{ value }, meta, { setValue, setTouched, setError: _setError }] = useField({ - name: id, - multiple: true - }); + const { + values, + touched, + errors, + initialValues, + setFieldValue, + } = useFormikContext(); - const setError = _setError as unknown as (s: string | undefined) => void; + const inputIdx = useRef(initialValues[id].length); - const { error, touched } = meta; + const selected: string[] = values[id] ?? []; - const [selected, setSelected] = useState(value); - const [inputShip, setInputShip] = useState(undefined as string | undefined); - const [inputTouched, setInputTouched] = useState(false); + console.log(selected); - const checkInput = useCallback((valid: boolean, ship: string | undefined) => { - if(valid) { - setInputShip(ship); - setError(error === INVALID_SHIP_ERR ? undefined : error); - } else if (ship === undefined) { - return; - } else { - setError(INVALID_SHIP_ERR); - setInputTouched(false); - } - }, [setError, error, setInputTouched, setInputShip]); + const name = () => `${props.id}[${inputIdx.current}]`; - const onChange = useCallback( - (e: any) => { - let ship = `~${deSig(e.target.value) || ""}`; - if(ob.isValidPatp(ship)) { - checkInput(true, ship); - } else { - checkInput(ship.length !== 1, undefined) - } - }, - [checkInput] + const pills = selected.slice(0, inputIdx.current); + + const [peers, nicknames] = useMemo( + () => getNicknameForShips(props.groups, props.contacts), + [props.contacts, props.groups] ); - const onBlur = useCallback(() => { - setInputTouched(true); - }, [setInputTouched]); - - const onSelect = useCallback( - (s: string) => { - setTouched(true); - checkInput(true, undefined); - s = `~${deSig(s)}`; - setSelected(v => _.uniq([...v, s])) - }, - [setTouched, checkInput, setSelected] - ); - - const onRemove = useCallback( - (s: string) => { - setSelected(ships => ships.filter(ship => ship !== s)) - }, - [setSelected] - ); - - useEffect(() => { - const newValue = inputShip ? [...selected, inputShip] : selected; - setValue(newValue); - }, [inputShip, selected]) - - const [peers, nicknames] = useMemo(() => { - const peerSet = new Set(); - const contacts = new Map(); - _.forEach(props.groups, (group, path) => { - if (group.members.size > 0) { - const groupEntries = group.members.values(); - for (const member of groupEntries) { - peerSet.add(member); - } - } - - const groupContacts = props.contacts[path]; - - if (groupContacts) { - const groupEntries = group.members.values(); - for (const member of groupEntries) { - if (groupContacts[member]) { - if (contacts.has(member)) { - contacts.get(member)?.push(groupContacts[member].nickname); - } else { - contacts.set(member, [groupContacts[member].nickname]); - } - } - } - } - }); - return [Array.from(peerSet), contacts] as const; - }, [props.contacts, props.groups]); - const renderCandidate = useCallback( (s: string, selected: boolean, onSelect: (s: string) => void) => { const detail = _.uniq(nicknames.get(s)).join(", "); @@ -158,62 +152,87 @@ export function ShipSearch(props: InviteSearchProps) { [nicknames] ); + const onChange = (e: ChangeEvent) => { + const newValue = + e.target.value?.length > 0 ? `~${deSig(e.target.value)}` : ""; + setFieldValue(name(), newValue); + }; + + const error = _.compact(errors[id] as string[]); + return ( - - - {caption && ( - - )} - - mt="2" - isExact={(s) => { - const ship = `~${deSig(s)}`; - const result = ob.isValidPatp(ship); - return result ? deSig(s) ?? undefined : undefined; - }} - placeholder="Search for ships" - candidates={peers} - renderCandidate={renderCandidate} - disabled={props.maxLength ? selected.length >= props.maxLength : false} - search={(s: string, t: string) => - t.toLowerCase().startsWith(s.toLowerCase()) - } - getKey={(s: string) => s} - onSelect={onSelect} - onChange={onChange} - onBlur={onBlur} - /> - - {selected.map((s) => ( - - {cite(s)} - onRemove(s)} - cursor="pointer" + { + const onAdd = () => { + inputIdx.current += 1; + arrayHelpers.push(""); + }; + + const onRemove = (idx: number) => { + inputIdx.current -= 1; + arrayHelpers.remove(idx); + }; + + return ( + + + {caption && ( + + )} + + + mt="2" + isExact={(s) => { + const ship = `~${deSig(s)}`; + const result = ob.isValidPatp(ship); + return result ? deSig(s) ?? undefined : undefined; + }} + placeholder="Search for ships" + candidates={peers} + renderCandidate={renderCandidate} + disabled={ + props.maxLength ? selected.length >= props.maxLength : false + } + search={(s: string, t: string) => + (t || "").toLowerCase().startsWith(s.toLowerCase()) + } + getKey={(s: string) => s} + onChange={onChange} + onSelect={onAdd} /> - - ))} - - - {error} - - + + {pills.map((s, i) => ( + + {cite(s)} + onRemove(i)} + cursor="pointer" + /> + + ))} + + 0}> + {error.join(", ")} + + + ); + }} + /> ); } From 91d3e2aee0009de4772328477422d61f66328251 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Thu, 28 Jan 2021 14:28:20 +1000 Subject: [PATCH 15/27] NewChannel: update for new designs --- .../ChannelPermissions.tsx | 130 +++++------- .../components/ChannelWritePerms.tsx | 45 ++++ .../views/landscape/components/NewChannel.tsx | 97 ++++----- .../landscape/components/PopoverRoutes.tsx | 192 ++++++++---------- 4 files changed, 223 insertions(+), 241 deletions(-) create mode 100644 pkg/interface/src/views/landscape/components/ChannelWritePerms.tsx diff --git a/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/ChannelPermissions.tsx b/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/ChannelPermissions.tsx index 3d576bde0..c92cd0477 100644 --- a/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/ChannelPermissions.tsx +++ b/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/ChannelPermissions.tsx @@ -3,21 +3,18 @@ import _ from "lodash"; import * as Yup from "yup"; import { Label, - ManagedTextInputField as Input, ManagedToggleSwitchField as Checkbox, Box, Col, Text, - Row, - ManagedRadioButtonField as Radio, } from "@tlon/indigo-react"; -import { Formik, Form, FormikHelpers } from "formik"; +import { Formik, Form } from "formik"; import { PermVariation, Association, Group, Groups, Rolodex } from "~/types"; -import { ShipSearch } from "~/views/components/ShipSearch"; +import { shipSearchSchemaInGroup, } from "~/views/components/ShipSearch"; import GlobalApi from "~/logic/api/global"; import { resourceFromPath } from "~/logic/lib/group"; -import { AsyncButton } from "~/views/components/AsyncButton"; import { FormSubmit } from "~/views/components/FormSubmit"; +import { ChannelWritePerms } from "../ChannelWritePerms"; function PermissionsSummary(props: { writersSize: number; @@ -70,7 +67,7 @@ interface FormSchema { const formSchema = (members: string[]) => { return Yup.object({ writePerms: Yup.string(), - writers: Yup.array(Yup.string().oneOf(members, "${value} is not in group")), + writers: shipSearchSchemaInGroup(members), readerComments: Yup.boolean(), }); }; @@ -105,13 +102,14 @@ export function GraphPermissions(props: GraphPermissionsProps) { }; const onSubmit = async (values: FormSchema, actions) => { + values.writers = _.compact(values.writers); const resource = resourceFromPath(association.group); const tag = { app: "graph", resource: association.resource, tag: "writers", }; - const allWriters = Array.from(writers).map(w => `~${w}`); + const allWriters = Array.from(writers).map((w) => `~${w}`); if (values.readerComments !== readerComments) { await api.metadata.update(association, { vip: values.readerComments ? "reader-comments" : "", @@ -123,36 +121,38 @@ export function GraphPermissions(props: GraphPermissionsProps) { actions.setStatus({ success: null }); return; } - await api.groups.removeTag( - resource, - tag, - allWriters - ); + await api.groups.removeTag(resource, tag, allWriters); } else if (values.writePerms === "self") { if (writePerms === "self") { actions.setStatus({ success: null }); return; } - await api.groups.removeTag( - resource, - tag, - allWriters - ); - await api.groups.addTag(resource, tag, [`~${hostShip}`]); + let promises: Promise[] = []; + allWriters.length > 0 && + promises.push(api.groups.removeTag(resource, tag, allWriters)); + promises.push(api.groups.addTag(resource, tag, [`~${hostShip}`])); + await Promise.all(promises); actions.setStatus({ success: null }); } else if (values.writePerms === "subset") { - const toRemove = _.difference(allWriters, values.writers) + const toRemove = _.difference(allWriters, values.writers); - const toAdd = [..._.difference(values.writers, allWriters), `~${hostShip}`]; + const toAdd = [ + ..._.difference(values.writers, allWriters), + `~${hostShip}`, + ]; - toRemove.length > 0 && await api.groups.removeTag(resource, tag, toRemove); - toAdd.length > 0 && await api.groups.addTag(resource, tag, toAdd); + let promises: Promise[] = []; + toRemove.length > 0 && + promises.push(api.groups.removeTag(resource, tag, toRemove)); + toAdd.length > 0 && + promises.push(api.groups.addTag(resource, tag, toAdd)); + await Promise.all(promises); actions.setStatus({ success: null }); } }; - const schema = formSchema(Array.from(group.members).map(m => `~${m}`)); + const schema = formSchema(Array.from(group.members).map((m) => `~${m}`)); return ( - {({ values }) => ( -
- - - - Permissions - - - Add or remove read/write privileges to this channel. Group - admins can always write to a channel - - - - - - - - - - - - {values.writePerms === "subset" && ( - - )} - - {association.metadata.module !== "chat" && ( - - )} - - Update Permissions - + + + + + Permissions + + + Add or remove read/write privileges to this channel. Group admins + can always write to a channel + - - )} + + + + + + {association.metadata.module !== "chat" && ( + + )} + Update Permissions + +
); } diff --git a/pkg/interface/src/views/landscape/components/ChannelWritePerms.tsx b/pkg/interface/src/views/landscape/components/ChannelWritePerms.tsx new file mode 100644 index 000000000..be12fcf60 --- /dev/null +++ b/pkg/interface/src/views/landscape/components/ChannelWritePerms.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { + Label, + Box, + ManagedRadioButtonField as Radio, + Col, +} from "@tlon/indigo-react"; +import { useFormikContext } from "formik"; +import { Groups, Rolodex } from "~/types"; +import { ShipSearch } from "~/views/components/ShipSearch"; + +export type WritePerms = "everyone" | "subset" | "self"; +export interface ChannelWriteFieldSchema { + writePerms: WritePerms; + writers: string[]; +} + +interface ChannelWritePermsProps { + groups: Groups; + contacts: Rolodex; +} + +export function ChannelWritePerms< + T extends ChannelWriteFieldSchema = ChannelWriteFieldSchema +>(props: ChannelWritePermsProps) { + const { values, errors } = useFormikContext(); + + return ( + + + + + + {values.writePerms === "subset" && ( + + )} + + ); +} diff --git a/pkg/interface/src/views/landscape/components/NewChannel.tsx b/pkg/interface/src/views/landscape/components/NewChannel.tsx index a7fc45bf6..e75bfb927 100644 --- a/pkg/interface/src/views/landscape/components/NewChannel.tsx +++ b/pkg/interface/src/views/landscape/components/NewChannel.tsx @@ -19,24 +19,25 @@ import { resourceFromPath } from '~/logic/lib/group'; import { Associations } from '~/types/metadata-update'; import { useWaitForProps } from '~/logic/lib/useWaitForProps'; import { Groups } from '~/types/group-update'; -import { ShipSearch } from '~/views/components/ShipSearch'; +import { ShipSearch, shipSearchSchemaInGroup, shipSearchSchema } from '~/views/components/ShipSearch'; import { Rolodex, Workspace } from '~/types'; +import {IconRadio} from '~/views/components/IconRadio'; +import {ChannelWriteFieldSchema, ChannelWritePerms} from './ChannelWritePerms'; -interface FormSchema { +type FormSchema = { name: string; description: string; ships: string[]; moduleType: 'chat' | 'publish' | 'link'; - writers: string[]; -} +} & ChannelWriteFieldSchema; -const formSchema = (group, groups) => Yup.object({ +const formSchema = (members?: string[]) => Yup.object({ name: Yup.string().required('Channel must have a name'), description: Yup.string(), ships: Yup.array(Yup.string()), moduleType: Yup.string().required('Must choose channel type'), - writers: Yup.array(Yup.string().test('ingroup', 'Writers must be in group', - value => groups?.[group]?.members?.has(value))) + writers: members ? shipSearchSchemaInGroup(members) : shipSearchSchema, + writePerms: Yup.string() }); interface NewChannelProps { @@ -59,15 +60,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) { + ((workspace?.type !== 'home') ? `-${Math.floor(Math.random() * 10000)}` : ''); try { - const { name, description, moduleType, ships, writers } = values; - if(moduleType === 'publish' && writers.length > 0) { - const resource = resourceFromPath(group); - await api.groups.addTag( - resource, - { app: 'publish', tag: `writers-${resId}` }, - writers.map(s => `~${s}`) - ); - } + let { name, description, moduleType, ships, writers } = values; if (group) { await api.graph.createManagedGraph( resId, @@ -76,6 +69,24 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) { group, moduleType ); + const tag = { + app: 'graph', + resource: `/ship/~${window.ship}/${resId}`, + tag: 'writers' + }; + + const resource = resourceFromPath(group); + console.log(writers); + writers = _.compact(writers); + console.log(writers); + const us = `~${window.ship}`; + if(values.writePerms === 'self') { + await api.groups.addTag(resource, tag, [us]); + } else if(values.writePerms === 'subset') { + writers.push(us); + await api.groups.addTag(resource, tag, writers); + } + } else { await api.graph.createUnmanagedGraph( resId, @@ -100,36 +111,39 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) { } }; + const members = group ? Array.from(groups[group]?.members).map(s => `~${s}`) : undefined; + return ( history.push(props.baseUrl)}> {'<- Back'} - + New Channel - { ({ errors, values }) =>
+ - Channel Type - - - + Channel Type + + + - {(workspace?.type === 'home') && + {(workspace?.type === 'home') ? ( } - {(workspace?.type !== 'home' && values.moduleType === 'publish') && - <> - ) : ( + - {errors.writers && - <> - - - - {Array.from(new Set([...errors.writers]))} - - - - } - } + )} + - } +
); diff --git a/pkg/interface/src/views/landscape/components/PopoverRoutes.tsx b/pkg/interface/src/views/landscape/components/PopoverRoutes.tsx index fa6601316..ada56b58a 100644 --- a/pkg/interface/src/views/landscape/components/PopoverRoutes.tsx +++ b/pkg/interface/src/views/landscape/components/PopoverRoutes.tsx @@ -15,27 +15,8 @@ import { Participants } from "./Participants"; import {useHashLink} from "~/logic/lib/useHashLink"; import {DeleteGroup} from "./DeleteGroup"; import {resourceFromPath} from "~/logic/lib/group"; - -const SidebarItem = ({ selected, icon, text, to, children = null }) => { - return ( - - - - {text} - - {children} - - ); -}; +import {ModalOverlay} from "~/views/components/ModalOverlay"; +import { SidebarItem } from "~/views/landscape/components/SidebarItem"; export function PopoverRoutes( props: { @@ -73,106 +54,95 @@ export function PopoverRoutes( render={(routeProps) => { const { view } = routeProps.match.params; return ( - - - - Group Settings - - Group - - {groupSize} - - { admin && ( - <> - - Administration - - - - - )} - - + Group Settings + + Group + + {groupSize} + + { admin && ( + <> + + Administration + + + + + )} + - - - {"<- Back"} - - - - {view === "settings" && ( - - )} - {view === "participants" && ( - - )} - + + + + {"<- Back"} + + + + {view === "settings" && ( + + )} + {view === "participants" && ( + + )} - + ); }} /> From aa62d18d2ff945d40e41565bee28214efec64d90 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Thu, 28 Jan 2021 14:59:26 +1000 Subject: [PATCH 16/27] ChannelSettings: refine delete/remove logic and buttons --- pkg/arvo/ted/graph/delete.hoon | 26 +++++++++- .../src/views/components/ShipSearch.tsx | 2 - .../ChannelPopoverRoutes/Sidebar.tsx | 20 ++++++-- .../components/ChannelPopoverRoutes/index.tsx | 48 ++++++++++++++++++- .../views/landscape/components/NewChannel.tsx | 2 - 5 files changed, 86 insertions(+), 12 deletions(-) diff --git a/pkg/arvo/ted/graph/delete.hoon b/pkg/arvo/ted/graph/delete.hoon index f98ddf075..97b93687e 100644 --- a/pkg/arvo/ted/graph/delete.hoon +++ b/pkg/arvo/ted/graph/delete.hoon @@ -1,4 +1,4 @@ -/- spider, graph-view, graph=graph-store, *metadata-store, *group +/- spider, graph-view, graph=graph-store, *metadata-store, *group, group-store /+ strandio, resource => |% @@ -48,6 +48,28 @@ (en-path:resource group-rid) [%graph (en-path:resource rid)] (pure:m ~) +:: +++ delete-tags + |= [graph=resource grp-rid=resource =group] + =/ m (strand ,~) + ^- form:m + =/ tags=(list [=tag tagged=(set ship)]) + %+ skim ~(tap by tags.group) + |= [=tag tagged=(set ship)] + ?@ tag %.n + ?& =(app.tag %graph) + =(resource.tag graph) + == + |- =* loop $ + ^- form:m + ?~ tags + (pure:m ~) + ;< ~ bind:m + %+ poke [entity.grp-rid %group-push-hook] + :- %group-update + !> ^- update:group-store + [%remove-tag grp-rid tag.i.tags tagged.i.tags] + loop(tags t.tags) -- :: ^- thread:spider @@ -64,6 +86,8 @@ ?~ ugroup-rid !! ;< =group bind:m (scry-group u.ugroup-rid) +;< ~ bind:m + (delete-tags rid.action u.ugroup-rid group) ?. hidden.group ;< ~ bind:m (delete-graph u.ugroup-rid rid.action) diff --git a/pkg/interface/src/views/components/ShipSearch.tsx b/pkg/interface/src/views/components/ShipSearch.tsx index 4cda31169..b65beb529 100644 --- a/pkg/interface/src/views/components/ShipSearch.tsx +++ b/pkg/interface/src/views/components/ShipSearch.tsx @@ -122,8 +122,6 @@ export function ShipSearch>( const selected: string[] = values[id] ?? []; - console.log(selected); - const name = () => `${props.id}[${inputIdx.current}]`; const pills = selected.slice(0, inputIdx.current); diff --git a/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/Sidebar.tsx b/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/Sidebar.tsx index ccacbd355..dfe7825da 100644 --- a/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/Sidebar.tsx +++ b/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/Sidebar.tsx @@ -54,11 +54,21 @@ export function ChannelPopoverRoutesSidebar(props: { text="Permissions" to={relativePath("/settings#permissions")} /> - + { isOwner ? ( + + ) : ( + + )} )} diff --git a/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/index.tsx b/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/index.tsx index c440a4bd3..8b96980b8 100644 --- a/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/index.tsx +++ b/pkg/interface/src/views/landscape/components/ChannelPopoverRoutes/index.tsx @@ -31,7 +31,7 @@ interface ChannelPopoverRoutesProps { } export function ChannelPopoverRoutes(props: ChannelPopoverRoutesProps) { - const { association, group } = props; + const { association, group, api } = props; useHashLink(); const overlayRef = useRef(); const history = useHistory(); @@ -41,11 +41,20 @@ export function ChannelPopoverRoutes(props: ChannelPopoverRoutesProps) { }); const handleUnsubscribe = async () => { - await wait(150); + const [,,ship,name] = association.resource.split('/'); + await api.graph.leaveGraph(ship, name); + }; + const handleRemove = async () => { + await api.metadata.remove('graph', association.resource, association.group); + }; + const handleArchive = async () => { + const [,,,name] = association.resource.split('/'); + await api.graph.deleteGraph(name); }; const canAdmin = isChannelAdmin(group, association.resource); const isOwner = isHost(association.resource); + return ( + { isOwner ? ( + + + Archive channel + + + Archiving a channel will prevent further updates to the channel. + Users who are currently joined to the channel will retain a copy + of the channel. + + + + Archive {props.association.metadata.title} + + + + + ) : ( + + + Remove channel from group + + + Removing a channel will prevent further updates to the channel. + Users who are currently joined to the channel will retain a copy + of the channel. + + + + Remove {props.association.metadata.title} + + + + + )} )} diff --git a/pkg/interface/src/views/landscape/components/NewChannel.tsx b/pkg/interface/src/views/landscape/components/NewChannel.tsx index e75bfb927..1db46a564 100644 --- a/pkg/interface/src/views/landscape/components/NewChannel.tsx +++ b/pkg/interface/src/views/landscape/components/NewChannel.tsx @@ -76,9 +76,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) { }; const resource = resourceFromPath(group); - console.log(writers); writers = _.compact(writers); - console.log(writers); const us = `~${window.ship}`; if(values.writePerms === 'self') { await api.groups.addTag(resource, tag, [us]); From 175fc0a383b2185db9e963147d7d6b48c90addcf Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Fri, 29 Jan 2021 15:45:06 +1000 Subject: [PATCH 17/27] metadata: fix breakage from merge --- pkg/arvo/app/graph-push-hook.hoon | 23 ++-- pkg/arvo/app/metadata-push-hook.hoon | 6 +- pkg/arvo/app/metadata-store.hoon | 2 +- pkg/arvo/lib/metadata-json.hoon | 157 --------------------------- 4 files changed, 16 insertions(+), 172 deletions(-) delete mode 100644 pkg/arvo/lib/metadata-json.hoon diff --git a/pkg/arvo/app/graph-push-hook.hoon b/pkg/arvo/app/graph-push-hook.hoon index 5b73f31e6..d210ddec2 100644 --- a/pkg/arvo/app/graph-push-hook.hoon +++ b/pkg/arvo/app/graph-push-hook.hoon @@ -1,6 +1,7 @@ /- *group +/- metadata=metadata-store /+ store=graph-store -/+ metadata +/+ mdl=metadata /+ res=resource /+ graph /+ group @@ -135,7 +136,7 @@ -- |_ =bowl:gall +* grp ~(. group bowl) - met ~(. metadata bowl) + met ~(. mdl bowl) gra ~(. graph bowl) ++ scry |= [care=@t desk=@t =path] @@ -155,21 +156,21 @@ (cat 3 'graph-permissions-' perm) :: ++ perm-mark - |= [=resource:res perm=@t vip=vip-metadata:met =indexed-post:store] + |= [=resource:res perm=@t vip=vip-metadata:metadata =indexed-post:store] ^- permissions:store =- (check vip) - !< check=$-(vip-metadata:met permissions:store) + !< check=$-(vip-metadata:metadata permissions:store) %. !>(indexed-post) =/ mark (get-mark:gra resource) ?~ mark |=(=vase !>([%no %no %no])) .^(tube:clay (scry %cc %home /[u.mark]/(perm-mark-name perm))) :: ++ add-mark - |= [=resource:res vip=vip-metadata:met =indexed-post:store] + |= [=resource:res vip=vip-metadata:metadata =indexed-post:store] (perm-mark resource %add vip indexed-post) :: ++ remove-mark - |= [=resource:res vip=vip-metadata:met =indexed-post:store] + |= [=resource:res vip=vip-metadata:metadata =indexed-post:store] (perm-mark resource %remove vip indexed-post) :: ++ get-permission @@ -189,8 +190,8 @@ :: ++ get-roles-writers-variation |= =resource:res - ^- (unit [is-admin=? writers=(set ship) vip=vip-metadata:met]) - =/ assoc=(unit association:met) + ^- (unit [is-admin=? writers=(set ship) vip=vip-metadata:metadata]) + =/ assoc=(unit association:metadata) (peek-association:met %graph resource) ?~ assoc ~ =/ role=(unit (unit role-tag)) @@ -200,7 +201,7 @@ ?~ role ~ =/ is-admin=? ?=(?([~ %admin] [~ %moderator]) u.role) - `[is-admin writers vip.metadata.u.assoc] + `[is-admin writers vip.metadatum.u.assoc] :: ++ node-to-indexed-post |= =node:store @@ -213,7 +214,7 @@ ^- ? %- (bond |.(%.n)) %+ biff (get-roles-writers-variation resource) - |= [is-admin=? writers=(set ship) vip=vip-metadata:met] + |= [is-admin=? writers=(set ship) vip=vip-metadata:metadata] %- some %+ levy ~(tap by nodes) |= [=index:store =node:store] @@ -240,7 +241,7 @@ ^- ? %- (bond |.(%.n)) %+ biff (get-roles-writers-variation) - |= [is-admin=? writers=(set ship) vip=vip-metadata:met] + |= [is-admin=? writers=(set ship) vip=vip-metadata:metadata] %- some %+ levy ~(tap by indices) |= =index:store diff --git a/pkg/arvo/app/metadata-push-hook.hoon b/pkg/arvo/app/metadata-push-hook.hoon index fee51711f..492b0fe7b 100644 --- a/pkg/arvo/app/metadata-push-hook.hoon +++ b/pkg/arvo/app/metadata-push-hook.hoon @@ -81,12 +81,12 @@ =/ role=(unit (unit role-tag)) (role-for-ship:grp group.update src.bowl) =/ =metadatum:store - (need (peek-metadata:met %contacts group.upd)) + (need (peek-metadatum:met %contacts group.update)) ?~ role %.n ?^ u.role ?=(?(%admin %moderator) u.u.role) - ?. ?=(%add -.upd) %.n - ?& =(src.bowl entity.resource.resource.upd) + ?. ?=(%add -.update) %.n + ?& =(src.bowl entity.resource.resource.update) ?=(%member-metadata vip.metadatum) == :: diff --git a/pkg/arvo/app/metadata-store.hoon b/pkg/arvo/app/metadata-store.hoon index ce832247f..fe8e8aa89 100644 --- a/pkg/arvo/app/metadata-store.hoon +++ b/pkg/arvo/app/metadata-store.hoon @@ -24,7 +24,7 @@ :: /group/%path associations for group :: /- store=metadata-store -/+ *metadata-json, default-agent, verb, dbug, resource, *migrate +/+ default-agent, verb, dbug, resource, *migrate |% +$ card card:agent:gall +$ base-state-0 diff --git a/pkg/arvo/lib/metadata-json.hoon b/pkg/arvo/lib/metadata-json.hoon deleted file mode 100644 index 6604dd4ae..000000000 --- a/pkg/arvo/lib/metadata-json.hoon +++ /dev/null @@ -1,157 +0,0 @@ -/- *metadata-store -/+ resource -^? -|% -++ associations-to-json - |= =associations - =, enjs:format - ^- json - %- pairs - %+ turn ~(tap by associations) - |= [=md-resource [group=resource =metadata]] - ^- [cord json] - :- - %- crip - ;: weld - (trip (spat (en-path:resource group))) - (weld "/" (trip app-name.md-resource)) - (trip (spat (en-path:resource resource.md-resource))) - == - %- pairs - :~ [%group s+(enjs-path:resource group)] - [%app-name s+app-name.md-resource] - [%resource s+(enjs-path:resource resource.md-resource)] - [%metadata (metadata-to-json metadata)] - == -:: -++ json-to-action - |= jon=json - ^- metadata-action - =, dejs:format - =< (parse-json jon) - |% - ++ parse-json - %- of - :~ [%add add] - [%remove remove] - == - :: - ++ add - %- ot - :~ [%group dejs-path:resource] - [%resource md-resource] - [%metadata metadata] - == - ++ remove - %- ot - :~ [%group dejs-path:resource] - [%resource md-resource] - == - :: - ++ nu - |= jon=json - ?> ?=([%s *] jon) - (rash p.jon hex) - :: - ++ vip - %- su - ;~ pose - (tag %reader-comments) - (tag %member-metadata) - (tag %$) - == - :: - ++ metadata - %- ot - :~ [%title so] - [%description so] - [%color nu] - [%date-created (se %da)] - [%creator (su ;~(pfix sig fed:ag))] - [%module so] - [%picture so] - [%preview bo] - [%vip vip] - == - :: - ++ tag |*(a=@tas (cold a (jest a))) - :: - ++ md-resource - ^- $-(json ^md-resource) - %- ot - :~ [%app-name so] - [%resource dejs-path:resource] - == - -- -:: -++ metadata-to-json - |= met=metadata - ^- json - =, enjs:format - %- pairs - :~ [%title s+title.met] - [%description s+description.met] - [%color s+(scot %ux color.met)] - [%date-created s+(scot %da date-created.met)] - [%creator s+(scot %p creator.met)] - [%module s+module.met] - [%picture s+picture.met] - [%preview b+preview.met] - [%vip s+`@t`vip.met] - == -++ hook-update-to-json - |= upd=metadata-hook-update - =, enjs:format - %+ frond %metadata-hook-update - %+ frond -.upd - %- pairs - ?- -.upd - %preview - :~ [%group s+(enjs-path:resource group.upd)] - [%channels (associations-to-json channels.upd)] - [%members (numb members.upd)] - [%channel-count (numb channel-count.upd)] - [%metadata (metadata-to-json metadata.upd)] - == - %req-preview - ~[group+s+(enjs-path:resource group.upd)] - == - -:: -++ update-to-json - |= upd=metadata-update - ^- json - =, enjs:format - %+ frond %metadata-update - %- pairs - :~ ?+ -.upd *[cord json] - %add - :- %add - %- pairs - :~ [%group s+(enjs-path:resource group.upd)] - [%app-name s+app-name.resource.upd] - [%resource s+(enjs-path:resource resource.resource.upd)] - [%metadata (metadata-to-json metadata.upd)] - == - %updated-metadata - :- %add - %- pairs - :~ [%group s+(enjs-path:resource group.upd)] - [%app-name s+app-name.resource.upd] - [%resource s+(enjs-path:resource resource.resource.upd)] - [%metadata (metadata-to-json metadata.upd)] - == - :: - %remove - :- %remove - %- pairs - :~ [%group s+(enjs-path:resource group.upd)] - [%app-name s+app-name.resource.upd] - [%resource s+(enjs-path:resource resource.resource.upd)] - == - :: - %associations - [%associations (associations-to-json associations.upd)] - :: - == == --- From 378ad0690f771655b7a89463fae03c1d11c449b6 Mon Sep 17 00:00:00 2001 From: Matilde Park Date: Tue, 2 Feb 2021 15:38:12 -0500 Subject: [PATCH 18/27] landscape: "DMs + Drafts" -> "My Channels" --- pkg/interface/src/logic/lib/omnibox.js | 2 +- pkg/interface/src/logic/lib/workspace.ts | 2 +- pkg/interface/src/views/apps/launch/app.js | 4 ++-- pkg/interface/src/views/components/leap/OmniboxResult.js | 2 +- .../src/views/landscape/components/GroupSwitcher.tsx | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/interface/src/logic/lib/omnibox.js b/pkg/interface/src/logic/lib/omnibox.js index 0c2e27c3e..eb1d547b0 100644 --- a/pkg/interface/src/logic/lib/omnibox.js +++ b/pkg/interface/src/logic/lib/omnibox.js @@ -63,7 +63,7 @@ const appIndex = function (apps) { const otherIndex = function() { const other = []; - other.push(result('DMs + Drafts', '/~landscape/home', 'home', null)); + other.push(result('My Channels', '/~landscape/home', 'home', null)); other.push(result('Notifications', '/~notifications', 'inbox', null)); other.push(result('Profile and Settings', '/~profile/identity', 'profile', null)); other.push(result('Log Out', '/~/logout', 'logout', null)); diff --git a/pkg/interface/src/logic/lib/workspace.ts b/pkg/interface/src/logic/lib/workspace.ts index 7532bac6e..7f63b447a 100644 --- a/pkg/interface/src/logic/lib/workspace.ts +++ b/pkg/interface/src/logic/lib/workspace.ts @@ -6,7 +6,7 @@ export function getTitleFromWorkspace( ) { switch (workspace.type) { case "home": - return "DMs + Drafts"; + return "My Channels"; case "group": const association = associations.groups[workspace.group]; return association?.metadata?.title || ""; diff --git a/pkg/interface/src/views/apps/launch/app.js b/pkg/interface/src/views/apps/launch/app.js index 7861d905f..9b1929d70 100644 --- a/pkg/interface/src/views/apps/launch/app.js +++ b/pkg/interface/src/views/apps/launch/app.js @@ -76,9 +76,9 @@ export default function LaunchApp(props) { - DMs + Drafts + My Channels diff --git a/pkg/interface/src/views/components/leap/OmniboxResult.js b/pkg/interface/src/views/components/leap/OmniboxResult.js index 1f3ca309b..6e57a987a 100644 --- a/pkg/interface/src/views/components/leap/OmniboxResult.js +++ b/pkg/interface/src/views/components/leap/OmniboxResult.js @@ -53,7 +53,7 @@ export class OmniboxResult extends Component { text = text.startsWith('Profile') ? window.ship : text; graphic = ; } else if (icon === 'home') { - graphic = ; + graphic = ; } else if (icon === 'notifications') { graphic = ; } else { diff --git a/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx b/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx index e98448f90..248e594a3 100644 --- a/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx +++ b/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx @@ -117,9 +117,9 @@ export function GroupSwitcher(props: { mr={2} color="gray" display="block" - icon="Mail" + icon="Home" /> - DMs + Drafts + My Channels } Date: Tue, 2 Feb 2021 16:13:14 -0500 Subject: [PATCH 19/27] landscape: remove leftover chat-store FE logic --- pkg/interface/src/logic/api/hark.ts | 37 ++-------- .../src/logic/reducers/hark-update.ts | 37 +--------- pkg/interface/src/logic/store/store.ts | 1 - pkg/interface/src/types/hark-update.ts | 16 +---- .../views/apps/chat/components/ChatWindow.tsx | 4 -- .../apps/chat/components/backlog-element.js | 34 --------- .../chat/components/resubscribe-element.js | 35 ---------- .../src/views/apps/notifications/header.tsx | 3 +- .../views/apps/notifications/notification.tsx | 7 +- .../views/landscape/components/GroupsPane.tsx | 1 - .../views/landscape/components/NewChannel.tsx | 11 ++- .../landscape/components/Sidebar/Apps.tsx | 37 +--------- .../components/Sidebar/SidebarList.tsx | 7 +- .../views/landscape/components/Skeleton.tsx | 69 ++++++++----------- pkg/interface/src/views/landscape/index.tsx | 9 ++- 15 files changed, 57 insertions(+), 251 deletions(-) delete mode 100644 pkg/interface/src/views/apps/chat/components/backlog-element.js delete mode 100644 pkg/interface/src/views/apps/chat/components/resubscribe-element.js diff --git a/pkg/interface/src/logic/api/hark.ts b/pkg/interface/src/logic/api/hark.ts index 288ce6b2b..c049b4a84 100644 --- a/pkg/interface/src/logic/api/hark.ts +++ b/pkg/interface/src/logic/api/hark.ts @@ -17,10 +17,7 @@ export class HarkApi extends BaseApi { private groupHookAction(action: any) { return this.action("hark-group-hook", "hark-group-hook-action", action); } - - private chatHookAction(action: any) { - return this.action("hark-chat-hook", "hark-chat-hook-action", action); - } + private actOnNotification(frond: string, intTime: BigInteger, index: NotifIndex) { const time = decToUd(intTime.toString()); @@ -36,9 +33,6 @@ export class HarkApi extends BaseApi { await this.graphHookAction({ 'set-mentions': mentions }); - return this.chatHookAction({ - 'set-mentions': mentions - }); } setWatchOnSelf(watchSelf: boolean) { @@ -89,15 +83,15 @@ export class HarkApi extends BaseApi { markEachAsRead(association: Association, parent: string, child: string, description: GraphNotifDescription, mod: string) { return this.harkAction({ 'read-each': { - index: + index: { graph: - { graph: association.resource, + { graph: association.resource, group: association.group, - description, + description, module: mod, - index: parent + index: parent } - }, + }, target: child } }); @@ -129,9 +123,6 @@ export class HarkApi extends BaseApi { const { group } = notif.index.group; return this.ignoreGroup(group); } - if('chat' in notif.index) { - return this.ignoreChat(notif.index.chat.chat); - } return Promise.resolve(); } @@ -147,9 +138,6 @@ export class HarkApi extends BaseApi { if('group' in notif.index) { return this.listenGroup(notif.index.group.group); } - if('chat' in notif.index) { - return this.listenChat(notif.index.chat.chat); - } return Promise.resolve(); } @@ -168,13 +156,6 @@ export class HarkApi extends BaseApi { }) } - ignoreChat(chat: string) { - return this.chatHookAction({ - ignore: chat - }); - } - - listenGroup(group: string) { return this.groupHookAction({ listen: group @@ -190,12 +171,6 @@ export class HarkApi extends BaseApi { }) } - listenChat(chat: string) { - return this.chatHookAction({ - listen: chat - }); - } - async getMore(): Promise { const offset = this.store.state['notifications']?.size || 0; const count = 3; diff --git a/pkg/interface/src/logic/reducers/hark-update.ts b/pkg/interface/src/logic/reducers/hark-update.ts index 8db766f53..e35f62f29 100644 --- a/pkg/interface/src/logic/reducers/hark-update.ts +++ b/pkg/interface/src/logic/reducers/hark-update.ts @@ -10,7 +10,7 @@ import _ from "lodash"; import {StoreState} from "../store/type"; import { BigIntOrderedMap } from '../lib/BigIntOrderedMap'; -type HarkState = Pick; +type HarkState = Pick; export const HarkReducer = (json: any, state: HarkState) => { @@ -32,39 +32,8 @@ export const HarkReducer = (json: any, state: HarkState) => { groupListen(groupHookData, state); groupIgnore(groupHookData, state); } - - const chatHookData = _.get(json, "hark-chat-hook-update", false); - if(chatHookData) { - - chatInitial(chatHookData, state); - chatListen(chatHookData, state); - chatIgnore(chatHookData, state); - - } }; -function chatInitial(json: any, state: HarkState) { - const data = _.get(json, "initial", false); - if (data) { - state.notificationsChatConfig = data; - } -} - - -function chatListen(json: any, state: HarkState) { - const data = _.get(json, "listen", false); - if (data) { - state.notificationsChatConfig = [...state.notificationsChatConfig, data]; - } -} - -function chatIgnore(json: any, state: HarkState) { - const data = _.get(json, "ignore", false); - if (data) { - state.notificationsChatConfig = state.notificationsChatConfig.filter(x => x !== data); - } -} - function groupInitial(json: any, state: HarkState) { const data = _.get(json, "initial", false); if (data) { @@ -211,7 +180,6 @@ function clearState(state){ notifications: new BigIntOrderedMap(), archivedNotifications: new BigIntOrderedMap(), notificationsGroupConfig: [], - notificationsChatConfig: [], notificationsGraphConfig: { watchOnSelf: false, mentions: false, @@ -324,9 +292,6 @@ function notifIdxEqual(a: NotifIndex, b: NotifIndex) { a.group.group === b.group.group && a.group.description === b.group.description ); - } else if ("chat" in a && "chat" in b) { - return a.chat.chat === b.chat.chat && - a.chat.mention === b.chat.mention; } return false; } diff --git a/pkg/interface/src/logic/store/store.ts b/pkg/interface/src/logic/store/store.ts index ad0ccbfb7..4a56d1a74 100644 --- a/pkg/interface/src/logic/store/store.ts +++ b/pkg/interface/src/logic/store/store.ts @@ -83,7 +83,6 @@ export default class GlobalStore extends BaseStore { notifications: new BigIntOrderedMap(), archivedNotifications: new BigIntOrderedMap(), notificationsGroupConfig: [], - notificationsChatConfig: [], notificationsGraphConfig: { watchOnSelf: false, mentions: false, diff --git a/pkg/interface/src/types/hark-update.ts b/pkg/interface/src/types/hark-update.ts index 9191a1709..156f96b66 100644 --- a/pkg/interface/src/types/hark-update.ts +++ b/pkg/interface/src/types/hark-update.ts @@ -2,7 +2,6 @@ import _ from "lodash"; import { Post } from "./graph-update"; import { GroupUpdate } from "./group-update"; import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap"; -import { Envelope } from './chat-update'; export type GraphNotifDescription = "link" | "comment" | "note" | "mention"; @@ -25,27 +24,17 @@ export interface GroupNotifIndex { description: string; } -export interface ChatNotifIndex { - chat: string; - mention: boolean; -} - export type NotifIndex = | { graph: GraphNotifIndex } - | { group: GroupNotifIndex } - | { chat: ChatNotifIndex }; + | { group: GroupNotifIndex }; export type GraphNotificationContents = Post[]; export type GroupNotificationContents = GroupUpdate[]; -export type ChatNotificationContents = Envelope[]; - export type NotificationContents = | { graph: GraphNotificationContents } - | { group: GroupNotificationContents } - | { chat: ChatNotificationContents }; - + | { group: GroupNotificationContents }; export interface Notification { read: boolean; time: number; @@ -68,7 +57,6 @@ export interface NotificationGraphConfig { } export interface Unreads { - chat: Record; graph: Record>; group: Record; } diff --git a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx index 5073b8549..14c0bbf3f 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx @@ -15,8 +15,6 @@ import VirtualScroller from "~/views/components/VirtualScroller"; import ChatMessage, { MessagePlaceholder } from './ChatMessage'; import { UnreadNotice } from "./unread-notice"; -import { ResubscribeElement } from "./resubscribe-element"; -import { BacklogElement } from "./backlog-element"; const INITIAL_LOAD = 20; const DEFAULT_BACKLOG_SIZE = 100; @@ -269,8 +267,6 @@ export default class ChatWindow extends Component - - {this.virtualList = list}} origin="bottom" diff --git a/pkg/interface/src/views/apps/chat/components/backlog-element.js b/pkg/interface/src/views/apps/chat/components/backlog-element.js deleted file mode 100644 index bef762ba5..000000000 --- a/pkg/interface/src/views/apps/chat/components/backlog-element.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { Box, LoadingSpinner, Text } from '@tlon/indigo-react'; - -export const BacklogElement = (props) => { - if (!props.isChatLoading) { - return null; - } - return ( - - - - Past messages are being restored - - - ); -}; diff --git a/pkg/interface/src/views/apps/chat/components/resubscribe-element.js b/pkg/interface/src/views/apps/chat/components/resubscribe-element.js deleted file mode 100644 index d12e39332..000000000 --- a/pkg/interface/src/views/apps/chat/components/resubscribe-element.js +++ /dev/null @@ -1,35 +0,0 @@ -import React, { Component } from 'react'; -import { Box, Text, Button } from '@tlon/indigo-react'; - -export class ResubscribeElement extends Component { - onClickResubscribe() { - this.props.api.chat.addSynced( - this.props.host, - this.props.station, - true); - } - - render() { - const { props } = this; - if (props.isChatUnsynced) { - return ( - - - Your ship has been disconnected from the chat's host. - This may be due to a bad connection, going offline, lack of permission, - or an over-the-air update. - - - - ); - } else { - return null; - } - } -} diff --git a/pkg/interface/src/views/apps/notifications/header.tsx b/pkg/interface/src/views/apps/notifications/header.tsx index e27a4ef31..7d7644b1a 100644 --- a/pkg/interface/src/views/apps/notifications/header.tsx +++ b/pkg/interface/src/views/apps/notifications/header.tsx @@ -36,7 +36,6 @@ export function Header(props: { time: number; read: boolean; associations: Associations; - chat?: boolean; } & PropFunc ) { const { description, channel, group, moduleIcon, read } = props; const contacts = props.contacts[group] || {}; @@ -65,7 +64,7 @@ export function Header(props: { const groupTitle = props.associations.groups?.[props.group]?.metadata?.title; - const app = props.chat ? 'chat' : 'graph'; + const app = 'graph'; const channelTitle = (channel && props.associations?.[app]?.[channel]?.metadata?.title) || channel; diff --git a/pkg/interface/src/views/apps/notifications/notification.tsx b/pkg/interface/src/views/apps/notifications/notification.tsx index 0eb96642a..cc55d3814 100644 --- a/pkg/interface/src/views/apps/notifications/notification.tsx +++ b/pkg/interface/src/views/apps/notifications/notification.tsx @@ -34,8 +34,7 @@ interface NotificationProps { function getMuted( idxNotif: IndexedNotification, groups: GroupNotificationsConfig, - graphs: NotificationGraphConfig, - chat: string[] + graphs: NotificationGraphConfig ) { const { index, notification } = idxNotif; if ("graph" in idxNotif.index) { @@ -64,7 +63,6 @@ function NotificationWrapper(props: { archived: boolean; graphConfig: NotificationGraphConfig; groupConfig: GroupNotificationsConfig; - chatConfig: string[]; }) { const { api, time, notif, children } = props; @@ -75,8 +73,7 @@ function NotificationWrapper(props: { const isMuted = getMuted( notif, props.groupConfig, - props.graphConfig, - props.chatConfig + props.graphConfig ); const onChangeMute = useCallback(async () => { diff --git a/pkg/interface/src/views/landscape/components/GroupsPane.tsx b/pkg/interface/src/views/landscape/components/GroupsPane.tsx index e860f1ec5..e37e74684 100644 --- a/pkg/interface/src/views/landscape/components/GroupsPane.tsx +++ b/pkg/interface/src/views/landscape/components/GroupsPane.tsx @@ -177,7 +177,6 @@ export function GroupsPane(props: GroupsPaneProps) { {...routeProps} api={api} baseUrl={baseUrl} - chatSynced={props.chatSynced} associations={associations} groups={groups} group={groupPath} diff --git a/pkg/interface/src/views/landscape/components/NewChannel.tsx b/pkg/interface/src/views/landscape/components/NewChannel.tsx index 1db46a564..99f2e6af3 100644 --- a/pkg/interface/src/views/landscape/components/NewChannel.tsx +++ b/pkg/interface/src/views/landscape/components/NewChannel.tsx @@ -44,7 +44,6 @@ interface NewChannelProps { api: GlobalApi; associations: Associations; contacts: Rolodex; - chatSynced: any; groups: Groups; group?: string; workspace: Workspace; @@ -69,13 +68,13 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) { group, moduleType ); - const tag = { + const tag = { app: 'graph', - resource: `/ship/~${window.ship}/${resId}`, - tag: 'writers' + resource: `/ship/~${window.ship}/${resId}`, + tag: 'writers' }; - const resource = resourceFromPath(group); + const resource = resourceFromPath(group); writers = _.compact(writers); const us = `~${window.ship}`; if(values.writePerms === 'self') { @@ -167,7 +166,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) { contacts={props.contacts} /> )} - + { - if (!(s in (chatSynced || {}))) { - return "unsubscribed"; - } - const mailbox = inbox?.[s]; - if (!mailbox) { - return undefined; - } - const { config } = mailbox; - if (config?.read !== config?.length) { - return "unread"; - } - return undefined; - }, - [inbox, chatSynced] - ); - - const lastUpdated = useCallback( - (s: string) => { - const mailbox = inbox?.[s]; - if (!mailbox) { - return 0; - } - return mailbox?.envelopes?.[0]?.when || 0; - }, - [inbox] - ); - - return { lastUpdated, getStatus }; -} export function useGraphModule( diff --git a/pkg/interface/src/views/landscape/components/Sidebar/SidebarList.tsx b/pkg/interface/src/views/landscape/components/Sidebar/SidebarList.tsx index a3d155fd8..832996bc8 100644 --- a/pkg/interface/src/views/landscape/components/Sidebar/SidebarList.tsx +++ b/pkg/interface/src/views/landscape/components/Sidebar/SidebarList.tsx @@ -45,12 +45,7 @@ export function SidebarList(props: { selected?: string; }) { const { selected, group, config } = props; - const associations = { - ...props.associations.chat, - ...props.associations.publish, - ...props.associations.link, - ...props.associations.graph, - }; + const associations = { ...props.associations.graph }; const ordered = Object.keys(associations) .filter((a) => { diff --git a/pkg/interface/src/views/landscape/components/Skeleton.tsx b/pkg/interface/src/views/landscape/components/Skeleton.tsx index 921be109e..a277bdbeb 100644 --- a/pkg/interface/src/views/landscape/components/Skeleton.tsx +++ b/pkg/interface/src/views/landscape/components/Skeleton.tsx @@ -1,20 +1,17 @@ -import React, { ReactNode, useEffect, useMemo } from "react"; -import { Box, Text } from "@tlon/indigo-react"; -import { Link } from "react-router-dom"; +import React, { ReactNode, useEffect, useMemo } from 'react'; +import { Box, Text } from '@tlon/indigo-react'; +import { Link } from 'react-router-dom'; -import { Sidebar } from "./Sidebar/Sidebar"; -import { ChatHookUpdate } from "~/types/chat-hook-update"; -import { Inbox } from "~/types/chat-update"; -import { Associations } from "~/types/metadata-update"; -import { Notebooks } from "~/types/publish-update"; -import GlobalApi from "~/logic/api/global"; -import { Path, AppName } from "~/types/noun"; -import { LinkCollections } from "~/types/link-update"; -import styled from "styled-components"; -import GlobalSubscription from "~/logic/subscription/global"; -import { Workspace, Groups, Graphs, Invites, Rolodex } from "~/types"; -import { useChat, useGraphModule } from "./Sidebar/Apps"; -import { Body } from "~/views/components/Body"; +import { Sidebar } from './Sidebar/Sidebar'; +import { Associations } from '~/types/metadata-update'; +import { Notebooks } from '~/types/publish-update'; +import GlobalApi from '~/logic/api/global'; +import { Path, AppName } from '~/types/noun'; +import { LinkCollections } from '~/types/link-update'; +import GlobalSubscription from '~/logic/subscription/global'; +import { Workspace, Groups, Graphs, Invites, Rolodex } from '~/types'; +import { useGraphModule } from './Sidebar/Apps'; +import { Body } from '~/views/components/Body'; interface SkeletonProps { contacts: Rolodex; @@ -22,14 +19,12 @@ interface SkeletonProps { recentGroups: string[]; groups: Groups; associations: Associations; - chatSynced: ChatHookUpdate | null; graphKeys: Set; graphs: Graphs; linkListening: Set; links: LinkCollections; notebooks: Notebooks; invites: Invites; - inbox: Inbox; selected?: string; selectedApp?: AppName; baseUrl: string; @@ -38,41 +33,37 @@ interface SkeletonProps { subscription: GlobalSubscription; includeUnmanaged: boolean; workspace: Workspace; - hideSidebar?: boolean; + unreads: any; } export function Skeleton(props: SkeletonProps) { - const chatConfig = useChat(props.inbox, props.chatSynced); const graphConfig = useGraphModule(props.graphKeys, props.graphs, props.unreads.graph); const config = useMemo( () => ({ - graph: graphConfig, - chat: chatConfig, + graph: graphConfig }), - [graphConfig, chatConfig] + [graphConfig] ); return ( - {!props.hideSidebar && ( - - )} + {props.children} ); diff --git a/pkg/interface/src/views/landscape/index.tsx b/pkg/interface/src/views/landscape/index.tsx index ec85986ee..fe00da563 100644 --- a/pkg/interface/src/views/landscape/index.tsx +++ b/pkg/interface/src/views/landscape/index.tsx @@ -74,7 +74,6 @@ export default class Landscape extends Component { render() { const { props } = this; - const { api } = props; return ( <> @@ -104,6 +103,14 @@ export default class Landscape extends Component { ); }} /> + { + const ws: Workspace = { type: 'messages' }; + return ( + + ); + }} + /> { return ( From f0033ab02f681ee23504e9ae0c30cfccdf6ae9da Mon Sep 17 00:00:00 2001 From: Matilde Park Date: Tue, 2 Feb 2021 18:45:40 -0500 Subject: [PATCH 20/27] landscape: add "messages" workspace and logic --- pkg/interface/src/logic/lib/omnibox.js | 8 ++- pkg/interface/src/logic/lib/util.ts | 13 ++++ pkg/interface/src/logic/lib/workspace.ts | 2 + pkg/interface/src/types/workspace.ts | 6 +- .../src/views/components/StatusBar.js | 5 +- .../landscape/components/GroupSwitcher.tsx | 4 +- .../landscape/components/InvitePopover.tsx | 7 +- .../views/landscape/components/NewChannel.tsx | 60 +++++++++++------ .../views/landscape/components/Resource.tsx | 8 +-- .../landscape/components/ResourceSkeleton.tsx | 43 ++++++++++--- .../landscape/components/Sidebar/Sidebar.tsx | 4 ++ .../components/Sidebar/SidebarItem.tsx | 64 +++++++++++-------- .../components/Sidebar/SidebarList.tsx | 16 +++-- .../components/Sidebar/SidebarListHeader.tsx | 63 ++++++++++++------ .../views/landscape/components/Skeleton.tsx | 1 + pkg/interface/src/views/landscape/index.tsx | 2 +- 16 files changed, 212 insertions(+), 94 deletions(-) diff --git a/pkg/interface/src/logic/lib/omnibox.js b/pkg/interface/src/logic/lib/omnibox.js index eb1d547b0..d79ae08e8 100644 --- a/pkg/interface/src/logic/lib/omnibox.js +++ b/pkg/interface/src/logic/lib/omnibox.js @@ -110,8 +110,12 @@ export default function index(contacts, associations, apps, currentGroup, groups landscape.push(obj); } else { const app = each.metadata.module || each['app-name']; - const group = (groups[each.group]?.hidden) - ? '/home' : each.group; + let group = each.group; + if (groups[each.group]?.hidden && app === 'chat') { + group = '/messages'; + } else if (groups[each.group]?.hidden) { + group = '/home'; + } const obj = result( title, `/~landscape${group}/join/${app}${each.resource}`, diff --git a/pkg/interface/src/logic/lib/util.ts b/pkg/interface/src/logic/lib/util.ts index 679511522..c39f9607e 100644 --- a/pkg/interface/src/logic/lib/util.ts +++ b/pkg/interface/src/logic/lib/util.ts @@ -392,3 +392,16 @@ export const useHovering = (): useHoveringInterface => { }; return { hovering, bind }; }; + +const DM_REGEX = /ship\/~([a-z]|-)*\/dm--/; +export function getItemTitle(association: Association) { + if(DM_REGEX.test(association.resource)) { + const [,,ship,name] = association.resource.split('/'); + if(ship.slice(1) === window.ship) { + return cite(`~${name.slice(4)}`); + } + return cite(ship); + + } + return association.metadata.title || association.resource +}; diff --git a/pkg/interface/src/logic/lib/workspace.ts b/pkg/interface/src/logic/lib/workspace.ts index 7f63b447a..e17b81e7b 100644 --- a/pkg/interface/src/logic/lib/workspace.ts +++ b/pkg/interface/src/logic/lib/workspace.ts @@ -7,6 +7,8 @@ export function getTitleFromWorkspace( switch (workspace.type) { case "home": return "My Channels"; + case "messages": + return "Messages"; case "group": const association = associations.groups[workspace.group]; return association?.metadata?.title || ""; diff --git a/pkg/interface/src/types/workspace.ts b/pkg/interface/src/types/workspace.ts index 5b4e68877..69da82ed3 100644 --- a/pkg/interface/src/types/workspace.ts +++ b/pkg/interface/src/types/workspace.ts @@ -9,4 +9,8 @@ interface HomeWorkspace { type: 'home' } -export type Workspace = HomeWorkspace | GroupWorkspace; +interface Messages { + type: 'messages' +} + +export type Workspace = HomeWorkspace | GroupWorkspace | Messages; diff --git a/pkg/interface/src/views/components/StatusBar.js b/pkg/interface/src/views/components/StatusBar.js index ca6cd9da6..94fa9f949 100644 --- a/pkg/interface/src/views/components/StatusBar.js +++ b/pkg/interface/src/views/components/StatusBar.js @@ -27,7 +27,7 @@ const StatusBar = (props) => { const invites = [].concat(...Object.values(props.invites).map(obj => Object.values(obj))); const metaKey = (window.navigator.platform.includes('Mac')) ? '⌘' : 'Ctrl+'; const { toggleOmnibox, hideAvatars } = - useLocalState(({ toggleOmnibox, hideAvatars }) => + useLocalState(({ toggleOmnibox, hideAvatars }) => ({ toggleOmnibox, hideAvatars }) ); @@ -91,6 +91,9 @@ const StatusBar = (props) => { > Submit an issue + props.history.push('/~landscape/messages')}> + + `${props.baseUrl}${to}`; return ( diff --git a/pkg/interface/src/views/landscape/components/InvitePopover.tsx b/pkg/interface/src/views/landscape/components/InvitePopover.tsx index 2f16a42a5..bcc851b15 100644 --- a/pkg/interface/src/views/landscape/components/InvitePopover.tsx +++ b/pkg/interface/src/views/landscape/components/InvitePopover.tsx @@ -47,10 +47,6 @@ export function InvitePopover(props: InvitePopoverProps) { useOutsideClick(innerRef, onOutsideClick); const onSubmit = async ({ ships, emails }: { ships: string[] }, actions) => { - if(props.workspace.type === 'home') { - history.push(`/~landscape/dm/${deSig(ships[0])}`); - return; - } // TODO: how to invite via email? try { const resource = resourceFromPath(association.group); @@ -105,14 +101,13 @@ export function InvitePopover(props: InvitePopoverProps) { Invite to - {title || "DM"} + {title} diff --git a/pkg/interface/src/views/landscape/components/NewChannel.tsx b/pkg/interface/src/views/landscape/components/NewChannel.tsx index 99f2e6af3..95fb4cf15 100644 --- a/pkg/interface/src/views/landscape/components/NewChannel.tsx +++ b/pkg/interface/src/views/landscape/components/NewChannel.tsx @@ -3,10 +3,7 @@ import { Box, ManagedTextInputField as Input, Col, - ManagedRadioButtonField as Radio, - Text, - Icon, - Row + Text } from '@tlon/indigo-react'; import { Formik, Form } from 'formik'; import * as Yup from 'yup'; @@ -21,8 +18,8 @@ import { useWaitForProps } from '~/logic/lib/useWaitForProps'; import { Groups } from '~/types/group-update'; import { ShipSearch, shipSearchSchemaInGroup, shipSearchSchema } from '~/views/components/ShipSearch'; import { Rolodex, Workspace } from '~/types'; -import {IconRadio} from '~/views/components/IconRadio'; -import {ChannelWriteFieldSchema, ChannelWritePerms} from './ChannelWritePerms'; +import { IconRadio } from '~/views/components/IconRadio'; +import { ChannelWriteFieldSchema, ChannelWritePerms } from './ChannelWritePerms'; type FormSchema = { name: string; @@ -32,7 +29,7 @@ type FormSchema = { } & ChannelWriteFieldSchema; const formSchema = (members?: string[]) => Yup.object({ - name: Yup.string().required('Channel must have a name'), + name: Yup.string(), description: Yup.string(), ships: Yup.array(Yup.string()), moduleType: Yup.string().required('Must choose channel type'), @@ -55,11 +52,16 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) { const waiter = useWaitForProps(props, 5000); const onSubmit = async (values: FormSchema, actions) => { + const name = (values.name) ? values.name : values.moduleType; const resId: string = stringToSymbol(values.name) - + ((workspace?.type !== 'home') ? `-${Math.floor(Math.random() * 10000)}` + + ((workspace?.type !== 'messages') ? `-${Math.floor(Math.random() * 10000)}` : ''); try { - let { name, description, moduleType, ships, writers } = values; + let { description, moduleType, ships, writers } = values; + ships = ships.filter(e => e !== ""); + if (workspace?.type === 'messages' && ships.length === 1) { + return history.push(`/~landscape/dm/${deSig(ships[0])}`); + } if (group) { await api.graph.createManagedGraph( resId, @@ -83,7 +85,6 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) { writers.push(us); await api.groups.addTag(resource, tag, writers); } - } else { await api.graph.createUnmanagedGraph( resId, @@ -115,13 +116,13 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) { history.push(props.baseUrl)}> {'<- Back'} - - New Channel + + {workspace?.type === 'messages' ? 'Direct Message' : 'New Channel'} - + Channel Type - - - + + + - {(workspace?.type === 'home') ? ( + {(workspace?.type === 'home' || workspace?.type === 'messages') ? ( ) : ( + />) : ( )} - - Create Channel + Create diff --git a/pkg/interface/src/views/landscape/components/Resource.tsx b/pkg/interface/src/views/landscape/components/Resource.tsx index e13136060..9cafcf5e7 100644 --- a/pkg/interface/src/views/landscape/components/Resource.tsx +++ b/pkg/interface/src/views/landscape/components/Resource.tsx @@ -28,14 +28,14 @@ type ResourceProps = StoreState & { } & RouteComponentProps; export function Resource(props: ResourceProps) { - const { association, api, notificationsGraphConfig, groups } = props; + const { association, api, notificationsGraphConfig, groups, contacts } = props; const app = association.metadata.module || association["app-name"]; const rid = association.resource; const selectedGroup = association.group; const relativePath = (p: string) => `${props.baseUrl}/resource/${app}${rid}${p}`; - const skelProps = { api, association, groups }; + const skelProps = { api, association, groups, contacts }; let title = props.association.metadata.title; if ('workspace' in props) { if ('group' in props.workspace && props.workspace.group in props.associations.groups) { @@ -65,12 +65,12 @@ export function Resource(props: ResourceProps) { render={(routeProps) => { return ( ); diff --git a/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx b/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx index f4caf0bdc..63bc35f86 100644 --- a/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx +++ b/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx @@ -15,6 +15,8 @@ import { ChannelSettings } from "./ChannelSettings"; import { ChannelMenu } from "./ChannelMenu"; import { NotificationGraphConfig, Groups } from "~/types"; import {isWriter} from "~/logic/lib/group"; +import urbitOb from 'urbit-ob'; +import { getItemTitle } from '~/logic/lib/util'; const TruncatedBox = styled(Box)` white-space: pre; @@ -24,6 +26,7 @@ const TruncatedBox = styled(Box)` type ResourceSkeletonProps = { groups: Groups; + contacts: any; association: Association; api: GlobalApi; baseUrl: string; @@ -35,16 +38,30 @@ type ResourceSkeletonProps = { export function ResourceSkeleton(props: ResourceSkeletonProps) { const { association, api, baseUrl, children, atRoot, groups } = props; const app = association?.metadata?.module || association["app-name"]; - const rid = association.resource; + const rid = association.resource; const group = groups[association.group]; - const workspace = - group?.hidden ? "/home" : association.group; + let workspace = association.group; - const title = props.title || association?.metadata?.title; + if (group?.hidden && app === "chat") { + workspace = "/messages"; + } else if (group?.hidden) { + workspace = "/home"; + } + + let title = (workspace === "/messages") + ? getItemTitle(association) + : association?.metadata?.title; + + let recipient = false; + + if (urbitOb.isValidPatp(title)) { + recipient = title; + title = (props.contacts?.[title]?.nickname) ? props.contacts[title].nickname : title; + } const [, , ship, resource] = rid.split("/"); - const resourcePath = (p: string) => baseUrl + `/resource/${app}/ship/${ship}/${resource}` + p; + const resourcePath = (p: string) => baseUrl + p; const isOwn = `~${window.ship}` === ship; let canWrite = (app === 'publish') ? true : false; @@ -78,7 +95,16 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) { {"<- Back"} - + {title} @@ -91,12 +117,13 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) { color="gray" > - {association?.metadata?.description} + {(workspace === "/messages") ? recipient : association?.metadata?.description} diff --git a/pkg/interface/src/views/landscape/components/Sidebar/Sidebar.tsx b/pkg/interface/src/views/landscape/components/Sidebar/Sidebar.tsx index 9dfe49a96..8998d4b92 100644 --- a/pkg/interface/src/views/landscape/components/Sidebar/Sidebar.tsx +++ b/pkg/interface/src/views/landscape/components/Sidebar/Sidebar.tsx @@ -93,6 +93,8 @@ export function Sidebar(props: SidebarProps) { handleSubmit={setConfig} selected={selected || ''} workspace={workspace} + api={props.api} + history={props.history} /> ); diff --git a/pkg/interface/src/views/landscape/components/Sidebar/SidebarItem.tsx b/pkg/interface/src/views/landscape/components/Sidebar/SidebarItem.tsx index 0e53ecc27..40cf6ff81 100644 --- a/pkg/interface/src/views/landscape/components/Sidebar/SidebarItem.tsx +++ b/pkg/interface/src/views/landscape/components/Sidebar/SidebarItem.tsx @@ -1,13 +1,14 @@ import React from "react"; import _ from 'lodash'; -import { Icon, Row, Box, Text } from "@tlon/indigo-react"; +import { Icon, Row, Box, Text, BaseImage } from "@tlon/indigo-react"; import { SidebarAppConfigs, SidebarItemStatus } from "./Sidebar"; import { HoverBoxLink } from "~/views/components/HoverBox"; import { Groups, Association } from "~/types"; - -import { cite, getModuleIcon } from "~/logic/lib/util"; +import { Sigil } from '~/logic/lib/sigil'; +import urbitOb from 'urbit-ob'; +import { getModuleIcon, getItemTitle, uxToHex } from "~/logic/lib/util"; function SidebarItemIndicator(props: { status?: SidebarItemStatus }) { switch (props.status) { @@ -24,31 +25,18 @@ function SidebarItemIndicator(props: { status?: SidebarItemStatus }) { } } -; - -const DM_REGEX = /ship\/~([a-z]|-)*\/dm--/; -function getItemTitle(association: Association) { - if(DM_REGEX.test(association.resource)) { - const [,,ship,name] = association.resource.split('/'); - if(ship.slice(1) === window.ship) { - return cite(`~${name.slice(4)}`); - } - return cite(ship); - - } - return association.metadata.title || association.resource -} - export function SidebarItem(props: { hideUnjoined: boolean; association: Association; + contacts: any; groups: Groups; path: string; selected: boolean; apps: SidebarAppConfigs; + workspace: Workspace; }) { const { association, path, selected, apps, groups } = props; - const title = getItemTitle(association); + let title = getItemTitle(association); const appName = association?.["app-name"]; const mod = association?.metadata?.module || appName; const rid = association?.resource @@ -58,12 +46,19 @@ export function SidebarItem(props: { if (!app) { return null; } + const DM = (isUnmanaged && props.workspace?.type === "messages"); const itemStatus = app.getStatus(path); const hasUnread = itemStatus === "unread" || itemStatus === "mention"; const isSynced = itemStatus !== "unsubscribed"; - const baseUrl = isUnmanaged ? `/~landscape/home` : `/~landscape${groupPath}`; + let baseUrl = `/~landscape${groupPath}`; + + if (DM) { + baseUrl = '/~landscape/messages'; + } else if (isUnmanaged) { + baseUrl = '/~landscape/home'; + } const to = isSynced ? `${baseUrl}/resource/${mod}${rid}` @@ -75,6 +70,21 @@ export function SidebarItem(props: { return null; } + let img = null; + + if (urbitOb.isValidPatp(title)) { + if (props.contacts?.[title] && props.contacts[title].avatar) { + img = ; + } else { + img = + } + if (props.contacts?.[title] && props.contacts[title].nickname) { + title = props.contacts[title].nickname; + } + } else { + img = + } + return ( - + {DM ? img : ( + + ) + } { const assoc = associations[a]; - return group - ? assoc.group === group - : !(assoc.group in props.associations.groups); + if (workspace?.type === 'messages') { + return (!(assoc.group in props.associations.groups) && assoc.metadata.module === "chat"); + } else { + return group + ? assoc.group === group + : (!(assoc.group in props.associations.groups) && assoc.metadata.module !== "chat"); + } }) .sort(sidebarSort(associations, props.apps)[config.sortBy]); @@ -69,6 +75,8 @@ export function SidebarList(props: { apps={props.apps} hideUnjoined={config.hideUnjoined} groups={props.groups} + contacts={props.contacts} + workspace={workspace} /> ); })} diff --git a/pkg/interface/src/views/landscape/components/Sidebar/SidebarListHeader.tsx b/pkg/interface/src/views/landscape/components/Sidebar/SidebarListHeader.tsx index 6d49d10f9..66364ac66 100644 --- a/pkg/interface/src/views/landscape/components/Sidebar/SidebarListHeader.tsx +++ b/pkg/interface/src/views/landscape/components/Sidebar/SidebarListHeader.tsx @@ -17,8 +17,11 @@ import { Link, useHistory } from 'react-router-dom'; import { getGroupFromWorkspace } from "~/logic/lib/workspace"; import { roleForShip } from "~/logic/lib/group"; import {Groups, Rolodex, Associations} from "~/types"; +import { NewChannel } from "~/views/landscape/components/NewChannel"; +import GlobalApi from "~/logic/api/global"; export function SidebarListHeader(props: { + api: GlobalApi; initialValues: SidebarListConfig; associations: Associations; groups: Groups; @@ -40,10 +43,12 @@ export function SidebarListHeader(props: { const groupPath = getGroupFromWorkspace(props.workspace); const role = props.groups?.[groupPath] ? roleForShip(props.groups[groupPath], window.ship) : undefined; - const memberMetadata = + const memberMetadata = groupPath ? props.associations.contacts?.[groupPath].metadata.vip === 'member-metadata' : false; - const isAdmin = memberMetadata || (role === "admin") || (props.workspace?.type === 'home'); + const isAdmin = memberMetadata || (role === "admin") || (props.workspace?.type === 'home') || (props.workspace?.type === "messages"); + + const noun = (props.workspace?.type === "messages") ? "Messages" : "Channels"; return ( - {props.initialValues.hideUnjoined ? "Joined Channels" : "All Channels"} + {props.initialValues.hideUnjoined ? `Joined ${noun}` : `All ${noun}`} - + + + } + > + + + ) + : ( + + to={!!groupPath + ? `/~landscape${groupPath}/new` + : `/~landscape/${props.workspace?.type}/new`}> - - - + DM - - + ) + } {props.children} diff --git a/pkg/interface/src/views/landscape/index.tsx b/pkg/interface/src/views/landscape/index.tsx index fe00da563..b04e3892d 100644 --- a/pkg/interface/src/views/landscape/index.tsx +++ b/pkg/interface/src/views/landscape/index.tsx @@ -27,7 +27,7 @@ type LandscapeProps = StoreState & { export function DMRedirect(props: LandscapeProps & RouteComponentProps & { ship: string; }) { const { ship, api, history, graphKeys } = props; const goToGraph = useCallback((graph: string) => { - history.push(`/~landscape/home/resource/chat/ship/~${graph}`); + history.push(`/~landscape/messages/resource/chat/ship/~${graph}`); }, [history]); useEffect(() => { From 1093ad0e42e24938cb42803abde412a75c263b9e Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Wed, 3 Feb 2021 09:49:29 +1000 Subject: [PATCH 21/27] group-view: add %invite action --- pkg/arvo/lib/group-view.hoon | 8 ++++++++ pkg/arvo/sur/group-view.hoon | 2 ++ 2 files changed, 10 insertions(+) diff --git a/pkg/arvo/lib/group-view.hoon b/pkg/arvo/lib/group-view.hoon index 483b97a86..f3919891a 100644 --- a/pkg/arvo/lib/group-view.hoon +++ b/pkg/arvo/lib/group-view.hoon @@ -14,6 +14,7 @@ remove+remove join+join leave+leave + invite+invite == :: ++ create @@ -33,6 +34,13 @@ :~ resource+dejs:resource ship+(su ;~(pfix sig fed:ag)) == + :: + ++ invite + %- ot + :~ resource+dejs:resource + ships+(as (su ;~(pfix sig fed:ag))) + description+so + == -- :: ++ enjs diff --git a/pkg/arvo/sur/group-view.hoon b/pkg/arvo/sur/group-view.hoon index 4ffbcfc8a..549ae5430 100644 --- a/pkg/arvo/sur/group-view.hoon +++ b/pkg/arvo/sur/group-view.hoon @@ -9,6 +9,8 @@ :: client side [%join =resource =ship] [%leave =resource] + :: + [%invite =resource ships=(set ship) description=@t] == :: From e17bf318594b44d136795a43926a7949109a3f6c Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Wed, 3 Feb 2021 09:52:00 +1000 Subject: [PATCH 22/27] group-view: add -group-invite thread --- pkg/arvo/ted/group/invite.hoon | 58 ++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 pkg/arvo/ted/group/invite.hoon diff --git a/pkg/arvo/ted/group/invite.hoon b/pkg/arvo/ted/group/invite.hoon new file mode 100644 index 000000000..ad42479ef --- /dev/null +++ b/pkg/arvo/ted/group/invite.hoon @@ -0,0 +1,58 @@ +/- spider, + metadata=metadata-store, + *group, + inv=invite-store, + store=group-store, + push-hook +/+ strandio, resource, view=group-view, grpl=group +=> +|% +++ strand strand:spider +++ poke poke:strandio +++ poke-our poke-our:strandio +++ gallify-bowl + |= =bowl:spider + ^- bowl:gall + :* [our src %$]:bowl + [~ ~] + [0 eny now byk]:bowl + == +:: +++ invite-ships + |= [ships=(set ship) rid=resource description=cord] + =/ m (strand ,~) + ^- form:m + ;< =bowl:spider bind:m get-bowl:strandio + =/ =action:inv + :^ %invites %groups (shaf %group-uid eny.bowl) + ^- multi-invite:inv + [our.bowl %group-push-hook rid ships description] + ;< ~ bind:m (poke-our %invite-hook invite-action+!>(action)) + (pure:m ~) +:: +++ add-pending + |= [ships=(set ship) rid=resource] + =/ m (strand ,~) + ^- form:m + =/ =action:store + [%change-policy rid %invite %add-invites ships] + ;< ~ bind:m (poke-our %group-push-hook %group-update !>(action)) + (pure:m ~) +-- +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<([~ =action:view] arg) +?> ?=(%invite -.action) +;< =bowl:spider bind:m get-bowl:strandio +=/ =bowl:gall (gallify-bowl bowl) +?> (~(is-admin grpl bowl) our.bowl resource.action) +;< ~ bind:m + (invite-ships [ships resource description]:action) +=/ =group + (need (~(scry-group grpl bowl) resource.action)) +?: ?=(%open -.policy.group) + (pure:m !>(~)) +;< ~ bind:m (add-pending [ships resource]:action) +(pure:m !>(~)) From 63027861dc3ea7774b5bf352602ede1be2e904d3 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Wed, 3 Feb 2021 09:52:25 +1000 Subject: [PATCH 23/27] invite-store: add %groups in +on-init --- pkg/arvo/app/invite-store.hoon | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/arvo/app/invite-store.hoon b/pkg/arvo/app/invite-store.hoon index 47de74ba8..d7bbbf0c3 100644 --- a/pkg/arvo/app/invite-store.hoon +++ b/pkg/arvo/app/invite-store.hoon @@ -38,7 +38,9 @@ %_ this invites.state %- ~(gas by *invites:store) - [%graph *invitatory:store]~ + :~ [%graph *invitatory:store] + [%groups *invitatory:store] + == == :: ++ on-save !>(state) From 8bb8f0b206b3eb1005b86d7f1f23b9663996b374 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Wed, 3 Feb 2021 10:00:42 +1000 Subject: [PATCH 24/27] invites: update interface for new thread --- pkg/interface/src/logic/api/groups.ts | 12 ++++++++ pkg/interface/src/views/components/Invite.tsx | 10 ++++++- .../landscape/components/InvitePopover.tsx | 29 ++++++++++++++----- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/pkg/interface/src/logic/api/groups.ts b/pkg/interface/src/logic/api/groups.ts index 4e5182abe..de2f4b488 100644 --- a/pkg/interface/src/logic/api/groups.ts +++ b/pkg/interface/src/logic/api/groups.ts @@ -67,6 +67,18 @@ export default class GroupsApi extends BaseApi { }); } + invite(ship: string, name: string, ships: Patp[], description: string) { + const resource = makeResource(ship, name); + return this.viewThread('group-invite', { + invite: { + resource, + ships, + description + } + }); + + } + private proxyAction(action: GroupAction) { return this.action('group-push-hook', 'group-update', action); } diff --git a/pkg/interface/src/views/components/Invite.tsx b/pkg/interface/src/views/components/Invite.tsx index 34bbabfed..70c16ab4c 100644 --- a/pkg/interface/src/views/components/Invite.tsx +++ b/pkg/interface/src/views/components/Invite.tsx @@ -12,8 +12,16 @@ export class InviteItem extends Component<{invite: Invite, onAccept: (i: any) => <> - {cite(props.invite.resource.ship)} invited you to {props.invite.resource.name} + + {cite(props.invite.resource.ship)} + {" "}invited you to{" "} + {props.invite.resource.name} + {props.invite.text && ( + + {props.invite.text} + + )} { + const onSubmit = async ({ ships, description }: FormSchema, actions) => { if(props.workspace.type === 'home') { history.push(`/~landscape/dm/${deSig(ships[0])}`); return; } // TODO: how to invite via email? try { - const resource = resourceFromPath(association.group); - await ships.reduce( - (acc, s) => acc.then(() => api.contacts.invite(resource, `~${deSig(s)}`)), - Promise.resolve() + const { ship, name } = resourceFromPath(association.group); + await api.groups.invite( + ship, name, + _.compact(ships).map(s => `~${deSig(s)}`), + description ); + actions.setStatus({ success: null }); onOutsideClick(); } catch (e) { @@ -66,7 +77,7 @@ export function InvitePopover(props: InvitePopoverProps) { } }; - const initialValues: FormSchema = { ships: [], emails: [] }; + const initialValues: FormSchema = { ships: [], emails: [], description: '' }; return ( @@ -115,6 +126,10 @@ export function InvitePopover(props: InvitePopoverProps) { maxLength={props.workspace.type === 'home' ? 1 : undefined} autoFocus /> + {/* Date: Wed, 3 Feb 2021 10:01:06 +1000 Subject: [PATCH 25/27] JoinGroup: check correct associations key --- pkg/interface/src/views/landscape/components/JoinGroup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/interface/src/views/landscape/components/JoinGroup.tsx b/pkg/interface/src/views/landscape/components/JoinGroup.tsx index cef007956..442891971 100644 --- a/pkg/interface/src/views/landscape/components/JoinGroup.tsx +++ b/pkg/interface/src/views/landscape/components/JoinGroup.tsx @@ -85,7 +85,7 @@ export function JoinGroup(props: JoinGroupProps) { await waiter((p: JoinGroupProps) => { return group in p.groups && (group in (p.associations?.graph ?? {}) - || group in (p.associations?.contacts ?? {})) + || group in (p.associations?.groups ?? {})) }); if(props.groups?.[group]?.hidden) { const { metadata } = associations.graph[group]; From 9d08d0d2b6503aa949bfcd90dd597010a7a50d9a Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Wed, 3 Feb 2021 10:34:16 +1000 Subject: [PATCH 26/27] graph-push-hook: fix +is-allowed --- pkg/arvo/app/graph-push-hook.hoon | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/arvo/app/graph-push-hook.hoon b/pkg/arvo/app/graph-push-hook.hoon index d210ddec2..6685a881e 100644 --- a/pkg/arvo/app/graph-push-hook.hoon +++ b/pkg/arvo/app/graph-push-hook.hoon @@ -97,7 +97,7 @@ ++ initial-watch |= [=path =resource:res] ^- vase - ?> (is-allowed resource bowl %.n) + ?> (is-allowed resource) !> ^- update:store ?~ path :: new subscribe @@ -185,8 +185,10 @@ reader.permissions :: ++ is-allowed - |= * - %.y + |= =resource:res + =/ group-res=resource:res + (need (peek-group:met %graph resource)) + (is-member:grp src.bowl group-res) :: ++ get-roles-writers-variation |= =resource:res From e0b1019cd9ccb3d9329ee9dbe4916086f1f81ce9 Mon Sep 17 00:00:00 2001 From: Matilde Park Date: Tue, 2 Feb 2021 20:05:29 -0500 Subject: [PATCH 27/27] hark: correct groupPath construction for dms --- pkg/interface/src/views/apps/notifications/graph.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pkg/interface/src/views/apps/notifications/graph.tsx b/pkg/interface/src/views/apps/notifications/graph.tsx index 36e3b3532..8d6cdc5b7 100644 --- a/pkg/interface/src/views/apps/notifications/graph.tsx +++ b/pkg/interface/src/views/apps/notifications/graph.tsx @@ -139,8 +139,13 @@ const GraphNodeContent = ({ group, post, contacts, mod, description, index, remo return null; }; -function getNodeUrl(mod: string, group: string, graph: string, index: string) { - const graphUrl = `/~landscape${group}/resource/${mod}${graph}`; +function getNodeUrl(mod: string, group: boolean, groupPath: string, graph: string, index: string) { + if (!group && mod === 'chat') { + groupPath = '/messages'; + } else if (!group) { + groupPath = '/home'; + } + const graphUrl = `/~landscape${groupPath}/resource/${mod}${graph}`; const idx = index.slice(1).split("/"); if (mod === "publish") { const [noteId] = idx; @@ -186,7 +191,7 @@ const GraphNode = ({ const groupContacts = contacts[groupPath] ?? {}; - const nodeUrl = getNodeUrl(mod, group?.hidden ? '/home' : groupPath, graph, index); + const nodeUrl = getNodeUrl(mod, group?.hidden, groupPath, graph, index); const onClick = useCallback(() => { if(!read) {