diff --git a/.github/workflows/merge-master.yml b/.github/workflows/merge-master.yml new file mode 100644 index 0000000000..627d9e802d --- /dev/null +++ b/.github/workflows/merge-master.yml @@ -0,0 +1,27 @@ +name: merge +on: + push: + branches: + - 'master' +jobs: + merge-to-next-js: + runs-on: ubuntu-latest + name: "Merge master to release/next-js" + steps: + - uses: actions/checkout@v2 + - uses: devmasx/merge-branch@v1.3.1 + with: + type: now + target_branch: release/next-js + github_token: ${{ secrets.JANEWAY_BOT_TOKEN }} + + merge-to-group-timer: + runs-on: ubuntu-latest + name: "Merge master to ops/group-timer" + steps: + - uses: actions/checkout@v2 + - uses: devmasx/merge-branch@v1.3.1 + with: + type: now + target_branch: ops/group-timer + github_token: ${{ secrets.JANEWAY_BOT_TOKEN }} diff --git a/.github/workflows/merge.yml b/.github/workflows/merge-release.yml similarity index 63% rename from .github/workflows/merge.yml rename to .github/workflows/merge-release.yml index ff520c2658..eb7df325ab 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge-release.yml @@ -1,17 +1,17 @@ -name: merge +name: ops-merge on: push: branches: - - 'master' + - 'release/*' jobs: - merge-to-next-js: + merge-release-to-ops: runs-on: ubuntu-latest - name: "Merge master to release/next-js" + name: "Merge to ops-tlon" steps: - uses: actions/checkout@v2 - uses: devmasx/merge-branch@v1.3.1 with: type: now - target_branch: release/next-js + target_branch: ops-tlon github_token: ${{ secrets.JANEWAY_BOT_TOKEN }} diff --git a/.github/workflows/ops-group-timer.yml b/.github/workflows/ops-group-timer.yml new file mode 100644 index 0000000000..f28a615abb --- /dev/null +++ b/.github/workflows/ops-group-timer.yml @@ -0,0 +1,20 @@ +name: group-timer +on: + push: + branches: + - 'ops/group-timer' +jobs: + glob: + runs-on: ubuntu-latest + name: "Create and deploy a glob to ~difmex-passed" + steps: + - uses: actions/checkout@v2 + with: + lfs: true + - uses: ./.github/actions/glob + with: + ship: 'difmex-passed' + credentials: ${{ secrets.JANEWAY_SERVICE_KEY }} + ssh-sec-key: ${{ secrets.JANEWAY_SSH_SEC_KEY }} + ssh-pub-key: ${{ secrets.JANEWAY_SSH_PUB_KEY }} + diff --git a/.github/workflows/publish-npm-packages.yml b/.github/workflows/publish-npm-packages.yml new file mode 100644 index 0000000000..0f0fc021e4 --- /dev/null +++ b/.github/workflows/publish-npm-packages.yml @@ -0,0 +1,60 @@ +name: publish-npm-packages +on: + push: + branches: + - 'master' +jobs: + publish-api: + runs-on: ubuntu-latest + name: "Publish '@urbit/api' if a new version is available" + steps: + - uses: actions/checkout@v2 + with: + lfs: true + - uses: actions/setup-node@v2 + with: + node-version: '14' + - run: 'npm install' + working-directory: 'pkg/npm/api' + - uses: JS-DevTools/npm-publish@v1 + with: + check-version: true + package: './pkg/npm/api/package.json' + token: ${{ secrets.NPM_TOKEN }} + + publish-http-api: + runs-on: ubuntu-latest + name: "Publish '@urbit/http-api' if a new version is available" + steps: + - uses: actions/checkout@v2 + with: + lfs: true + - uses: actions/setup-node@v2 + with: + node-version: '14' + - run: 'npm install' + working-directory: 'pkg/npm/http-api' + - uses: JS-DevTools/npm-publish@v1 + with: + check-version: true + package: './pkg/npm/http-api/package.json' + token: ${{ secrets.NPM_TOKEN }} + + publish-eslint-config: + runs-on: ubuntu-latest + name: "Publish '@urbit/eslint-config' if a new version is available" + steps: + - uses: actions/checkout@v2 + with: + lfs: true + - uses: actions/setup-node@v2 + with: + node-version: '14' + - run: 'npm install' + working-directory: 'pkg/npm/eslint-config' + - uses: JS-DevTools/npm-publish@v1 + with: + check-version: true + package: './pkg/npm/eslint-config/package.json' + token: ${{ secrets.NPM_TOKEN }} + diff --git a/bin/solid.pill b/bin/solid.pill index 62daecc18a..bd57c199a4 100644 --- a/bin/solid.pill +++ b/bin/solid.pill @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9433e0a7f1edbdcc6c8ac3e70c9516061d35218e5a1dc3192b2189dfb28cdc88 -size 9539470 +oid sha256:24e674adc2bc225cbf522da9ebbb6f1ca0364730392be4e59fbbd65b5028efa5 +size 9283548 diff --git a/pkg/arvo/app/contact-push-hook.hoon b/pkg/arvo/app/contact-push-hook.hoon index 1503b2029e..f363e196e9 100644 --- a/pkg/arvo/app/contact-push-hook.hoon +++ b/pkg/arvo/app/contact-push-hook.hoon @@ -67,18 +67,20 @@ ++ on-arvo on-arvo:def ++ on-fail on-fail:def :: -++ should-proxy-update - |= =vase - ^- ? - =/ =update:store !<(update:store vase) +++ transform-proxy-update + |= vas=vase + ^- (unit vase) + :: TODO: should check if user is allowed to %add, %remove, %edit + :: contact + =/ =update:store !<(update:store vas) ?- -.update - %initial %.n - %add %.y - %remove %.y - %edit %.y - %allow %.n - %disallow %.n - %set-public %.n + %initial ~ + %add `vas + %remove `vas + %edit `vas + %allow ~ + %disallow ~ + %set-public ~ == :: ++ resource-for-update resource-for-update:con diff --git a/pkg/arvo/app/dbug.hoon b/pkg/arvo/app/dbug.hoon index f7132f05f9..e2c340ce29 100644 --- a/pkg/arvo/app/dbug.hoon +++ b/pkg/arvo/app/dbug.hoon @@ -593,10 +593,10 @@ %& (ship p.lane) :: %| - ?~ l=((soft ,[=@tas =@if =@ud]) (cue p.lane)) - s+(scot %x p.lane) - =, u.l - (tape "%{(trip tas)}, {(scow %if if)}, {(scow %ud ud)}") + %- tape + =/ ip=@if (end [0 32] p.lane) + =/ pt=@ud (cut 0 [32 16] p.lane) + "{(scow %if ip)}:{((d-co:co 1) pt)} ({(scow %ux p.lane)})" == == :: diff --git a/pkg/arvo/app/file-server.hoon b/pkg/arvo/app/file-server.hoon index f32697115f..121005b290 100644 --- a/pkg/arvo/app/file-server.hoon +++ b/pkg/arvo/app/file-server.hoon @@ -240,7 +240,12 @@ =/ mime-type=@t (rsh 3 (crip )) :: Should maybe inspect to see how long cache should hold :: - [[200 ['content-type' mime-type] max-1-da:gen ~] `q.u.data] + =/ headers + :~ content-type+mime-type + max-1-da:gen + 'Service-Worker-Allowed'^'/' + == + [[200 headers] `q.u.data] == :: ++ lowercase diff --git a/pkg/arvo/app/glob.hoon b/pkg/arvo/app/glob.hoon index 2ea06544ba..9a9b0652dc 100644 --- a/pkg/arvo/app/glob.hoon +++ b/pkg/arvo/app/glob.hoon @@ -5,7 +5,7 @@ /- glob /+ default-agent, verb, dbug |% -++ hash 0v5.pmklm.qttdl.n8bs0.0tnc4.gg633 +++ hash 0v7.sjbvb.4gg0l.1qmbv.fmobl.d2tsq +$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))] +$ all-states $% state-0 @@ -105,12 +105,15 @@ (cat 3 js-name '.js') =+ .^(js=@t %cx :(weld home /app/landscape/js/bundle /[js-name]/js)) =+ .^(map=@t %cx :(weld home /app/landscape/js/bundle /[map-name]/map)) + =+ .^(sw=@t %cx :(weld home /app/landscape/js/bundle /serviceworker/js)) =+ !<(=js=mime (js-tube !>(js))) + =+ !<(=sw=mime (js-tube !>(sw))) =+ !<(=map=mime (map-tube !>(map))) =/ =glob:glob %- ~(gas by *glob:glob) :~ /[js-name]/js^js-mime /[map-name]/map^map-mime + /serviceworker/js^sw-mime == =/ =path /(cat 3 'glob-' (scot %uv (sham glob)))/glob [%pass /make %agent [our.bowl %hood] %poke %drum-put !>([path (jam glob)])]~ diff --git a/pkg/arvo/app/graph-push-hook.hoon b/pkg/arvo/app/graph-push-hook.hoon index aff6801136..0ac5026377 100644 --- a/pkg/arvo/app/graph-push-hook.hoon +++ b/pkg/arvo/app/graph-push-hook.hoon @@ -63,31 +63,110 @@ =* mark i.t.wire :_ this (build-permissions mark i.t.t.wire %next)^~ + :: + [%transform-add @ ~] + =* mark i.t.wire + :_ this + (build-transform-add mark %next)^~ == :: ++ on-fail on-fail:def -:: -++ should-proxy-update - |= =vase - ^- ? - =/ =update:store !<(update:store vase) +++ transform-proxy-update + |= vas=vase + ^- (unit vase) + =/ =update:store !<(update:store vas) =* rid resource.q.update + =. p.update now.bowl ?- -.q.update - %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) - %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 %.n + %add-nodes + ?. (is-allowed-add:hc rid nodes.q.update) + ~ + =/ mark (get-mark:gra rid) + ?~ mark `vas + |^ + =/ transform + !< $-([index:store post:store atom ?] [index:store post:store]) + %. !>(*indexed-post:store) + .^(tube:clay (scry:hc %cc %home /[u.mark]/transform-add-nodes)) + =/ [* result=(list [index:store node:store])] + %+ roll + (flatten-node-map ~(tap by nodes.q.update)) + (transform-list transform) + =. nodes.q.update + %- ~(gas by *(map index:store node:store)) + result + [~ !>(update)] + :: + ++ flatten-node-map + |= lis=(list [index:store node:store]) + ^- (list [index:store node:store]) + |^ + %- sort-nodes + %+ welp + (turn lis empty-children) + %- zing + %+ turn lis + |= [=index:store =node:store] + ^- (list [index:store node:store]) + ?: ?=(%empty -.children.node) + ~ + %+ turn + (tap-deep:gra index p.children.node) + empty-children + :: + ++ empty-children + |= [=index:store =node:store] + ^- [index:store node:store] + [index node(children [%empty ~])] + :: + ++ sort-nodes + |= unsorted=(list [index:store node:store]) + ^- (list [index:store node:store]) + %+ sort unsorted + |= [p=[=index:store *] q=[=index:store *]] + ^- ? + (lth (lent index.p) (lent index.q)) + -- + :: + ++ transform-list + |= transform=$-([index:store post:store atom ?] [index:store post:store]) + |= $: [=index:store =node:store] + [indices=(set index:store) lis=(list [index:store node:store])] + == + =/ l (lent index) + =/ parent-modified=? + %- ~(rep in indices) + |= [i=index:store out=_|] + ?: out out + =/ k (lent i) + ?: (lte l k) + %.n + =((swag [0 k] index) i) + =/ [ind=index:store =post:store] + (transform index post.node now.bowl parent-modified) + :- (~(put in indices) index) + (snoc lis [ind node(post post)]) + -- + :: + %remove-nodes + ?. (is-allowed-remove:hc resource.q.update indices.q.update) + ~ + `vas + :: + %add-graph ~ + %remove-graph ~ + %add-signatures ~ + %remove-signatures ~ + %archive-graph ~ + %unarchive-graph ~ + %add-tag ~ + %remove-tag ~ + %keys ~ + %tags ~ + %tag-queries ~ + %run-updates ~ == +:: ++ resource-for-update resource-for-update:gra :: ++ initial-watch @@ -111,7 +190,7 @@ |= =vase ^- [(list card) agent] =/ =update:store !<(update:store vase) - ?+ -.q.update [~ this] + ?+ -.q.update [~ this] %add-graph ?~ mark.q.update `this =* mark u.mark.q.update @@ -119,6 +198,7 @@ :_ this(marks (~(put in marks) mark)) :~ (build-permissions:hc mark %add %sing) (build-permissions:hc mark %remove %sing) + (build-transform-add:hc mark %sing) == :: %remove-graph @@ -133,19 +213,14 @@ |_ =bowl:gall +* grp ~(. group bowl) met ~(. mdl bowl) - gra ~(. graph 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) - == -:: ++ perm-mark-name |= perm=@t ^- @t @@ -216,6 +291,8 @@ %- some %+ levy ~(tap by nodes) |= [=index:store =node:store] + ?. =(author.post.node src.bowl) + %.n =/ =permissions:store %^ add-mark resource vip (node-to-indexed-post node) @@ -262,5 +339,13 @@ =/ =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] +:: +++ build-transform-add + |= [=mark mode=?(%sing %next)] + ^- card + =/ =wire /transform-add/[mark] + =/ =mood:clay [%c da+now.bowl /[mark]/transform-add-nodes] + =/ =rave:clay ?:(?=(%sing mode) [mode mood] [mode mood]) + [%pass wire %arvo %c %warp our.bowl %home `rave] -- diff --git a/pkg/arvo/app/graph-store.hoon b/pkg/arvo/app/graph-store.hoon index 57dbb5e823..87f4644803 100644 --- a/pkg/arvo/app/graph-store.hoon +++ b/pkg/arvo/app/graph-store.hoon @@ -386,14 +386,14 @@ :: ?~ t.index =* p post.node + ?~ hash.p node(signatures.post *signatures:store) =/ =validated-portion:store [parent-hash author.p time-sent.p contents.p] =/ =hash:store `@ux`(sham validated-portion) - ?~ hash.p node(signatures.post *signatures:store) - ~| "signatures do not match the calculated hash" - ?> (are-signatures-valid:sigs our.bowl signatures.p hash now.bowl) ~| "hash of post does not match calculated hash" ?> =(hash u.hash.p) + ~| "signatures do not match the calculated hash" + ?> (are-signatures-valid:sigs our.bowl signatures.p hash now.bowl) node :: recurse children :: diff --git a/pkg/arvo/app/group-push-hook.hoon b/pkg/arvo/app/group-push-hook.hoon index 29bd836299..e8736ccb5b 100644 --- a/pkg/arvo/app/group-push-hook.hoon +++ b/pkg/arvo/app/group-push-hook.hoon @@ -110,12 +110,12 @@ ++ on-arvo on-arvo:def ++ on-fail on-fail:def :: -++ should-proxy-update - |= =vase - =/ =update:store - !<(update:store vase) +++ transform-proxy-update + |= vas=vase + ^- (unit vase) + =/ =update:store !<(update:store vas) ?: ?=(%initial -.update) - %.n + ~ |^ =/ role=(unit (unit role-tag)) (role-for-ship:grp resource.update src.bowl) @@ -128,24 +128,36 @@ %moderator moderator %janitor member == + :: ++ member - ?: ?=(%add-members -.update) - =(~(tap in ships.update) ~[src.bowl]) - ?: ?=(%remove-members -.update) - =(~(tap in ships.update) ~[src.bowl]) - %.n + ?: ?| ?& ?=(%add-members -.update) + =(~(tap in ships.update) ~[src.bowl]) + == + ?& ?=(%remove-members -.update) + =(~(tap in ships.update) ~[src.bowl]) + == == + `vas + ~ + :: ++ admin - !?=(?(%remove-group %add-group) -.update) + ?. ?=(?(%remove-group %add-group) -.update) + `vas + ~ + :: ++ moderator - ?= $? %add-members %remove-members - %add-tag %remove-tag == - -.update + ?: ?=(?(%add-members %remove-members %add-tag %remove-tag) -.update) + `vas + ~ + :: ++ non-member - ?& ?=(%add-members -.update) - (can-join:grp resource.update src.bowl) - =(~(tap in ships.update) ~[src.bowl]) - == + ?: ?& ?=(%add-members -.update) + (can-join:grp resource.update src.bowl) + =(~(tap in ships.update) ~[src.bowl]) + == + `vas + ~ -- +:: ++ resource-for-update resource-for-update:grp :: ++ take-update diff --git a/pkg/arvo/app/hark-graph-hook.hoon b/pkg/arvo/app/hark-graph-hook.hoon index 99d9e2450c..3e779a8bcf 100644 --- a/pkg/arvo/app/hark-graph-hook.hoon +++ b/pkg/arvo/app/hark-graph-hook.hoon @@ -24,8 +24,6 @@ watch-on-self=_& == :: -+$ notif-kind - [name=@t parent-lent=@ud mode=?(%each %count %none) watch=?] :: ++ scry |* [[our=@p now=@da] =mold p=path] @@ -223,11 +221,11 @@ |= [=index:graph-store out=(list card)] =| =indexed-post:graph-store =. index.p.indexed-post index - =+ !<(u-notif-kind=(unit notif-kind) (tube !>(indexed-post))) + =+ !<(u-notif-kind=(unit notif-kind:hook) (tube !>(indexed-post))) ?~ u-notif-kind out =* notif-kind u.u-notif-kind =/ =stats-index:store - [%graph rid (scag parent-lent.notif-kind index)] + [%graph rid (scag parent.index-len.notif-kind index)] ?. ?=(%each mode.notif-kind) out :_ out (poke-hark %read-each stats-index index) @@ -382,8 +380,12 @@ update-core(hark-pokes [action hark-pokes]) :: ++ new-watch - |= =index:graph-store - update-core(new-watches [index new-watches]) + |= [=index:graph-store =watch-for:hook =index-len:hook] + =? new-watches =(%siblings watch-for) + [(scag parent.index-len index) new-watches] + =? new-watches =(%children watch-for) + [(scag self.index-len index) new-watches] + update-core :: ++ check |- ^+ update-core @@ -411,7 +413,7 @@ |= =node:graph-store ^+ update-core =. update-core (check-node-children node) - =+ !< notif-kind=(unit notif-kind) + =+ !< notif-kind=(unit notif-kind:hook) (get-conversion !>([0 post.node])) ?~ notif-kind update-core @@ -421,11 +423,11 @@ name.u.notif-kind =* not-kind u.notif-kind =/ parent=index:post - (scag parent-lent.not-kind index.post.node) + (scag parent.index-len.not-kind index.post.node) =/ notif-index=index:store [%graph group rid module desc parent] ?: =(our.bowl author.post.node) - (self-post node notif-index [mode watch]:not-kind) + (self-post node notif-index not-kind) =. update-core (update-unread-count not-kind notif-index [time-sent index]:post.node) =? update-core @@ -438,7 +440,7 @@ update-core :: ++ update-unread-count - |= [=notif-kind =index:store time=@da ref=index:graph-store] + |= [=notif-kind:hook =index:store time=@da ref=index:graph-store] =/ =stats-index:store (to-stats-index:store index) ?- mode.notif-kind @@ -450,19 +452,18 @@ ++ self-post |= $: =node:graph-store =index:store - mode=?(%count %each %none) - watch=? + =notif-kind:hook == ^+ update-core - ?: ?=(%none mode) update-core + ?: ?=(%none mode.notif-kind) update-core =/ =stats-index:store (to-stats-index:store index) =. update-core (hark %seen-index time-sent.post.node stats-index) - =? update-core ?=(%count mode) + =? update-core ?=(%count mode.notif-kind) (hark %read-count stats-index) - =? update-core &(watch watch-on-self) - (new-watch index.post.node) + =? update-core watch-on-self + (new-watch index.post.node [watch-for index-len]:notif-kind) update-core :: ++ add-unread diff --git a/pkg/arvo/app/hark-store.hoon b/pkg/arvo/app/hark-store.hoon index aa522185e6..fd1ab4f965 100644 --- a/pkg/arvo/app/hark-store.hoon +++ b/pkg/arvo/app/hark-store.hoon @@ -23,6 +23,7 @@ state-2 state-3 state-4 + state-5 == +$ unread-stats [indices=(set index:graph-store) last=@da] @@ -46,8 +47,11 @@ +$ state-4 [%4 base-state] :: ++$ state-5 + [%5 base-state] +:: +$ inflated-state - $: state-4 + $: state-5 cache == :: $cache: useful to have precalculated, but can be derived from state @@ -88,9 +92,18 @@ =| cards=(list card) |^ ?- -.old - %4 + %5 :- (flop cards) this(-.state old, +.state (inflate-cache:ha old)) + :: + %4 + %_ $ + -.old %5 + :: + last-seen.old + %- ~(run by last-seen.old) + |=(old=@da (min old now.bowl)) + == :: %3 %_ $ @@ -279,7 +292,6 @@ %+ turn ~(tap by unreads-count) |= [=stats-index:store count=@ud] - ?> ?=(%graph -.stats-index) :* stats-index ~(wyt in (~(gut by by-index) stats-index ~)) [%count count] @@ -297,10 +309,27 @@ (~(gut by last-seen) stats-index *time) == :: + ++ give-group-unreads + ^- (list [stats-index:store stats:store]) + %+ murn ~(tap by by-index) + |= [=stats-index:store nots=(set [time index:store])] + ?. ?=(%group -.stats-index) + ~ + :- ~ + :* stats-index + ~(wyt in nots) + [%count 0] + *time + == + :: ++ give-unreads ^- update:store :- %unreads - (weld give-each-unreads give-since-unreads) + ;: weld + give-each-unreads + give-since-unreads + give-group-unreads + == -- :: ++ on-peek @@ -749,8 +778,10 @@ == :: ++ inflate-cache - |= state-4 + |= state-5 ^+ +.state + =. +.state + *cache =/ nots=(list [p=@da =timebox:store]) (tap:orm notifications) |- =* outer $ diff --git a/pkg/arvo/app/landscape/fonts/sourcecodepro-bold.woff2 b/pkg/arvo/app/landscape/fonts/sourcecodepro-bold.woff2 new file mode 100644 index 0000000000..4efbf2991d Binary files /dev/null and b/pkg/arvo/app/landscape/fonts/sourcecodepro-bold.woff2 differ diff --git a/pkg/arvo/app/landscape/fonts/sourcecodepro-extralight.woff2 b/pkg/arvo/app/landscape/fonts/sourcecodepro-extralight.woff2 index 1c2a392905..4af3968e0e 100644 Binary files a/pkg/arvo/app/landscape/fonts/sourcecodepro-extralight.woff2 and b/pkg/arvo/app/landscape/fonts/sourcecodepro-extralight.woff2 differ diff --git a/pkg/arvo/app/landscape/fonts/sourcecodepro-light.woff2 b/pkg/arvo/app/landscape/fonts/sourcecodepro-light.woff2 index 8f7849083b..289b6c97c5 100644 Binary files a/pkg/arvo/app/landscape/fonts/sourcecodepro-light.woff2 and b/pkg/arvo/app/landscape/fonts/sourcecodepro-light.woff2 differ diff --git a/pkg/arvo/app/landscape/fonts/sourcecodepro-medium.woff2 b/pkg/arvo/app/landscape/fonts/sourcecodepro-medium.woff2 index 7dfd91c5bb..0cce5e955e 100644 Binary files a/pkg/arvo/app/landscape/fonts/sourcecodepro-medium.woff2 and b/pkg/arvo/app/landscape/fonts/sourcecodepro-medium.woff2 differ diff --git a/pkg/arvo/app/landscape/fonts/sourcecodepro-regular.woff2 b/pkg/arvo/app/landscape/fonts/sourcecodepro-regular.woff2 index c10f38d326..aae63a9460 100644 Binary files a/pkg/arvo/app/landscape/fonts/sourcecodepro-regular.woff2 and b/pkg/arvo/app/landscape/fonts/sourcecodepro-regular.woff2 differ diff --git a/pkg/arvo/app/landscape/fonts/sourcecodepro-semibold.woff2 b/pkg/arvo/app/landscape/fonts/sourcecodepro-semibold.woff2 index b2b00d64a5..3a24fb6ecf 100644 Binary files a/pkg/arvo/app/landscape/fonts/sourcecodepro-semibold.woff2 and b/pkg/arvo/app/landscape/fonts/sourcecodepro-semibold.woff2 differ diff --git a/pkg/arvo/app/landscape/fonts/sourcecodeprop-bold.woff2 b/pkg/arvo/app/landscape/fonts/sourcecodeprop-bold.woff2 deleted file mode 100644 index 45ee40a2b0..0000000000 Binary files a/pkg/arvo/app/landscape/fonts/sourcecodeprop-bold.woff2 and /dev/null differ diff --git a/pkg/arvo/app/landscape/index.html b/pkg/arvo/app/landscape/index.html index 80086a1ec1..9e69f790fe 100644 --- a/pkg/arvo/app/landscape/index.html +++ b/pkg/arvo/app/landscape/index.html @@ -24,6 +24,6 @@
- + diff --git a/pkg/arvo/app/language-server.hoon b/pkg/arvo/app/language-server.hoon index 88b249157f..a93e57c7fc 100644 --- a/pkg/arvo/app/language-server.hoon +++ b/pkg/arvo/app/language-server.hoon @@ -5,7 +5,7 @@ easy-print=language-server-easy-print, rune-snippet=language-server-rune-snippet, build=language-server-build, - default-agent + default-agent, verb |% +$ card card:agent:gall +$ lsp-req @@ -44,6 +44,7 @@ == -- ^- agent:gall +%+ verb | =| state-zero =* state - =< @@ -69,7 +70,7 @@ |= old-state=vase ^- (quip card _this) ~& > %lsp-upgrade - [~ this(state *state-zero)] + [~ this(state !<(state-zero old-state))] :: ++ on-poke ^+ on-poke:*agent:gall @@ -275,12 +276,14 @@ ++ handle-did-open |= item=text-document-item:lsp-sur ^- (quip card _state) + =/ =path + (uri-to-path:build uri.item) + ?: ?=(%sys -.path) + `state =/ buf=wall (to-wall (trip text.item)) =. bufs (~(put by bufs) uri.item buf) - =/ =path - (uri-to-path:build uri.item) :_ state %+ weld (give-rpc-notification (get-diagnostics uri.item)) @@ -318,12 +321,12 @@ ?~ p.tab-list ~ ?~ u.p.tab-list ~ :- ~ - %- crip - ;: weld - "`" - ~(ram re ~(duck easy-print detail.i.u.p.tab-list)) - "`" - == + =- (crip :(weld "```hoon\0a" tape "\0a```")) + ^- =tape + %- zing + %+ join "\0a" + %+ scag 40 + (~(win re ~(duck easy-print detail.i.u.p.tab-list)) 0 140) :: ++ sync-buf |= [buf=wall changes=(list change:lsp-sur)] diff --git a/pkg/arvo/app/metadata-push-hook.hoon b/pkg/arvo/app/metadata-push-hook.hoon index 6cc959f591..30dea7e233 100644 --- a/pkg/arvo/app/metadata-push-hook.hoon +++ b/pkg/arvo/app/metadata-push-hook.hoon @@ -56,22 +56,27 @@ ++ on-arvo on-arvo:def ++ on-fail on-fail:def :: -++ should-proxy-update - |= =vase - =+ !<(=update:store vase) +++ transform-proxy-update + |= vas=vase + ^- (unit vase) + =/ =update:store !<(update:store vas) ?. ?=(?(%add %remove) -.update) - %.n + ~ =/ role=(unit (unit role-tag)) (role-for-ship:grp group.update src.bowl) =/ =metadatum:store (need (peek-metadatum:met %groups group.update)) - ?~ role %.n + ?~ role ~ ?^ u.role - ?=(?(%admin %moderator) u.u.role) - ?. ?=(%add -.update) %.n - ?& =(src.bowl entity.resource.resource.update) - ?=(%member-metadata vip.metadatum) - == + ?: ?=(?(%admin %moderator) u.u.role) + `vas + ~ + ?. ?=(%add -.update) ~ + ?: ?& =(src.bowl entity.resource.resource.update) + ?=(%member-metadata vip.metadatum) + == + `vas + ~ :: ++ resource-for-update resource-for-update:met ++ take-update diff --git a/pkg/arvo/app/settings-store.hoon b/pkg/arvo/app/settings-store.hoon index 7410533ed4..2dbb0778ba 100644 --- a/pkg/arvo/app/settings-store.hoon +++ b/pkg/arvo/app/settings-store.hoon @@ -1,16 +1,15 @@ /- *settings -/+ verb, dbug, default-agent +/+ verb, dbug, default-agent, agentio |% +$ card card:agent:gall +$ versioned-state $% state-0 + state-1 == -+$ state-0 - $: %0 - =settings - == ++$ state-0 [%0 settings=settings-0] ++$ state-1 [%1 =settings] -- -=| state-0 +=| state-1 =* state - :: %- agent:dbug @@ -21,10 +20,14 @@ +* this . do ~(. +> bol) def ~(. (default-agent this %|) bol) + io ~(. agentio bol) :: ++ on-init ^- (quip card _this) - `this + =^ cards state + (put-entry:do %tutorial %seen b+|) + [cards this] + :: ++ on-save !>(state) :: @@ -32,8 +35,10 @@ |= =old=vase ^- (quip card _this) =/ old !<(versioned-state old-vase) + |- ?- -.old - %0 [~ this(state old)] + %0 $(old [%1 +.old]) + %1 [~ this(state old)] == :: ++ on-poke diff --git a/pkg/arvo/app/test.hoon b/pkg/arvo/app/test.hoon index 9c931799e4..b8f93b5fea 100644 --- a/pkg/arvo/app/test.hoon +++ b/pkg/arvo/app/test.hoon @@ -40,7 +40,7 @@ =. mar-ok.state %.y =+ .^(paz=(list path) ct+(en-beam now-beak /mar)) |- ^+ [fex this] - ?~ paz [fex this] + ?~ paz [(flop fex) this] =/ xap=path (flop i.paz) ?. ?=([%hoon *] xap) $(paz t.paz) @@ -63,7 +63,7 @@ ?> =(~ app.state) =. app-ok.state %.y =+ .^(app-arch=arch cy+(en-beam now-beak /app)) - =/ daz ~(tap in ~(key by dir.app-arch)) + =/ daz (sort ~(tap in ~(key by dir.app-arch)) |=((pair) !(aor p q))) |- ^+ [fex this] ?~ daz [fex this] =/ dap-pax=path /app/[i.daz]/hoon @@ -86,7 +86,7 @@ =. gen-ok.state %.y =+ .^(paz=(list path) ct+(en-beam now-beak /gen)) |- ^+ [fex this] - ?~ paz [fex this] + ?~ paz [(flop fex) this] =/ xap=path (flop i.paz) ?. ?=([%hoon *] xap) $(paz t.paz) @@ -106,11 +106,18 @@ ++ on-peek on-peek:def ++ on-agent on-agent:def ++ on-arvo + => |% + ++ report + |* [=path ok=?] + =/ =tank leaf+"{?:(ok "built " "FAILED")} {(spud path)}" + ~>(%slog.[0 tank] same) + -- + :: |= [=wire =sign-arvo] ^- [(list card) _this] - ?. ?=([%build *] wire) - (on-arvo:def wire sign-arvo) - ?. ?=(%writ +<.sign-arvo) + ?. ?& ?=([%build *] wire) + ?=([%clay %writ *] sign-arvo) + == (on-arvo:def wire sign-arvo) =/ =path t.wire ?+ path ~|(path+path !!) @@ -118,41 +125,29 @@ =/ ok ?~ p.sign-arvo | (~(nest ut -:!>(*agent:gall)) | -:!<(vase q.r.u.p.sign-arvo)) - ~& ?: ok - agent-built+path - agent-failed+path + %- (report path ok) =? app-ok.state !ok %.n =. app.state (~(del in app.state) path) ~? =(~ app.state) - ?: app-ok.state - %all-agents-built - %some-agents-failed + ?:(app-ok.state %all-agents-built %some-agents-failed) [~ this] :: [%mar *] =/ ok ?=(^ p.sign-arvo) - ~& ?: ok - mark-built+path - mark-failed+path + %- (report path ok) =? mar-ok.state !ok %.n =. mar.state (~(del in mar.state) path) ~? =(~ mar.state) - ?: mar-ok.state - %all-marks-built - %some-marks-failed + ?:(mar-ok.state %all-marks-built %some-marks-failed) [~ this] :: [%gen *] =/ ok ?=(^ p.sign-arvo) - ~& ?: ok - generator-built+path - generator-failed+path + %- (report path ok) =? gen-ok.state !ok %.n =. gen.state (~(del in gen.state) path) ~? =(~ gen.state) - ?: gen-ok.state - %all-generators-built - %some-generators-failed + ?:(gen-ok.state %all-generators-built %some-generators-failed) [~ this] == ++ on-fail on-fail:def diff --git a/pkg/arvo/gen/hood/merge.hoon b/pkg/arvo/gen/hood/merge.hoon index 35991526f3..f929620ca4 100644 --- a/pkg/arvo/gen/hood/merge.hoon +++ b/pkg/arvo/gen/hood/merge.hoon @@ -21,7 +21,7 @@ |^ :- %kiln-merge ^- $@(~ [syd=desk her=ship sud=desk cas=case gem=?(germ %auto)]) ?- arg - ~ ((slog (turn help-text |=(=@t leaf+(trip t)))) ~) + ~ ((slog (turn `wain`help-text |=(=@t leaf+(trip t)))) ~) [@ @ ~] =+(arg [sud ?.(=(our her) her (sein:title p.bek now her)) sud (opt-case da+now) gem]) :: diff --git a/pkg/arvo/gen/tally.hoon b/pkg/arvo/gen/tally.hoon index 988ab1bbed..fbaf04e0a9 100644 --- a/pkg/arvo/gen/tally.hoon +++ b/pkg/arvo/gen/tally.hoon @@ -34,10 +34,8 @@ =/ groups=(list [local=? resource:re members=@ud]) %+ murn %~ tap in - %~ key by - dir:(scry arch %y %group-store /groups) - |= i=@ta - =/ r=resource:re (de-path:re (stab i)) + (scry (set resource:re) %y %group-store /groups) + |= r=resource:re =/ g=(unit group:gr) %+ scry (unit group:gr) [%x %group-store [%groups (snoc (en-path:re r) %noun)]] @@ -59,18 +57,28 @@ %~ tap by %+ scry associations:md [%x %metadata-store [%group (snoc (en-path:re r) %noun)]] - |= [[* m=md-resource:md] metadata:md] + |= [m=md-resource:md association:md] ::NOTE we only count graphs for now - ?. &(=(%graph app-name.m) =(our creator)) ~ - `[module (de-path:re app-path.m)] + ?. &(=(%graph app-name.m) =(our creator.metadatum)) ~ + `[module.metadatum resource.m] +:: for sanity checks +:: +=/ real=(set resource:re) + =/ upd=update:ga + %+ scry update:ga + [%x %graph-store /keys/graph-update] + ?> ?=(%keys -.q.upd) + resources.q.upd :: count activity per channel :: =/ activity=(list [resource:re members=@ud (list [resource:re mod=term week=@ud authors=@ud])]) %+ turn crowds |= [g=resource:re m=@ud] :+ g m - %+ turn (~(got by channels) g) + %+ murn (~(got by channels) g) |= [m=term r=resource:re] + ?. (~(has in real) r) ~ + %- some :+ r m ::NOTE graph-store doesn't use the full resource-style path here! =/ upd=update:ga diff --git a/pkg/arvo/lib/gcp.hoon b/pkg/arvo/lib/gcp.hoon new file mode 100644 index 0000000000..0b472ea975 --- /dev/null +++ b/pkg/arvo/lib/gcp.hoon @@ -0,0 +1,15 @@ +/- *gcp +|% +++ token-to-json + |= =token + ^- json + =, enjs:format + %+ frond %gcp-token + %: pairs + [%'accessKey' s+access-key.token] + :- %'expiresIn' + %- numb + (div (mul 1.000 expires-in.token) ~s1) + ~ + == +-- diff --git a/pkg/arvo/lib/graph-view.hoon b/pkg/arvo/lib/graph-view.hoon index 78cd3fe5fc..22e876a713 100644 --- a/pkg/arvo/lib/graph-view.hoon +++ b/pkg/arvo/lib/graph-view.hoon @@ -1,4 +1,4 @@ -/- sur=graph-view +/- sur=graph-view, store=graph-store /+ resource, group-store ^? =< [sur .] @@ -17,6 +17,7 @@ leave+leave groupify+groupify eval+so + pending-indices+pending-indices ::invite+invite == :: @@ -51,6 +52,9 @@ :~ resource+(un dejs:resource) to+(uf ~ (mu dejs:resource)) == + :: + ++ pending-indices (op hex (su ;~(pfix fas (more fas dem)))) + :: ++ invite !! :: ++ associated @@ -60,4 +64,35 @@ == -- -- +:: +++ enjs + =, enjs:format + |% + ++ action + |= act=^action + ^- json + ?> ?=(%pending-indices -.act) + %+ frond %pending-indices + %- pairs + %+ turn ~(tap by pending.act) + |= [h=hash:store i=index:store] + ^- [@t json] + =/ idx (index i) + ?> ?=(%s -.idx) + [p.idx s+(scot %ux h)] + :: + ++ index + |= i=index:store + ^- json + ?: =(~ i) s+'/' + =/ j=^tape "" + |- + ?~ i [%s (crip j)] + =/ k=json (numb i.i) + ?> ?=(%n -.k) + %_ $ + i t.i + j (weld j (weld "/" (trip +.k))) + == + -- -- diff --git a/pkg/arvo/lib/graph.hoon b/pkg/arvo/lib/graph.hoon index 4f8ebe37b8..efdb38f2a0 100644 --- a/pkg/arvo/lib/graph.hoon +++ b/pkg/arvo/lib/graph.hoon @@ -104,26 +104,35 @@ resources.q.update :: ++ tap-deep - |= =graph:store + |= [=index:store =graph:store] ^- (list [index:store node:store]) - =| =index:store - =/ nodes=(list [atom node:store]) - (tap:orm:store graph) - |- =* tap-nodes $ - ^- (list [index:store node:store]) - %- zing - %+ turn - nodes - |= [=atom =node:store] - ^- (list [index:store node:store]) - %+ welp - ^- (list [index:store node:store]) - [(snoc index atom) node]~ - ?. ?=(%graph -.children.node) - ~ - %_ tap-nodes - index (snoc index atom) - nodes (tap:orm:store p.children.node) + %+ roll (tap:orm:store graph) + |= $: [=atom =node:store] + lis=(list [index:store node:store]) + == + =/ child-index (snoc index atom) + =/ childless-node node(children [%empty ~]) + ?: ?=(%empty -.children.node) + (snoc lis [child-index childless-node]) + %+ weld + (snoc lis [child-index childless-node]) + (tap-deep child-index p.children.node) +:: +++ got-deep + |= [=graph:store =index:store] + ^- node:store + =/ ind index + ?> ?=(^ index) + =/ =node:store (need (get:orm:store graph `atom`i.index)) + =. ind t.index + |- ^- node:store + ?~ ind + node + ?: ?=(%empty -.children.node) + !! + %_ $ + ind t.ind + node (need (get:orm:store p.children.node i.ind)) == :: ++ get-mark diff --git a/pkg/arvo/lib/language-server/complete.hoon b/pkg/arvo/lib/language-server/complete.hoon index 6e352e8196..56b3ca498e 100644 --- a/pkg/arvo/lib/language-server/complete.hoon +++ b/pkg/arvo/lib/language-server/complete.hoon @@ -185,6 +185,7 @@ [%zpmc *] (both p.gen q.gen) [%zpts *] loop(gen p.gen) [%zppt *] (both q.gen r.gen) + [%zpgl *] (spec-and-hoon p.gen q.gen) [%zpzp *] ~ * =+ doz=~(open ap gen) @@ -245,15 +246,25 @@ ^- (unit [term type]) ~ :: +++ get-id-sym + |= [pos=@ud =tape] + %^ get-id pos tape + ^- $-(nail (like (unit @t))) + ;~(sfix (punt sym) (star ;~(pose prn (just `@`10)))) +:: +++ get-id-cord + |= [pos=@ud =tape] + %^ get-id pos tape + ^- $-(nail (like (unit @t))) + ;~(sfix (punt (cook crip (star prn))) (star ;~(pose prn (just `@`10)))) +:: ++ get-id - |= [pos=@ud txt=tape] + |= [pos=@ud txt=tape seek=$-(nail (like (unit @t)))] ^- [forward=(unit @t) backward=(unit @t) id=(unit @t)] - =/ seek - ;~(sfix (punt (cook crip (star prn))) (star ;~(pose prn (just `@`10)))) =/ forward=(unit @t) (scan (slag pos txt) seek) =/ backward=(unit @t) - %- (lift |=(t=@tas (swp 3 t))) + %- (lift |=(t=@t (swp 3 t))) (scan (flop (scag pos txt)) seek) =/ id=(unit @t) ?~ forward @@ -272,7 +283,7 @@ ^- [back-pos=@ud fore-pos=@ud txt=tape] :: Find beg-pos by searching backward to where the current term :: begins - =+ (get-id pos txt) + =+ (get-id-sym pos txt) =/ back-pos ?~ backward pos @@ -343,7 +354,7 @@ [%| p.res] :- %& ~? > debug %parsed-good - ((cury tab-list-hoon sut) hoon.p.res) + ((cury tab-list-hoon sut) hoon:`pile:clay`p.res) :: :: Generators ++ tab-generators diff --git a/pkg/arvo/lib/language-server/parser.hoon b/pkg/arvo/lib/language-server/parser.hoon index 339d319721..d4f5002fb6 100644 --- a/pkg/arvo/lib/language-server/parser.hoon +++ b/pkg/arvo/lib/language-server/parser.hoon @@ -5,75 +5,59 @@ ++ pile-rule |= pax=path %- full - %+ ifix [gay gay] - %+ cook |=(pile +<) - ;~ pfix + %+ ifix + :_ gay :: parse optional /? and ignore :: - ;~ pose - (cold ~ ;~(plug fas wut gap dem gap)) - (easy ~) + ;~(plug gay (punt ;~(plug fas wut gap dem gap))) + |^ + ;~ plug + %+ cook (bake zing (list (list taut))) + %+ rune hep + (most ;~(plug com gaw) taut-rule) + :: + %+ cook (bake zing (list (list taut))) + %+ rune lus + (most ;~(plug com gaw) taut-rule) + :: + %+ rune tis + ;~(plug sym ;~(pfix gap fas (more fas urs:ab))) + :: + %+ rune cen + ;~(plug sym ;~(pfix gap ;~(pfix cen sym))) + :: + %+ rune buc + ;~ (glue gap) + sym + ;~(pfix cen sym) + ;~(pfix cen sym) == :: - ;~ plug - ;~ pose - ;~ sfix - %+ cook |=((list (list taut)) (zing +<)) - %+ more gap - ;~ pfix ;~(plug fas hep gap) - (most ;~(plug com gaw) taut-rule) - == - gap - == - (easy ~) - == - :: - ;~ pose - ;~ sfix - %+ cook |=((list (list taut)) (zing +<)) - %+ more gap - ;~ pfix ;~(plug fas lus gap) - (most ;~(plug com gaw) taut-rule) - == - gap - == - (easy ~) - == - :: - ;~ pose - ;~ sfix - %+ cook |=((list [face=term =path]) +<) - %+ more gap - ;~ pfix ;~(plug fas tis gap) - %+ cook |=([term path] +<) - ;~(plug sym ;~(pfix ;~(plug gap fas) (more fas urs:ab))) - == - gap - == - (easy ~) - == - :: - ;~ pose - ;~ sfix - %+ cook |=((list [face=term =mark =path]) +<) - %+ more gap - ;~ pfix ;~(plug fas tar gap) - %+ cook |=([term mark path] +<) - ;~ plug - sym - ;~(pfix ;~(plug gap cen) sym) - ;~(pfix ;~(plug gap fas) (more fas urs:ab)) - == - == - gap - == - (easy ~) - == - :: - %+ cook |=(huz=(list hoon) `hoon`tssg+huz) - (most gap tall:(vang & pax)) + %+ rune tar + ;~ (glue gap) + sym + ;~(pfix cen sym) + ;~(pfix fas (more fas urs:ab)) == + :: + %+ stag %tssg + (most gap tall:(vang & pax)) == + :: + ++ pant + |* fel=^rule + ;~(pose fel (easy ~)) + :: + ++ mast + |* [bus=^rule fel=^rule] + ;~(sfix (more bus fel) bus) + :: + ++ rune + |* [bus=^rule fel=^rule] + %- pant + %+ mast gap + ;~(pfix fas bus gap fel) + -- :: ++ taut-rule %+ cook |=(taut +<) diff --git a/pkg/arvo/lib/metadata.hoon b/pkg/arvo/lib/metadata.hoon index 1079b364cb..7b382c5800 100644 --- a/pkg/arvo/lib/metadata.hoon +++ b/pkg/arvo/lib/metadata.hoon @@ -13,9 +13,7 @@ =/ members ~(wyt in (members:grp rid)) =/ =metadatum:store - %- need - %+ mate (peek-metadatum %groups rid) - (peek-metadatum %graph rid) + (need (peek-metadatum %groups rid)) [rid channels members channel-count metadatum] :: ++ channels diff --git a/pkg/arvo/lib/push-hook.hoon b/pkg/arvo/lib/push-hook.hoon index 703467d738..107a9e3d2a 100644 --- a/pkg/arvo/lib/push-hook.hoon +++ b/pkg/arvo/lib/push-hook.hoon @@ -85,15 +85,15 @@ ++ take-update |~ vase *[(list card) _^|(..on-init)] - :: +should-proxy-update: should forward update to store + :: +transform-proxy-update: optionally transform update :: - :: If %.y is produced, then the update is forwarded to the local - :: store. If %.n is produced then the update is not forwarded and - :: the poke fails. + :: If ^ is produced, then the update is forwarded to the local + :: store. If ~ is produced, the update is not forwarded and the + :: poke fails. :: - ++ should-proxy-update + ++ transform-proxy-update |~ vase - *? + *(unit vase) :: +initial-watch: produce initial state for a subscription :: :: .resource is the resource being subscribed to. @@ -301,20 +301,20 @@ +* og ~(. push-hook bowl) :: ++ poke-update - |= =vase + |= vas=vase ^- (quip card:agent:gall _state) - ?> (should-proxy-update:og vase) - =/ wire - (make-wire /store) + =/ vax=(unit vase) (transform-proxy-update:og vas) + ?> ?=(^ vax) + =/ wire (make-wire /store) :_ state - [%pass wire %agent [our.bowl store-name.config] %poke update-mark.config vase]~ + [%pass wire %agent [our.bowl store-name.config] %poke update-mark.config u.vax]~ :: ++ poke-hook-action |= =action ^- (quip card:agent:gall _state) |^ ?- -.action - %add (add +.action) + %add (add +.action) %remove (remove +.action) %revoke (revoke +.action) == diff --git a/pkg/arvo/lib/settings.hoon b/pkg/arvo/lib/settings.hoon index 8730de6e83..edf13e8d4f 100644 --- a/pkg/arvo/lib/settings.hoon +++ b/pkg/arvo/lib/settings.hoon @@ -50,7 +50,7 @@ %- pairs :~ bucket-key+s+b entry-key+s+k - value+(val v) + value+(value v) == :: ++ del-entry @@ -68,6 +68,7 @@ %s val %b val %n (numb p.val) + %a [%a (turn p.val value)] == :: ++ bucket @@ -105,7 +106,7 @@ %- ot :~ bucket-key+so entry-key+so - value+val + value+value == :: ++ del-entry @@ -121,6 +122,7 @@ %s jon %b jon %n [%n (rash p.jon dem)] + %a [%a (turn p.jon value)] == :: ++ bucket diff --git a/pkg/arvo/lib/shoe.hoon b/pkg/arvo/lib/shoe.hoon index d3769ad00f..0348e10e52 100644 --- a/pkg/arvo/lib/shoe.hoon +++ b/pkg/arvo/lib/shoe.hoon @@ -291,7 +291,7 @@ ++ tab |= pos=@ud ^- (quip card _cli-state) - =+ (get-id:auto pos (tufa buf.cli-state)) + =+ (get-id-cord:auto pos (tufa buf.cli-state)) =/ needle=term (fall id %$) :: autocomplete empty command iff user at start of command diff --git a/pkg/arvo/lib/strandio.hoon b/pkg/arvo/lib/strandio.hoon index d55e8a8d7a..a8b52ede1f 100644 --- a/pkg/arvo/lib/strandio.hoon +++ b/pkg/arvo/lib/strandio.hoon @@ -442,19 +442,19 @@ ;< ~ bind:m (send-request (hiss-to-request:html hiss)) take-maybe-sigh :: -:: +build-fail: build the source file at the specified $beam +:: +build-file: build the source file at the specified $beam :: ++ build-file |= [[=ship =desk =case] =spur] =* arg +< - =/ m (strand ,vase) + =/ m (strand ,(unit vase)) ^- form:m ;< =riot:clay bind:m (warp ship desk ~ %sing %a case spur) ?~ riot - (strand-fail %build-file >arg< ~) + (pure:m ~) ?> =(%vase p.r.u.riot) - (pure:m !<(vase q.r.u.riot)) + (pure:m (some !<(vase q.r.u.riot))) :: +build-mark: build a mark definition to a $dais :: ++ build-mark @@ -468,9 +468,9 @@ (strand-fail %build-mark >arg< ~) ?> =(%dais p.r.u.riot) (pure:m !<(dais:clay q.r.u.riot)) -:: +build-cast: build a mark conversion gate ($tube) +:: +build-tube: build a mark conversion gate ($tube) :: -++ build-cast +++ build-tube |= [[=ship =desk =case] =mars:clay] =* arg +< =/ m (strand ,tube:clay) @@ -478,10 +478,37 @@ ;< =riot:clay bind:m (warp ship desk ~ %sing %c case /[a.mars]/[b.mars]) ?~ riot - (strand-fail %build-cast >arg< ~) + (strand-fail %build-tube >arg< ~) ?> =(%tube p.r.u.riot) (pure:m !<(tube:clay q.r.u.riot)) :: +:: +build-nave: build a mark definition to a $nave +:: +++ build-nave + |= [[=ship =desk =case] mak=mark] + =* arg +< + =/ m (strand ,vase) + ^- form:m + ;< =riot:clay bind:m + (warp ship desk ~ %sing %b case /[mak]) + ?~ riot + (strand-fail %build-nave >arg< ~) + ?> =(%nave p.r.u.riot) + (pure:m q.r.u.riot) +:: +build-cast: build a mark conversion gate (static) +:: +++ build-cast + |= [[=ship =desk =case] =mars:clay] + =* arg +< + =/ m (strand ,vase) + ^- form:m + ;< =riot:clay bind:m + (warp ship desk ~ %sing %f case /[a.mars]/[b.mars]) + ?~ riot + (strand-fail %build-cast >arg< ~) + ?> =(%cast p.r.u.riot) + (pure:m q.r.u.riot) +:: :: Read from Clay :: ++ warp diff --git a/pkg/arvo/mar/drum-put.hoon b/pkg/arvo/mar/drum-put.hoon index 29b6a10c34..e6094158ed 100644 --- a/pkg/arvo/mar/drum-put.hoon +++ b/pkg/arvo/mar/drum-put.hoon @@ -11,6 +11,6 @@ -- ++ grab :: convert from |% - ++ noun [path @] :: clam from %noun + +$ noun [path @] :: clam from %noun -- -- diff --git a/pkg/arvo/mar/elem.hoon b/pkg/arvo/mar/elem.hoon index c7533ad908..6ce6f6f7fe 100644 --- a/pkg/arvo/mar/elem.hoon +++ b/pkg/arvo/mar/elem.hoon @@ -6,7 +6,7 @@ =, html |_ own=manx :: -++ grad %mime +++ grad %noun ++ grow :: convert to |% ++ hymn ;html:(head body:"+{own}") :: convert to %hymn diff --git a/pkg/arvo/mar/gcp-token.hoon b/pkg/arvo/mar/gcp-token.hoon new file mode 100644 index 0000000000..3816324f23 --- /dev/null +++ b/pkg/arvo/mar/gcp-token.hoon @@ -0,0 +1,13 @@ +/+ *gcp +|_ tok=token +++ grad %noun +++ grow + |% + ++ noun tok + ++ json (token-to-json tok) + -- +++ grab + |% + ++ noun token + -- +-- diff --git a/pkg/arvo/mar/graph/validator/chat.hoon b/pkg/arvo/mar/graph/validator/chat.hoon index 344b3822de..20c9c82c1d 100644 --- a/pkg/arvo/mar/graph/validator/chat.hoon +++ b/pkg/arvo/mar/graph/validator/chat.hoon @@ -18,8 +18,14 @@ :: ++ notification-kind ?+ index.p.i ~ - [@ ~] `[%message 0 %count %.n] + [@ ~] `[%message [0 1] %count %none] == + :: + ++ transform-add-nodes + |= [=index =post =atom was-parent-modified=?] + ^- [^index ^post] + =- [- post(index -)] + [atom ~] -- ++ grab |% diff --git a/pkg/arvo/mar/graph/validator/link.hoon b/pkg/arvo/mar/graph/validator/link.hoon index 143a992586..6026867452 100644 --- a/pkg/arvo/mar/graph/validator/link.hoon +++ b/pkg/arvo/mar/graph/validator/link.hoon @@ -26,9 +26,22 @@ :: ++ notification-kind ?+ index.p.i ~ - [@ ~] `[%link 0 %each %.y] - [@ @ %1 ~] `[%comment 1 %count %.n] - [@ @ @ ~] `[%edit-comment 1 %none %.n] + [@ ~] `[%link [0 1] %each %children] + [@ @ %1 ~] `[%comment [1 2] %count %siblings] + [@ @ @ ~] `[%edit-comment [1 2] %none %none] + == + :: + ++ transform-add-nodes + |= [=index =post =atom was-parent-modified=?] + ^- [^index ^post] + =- [- post(index -)] + ?+ index ~|(transform+[index post] !!) + [@ ~] [atom ~] + [@ @ ~] [i.index atom ~] + [@ @ @ ~] + ?: was-parent-modified + [i.index atom i.t.t.index ~] + index == -- ++ grab diff --git a/pkg/arvo/mar/graph/validator/publish.hoon b/pkg/arvo/mar/graph/validator/publish.hoon index 62fc8c466b..f888d06514 100644 --- a/pkg/arvo/mar/graph/validator/publish.hoon +++ b/pkg/arvo/mar/graph/validator/publish.hoon @@ -25,10 +25,31 @@ :: ++ notification-kind ?+ index.p.i ~ - [@ %1 %1 ~] `[%note 0 %each %.n] - [@ %1 @ ~] `[%edit-note 0 %none %.n] - [@ %2 @ %1 ~] `[%comment 1 %count %.n] - [@ %2 @ @ ~] `[%edit-comment 1 %none %.n] + [@ %1 %1 ~] `[%note [0 1] %each %children] + [@ %1 @ ~] `[%edit-note [0 1] %none %none] + [@ %2 @ %1 ~] `[%comment [1 3] %count %siblings] + [@ %2 @ @ ~] `[%edit-comment [1 3] %none %none] + == + :: + ++ transform-add-nodes + |= [=index =post =atom was-parent-modified=?] + ^- [^index ^post] + =- [- post(index -)] + ?+ index ~|(transform+[index post] !!) + [@ ~] [atom ~] + [@ %1 ~] [atom %1 ~] + :: + [@ %1 @ ~] + ?: was-parent-modified + [atom %1 i.t.t.index ~] + index + :: + [@ %2 ~] [atom %2 ~] + [@ %2 @ ~] [i.index %2 atom ~] + [@ %2 @ @ ~] + ?: was-parent-modified + [i.index %2 atom i.t.t.t.index ~] + index == -- ++ grab diff --git a/pkg/arvo/mar/graph/view-action.hoon b/pkg/arvo/mar/graph/view-action.hoon index ab8b0ef12e..f60413106d 100644 --- a/pkg/arvo/mar/graph/view-action.hoon +++ b/pkg/arvo/mar/graph/view-action.hoon @@ -4,6 +4,7 @@ ++ grow |% ++ noun act + ++ json (action:enjs act) -- ++ grab |% diff --git a/pkg/arvo/mar/helm-hi.hoon b/pkg/arvo/mar/helm-hi.hoon index 85c617f615..e1d586411b 100644 --- a/pkg/arvo/mar/helm-hi.hoon +++ b/pkg/arvo/mar/helm-hi.hoon @@ -6,7 +6,7 @@ =, format |_ txt=cord :: -++ grad %mime +++ grad %noun ++ grab :: convert from |% ++ noun @t :: clam from %noun diff --git a/pkg/arvo/mar/html.hoon b/pkg/arvo/mar/html.hoon index 4e161fae16..203d75f612 100644 --- a/pkg/arvo/mar/html.hoon +++ b/pkg/arvo/mar/html.hoon @@ -6,7 +6,6 @@ :::: compute :: =, html -^| |_ htm=@t ++ grow :: convert to ^? diff --git a/pkg/arvo/mar/hymn.hoon b/pkg/arvo/mar/hymn.hoon index d8ad099697..cf01508eae 100644 --- a/pkg/arvo/mar/hymn.hoon +++ b/pkg/arvo/mar/hymn.hoon @@ -6,7 +6,7 @@ =, html |_ own=manx :: -++ grad %mime +++ grad %noun ++ grow :: convert to |% ++ html (crip (en-xml own)) :: convert to %html diff --git a/pkg/arvo/mar/language-server/rpc/notification.hoon b/pkg/arvo/mar/language-server/rpc/notification.hoon index 88fdaa8c1b..d464da489a 100644 --- a/pkg/arvo/mar/language-server/rpc/notification.hoon +++ b/pkg/arvo/mar/language-server/rpc/notification.hoon @@ -4,7 +4,7 @@ ++ grad %noun ++ grab |% - ++ noun not + ++ noun all:notification ++ json |= jon=^json (notification:dejs:lsp-json jon) diff --git a/pkg/arvo/mar/language-server/rpc/request.hoon b/pkg/arvo/mar/language-server/rpc/request.hoon index 48db6b7eae..989a6f41c4 100644 --- a/pkg/arvo/mar/language-server/rpc/request.hoon +++ b/pkg/arvo/mar/language-server/rpc/request.hoon @@ -8,7 +8,7 @@ -- ++ grab |% - ++ noun req + ++ noun all:request ++ json |= jon=^json (request:dejs:lsp-json jon) diff --git a/pkg/arvo/mar/mime.hoon b/pkg/arvo/mar/mime.hoon index 87e920250b..83b4daeb5e 100644 --- a/pkg/arvo/mar/mime.hoon +++ b/pkg/arvo/mar/mime.hoon @@ -3,7 +3,6 @@ :: /? 310 :: -^| |_ own=mime ++ grow ^? @@ -14,7 +13,7 @@ ++ grab :: convert from ^? |% - +$ noun mime :: clam from %noun + ++ noun mime :: clam from %noun ++ tape |=(a=_"" [/application/x-urb-unknown (as-octt:mimes:html a)]) -- @@ -25,5 +24,9 @@ ++ diff |=(mime +<) ++ pact |=(mime +<) ++ join |=([mime mime] `(unit mime)`~) + ++ mash + |= [[ship desk mime] [ship desk mime]] + ^- mime + ~|(%mime-mash !!) -- -- diff --git a/pkg/arvo/mar/noun.hoon b/pkg/arvo/mar/noun.hoon index fb07674981..5c798d3c2a 100644 --- a/pkg/arvo/mar/noun.hoon +++ b/pkg/arvo/mar/noun.hoon @@ -14,5 +14,6 @@ ++ diff |=(* +<) ++ pact |=(* +<) ++ join |=([* *] *(unit *)) + ++ mash |=([[ship desk *] [ship desk *]] `*`~|(%noun-mash !!)) -- -- diff --git a/pkg/arvo/mar/pill.hoon b/pkg/arvo/mar/pill.hoon index 6d69bd167f..f1e1ffa739 100644 --- a/pkg/arvo/mar/pill.hoon +++ b/pkg/arvo/mar/pill.hoon @@ -15,7 +15,7 @@ ++ mime |= (pair mite octs) =+ o=(pair ,* ,*) :: ,*) - =+ (,[boot-ova=* kernel-ova=(list o) userspace-ova=(list o)] (cue q.q)) + =+ (,[%pill nam=term boot-ova=(list) kernel-ova=(list o) userspace-ova=(list o)] (cue q.q)) =/ convert |= ova=(list o) ^- (list unix-event) @@ -30,7 +30,7 @@ :: =/ boot-ova (convert boot-ova) =/ kernel-ova (convert kernel-ova) =/ userspace-ova (convert userspace-ova) - [boot-ova kernel-ova userspace-ova] + [%pill nam boot-ova kernel-ova userspace-ova] -- ++ grad %mime -- diff --git a/pkg/arvo/mar/png.hoon b/pkg/arvo/mar/png.hoon index e68e502cd1..6a60a6a27b 100644 --- a/pkg/arvo/mar/png.hoon +++ b/pkg/arvo/mar/png.hoon @@ -1,4 +1,4 @@ -|_ dat=@t +|_ dat=@ ++ grow |% ++ mime [/image/png (as-octs:mimes:html dat)] @@ -6,7 +6,7 @@ ++ grab |% ++ mime |=([p=mite q=octs] q.q) - ++ noun @t + ++ noun @ -- ++ grad %mime -- diff --git a/pkg/arvo/mar/snip.hoon b/pkg/arvo/mar/snip.hoon index 205348f495..5760a7bb5e 100644 --- a/pkg/arvo/mar/snip.hoon +++ b/pkg/arvo/mar/snip.hoon @@ -43,7 +43,7 @@ :: =, mimes:html |_ [hed=marl tal=marl] -++ grad %mime +++ grad %noun :: ++ grow :: convert to |% @@ -55,6 +55,7 @@ ++ html (crip (en-xml hymn)) :: convert to %html ++ mime [/text/html (as-octs html)] :: convert to %mime -- + ++ noun [hed tal] -- ++ grab |% :: convert from ++ noun ,[marl marl] :: clam from %noun diff --git a/pkg/arvo/mar/urb.hoon b/pkg/arvo/mar/urb.hoon index 85686ad6ef..49545e73f5 100644 --- a/pkg/arvo/mar/urb.hoon +++ b/pkg/arvo/mar/urb.hoon @@ -6,7 +6,7 @@ =, html |_ own=manx :: -++ grad %mime +++ grad %noun ++ grow :: convert to |% ++ hymn ;html:(head body:"+{own}") :: convert to %hymn diff --git a/pkg/arvo/sur/gcp.hoon b/pkg/arvo/sur/gcp.hoon new file mode 100644 index 0000000000..268ba4d1e8 --- /dev/null +++ b/pkg/arvo/sur/gcp.hoon @@ -0,0 +1,6 @@ +|% ++$ token + $: access-key=@t + expires-in=@dr + == +-- diff --git a/pkg/arvo/sur/graph-view.hoon b/pkg/arvo/sur/graph-view.hoon index 27caaca7f7..939ac433a7 100644 --- a/pkg/arvo/sur/graph-view.hoon +++ b/pkg/arvo/sur/graph-view.hoon @@ -42,6 +42,7 @@ [%groupify rid=resource to=(unit resource)] [%forward rid=resource =update:store] [%eval =cord] + [%pending-indices pending=(map hash:store index:store)] == -- diff --git a/pkg/arvo/sur/hark-graph-hook.hoon b/pkg/arvo/sur/hark-graph-hook.hoon index 58f42b3ac5..8598acc2f1 100644 --- a/pkg/arvo/sur/hark-graph-hook.hoon +++ b/pkg/arvo/sur/hark-graph-hook.hoon @@ -1,6 +1,17 @@ /- *resource, graph-store, post ^? |% +:: ++$ mode ?(%each %count %none) +:: ++$ watch-for ?(%siblings %children %none) +:: ++$ index-len + [parent=@ud self=@ud] +:: ++$ notif-kind + [name=@t =index-len =mode =watch-for] +:: +$ action $% [?(%listen %ignore) graph=resource =index:post] diff --git a/pkg/arvo/sur/settings.hoon b/pkg/arvo/sur/settings.hoon index c2cf521ce6..67a071f7da 100644 --- a/pkg/arvo/sur/settings.hoon +++ b/pkg/arvo/sur/settings.hoon @@ -1,11 +1,21 @@ |% ++$ settings-0 (map key bucket-0) ++$ bucket-0 (map key val-0) ++$ val-0 + $% [%s p=@t] + [%b p=?] + [%n p=@] + == +:: +$ settings (map key bucket) +$ bucket (map key val) +$ key term +$ val + $~ [%n 0] $% [%s p=@t] [%b p=?] [%n p=@] + [%a p=(list val)] == +$ event $% [%put-bucket =key =bucket] diff --git a/pkg/arvo/sys/lull.hoon b/pkg/arvo/sys/lull.hoon index 5323878c7a..9a1bc4c4cf 100644 --- a/pkg/arvo/sys/lull.hoon +++ b/pkg/arvo/sys/lull.hoon @@ -796,7 +796,7 @@ $: face=(unit term) file-path=term == - +$ care ?(%a %b %c %d %p %r %s %t %u %v %w %x %y %z) :: clay submode + +$ care ?(%a %b %c %d %e %f %p %r %s %t %u %v %w %x %y %z) :: clay submode +$ case :: ship desk case spur $% [%da p=@da] :: date [%tas p=@tas] :: label @@ -928,12 +928,16 @@ :: /- sur-file :: surface imports from /sur :: /+ lib-file :: library imports from /lib :: /= face /path :: imports built hoon file at path + :: /% face %mark :: imports mark definition from /mar + :: /$ face %from %to :: imports mark converter from /mar :: /* face %mark /path :: unbuilt file imports, as mark :: +$ pile $: sur=(list taut) lib=(list taut) raw=(list [face=term =path]) + maz=(list [face=term =mark]) + caz=(list [face=term =mars]) bar=(list [face=term =mark =path]) =hoon == @@ -942,9 +946,25 @@ +$ taut [face=(unit term) pax=term] :: $mars: mark conversion request :: $tube: mark conversion gate + :: $nave: typed mark core :: +$ mars [a=mark b=mark] +$ tube $-(vase vase) + ++ nave + |$ [typ dif] + $_ + ^? + |% + ++ bunt *typ + ++ diff |~([old=typ new=typ] *dif) + ++ form *mark + ++ join |~([a=dif b=dif] *(unit (unit dif))) + ++ mash + |~ [a=[ship desk dif] b=[ship desk dif]] + *(unit dif) + ++ pact |~([typ dif] *typ) + ++ vale |~(noun *typ) + -- :: $dais: processed mark core :: +$ dais @@ -959,7 +979,6 @@ *(unit vase) ++ pact |~(diff=vase sam) ++ vale |~(noun sam) - ++ volt |~(noun sam) -- :: ++ get-fit diff --git a/pkg/arvo/sys/vane/ames.hoon b/pkg/arvo/sys/vane/ames.hoon index ad799da549..bd267477e4 100644 --- a/pkg/arvo/sys/vane/ames.hoon +++ b/pkg/arvo/sys/vane/ames.hoon @@ -829,7 +829,7 @@ :: lifecycle arms; mostly pass-throughs to the contained adult ames :: ++ scry scry:adult-core - ++ stay [%4 %larva queued-events ames-state.adult-gate] + ++ stay [%5 %larva queued-events ames-state.adult-gate] ++ load |= $= old $% $: %4 @@ -839,6 +839,13 @@ == [%adult state=_ames-state.adult-gate] == == + $: %5 + $% $: %larva + events=(qeu queued-event) + state=_ames-state.adult-gate + == + [%adult state=_ames-state.adult-gate] + == == == ?- old [%4 %adult *] (load:adult-core %4 state.old) @@ -848,6 +855,14 @@ =. queued-events events.old =. adult-gate (load:adult-core %4 state.old) larval-gate + :: + [%5 %adult *] (load:adult-core %5 state.old) + :: + [%5 %larva *] + ~> %slog.1^leaf/"ames: larva: load" + =. queued-events events.old + =. adult-gate (load:adult-core %5 state.old) + larval-gate == -- :: adult ames, after metamorphosis from larva @@ -919,13 +934,38 @@ [moves ames-gate] :: +stay: extract state before reload :: -++ stay [%4 %adult ames-state] +++ stay [%5 %adult ames-state] :: +load: load in old state after reload :: ++ load - |= old-state=[%4 ^ames-state] + |= $= old-state + $% [%4 ^ames-state] + [%5 ^ames-state] + == + |^ ^+ ames-gate + =? old-state ?=(%4 -.old-state) %5^(state-4-to-5 +.old-state) + :: + ?> ?=(%5 -.old-state) ames-gate(ames-state +.old-state) + :: + ++ state-4-to-5 + |= =^ames-state + ^- ^^ames-state + =. peers.ames-state + %- ~(run by peers.ames-state) + |= =ship-state + ?. ?=(%known -.ship-state) + ship-state + =. snd.ship-state + %- ~(run by snd.ship-state) + |= =message-pump-state + =. num-live.metrics.packet-pump-state.message-pump-state + ~(wyt in live.packet-pump-state.message-pump-state) + message-pump-state + ship-state + ames-state + -- :: +scry: dereference namespace :: ++ scry @@ -1217,7 +1257,7 @@ on-hear-forward :: ?: ?& ?=(%pawn (clan:title sndr.packet)) - !(~(has by peers.ames-state) sndr.packet) + !?=([~ %known *] (~(get by peers.ames-state) sndr.packet)) == on-hear-open on-hear-shut @@ -1289,14 +1329,9 @@ |= [=lane =packet dud=(unit goof)] ^+ event-core =/ sndr-state (~(get by peers.ames-state) sndr.packet) - :: if we don't know them, maybe enqueue a jael %public-keys request - :: - :: Ignore encrypted packets from alien comets. - :: TODO: maybe crash? + :: if we don't know them, ask jael for their keys and enqueue :: ?. ?=([~ %known *] sndr-state) - ?: =(%pawn (clan:title sndr.packet)) - event-core (enqueue-alien-todo sndr.packet |=(alien-agenda +<)) :: decrypt packet contents using symmetric-key.channel :: @@ -1909,6 +1944,11 @@ =/ =bone bone.shut-packet :: ?: ?=(%& -.meat.shut-packet) + =+ ?~ dud ~ + %. ~ + %+ slog + leaf+"ames: {} fragment crashed {}" + ?.(msg.veb ~ tang.u.dud) (run-message-sink bone %hear lane shut-packet ?=(~ dud)) :: Just try again on error, printing trace :: @@ -1917,7 +1957,10 @@ :: =+ ?~ dud ~ %. ~ - (slog leaf+"ames: crashed on message ack" >mote.u.dud< tang.u.dud) + %+ slog leaf+"ames: {} ack crashed {}" + ?. msg.veb ~ + :- >[bone=bone message-num=message-num meat=meat]:shut-packet< + tang.u.dud (run-message-pump bone %hear [message-num +.meat]:shut-packet) :: +on-memo: handle request to send message :: @@ -2203,12 +2246,15 @@ ?. ?=([%hear * * ok=%.n] task) :: fresh boon; give message to client vane :: - %- (trace msg.veb |.("boon {}")) + %- %+ trace msg.veb + =/ dat [her.channel bone=bone message-num=message-num -.task] + |.("sink boon {}") peer-core :: we previously crashed on this message; notify client vane :: %- %+ trace msg.veb - |.("crashed on boon {}") + =/ dat [her.channel bone=bone message-num=message-num -.task] + |.("crashed on sink boon {}") boon-to-lost :: +boon-to-lost: convert all boons to losts :: @@ -2226,7 +2272,9 @@ ++ on-sink-nack-trace |= [=message-num message=*] ^+ peer-core - %- (trace msg.veb |.("nack trace {}")) + %- %+ trace msg.veb + =/ dat [her.channel bone=bone message-num=message-num] + |.("sink naxplanation {}") :: =+ ;; =naxplanation message :: ack nack-trace message (only applied if we don't later crash) @@ -2243,7 +2291,9 @@ ++ on-sink-plea |= [=message-num message=*] ^+ peer-core - %- (trace msg.veb |.("plea {}")) + %- %+ trace msg.veb + =/ dat [her.channel bone=bone message-num=message-num] + |.("sink plea {}") :: is this the first time we're trying to process this message? :: ?. ?=([%hear * * ok=%.n] task) diff --git a/pkg/arvo/sys/vane/clay.hoon b/pkg/arvo/sys/vane/clay.hoon index 638827a180..af2f653037 100644 --- a/pkg/arvo/sys/vane/clay.hoon +++ b/pkg/arvo/sys/vane/clay.hoon @@ -118,9 +118,11 @@ :: Ford cache :: +$ ford-cache - $: vases=(map path [res=vase dez=(set path)]) + $: files=(map path [res=vase dez=(set path)]) + naves=(map mark [res=vase dez=(set path)]) marks=(map mark [res=dais dez=(set path)]) - casts=(map mars [res=tube dez=(set path)]) + casts=(map mars [res=vase dez=(set path)]) + tubes=(map mars [res=tube dez=(set path)]) == :: $reef-cache: built system files :: @@ -462,7 +464,9 @@ +$ build $% [%file =path] [%mark =mark] + [%dais =mark] [%cast =mars] + [%tube =mars] [%vale =path] == +$ state @@ -494,8 +498,9 @@ =? stack.nub ?=(^ stack.nub) stack.nub(i (~(uni in i.stack.nub) top)) [top stack.nub] + :: +read-file: retrieve marked, validated file contents at path :: - ++ get-value + ++ read-file |= =path ^- [cage state] ~| %error-validating^path @@ -519,13 +524,13 @@ ?< (~(has in deletes) path) ~| %file-not-found^path :_(nub (need (~(get an ankh) path))) - :: +get-mark: build a mark definition + :: +build-nave: build a statically typed mark core :: - ++ get-mark + ++ build-nave |= mak=mark - ^- [dais state] + ^- [vase state] ~| %error-building-mark^mak - ?^ got=(~(get by marks.cache.nub) mak) + ?^ got=(~(get by naves.cache.nub) mak) =? stack.nub ?=(^ stack.nub) stack.nub(i (~(uni in i.stack.nub) dez.u.got)) [res.u.got nub] @@ -533,99 +538,128 @@ ~|(cycle+mark+mak^stack.nub !!) =. cycle.nub (~(put in cycle.nub) mark+mak) =. stack.nub [~ stack.nub] + =; res=[=vase nub=state] + =. nub nub.res + =^ top stack.nub pop-stack + =. naves.cache.nub (~(put by naves.cache.nub) mak [vase.res top]) + [vase.res nub] + =^ cor=vase nub (build-fit %mar mak) + =/ gad=vase (slap cor limb/%grad) + ?@ q.gad + =+ !<(mok=mark gad) + =^ deg=vase nub $(mak mok) + =^ tub=vase nub (build-cast mak mok) + =^ but=vase nub (build-cast mok mak) + :_ nub + ^- vase :: vase of nave + %+ slap + (with-faces deg+deg tub+tub but+but cor+cor nave+!>(nave) ~) + !, *hoon + =/ typ _+<.cor + =/ dif diff:deg + ^- (nave typ dif) + |% + ++ bunt +<.cor + ++ diff + |= [old=typ new=typ] + ^- dif + (diff:deg (tub old) (tub new)) + ++ form form:deg + ++ join join:deg + ++ mash mash:deg + ++ pact + |= [v=typ d=dif] + ^- typ + (but (pact:deg (tub v) d)) + ++ vale noun:grab:cor + -- + :_ nub + ^- vase :: vase of nave + %+ slap (slop (with-face cor+cor) bud) + !, *hoon + =/ typ _+<.cor + =/ dif _*diff:grad:cor + ^- (nave:clay typ dif) + |% + ++ bunt +<.cor + ++ diff |=([old=typ new=typ] (diff:~(grad cor old) new)) + ++ form form:grad:cor + ++ join + |= [a=dif b=dif] + ^- (unit (unit dif)) + ?: =(a b) + ~ + `(join:grad:cor a b) + ++ mash + |= [a=[=ship =desk =dif] b=[=ship =desk =dif]] + ^- (unit dif) + ?: =(dif.a dif.b) + ~ + `(mash:grad:cor a b) + ++ pact |=([v=typ d=dif] (pact:~(grad cor v) d)) + ++ vale noun:grab:cor + -- + :: +build-dais: build a dynamically typed mark definition + :: + ++ build-dais + |= mak=mark + ^- [dais state] + ~| %error-building-dais^mak + ?^ got=(~(get by marks.cache.nub) mak) + =? stack.nub ?=(^ stack.nub) + stack.nub(i (~(uni in i.stack.nub) dez.u.got)) + [res.u.got nub] + ?: (~(has in cycle.nub) dais+mak) + ~|(cycle+dais+mak^stack.nub !!) + =. cycle.nub (~(put in cycle.nub) dais+mak) + =. stack.nub [~ stack.nub] =; res=[=dais nub=state] =. nub nub.res =^ top stack.nub pop-stack =. marks.cache.nub (~(put by marks.cache.nub) mak [dais.res top]) [dais.res nub] - =^ cor=vase nub (build-fit %mar mak) - =/ gad=vase (slap cor %limb %grad) - ?@ q.gad - =+ !<(mok=mark gad) - =^ deg=dais nub $(mak mok) - =^ tub=tube nub (get-cast mak mok) - =^ but=tube nub (get-cast mok mak) - :_ nub - ^- dais - |_ sam=vase - ++ bunt (slap cor $+6) - ++ diff - |= new=vase - ^- vase - (~(diff deg (tub sam)) (tub new)) - ++ form form:deg - ++ join join:deg - ++ mash mash:deg - ++ pact - |= diff=vase - ^+ sam - (but (~(pact deg (tub sam)) diff)) - ++ vale - |= =noun - ^+ sam - (slam (slap cor !,(*hoon noun:grab)) !>(noun)) - ++ volt - |= =noun - ^+ sam - [p:bunt noun] - -- + =^ nav=vase nub (build-nave mak) :_ nub - =+ !<(fom=mark (slap gad %limb %form)) ^- dais |_ sam=vase - ++ bunt (slap cor $+6) + ++ bunt (slap nav limb/%bunt) ++ diff |= new=vase - ^- vase - %+ slap - (with-faces cor+cor sam+sam new+new ~) - !, *hoon - (diff:~(grad cor sam) new) - ++ form fom + (slam (slap nav limb/%diff) (slop sam new)) + ++ form !<(mark (slap nav limb/%form)) ++ join |= [a=vase b=vase] ^- (unit (unit vase)) - ?: =(q.a q.b) - ~ - =; res `?~(q.res ~ `(slap res !,(*hoon ?~(. !! u)))) - (slam (slap cor !,(*hoon join:grad)) (slop a b)) + =/ res=vase (slam (slap nav limb/%join) (slop a b)) + ?~ q.res ~ + ?~ +.q.res [~ ~] + ``(slap res !,(*hoon ?>(?=([~ ~ *] .) u.u))) ++ mash |= [a=[=ship =desk diff=vase] b=[=ship =desk diff=vase]] ^- (unit vase) - ?: =(q.diff.a q.diff.b) + =/ res=vase + %+ slam (slap nav limb/%mash) + %+ slop + :(slop !>(ship.a) !>(desk.a) diff.a) + :(slop !>(ship.b) !>(desk.b) diff.b) + ?~ q.res ~ - :- ~ - %+ slam (slap cor !,(*hoon mash:grad)) - %+ slop - :(slop !>(ship.a) !>(desk.a) diff.a) - :(slop !>(ship.b) !>(desk.b) diff.b) + `(slap res !,(*hoon ?>((^ .) u))) ++ pact |= diff=vase - ^+ sam - %+ slap - (with-faces cor+cor sam+sam diff+diff ~) - !, *hoon - (pact:~(grad cor sam) diff) + (slam (slap nav limb/%pact) (slop sam diff)) ++ vale |= =noun - ^+ sam - (slam (slap cor !,(*hoon noun:grab)) !>(noun)) - ++ volt - |= =noun - ^+ sam - [p:bunt noun] + (slam (slap nav limb/%vale) noun/noun) -- - :: +get-cast: produce a $tube mark conversion gate from .a to .b + :: +build-cast: produce gate to convert mark .a to, statically typed :: - ++ get-cast + ++ build-cast |= [a=mark b=mark] - ^- [tube state] + ^- [vase state] ~| error-building-cast+[a b] ?: =([%mime %hoon] [a b]) - :_ nub - |= sam=vase - =+ !<(=mime sam) - !>(q.q.mime) + :_(nub !>(|=(m=mime q.q.m))) ?^ got=(~(get by casts.cache.nub) [a b]) =? stack.nub ?=(^ stack.nub) stack.nub(i (~(uni in i.stack.nub) dez.u.got)) @@ -633,11 +667,11 @@ ?: (~(has in cycle.nub) cast+[a b]) ~|(cycle+cast+[a b]^stack.nub !!) =. stack.nub [~ stack.nub] - =; res=[=tube nub=state] + =; res=[=vase nub=state] =. nub nub.res =^ top stack.nub pop-stack - =. casts.cache.nub (~(put by casts.cache.nub) [a b] [tube.res top]) - [tube.res nub] + =. casts.cache.nub (~(put by casts.cache.nub) [a b] [vase.res top]) + [vase.res nub] :: try +grow; is there a +grow core with a .b arm? :: =^ old=vase nub (build-fit %mar a) @@ -649,47 +683,57 @@ :: +grow core has .b arm; use that :: :_ nub - ^- tube - |= sam=vase - ^- vase - %+ slap - (with-faces old+old sam+sam ~) - :+ %sgzp !,(*hoon old=old) - :+ %sgzp !,(*hoon sam=sam) - :+ %tsgl [%limb b] - !, *hoon - ~(grow old sam) + %+ slap (with-faces cor+old ~) + ^- hoon + :+ %brcl !,(*hoon v=+<.cor) + :+ %tsgl limb/b + !,(*hoon ~(grow cor v)) :: try direct +grab :: =^ new=vase nub (build-fit %mar b) - =/ rab - %- mule |. - %+ slap new - :+ %tsgl [%limb a] - [%limb %grab] + =/ rab (mule |.((slap new tsgl/[limb/a limb/%grab]))) ?: &(?=(%& -.rab) ?=(^ q.p.rab)) - :_(nub |=(sam=vase ~|([%grab a b] (slam p.rab sam)))) + :_(nub p.rab) :: try +jump :: - =/ jum - %- mule |. - %+ slap old - :+ %tsgl [%limb b] - [%limb %jump] + =/ jum (mule |.((slap old tsgl/[limb/b limb/%jump]))) ?: ?=(%& -.jum) (compose-casts a !<(mark p.jum) b) - :: try indirect +grab - :: ?: ?=(%& -.rab) (compose-casts a !<(mark p.rab) b) + ?: ?=(%noun b) + :_(nub !>(|=(* +<))) ~|(no-cast-from+[a b] !!) :: ++ compose-casts |= [x=mark y=mark z=mark] + ^- [vase state] + =^ uno=vase nub (build-cast x y) + =^ dos=vase nub (build-cast y z) + :_ nub + %+ slap + (with-faces uno+uno dos+dos cork+!>(cork) ~) + !,(*hoon (cork uno dos)) + :: +build-tube: produce a $tube mark conversion gate from .a to .b + :: + ++ build-tube + |= [a=mark b=mark] ^- [tube state] - =^ uno=tube nub (get-cast x y) - =^ dos=tube nub (get-cast y z) - :_(nub |=(sam=vase (dos (uno sam)))) + ~| error-building-tube+[a b] + ?^ got=(~(get by tubes.cache.nub) [a b]) + =? stack.nub ?=(^ stack.nub) + stack.nub(i (~(uni in i.stack.nub) dez.u.got)) + [res.u.got nub] + ?: (~(has in cycle.nub) tube+[a b]) + ~|(cycle+tube+[a b]^stack.nub !!) + =. stack.nub [~ stack.nub] + =; res=[=tube nub=state] + =. nub nub.res + =^ top stack.nub pop-stack + =. tubes.cache.nub (~(put by tubes.cache.nub) [a b] [tube.res top]) + [tube.res nub] + =^ gat=vase nub (build-cast a b) + :_(nub |=(v=vase (slam gat v))) :: ++ lobe-to-page |= =lobe @@ -713,7 +757,7 @@ ?: =(mak p.page) (page-to-cage page) =^ [mark vax=vase] nub (page-to-cage page) - =^ =tube nub (get-cast p.page mak) + =^ =tube nub (build-tube p.page mak) :_(nub [mak (tube vax)]) :: ++ page-to-cage @@ -723,7 +767,7 @@ :_(nub [%hoon -:!>(*@t) q.page]) ?: =(%mime p.page) :_(nub [%mime !>(;;(mime q.page))]) - =^ =dais nub (get-mark p.page) + =^ =dais nub (build-dais p.page) :_(nub [p.page (vale:dais q.page)]) :: ++ cast-path @@ -731,10 +775,10 @@ ^- [cage state] =/ mok (head (flop path)) ~| error-casting-path+[path mok mak] - =^ cag=cage nub (get-value path) + =^ cag=cage nub (read-file path) ?: =(mok mak) [cag nub] - =^ =tube nub (get-cast mok mak) + =^ =tube nub (build-tube mok mak) ~| error-running-cast+[path mok mak] :_(nub [mak (tube q.cag)]) :: @@ -746,14 +790,14 @@ =+ ;;(dif=(urge cord) q.diff) =/ new=@t (of-wain:format (lurk:differ txt dif)) :_(nub [%hoon !>(new)]) - =^ dys=dais nub (get-mark p.old) - =^ syd=dais nub (get-mark p.diff) + =^ dys=dais nub (build-dais p.old) + =^ syd=dais nub (build-dais p.diff) :_(nub [p.old (~(pact dys (vale:dys q.old)) (vale:syd q.diff))]) :: ++ prelude |= =path ^- vase - =^ cag=cage nub (get-value path) + =^ cag=cage nub (read-file path) ?> =(%hoon p.cag) =/ tex=tape (trip !<(@t q.cag)) =/ =pile (parse-pile path tex) @@ -765,7 +809,7 @@ |= =path ^- [vase state] ~| %error-building^path - ?^ got=(~(get by vases.cache.nub) path) + ?^ got=(~(get by files.cache.nub) path) =? stack.nub ?=(^ stack.nub) stack.nub(i (~(uni in i.stack.nub) dez.u.got)) [res.u.got nub] @@ -773,13 +817,13 @@ ~|(cycle+file+path^stack.nub !!) =. cycle.nub (~(put in cycle.nub) file+path) =. stack.nub [(sy path ~) stack.nub] - =^ cag=cage nub (get-value path) + =^ cag=cage nub (read-file path) ?> =(%hoon p.cag) =/ tex=tape (trip !<(@t q.cag)) =/ =pile (parse-pile path tex) =^ res=vase nub (run-pile pile) =^ top stack.nub pop-stack - =. vases.cache.nub (~(put by vases.cache.nub) path [res top]) + =. files.cache.nub (~(put by files.cache.nub) path [res top]) [res nub] :: ++ run-pile @@ -787,6 +831,8 @@ =^ sut=vase nub (run-tauts bud %sur sur.pile) =^ sut=vase nub (run-tauts sut %lib lib.pile) =^ sut=vase nub (run-raw sut raw.pile) + =^ sut=vase nub (run-maz sut maz.pile) + =^ sut=vase nub (run-caz sut caz.pile) =^ sut=vase nub (run-bar sut bar.pile) =/ res=vase (road |.((slap sut hoon.pile))) [res nub] @@ -824,6 +870,16 @@ :: %+ rune tis ;~(plug sym ;~(pfix gap fas (more fas urs:ab))) + :: + %+ rune cen + ;~(plug sym ;~(pfix gap ;~(pfix cen sym))) + :: + %+ rune buc + ;~ (glue gap) + sym + ;~(pfix cen sym) + ;~(pfix cen sym) + == :: %+ rune tar ;~ (glue gap) @@ -875,6 +931,22 @@ =. p.pin [%face face.i.raw p.pin] $(sut (slop pin sut), raw t.raw) :: + ++ run-maz + |= [sut=vase maz=(list [face=term =mark])] + ^- [vase state] + ?~ maz [sut nub] + =^ pin=vase nub (build-nave mark.i.maz) + =. p.pin [%face face.i.maz p.pin] + $(sut (slop pin sut), maz t.maz) + :: + ++ run-caz + |= [sut=vase caz=(list [face=term =mars])] + ^- [vase state] + ?~ caz [sut nub] + =^ pin=vase nub (build-cast mars.i.caz) + =. p.pin [%face face.i.caz p.pin] + $(sut (slop pin sut), caz t.caz) + :: ++ run-bar |= [sut=vase bar=(list [face=term =mark =path])] ^- [vase state] @@ -1501,9 +1573,11 @@ %+ turn (tail (spud pux)) :: lose leading '/' |=(c=@tD `@tD`?:(=('/' c) '-' c)) :: convert '/' to '-' :: - :* ((invalidate path vase) vases.ford-cache invalid) + :* ((invalidate path vase) files.ford-cache invalid) + ((invalidate mark vase) naves.ford-cache invalid) ((invalidate mark dais) marks.ford-cache invalid) - ((invalidate mars tube) casts.ford-cache invalid) + ((invalidate mars vase) casts.ford-cache invalid) + ((invalidate mars tube) tubes.ford-cache invalid) == :: ++ invalidate @@ -1614,24 +1688,26 @@ :: ++ checkout-changes |= [=ford=args:ford:fusion changes=(map path (each page lobe))] - =/ cans=(list [=path change=(each page lobe)]) ~(tap by changes) - |- ^- [(map path [=lobe =cage]) ford-cache] - ?~ cans - [~ ford-cache.ford-args] + ^- [(map path [=lobe =cage]) ford-cache] + %+ roll `(list [path (each page lobe)])`~(tap by changes) + |= $: [=path change=(each page lobe)] + [built=(map path [lobe cage]) cache=_ford-cache.ford-args] + == + ^+ [built cache] + =. ford-cache.ford-args cache =^ cage ford-cache.ford-args - :: ~> %slog.[0 leaf+"clay: validating {(spud path.i.cans)}"] + :: ~> %slog.[0 leaf/"clay: validating {(spud path)}"] %- wrap:fusion - (get-value:(ford:fusion ford-args) path.i.cans) + (read-file:(ford:fusion ford-args) path) =/ =lobe - ?- -.change.i.cans - %| p.change.i.cans + ?- -.change + %| p.change :: Don't use p.change.i.cans because that's before casting to :: the correct mark. :: %& (page-to-lobe [p q.q]:cage) == - =^ so-far ford-cache.ford-args $(cans t.cans) - [(~(put by so-far) path.i.cans lobe cage) ford-cache.ford-args] + [(~(put by built) path [lobe cage]) ford-cache.ford-args] :: :: Update ankh :: @@ -2209,7 +2285,7 @@ ^- dais =^ =dais fod.dom %- wrap:fusion - (get-mark:(ford:fusion static-ford-args) mark) + (build-dais:(ford:fusion static-ford-args) mark) dais :: :: Diff two files on bob-desk @@ -2639,6 +2715,8 @@ %b ~| %i-guess-you-ought-to-build-your-own-marks !! %c ~| %casts-should-be-compiled-on-your-own-ship !! %d ~| %totally-temporary-error-please-replace-me !! + %e ~| %yes-naves-also-shouldnt-cross-the-network !! + %f ~| %even-static-casts-should-be-built-locally !! %p ~| %requesting-foreign-permissions-is-invalid !! %r ~| %no-cages-please-they-are-just-way-too-big !! %s ~| %please-dont-get-your-takos-over-a-network !! @@ -3392,7 +3470,7 @@ ^- [(unit (unit (each cage lobe))) ford-cache] ?. =(aeon let.dom) [~ fod.dom] - =/ cached=(unit [=vase *]) (~(get by vases.fod.dom) path) + =/ cached=(unit [=vase *]) (~(get by files.fod.dom) path) ?^ cached :_(fod.dom [~ ~ %& %vase !>(vase.u.cached)]) =/ x (read-x aeon path) @@ -3421,7 +3499,7 @@ :_(fod.dom [~ ~ %& %dais !>(dais.u.cached)]) =^ =dais fod.dom %- wrap:fusion - (get-mark:(ford:fusion static-ford-args) i.path) + (build-dais:(ford:fusion static-ford-args) i.path) :_(fod.dom [~ ~ %& %dais !>(dais)]) :: ++ read-c @@ -3432,14 +3510,46 @@ [~ fod.dom] ?. ?=([@ @ ~] path) [[~ ~] fod.dom] - =/ cached=(unit [=tube *]) (~(get by casts.fod.dom) [i i.t]:path) + =/ cached=(unit [=tube *]) (~(get by tubes.fod.dom) [i i.t]:path) ?^ cached :_(fod.dom [~ ~ %& %tube !>(tube.u.cached)]) =^ =tube fod.dom %- wrap:fusion - (get-cast:(ford:fusion static-ford-args) [i i.t]:path) + (build-tube:(ford:fusion static-ford-args) [i i.t]:path) :_(fod.dom [~ ~ %& %tube !>(tube)]) :: + ++ read-e + !. + |= [=aeon =path] + ^- [(unit (unit (each cage lobe))) ford-cache] + ?. =(aeon let.dom) + [~ fod.dom] + ?. ?=([@ ~] path) + [[~ ~] fod.dom] + =/ cached=(unit [=vase *]) (~(get by naves.fod.dom) i.path) + ?^ cached + :_(fod.dom [~ ~ %& %nave !>(vase.u.cached)]) + =^ =vase fod.dom + %- wrap:fusion + (build-nave:(ford:fusion static-ford-args) i.path) + :_(fod.dom [~ ~ %& %nave !>(vase)]) + :: + ++ read-f + !. + |= [=aeon =path] + ^- [(unit (unit (each cage lobe))) ford-cache] + ?. =(aeon let.dom) + [~ fod.dom] + ?. ?=([@ @ ~] path) + [[~ ~] fod.dom] + =/ cached=(unit [=vase *]) (~(get by casts.fod.dom) [i i.t]:path) + ?^ cached + :_(fod.dom [~ ~ %& %cast vase.u.cached]) + =^ =vase fod.dom + %- wrap:fusion + (build-cast:(ford:fusion static-ford-args) [i i.t]:path) + :_(fod.dom [~ ~ %& %cast vase]) + :: :: Gets the permissions that apply to a particular node. :: :: If the node has no permissions of its own, we use its parent's. @@ -3784,7 +3894,8 @@ :: virtualize to catch and produce deterministic failures :: !. - =- ?:(?=(%& -<) p.- ((slog p.-) [[~ ~] fod])) + =- ?: ?=(%& -<) p.- + ((slog leaf+"gall: read-at-aeon fail {}" p.-) [[~ ~] fod]) %- mule |. ?- care.mun %d @@ -3800,6 +3911,8 @@ %a (read-a yon path.mun) %b (read-b yon path.mun) %c (read-c yon path.mun) + %e (read-e yon path.mun) + %f (read-f yon path.mun) %p :_(fod (read-p path.mun)) %r :_(fod (bind (read-r yon path.mun) (lift |=(a=cage [%& a])))) %s :_(fod (bind (read-s yon path.mun) (lift |=(a=cage [%& a])))) @@ -3845,7 +3958,7 @@ :: :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: =| :: instrument state - $: ver=%6 :: vane version + $: ver=%7 :: vane version ruf=raft :: revision tree == :: |= [now=@da eny=@uvJ rof=roof] :: current invocation @@ -4087,8 +4200,77 @@ == :: ++ load - |= old=[%6 raft] - ..^$(ruf +.old) + => |% + +$ raft-any + $% [%7 raft-7] + [%6 raft-6] + == + +$ raft-7 raft + +$ dojo-7 dojo + +$ ford-cache-7 ford-cache + +$ raft-6 + $: rom=room-6 :: domestic + hoy=(map ship rung-6) :: foreign + ran=rang :: hashes + mon=(map term beam) :: mount points + hez=(unit duct) :: sync duct + cez=(map @ta crew) :: permission groups + pud=(unit [=desk =yoki]) :: pending update + == :: + +$ room-6 [hun=duct dos=(map desk dojo-6)] + +$ dojo-6 + $: qyx=cult :: subscribers + dom=dome-6 :: desk state + per=regs :: read perms per path + pew=regs :: write perms per path + == + +$ dome-6 + $: ank=ankh :: state + let=aeon :: top id + hit=(map aeon tako) :: versions by id + lab=(map @tas aeon) :: labels + mim=(map path mime) :: mime cache + fod=ford-cache-6 :: ford cache + fer=(unit reef-cache) :: reef cache + == + +$ rung-6 + $: rus=(map desk rede-6) + == + +$ rede-6 + $: lim=@da + ref=(unit rind) + qyx=cult + dom=dome-6 + per=regs + pew=regs + == + +$ ford-cache-6 * :: discard old cache + -- + |= old=raft-any + |^ + =? old ?=(%6 -.old) 7+(raft-6-to-7 +.old) + ?> ?=(%7 -.old) + ..^^$(ruf +.old) + :: +raft-6-to-7: delete stale ford caches (they could all be invalid) + :: + ++ raft-6-to-7 + |= raf=raft-6 + ^- raft-7 + %= raf + dos.rom + %- ~(run by dos.rom.raf) + |= doj=dojo-6 + ^- dojo-7 + doj(fod.dom *ford-cache-7) + :: + hoy + %- ~(run by hoy.raf) + |= =rung-6 + %- ~(run by rus.rung-6) + |= =rede-6 + rede-6(dom dom.rede-6(fod *ford-cache-7)) + == + -- :: ++ scry :: inspect ^- roon @@ -4138,7 +4320,7 @@ dos.rom %- ~(run by dos.rom.ruf) |= =dojo - dojo(fod.dom [~ ~ ~]) + dojo(fod.dom [~ ~ ~ ~ ~]) :: hoy %- ~(run by hoy.ruf) @@ -4147,7 +4329,7 @@ rus %- ~(run by rus.rung) |= =rede - rede(fod.dom [~ ~ ~]) + rede(fod.dom [~ ~ ~ ~ ~]) == == :: @@ -4322,9 +4504,11 @@ :+ desk %| :~ ankh+&+ank.dom.dojo mime+&+mim.dom.dojo - ford-vases+&+vases.fod.dom.dojo + ford-files+&+files.fod.dom.dojo + ford-naves+&+naves.fod.dom.dojo ford-marks+&+marks.fod.dom.dojo ford-casts+&+casts.fod.dom.dojo + ford-tubes+&+tubes.fod.dom.dojo == :~ domestic+|+domestic foreign+&+hoy.ruf diff --git a/pkg/arvo/sys/vane/gall.hoon b/pkg/arvo/sys/vane/gall.hoon index 7bd5b3da23..a481307874 100644 --- a/pkg/arvo/sys/vane/gall.hoon +++ b/pkg/arvo/sys/vane/gall.hoon @@ -680,12 +680,9 @@ :: note this should only happen on reverse bones, so only facts :: and kicks :: - =/ sys-wire [%sys wire] :: TODO: %drip %kick so app crash can't kill the remote %pull :: - =/ =ames-request-all [%0 %u ~] - =. mo-core - (mo-pass sys-wire %a %plea ship %g /ge/[foreign-agent] ames-request-all) + =. mo-core (mo-send-foreign-request ship foreign-agent %leave ~) =. mo-core (mo-give %unto %kick ~) mo-core == @@ -942,15 +939,13 @@ =/ sky (rof ~ %cb [our %home case] /[mark.ames-response]) ?- sky ?(~ [~ ~]) - =/ ror "gall: ames mark fail {}" - (mo-give %done `vale+[leaf+ror]~) + (mean leaf+"gall: ames mark fail {}" ~) :: [~ ~ *] =+ !<(=dais:clay q.u.u.sky) =/ res (mule |.((vale:dais noun.ames-response))) ?: ?=(%| -.res) - =/ ror "gall: ames vale fail {}" - (mo-give %done `vale+[leaf+ror p.res]) + (mean leaf+"gall: ames vale fail {}" p.res) =. mo-core %+ mo-pass /nowhere [%c %warp our %home ~ %sing %b case /[mark.ames-response]] diff --git a/pkg/arvo/sys/zuse.hoon b/pkg/arvo/sys/zuse.hoon index 17d90e0462..47678da1a4 100644 --- a/pkg/arvo/sys/zuse.hoon +++ b/pkg/arvo/sys/zuse.hoon @@ -3297,11 +3297,14 @@ %- flop |- ^- ^tape ?:(=(0 a) ~ [(add '0' (mod a 10)) $(a (div a 10))]) + :: :: ++sect:enjs:format + ++ sect :: s timestamp + |= a=^time + (numb (unt:chrono:userlib a)) :: :: ++time:enjs:format ++ time :: ms timestamp |= a=^time - =- (numb (div (mul - 1.000) ~s1)) - (add (div ~s1 2.000) (sub a ~1970.1.1)) + (numb (unm:chrono:userlib a)) :: :: ++path:enjs:format ++ path :: string from path |= a=^path @@ -3365,10 +3368,10 @@ (poq (wit jon)) :: :: ++di:dejs:format ++ di :: millisecond date - %+ cu - |= a=@u ^- @da - (add ~1970.1.1 (div (mul ~s1 a) 1.000)) - ni + (cu from-unix-ms:chrono:userlib ni) + :: :: ++du:dejs:format + ++ du :: second date + (cu from-unix:chrono:userlib ni) :: :: ++mu:dejs:format ++ mu :: true unit |* wit=fist @@ -3578,10 +3581,7 @@ (bind (stud:chrono:userlib p.jon) |=(a=date (year a))) :: ++ di :: millisecond date - %+ cu - |= a=@u ^- @da - (add ~1970.1.1 (div (mul ~s1 a) 1.000)) - ni + (cu from-unix-ms:chrono:userlib ni) :: ++ mu :: true unit |* wit=fist @@ -5308,55 +5308,47 @@ =/ acc [stop=`?`%.n state=state] =< abet =< main |% + ++ this . ++ abet [state.acc a] :: +main: main recursive loop; performs a partial inorder traversal :: ++ main - ^+ . + ^+ this :: stop if empty or we've been told to stop :: - ?~ a . - ?: stop.acc . + ?: =(~ a) this + ?: stop.acc this :: inorder traversal: left -> node -> right, until .f sets .stop :: - => left - ?: stop.acc . - => node - ?: stop.acc . - right + =. this left + ?: stop.acc this + =^ del this node + =? this !stop.acc right + =? a del (nip a) + this :: +node: run .f on .n.a, updating .a, .state, and .stop :: ++ node - ^+ . + ^+ [del=*? this] :: run .f on node, updating .stop.acc and .state.acc :: - =^ res acc - ?> ?=(^ a) - (f state.acc n.a) - :: apply update to .a from .f's product - :: - =. a - :: if .f requested node deletion, merge and balance .l.a and .r.a - :: - ?~ res (nip a) - :: we kept the node; replace its .val; order is unchanged - :: - ?> ?=(^ a) - a(val.n u.res) - :: - ..node + ?> ?=(^ a) + =^ res acc (f state.acc n.a) + ?~ res + [del=& this] + [del=| this(val.n.a u.res)] :: +left: recurse on left subtree, copying mutant back into .l.a :: ++ left - ^+ . - ?~ a . + ^+ this + ?~ a this =/ lef main(a l.a) lef(a a(l a.lef)) :: +right: recurse on right subtree, copying mutant back into .r.a :: ++ right - ^+ . - ?~ a . + ^+ this + ?~ a this =/ rig main(a r.a) rig(a a(r a.rig)) -- @@ -5486,13 +5478,20 @@ :: :::: ++ chrono ^? |% - :: +from-unix: unix timestamp to @da + :: +from-unix: unix seconds to @da :: ++ from-unix |= timestamp=@ud ^- @da %+ add ~1970.1.1 (mul timestamp ~s1) + :: +from-unix-ms: unix milliseconds to @da + :: + ++ from-unix-ms + |= timestamp=@ud + ^- @da + %+ add ~1970.1.1 + (div (mul ~s1 timestamp) 1.000) :: :: ++dawn:chrono: ++ dawn :: Jan 1 weekday |= yer=@ud @@ -5611,9 +5610,13 @@ ++ dd :: two digits (bass 10 (stun 2^2 dit)) -- :: + :: :: ++unm:chrono:userlib + ++ unm :: Urbit to Unix ms + |= a=@da + (div (mul (sub a ~1970.1.1) 1.000) ~s1) :: :: ++unt:chrono:userlib ++ unt :: Urbit to Unix time - |= a=@ + |= a=@da (div (sub a ~1970.1.1) ~s1) :: :: ++yu:chrono:userlib ++ yu :: UTC format constants diff --git a/pkg/arvo/ted/aqua/behn.hoon b/pkg/arvo/ted/aqua/behn.hoon index 17a56e0841..517d2f3c72 100644 --- a/pkg/arvo/ted/aqua/behn.hoon +++ b/pkg/arvo/ted/aqua/behn.hoon @@ -74,13 +74,15 @@ =. next-timer ~ =. this %- emit-aqua-events + ?^ error + :: Should pass through errors to aqua, but doesn't + :: + %- (slog leaf+"aqua-behn: timer failed" u.error) + ~ :_ ~ ^- aqua-event :+ %event who - :- //behn/0v1n.2m9vh - ?~ error - [%wake ~] - [%crud %fail u.error] + [//behn/0v1n.2m9vh [%wake ~]] ..abet-pe -- -- diff --git a/pkg/arvo/ted/build-cast.hoon b/pkg/arvo/ted/build-cast.hoon index 017a2ee82d..6122babca2 100644 --- a/pkg/arvo/ted/build-cast.hoon +++ b/pkg/arvo/ted/build-cast.hoon @@ -9,5 +9,5 @@ ?~ bem=(de-beam pax) (strand-fail:strand %path-not-beam >pax< ~) =/ =mars:clay [i i.t]:?>(?=([@ @ ~] s.u.bem) s.u.bem) -;< =tube:clay bind:m (build-cast:strandio -.u.bem mars) -(pure:m !>(tube)) +;< =vase bind:m (build-cast:strandio -.u.bem mars) +(pure:m vase) diff --git a/pkg/arvo/ted/build-file.hoon b/pkg/arvo/ted/build-file.hoon index 5311b0795c..5b4a884cb8 100644 --- a/pkg/arvo/ted/build-file.hoon +++ b/pkg/arvo/ted/build-file.hoon @@ -6,6 +6,9 @@ =/ m (strand ,vase) ^- form:m =+ !<([~ pax=path] arg) -?^ bem=(de-beam pax) - (build-file:strandio u.bem) -(strand-fail:strand %path-not-beam >pax< ~) +?~ bem=(de-beam pax) + (strand-fail:strand %path-not-beam >pax< ~) +;< vax=(unit vase) bind:m (build-file:strandio u.bem) +?^ vax + (pure:m u.vax) +(strand-fail:strand %build-file >u.bem< ~) diff --git a/pkg/arvo/ted/build-nave.hoon b/pkg/arvo/ted/build-nave.hoon new file mode 100644 index 0000000000..f85cbc520e --- /dev/null +++ b/pkg/arvo/ted/build-nave.hoon @@ -0,0 +1,13 @@ +/- spider +/+ strandio +=, strand=strand:spider +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<([~ pax=path] arg) +?~ bem=(de-beam pax) + (strand-fail:strand %path-not-beam >pax< ~) +=/ =mark (rear s.u.bem) +;< =vase bind:m (build-nave:strandio -.u.bem mark) +(pure:m vase) diff --git a/pkg/arvo/ted/build-tube.hoon b/pkg/arvo/ted/build-tube.hoon new file mode 100644 index 0000000000..da075e172b --- /dev/null +++ b/pkg/arvo/ted/build-tube.hoon @@ -0,0 +1,13 @@ +/- spider +/+ strandio +=, strand=strand:spider +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<([~ pax=path] arg) +?~ bem=(de-beam pax) + (strand-fail:strand %path-not-beam >pax< ~) +=/ =mars:clay [i i.t]:?>(?=([@ @ ~] s.u.bem) s.u.bem) +;< =tube:clay bind:m (build-tube:strandio -.u.bem mars) +(pure:m !>(tube)) diff --git a/pkg/arvo/ted/gcp/get-token.hoon b/pkg/arvo/ted/gcp/get-token.hoon new file mode 100644 index 0000000000..3562aaf268 --- /dev/null +++ b/pkg/arvo/ted/gcp/get-token.hoon @@ -0,0 +1,144 @@ +:: Gets a Google Storage access token. +:: +:: This thread produces a pair of [access-key expires-in], where +:: access-key is a @t that can be used as a bearer token to talk +:: to the GCP Storage API on behalf of some service account, and +:: expires-in is a @dr after which the token will stop working and +:: need to be refreshed. +:: +:: It expects settings-store to contain relevant fields from +:: a GCP service account JSON file, generally as poked by +:: sh/poke-gcp-account-json. Specifically, it depends on the +:: `token_uri`, `client_email`, `private_key_id`, and `private_key` +:: fields. If these fields are not in settings-store at the time +:: the thread is run, it will fail. +:: +:: The thread works by first constructing a self-signed JWT using +:: the fields in settings-store. Then, it sends this JWT to the +:: specified token URI (usually https://oauth2.googleapis.com/token), +:: which responds with a bearer token and expiry. +:: +:: +/- gcp, spider, settings +/+ jose, pkcs, primitive-rsa, strandio +=, strand=strand:spider +=, rsa=primitive-rsa +^- thread:spider +|^ +|= * +=/ m (strand ,vase) +^- form:m +;< =bowl:spider bind:m get-bowl:strandio +;< iss=@t bind:m (read-setting %client-email) +;< =key:rsa bind:m read-private-key +;< kid=@t bind:m (read-setting %private-key-id) +;< aud=@t bind:m (read-setting %token-uri) +=* scope + 'https://www.googleapis.com/auth/devstorage.read_write' +=/ jot=@t + (make-jwt key kid iss scope aud now.bowl) +;< =token:gcp bind:m + (get-access-token jot aud) +(pure:m !>(token)) +:: +++ read-setting + |= key=term + =/ m (strand @t) ^- form:m + ;< has=? bind:m + %+ scry:strandio ? + /gx/settings-store/has-entry/gcp-store/[key]/noun + ?. has + (strand-fail:strandio (rap 3 %gcp-missing- key ~) ~) + ;< =data:settings bind:m + %+ scry:strandio + data:settings + /gx/settings-store/entry/gcp-store/[key]/settings-data + ?> ?=([%entry %s @] data) + (pure:m p.val.data) +:: +++ read-private-key + =/ m (strand ,key:rsa) ^- form:m + ;< dat=@t bind:m (read-setting %private-key) + %- pure:m + %. dat + ;: cork + to-wain:format + ring:de:pem:pkcs8:pkcs + need + == +:: construct and return a self-signed JWT issued now, expiring in ~h1. +:: TODO: maybe move this into lib/jose/hoon +:: +++ make-jwt + |= [=key:rsa kid=@t iss=@t scope=@t aud=@t iat=@da] + ^- @t + =/ job=json + =, enjs:format + %^ sign:jws:jose key + :: the JWT's "header" + %: pairs + alg+s+'RS256' + typ+s+'JWT' + kid+s+kid + ~ + == + :: the JWT's "payload" + %: pairs + iss+s+iss + sub+s+iss :: per g.co, use iss for sub + scope+s+scope + aud+s+aud + iat+(sect iat) + exp+(sect (add iat ~h1)) + ~ + == + =/ [pod=@t pad=@t sig=@t] + =, dejs:format + ((ot 'protected'^so 'payload'^so 'signature'^so ~) job) + (rap 3 (join '.' `(list @t)`~[pod pad sig])) +:: RPC to get an access token. Probably only works with Google. +:: Described at: +:: https://developers.google.com/identity/protocols/oauth2/service-account +:: +++ get-access-token + |= [jot=@t url=@t] + =/ m (strand ,token:gcp) ^- form:m + ;< ~ bind:m + %: send-request:strandio + method=%'POST' + url=url + header-list=['Content-Type'^'application/json' ~] + ^= body + %- some %- as-octt:mimes:html + %- en-json:html + %: pairs:enjs:format + :- 'grant_type' + s+'urn:ietf:params:oauth:grant-type:jwt-bearer' + assertion+s+jot + ~ + == + == + ;< rep=client-response:iris bind:m + take-client-response:strandio + ?> ?=(%finished -.rep) + ?~ full-file.rep + (strand-fail:strandio %gcp-no-response ~) + =/ body=@t q.data.u.full-file.rep + =/ jon=(unit json) (de-json:html body) + ?~ jon + ~| body + (strand-fail:strandio %gcp-bad-body ~) + =* job u.jon + ~| job + =, dejs:format + =/ [typ=@t =token:gcp] + %. job + %: ot + 'token_type'^so + 'access_token'^so + 'expires_in'^(cu |=(a=@ (mul a ~s1)) ni) + ~ + == + ?> =('Bearer' typ) + (pure:m token) +-- diff --git a/pkg/arvo/ted/gcp/is-configured.hoon b/pkg/arvo/ted/gcp/is-configured.hoon new file mode 100644 index 0000000000..02b67e60b3 --- /dev/null +++ b/pkg/arvo/ted/gcp/is-configured.hoon @@ -0,0 +1,49 @@ +:: Tells whether GCP Storage appears to be configured. +:: +:: Thread since it needs to be called from Landscape. +:: +:: +/- gcp, spider, settings +/+ strandio +=, strand=strand:spider +=, enjs:format +^- thread:spider +|^ +|= * +=/ m (strand ,vase) +^- form:m +;< has=? bind:m + %: has-settings + %client-email + %private-key + %private-key-id + %token-uri + ~ + == +%- pure:m +!> +%+ frond %gcp-configured +b+has +:: +++ has-settings + |= set=(list @tas) + =/ m (strand ?) + ^- form:m + ?~ set + (pure:m %.y) + ;< has=? bind:m (has-setting i.set) + ?. has + (pure:m %.n) + ;< has=? bind:m (has-settings t.set) + (pure:m has) +:: +++ has-setting + |= key=@tas + =/ m (strand ?) + ^- form:m + ;< has=? bind:m + %+ scry:strandio ? + /gx/settings-store/has-entry/gcp-store/[key]/noun + (pure:m has) +:: +-- diff --git a/pkg/arvo/ted/graph/add-nodes.hoon b/pkg/arvo/ted/graph/add-nodes.hoon new file mode 100644 index 0000000000..3041eb61cd --- /dev/null +++ b/pkg/arvo/ted/graph/add-nodes.hoon @@ -0,0 +1,131 @@ +/- spider +/+ strandio, store=graph-store, gra=graph, graph-view, sig=signatures +=, strand=strand:spider +=> +|% +++ scry-graph + |= rid=resource:store + =/ m (strand ,graph:store) + ^- form:m + ;< =update:store bind:m + %+ scry:strandio update:store + /gx/graph-store/graph/(scot %p entity.rid)/[name.rid]/noun + ?> ?=(%0 -.update) + ?> ?=(%add-graph -.q.update) + (pure:m graph.q.update) +-- +:: +^- thread:spider +|= arg=vase +=/ m (strand:spider ,vase) +^- form:m +=+ !<([~ =update:store] arg) +?> ?=(%add-nodes -.q.update) +=* poke-our poke-our:strandio +;< =bowl:spider bind:m get-bowl:strandio +;< =graph:store bind:m (scry-graph resource.q.update) +|^ +=. nodes.q.update + %- ~(gas by *(map index:store node:store)) + %+ turn + (concat-by-parent (sort-nodes nodes.q.update)) + add-hash-to-node +=/ hashes (nodes-to-pending-indices nodes.q.update) +;< ~ bind:m + %^ poke-our %graph-push-hook + %graph-update + !>(update) +(pure:m !>(`action:graph-view`[%pending-indices hashes])) +:: +++ sort-nodes + |= nodes=(map index:store node:store) + ^- (list [index:store node:store]) + %+ sort ~(tap by nodes) + |= [p=[=index:store *] q=[=index:store *]] + ^- ? + (lth (lent index.p) (lent index.q)) +:: +++ concat-by-parent + |= lis=(list [index:store node:store]) + ^- (list [index:store node:store]) + %~ tap by + %+ roll lis + |= $: [=index:store =node:store] + nds=(map index:store node:store) + == + ?: ?=(~ index) !! + ?: ?=([@ ~] index) + (~(put by nds) index node) + =/ ind (snip `(list atom)`index) + =/ nod (~(get by nds) ind) + ?~ nod + (~(put by nds) index node) + =. children.u.nod + :- %graph + ?: ?=(%empty -.children.u.nod) + %+ gas:orm:store *graph:store + [(rear index) node]~ + %^ put:orm:store p.children.u.nod + (rear index) + node + (~(put by nds) ind u.nod) +:: +++ add-hash-to-node + =| parent-hash=(unit hash:store) + |= [=index:store =node:store] + ^- [index:store node:store] + =* loop $ + :- index + =* p post.node + =/ =hash:store + =- `@ux`(sham -) + :^ ?^ parent-hash + parent-hash + (index-to-parent-hash index) + author.p + time-sent.p + contents.p + %_ node + hash.post `hash + :: + :: TODO: enable signing our own post as soon as we're ready + :: signatures.post + :: %- ~(gas in *signatures:store) + :: [(sign:sig our.bowl now.bowl hash)]~ + :: + children + ?: ?=(%empty -.children.node) + children.node + :- %graph + %+ gas:orm:store *graph:store + %+ turn (tap:orm:store p.children.node) + |= [=atom =node:store] + =/ [* nod=node:store] + %_ loop + parent-hash `hash + index (snoc index atom) + node node + == + [atom nod] + == +:: +++ index-to-parent-hash + |= =index:store + ^- (unit hash:store) + ?: ?=(~ index) + !! + ?: ?=([@ ~] index) + ~ + =/ node (got-deep:gra graph (snip `(list atom)`index)) + hash.post.node +:: +++ nodes-to-pending-indices + |= nodes=(map index:store node:store) + ^- (map hash:store index:store) + %- ~(gas by *(map hash:store index:store)) + %+ turn ~(tap by nodes) + |= [=index:store =node:store] + ^- [hash:store index:store] + ?> ?=(^ hash.post.node) + [u.hash.post.node index] +-- diff --git a/pkg/arvo/ted/graph/groupify.hoon b/pkg/arvo/ted/graph/groupify.hoon index 7117f01e64..b3681e0171 100644 --- a/pkg/arvo/ted/graph/groupify.hoon +++ b/pkg/arvo/ted/graph/groupify.hoon @@ -1,4 +1,5 @@ -/- spider, graph-view, graph=graph-store, *metadata-store, *group, *metadata-store +/- spider, graph-view, graph=graph-store, + met=metadata-store, *group, *metadata-store /+ strandio, resource => |% @@ -28,11 +29,11 @@ :: ++ scry-metadatum |= rid=resource - =/ m (strand ,metadata) + =/ m (strand ,metadatum:met) ^- form:m =/ enc-path=@t (scot %t (spat (en-path:resource rid))) - ;< umeta=(unit metadata) bind:m - %+ scry:strandio (unit metadata) + ;< umeta=(unit metadatum:met) bind:m + %+ scry:strandio (unit metadatum:met) %+ weld /gx/metadata-store/metadata /[enc-path]/graph/[enc-path]/noun ?> ?=(^ umeta) @@ -48,24 +49,25 @@ ;< =group bind:m (scry-group rid.action) ?. hidden.group (strand-fail:strandio %bad-request ~) -;< =metadata bind:m (scry-metadatum rid.action) +;< =metadatum:met bind:m (scry-metadatum rid.action) ?~ to.action ;< ~ bind:m %+ poke-our %contact-view :- %contact-view-action - !>([%groupify rid.action title.metadata description.metadata]) + !>([%groupify rid.action title.metadatum description.metadatum]) (pure:m !>(~)) ;< new=^group bind:m (scry-group u.to.action) ?< hidden.new -=/ new-path (en-path:resource u.to.action) -=/ app-path (en-path:resource rid.action) -=/ add-md=metadata-action - [%add new-path graph+app-path metadata] -;< ~ bind:m - (poke-our %metadata-store metadata-action+!>(add-md)) ;< ~ bind:m %+ poke-our %metadata-store - metadata-action+!>([%remove app-path graph+app-path]) + :- %metadata-action + !> ^- action:met + [%add u.to.action [%graph rid.action] metadatum] +;< ~ bind:m + %+ poke-our %metadata-store + :- %metadata-action + !> ^- action:met + [%remove rid.action [%graph rid.action]] ;< ~ bind:m (poke-our %group-store %group-update !>([%remove-group rid.action ~])) (pure:m !>(~)) diff --git a/pkg/arvo/ted/graph/restore.hoon b/pkg/arvo/ted/graph/restore.hoon index ae03aa15e3..e399deb4da 100644 --- a/pkg/arvo/ted/graph/restore.hoon +++ b/pkg/arvo/ted/graph/restore.hoon @@ -1,4 +1,4 @@ -/- spider, graph=graph-store, *metadata-store, *group, group-store +/- spider, graph=graph-store, met=metadata-store, *group, group-store, push-hook /+ strandio, resource, graph-view => |% @@ -23,17 +23,22 @@ :: :: Setup metadata :: -=/ =metadata - %* . *metadata +=/ =metadatum:met + %* . *metadatum:met title title description description date-created now.bowl creator our.bowl module module == -=/ act=metadata-action - [%add (en-path:resource group) graph+(en-path:resource rid) metadata] -;< ~ bind:m (poke-our %metadata-hook %metadata-action !>(act)) ;< ~ bind:m - (poke-our %metadata-hook %metadata-hook-action !>([%add-owned (en-path:resource group)])) + %+ poke-our %metadata-push-hook + :- %metadata-action + !> ^- action:met + [%add group [%graph rid] metadatum] +;< ~ bind:m + %+ poke-our %metadata-push-hook + :- %push-hook-action + !> ^- action:push-hook + [%add group] (pure:m !>(~)) diff --git a/pkg/arvo/ted/migrate-channels.hoon b/pkg/arvo/ted/migrate-channels.hoon deleted file mode 100644 index c6d9937042..0000000000 --- a/pkg/arvo/ted/migrate-channels.hoon +++ /dev/null @@ -1,84 +0,0 @@ -/- spider, *metadata-store -/+ strandio -=, strand=strand:spider -^- thread:spider -|= arg=vase -=/ m (strand ,vase) -^- form:m -=/ [~ og-path=path ng-path=path] !<([~ path path] arg) -;< bol=bowl:spider bind:m get-bowl:strandio -|^ -:: -=/ og=(unit (set ship)) (scry-for (unit (set ship)) %group-store og-path) -?~ og - (pure:m !>("no such group: {}")) -=/ ng=(unit (set ship)) (scry-for (unit (set ship)) %group-store ng-path) -?~ ng - (pure:m !>("no such group: {}")) -:: -=/ assoc=associations (scry-for associations %metadata-store [%group og-path]) -=/ assoc-list=(list [[group-path md-resource] metadata]) ~(tap by assoc) -:: -|- -=* loop $ -?~ assoc-list - ;< ~ bind:m - (poke-our:strandio %group-store %group-action !>([%unbundle og-path])) - (pure:m !>("done")) -=/ [[g-path=group-path res=md-resource] meta=metadata] i.assoc-list -?. =(our.bol creator.meta) - loop(assoc-list t.assoc-list) -?> =(g-path og-path) -=/ output=(list card:agent:gall) - ?+ app-name.res ~ - :: - ?(%chat %link) - %- (slog leaf+"migrating {} : {}" ~) - :~ :* %pass /poke %agent - [our.bol %metadata-hook] - %poke %metadata-action - !>([%add ng-path res meta]) - == - :* %pass /poke %agent - [our.bol %metadata-hook] - %poke %metadata-action - !>([%remove g-path res]) - == - == - %publish - %- (slog leaf+"migrating {} : {}" ~) - =/ book (scry-for notebook %publish [%book app-path.res]) - ?> ?=([@tas @tas ~] app-path.res) - :~ :* %pass /poke %agent - [our.bol %publish] - %poke %publish-action - !> - :* %edit-book - i.t.app-path.res - title.book - description.book - comments.book - `[ng-path ~ %.y %.n] - == - == - :* %pass /poke %agent - [our.bol %metadata-hook] - %poke %metadata-action - !>([%remove g-path res]) - == - == - == -:: -;< ~ bind:m (send-raw-cards:strandio output) -loop(assoc-list t.assoc-list) -:: -++ scry-for - |* [mol=mold app=term pax=path] - .^ mol - %gx - (scot %p our.bol) - app - (scot %da now.bol) - (snoc `path`pax %noun) - == --- diff --git a/pkg/arvo/ted/ph/migrate/make-groups.hoon b/pkg/arvo/ted/ph/migrate/make-groups.hoon deleted file mode 100644 index 62a09dffa6..0000000000 --- a/pkg/arvo/ted/ph/migrate/make-groups.hoon +++ /dev/null @@ -1,46 +0,0 @@ -/- spider, - contact-view, - *resource -/+ *ph-io, strandio -=, strand=strand:spider -:: -^- thread:spider -|= vase -=/ m (strand ,vase) -;< ~ bind:m start-simple -;< bol=bowl:spider bind:m get-bowl:strandio -:: -:: group setup -:: - ~zod creates an open group -:: - ~zod creates and invite-only group, and invites ~bus and ~web -:: - ~bus and ~web join the first, but not the second group, to keep -:: invite-store populated -:: -=/ group-1=contact-view-action:contact-view - :* %create - %group-1 - [%open ~ ~] - 'Group 1' - 'this is group 1' - == -=/ group-2=contact-view-action:contact-view - :* %create - %group-2 - [%invite (sy ~bus ~web ~)] - 'Group 2' - 'this is group 2' - == -=/ join=contact-view-action:contact-view [%join ~zod %group-1] -;< ~ bind:m (poke-app ~zod %contact-view %contact-view-action group-1) -;< ~ bind:m (wait-for-output ~zod ">=") -;< ~ bind:m (poke-app ~zod %contact-view %contact-view-action group-2) -;< ~ bind:m (wait-for-output ~zod ">=") -;< ~ bind:m (sleep ~s10) -;< ~ bind:m (poke-app ~bus %contact-view %contact-view-action join) -;< ~ bind:m (wait-for-output ~bus ">=") -;< ~ bind:m (poke-app ~web %contact-view %contact-view-action join) -;< ~ bind:m (wait-for-output ~web ">=") -;< ~ bind:m (send-hi ~bus ~zod) -;< ~ bind:m (send-hi ~web ~zod) -;< ~ bind:m (sleep ~s2) -(pure:m *vase) diff --git a/pkg/arvo/ted/ph/migrate/post-import-groups.hoon b/pkg/arvo/ted/ph/migrate/post-import-groups.hoon deleted file mode 100644 index 8681c14527..0000000000 --- a/pkg/arvo/ted/ph/migrate/post-import-groups.hoon +++ /dev/null @@ -1,24 +0,0 @@ -/- spider, - contact-view, - *resource, - group-store -/+ *ph-io, strandio -=, strand=strand:spider -:: -^- thread:spider -|= vase -=/ m (strand ,vase) -;< ~ bind:m start-simple -;< bol=bowl:spider bind:m get-bowl:strandio -:: -=/ join-2=contact-view-action:contact-view [%join ~zod %group-2] -=/ add-members-1=action:group-store - [%add-members [~zod %group-1] (sy ~def ~ten ~)] -=/ add-members-2=action:group-store - [%add-members [~zod %group-2] (sy ~def ~ten ~)] -;< ~ bind:m (poke-app ~bus %contact-view %contact-view-action join-2) -;< ~ bind:m (poke-app ~web %contact-view %contact-view-action join-2) -;< ~ bind:m (poke-app ~zod %group-store %group-action add-members-1) -;< ~ bind:m (poke-app ~zod %group-store %group-action add-members-2) -:: -(pure:m *vase) diff --git a/pkg/arvo/ted/ph/migrate/post-import-metadata-contacts.hoon b/pkg/arvo/ted/ph/migrate/post-import-metadata-contacts.hoon deleted file mode 100644 index 81a9825937..0000000000 --- a/pkg/arvo/ted/ph/migrate/post-import-metadata-contacts.hoon +++ /dev/null @@ -1,61 +0,0 @@ -/- spider, - contact-view, - contact-store, - group-store, - metadata-store, - post, - graph-store, - *resource -/+ *ph-io, strandio -=, strand=strand:spider -:: -:: -^- thread:spider -|= vase -=/ m (strand ,vase) -;< ~ bind:m start-simple -;< bol=bowl:spider bind:m get-bowl:strandio -:: -:: test metadata import -:: -=/ change-group-1=metadata-action:metadata-store - :* %add - /ship/~zod/group-1 - [%contacts /ship/~zod/group-1] - 'New Group 1 Title' - 'new description' - 0x0 - now.bol - ~zod - 'fake' - == -=/ change-web-book=metadata-action:metadata-store - :* %add - /ship/~web/graph-3 - [%graph /ship/~web/graph-3] - 'New Graph 3 Title' - 'new description' - 0x0 - now.bol - ~web - 'fake' - == -;< ~ bind:m (poke-app ~zod %metadata-hook %metadata-action change-group-1) -;< ~ bind:m (sleep ~s5) -;< ~ bind:m (poke-app ~web %metadata-hook %metadata-action change-web-book) -;< ~ bind:m (sleep ~s5) -:: -:: test contacts import -:: -=/ add-zod=contact-action:contact-store - :* %add /ship/~zod/group-1 ~zod - 'ZOD' '' '' '' '' 0x0 ~ - == -=/ add-bus=contact-action:contact-store - :* %add /ship/~zod/group-2 ~bus - 'BUS' '' '' '' '' 0x0 ~ - == -;< ~ bind:m (poke-app ~zod %contact-hook %contact-action add-zod) -;< ~ bind:m (sleep ~s5) -;< ~ bind:m (poke-app ~bus %contact-hook %contact-action add-bus) -(pure:m *vase) diff --git a/pkg/arvo/ted/test.hoon b/pkg/arvo/ted/test.hoon index 3b0027b3e5..a25496fc22 100644 --- a/pkg/arvo/ted/test.hoon +++ b/pkg/arvo/ted/test.hoon @@ -92,8 +92,12 @@ ?. =(%hoon (rear p)) ~ (some [[-.i.bez p] ~]) loop(bez t.bez, fiz (~(gas in fiz) foz)) - ~| bad-test-beam+i.bez - =/ tex=term =-(?>(((sane %tas) -) -) (rear s.i.bez)) + :: + :: XX this logic appears to be vestigial + :: + =/ tex=term + ~| bad-test-beam+i.bez + =-(?>(((sane %tas) -) -) (rear s.i.bez)) =/ xup=path (snip s.i.bez) ;< hov=? bind:m (check-for-file:strandio i.bez(s (snoc xup %hoon))) ?. hov @@ -107,16 +111,23 @@ =/ paz=(list path) (tail !<([~ (list path)] arg)) =/ bez=(list beam) - (turn paz |=(p=path (need (de-beam p)))) + (turn paz |=(p=path ~|([%test-not-beam p] (need (de-beam p))))) ;< fiz=(set [=beam test=(unit term)]) bind:m (find-test-files bez) => .(fiz (sort ~(tap in fiz) aor)) =| test-arms=(map path (list test-arm)) +=| build-ok=? |- ^- form:m =* gather-tests $ ?^ fiz - ~> %slog.0^leaf+"test: building {(spud s.beam.i.fiz)}" - ;< cor=vase bind:m (build-file:strandio beam.i.fiz) - =/ arms=(list test-arm) (get-test-arms cor) + ;< cor=(unit vase) bind:m (build-file:strandio beam.i.fiz) + ?~ cor + ~> %slog.0^leaf+"FAILED {(spud s.beam.i.fiz)} (build)" + gather-tests(fiz t.fiz, build-ok |) + ~> %slog.0^leaf+"built {(spud s.beam.i.fiz)}" + =/ arms=(list test-arm) (get-test-arms u.cor) + :: + :: XX this logic appears to be vestigial + :: =? arms ?=(^ test.i.fiz) |- ^+ arms ?~ arms ~|(no-test-arm+i.fiz !!) @@ -127,7 +138,7 @@ gather-tests(fiz t.fiz) %- pure:m !> ^= ok %+ roll (resolve-test-paths test-arms) -|= [[=path =test-func] ok=_`?`%&] +|= [[=path =test-func] ok=_build-ok] ^+ ok =/ res (run-test path test-func) %- (slog (flop tang.res)) diff --git a/pkg/arvo/tests/lib/pkcs.hoon b/pkg/arvo/tests/lib/pkcs.hoon index 3c3936d634..b9e4e51cfe 100644 --- a/pkg/arvo/tests/lib/pkcs.hoon +++ b/pkg/arvo/tests/lib/pkcs.hoon @@ -327,7 +327,7 @@ :: echo "hello" | openssl dgst -sha256 -sign private.pem | base64 %+ expect-eq !> exp2b64 - !> (en:base64 (met 3 sig) (swp 3 sig)) + !> (en:base64:mimes:html (met 3 sig) (swp 3 sig)) == :: ++ test-csr diff --git a/pkg/arvo/tests/sys/vane/clay.hoon b/pkg/arvo/tests/sys/vane/clay.hoon index 376b7466e4..636efd3a35 100644 --- a/pkg/arvo/tests/sys/vane/clay.hoon +++ b/pkg/arvo/tests/sys/vane/clay.hoon @@ -1,9 +1,15 @@ /+ *test /= clay-raw /sys/vane/clay -/* hello-gen %hoon /gen/hello/hoon -/* strandio-lib %hoon /lib/strandio/hoon -/* strand-lib %hoon /lib/strand/hoon -/* spider-sur %hoon /sur/spider/hoon +/* gen-hello %hoon /gen/hello/hoon +/* lib-cram %hoon /lib/cram/hoon +/* lib-strandio %hoon /lib/strandio/hoon +/* lib-strand %hoon /lib/strand/hoon +/* sur-spider %hoon /sur/spider/hoon +/* mar-html %hoon /mar/html/hoon +/* mar-mime %hoon /mar/mime/hoon +/* mar-udon %hoon /mar/udon/hoon +/* mar-txt %hoon /mar/txt/hoon +/* mar-txt-diff %hoon /mar/txt-diff/hoon :: !: =, format @@ -14,32 +20,48 @@ :: |% ++ test-parse-pile ^- tang + =/ src "." %+ expect-eq !> ^- pile:fusion - :* ~ ~ ~ ~ + :* ~ ~ ~ ~ ~ ~ tssg+[%dbug [/sur/foo/hoon [[1 1] [1 2]]] [%cnts ~[[%.y 1]] ~]]~ == - !> (parse-pile:(ford):fusion /sur/foo/hoon ".") + !> (parse-pile:(ford):fusion /sur/foo/hoon src) +:: +++ test-parse-fascen ^- tang + =/ src "/% moo %mime\0a." + %+ expect-eq + !> ^- pile:fusion + :* sur=~ lib=~ raw=~ + maz=[face=%moo mark=%mime]~ + caz=~ bar=~ + tssg+[%dbug [/sur/foo/hoon [[2 1] [2 2]]] [%cnts ~[[%.y 1]] ~]]~ + == + !> (parse-pile:(ford):fusion /sur/foo/hoon src) +:: +++ test-parse-fasbuc ^- tang + =/ src "/$ goo %mime %txt\0a." + %+ expect-eq + !> ^- pile:fusion + :* sur=~ lib=~ raw=~ maz=~ + caz=[face=%goo from=%mime to=%txt]~ + bar=~ + tssg+[%dbug [/sur/foo/hoon [[2 1] [2 2]]] [%cnts ~[[%.y 1]] ~]]~ + == + !> (parse-pile:(ford):fusion /sur/foo/hoon src) :: ++ test-parse-multiline-faslus ^- tang =/ src """ - :: :: :: - :::: /hoon/hood/app :: :: - :: :: :: + :: /? 310 :: zuse version + :: /- *sole + :: /+ sole :: libraries - :: XX these should really be separate apps, as - :: none of them interact with each other in - :: any fashion; however, to reduce boot-time - :: complexity and work around the current - :: non-functionality of end-to-end acknowledgments, - :: they have been bundled into :hood - :: - :: |command handlers + :: /+ hood-helm, hood-kiln, hood-drum, hood-write - :: :: :: + :: . """ %+ expect-eq @@ -52,34 +74,21 @@ [`%hood-drum %hood-drum] [`%hood-write %hood-write] == - raw=~ bar=~ - hoon=tssg+[p:(need q:(tall:(vang & /app/hood/hoon) [17 1] "."))]~ + raw=~ maz=~ caz=~ bar=~ + tssg+[%dbug [/sur/foo/hoon [[10 1] [10 2]]] [%cnts ~[[%.y 1]] ~]]~ == - !> (parse-pile:(ford):fusion /app/hood/hoon src) + !> (parse-pile:(ford):fusion /sur/foo/hoon src) :: ++ test-cycle ^- tang - =/ source=@t - ''' - /+ self - . - ''' - =/ =ankh:clay - :- fil=~ - %- ~(gas by *(map @tas ankh:clay)) - :~ :+ %lib fil=~ - %- ~(gas by *(map @tas ankh:clay)) - :~ :+ %self fil=~ - %- ~(gas by *(map @tas ankh:clay)) - :~ :+ %hoon fil=`[*lobe:clay hoon+!>(source)] dir=~ - == == == + =/ source=@t '/+ self\0a.' %- expect-fail |. =/ ford %: ford:fusion bud - ankh + *ankh:clay deletes=~ - changes=~ + changes=(my [/lib/self/hoon &+hoon+source]~) file-store=~ *ford-cache:fusion == @@ -89,22 +98,122 @@ %- expect-fail |. (parse-pile:(ford):fusion /sur/foo/hoon "[") :: -++ test-hello-gen ^- tang - =/ =ankh:clay - :- fil=~ - %- ~(gas by *(map @tas ankh:clay)) - :~ :+ %gen fil=~ - %- ~(gas by *(map @tas ankh:clay)) - :~ :+ %hello fil=~ - %- ~(gas by *(map @tas ankh:clay)) - :~ :+ %hoon fil=`[*lobe:clay hoon+!>(hello-gen)] dir=~ - == == == +++ test-mar-mime ^- tang =/ ford %: ford:fusion bud - ankh + *ankh:clay deletes=~ - changes=(my [/gen/hello/hoon &+hoon+hello-gen]~) + changes=(my [/mar/mime/hoon &+hoon+mar-mime]~) + file-store=~ + *ford-cache:fusion + == + =/ [res=vase nub=state:ford:fusion] (build-nave:ford %mime) + ;: weld + %+ expect-eq + !>(*mime) + (slap res limb/%bunt) + :: + %+ expect-eq + !> (~(gas in *(set path)) /mar/mime/hoon ~) + !> dez:(~(got by files.cache.nub) /mar/mime/hoon) + == +:: +++ test-mar-udon ^- tang + =/ ford + %: ford:fusion + bud + *ankh:clay + deletes=~ + ^= changes + %- my + :~ [/mar/udon/hoon &+hoon+mar-udon] + [/lib/cram/hoon &+hoon+lib-cram] + [/mar/txt/hoon &+hoon+mar-txt] + [/mar/txt-diff/hoon &+hoon+mar-txt-diff] + == + file-store=~ + *ford-cache:fusion + == + =/ [res=vase nub=state:ford:fusion] (build-nave:ford %udon) + ;: weld + %+ expect-eq + !>(*@t) + (slap res limb/%bunt) + :: + %+ expect-eq + !> (~(gas in *(set path)) /mar/udon/hoon /lib/cram/hoon ~) + !> dez:(~(got by files.cache.nub) /mar/udon/hoon) + == +:: +++ test-cast-html-mime ^- tang + =/ changes + %- my + :~ [/mar/mime/hoon &+hoon+mar-mime] + [/mar/html/hoon &+hoon+mar-html] + == + =/ ford + %: ford:fusion + bud + *ankh:clay + deletes=~ + changes + file-store=~ + *ford-cache:fusion + == + =/ [res=vase nub=state:ford:fusion] (build-cast:ford %html %mime) + %+ expect-eq + (slam res !>('')) + !> `mime`[/text/html 13 ''] +:: +++ test-fascen ^- tang + =/ changes + %- my + :~ [/mar/mime/hoon &+hoon+mar-mime] + [/lib/foo/hoon &+hoon+'/% moo %mime\0abunt:moo'] + == + =/ ford + %: ford:fusion + bud + *ankh:clay + deletes=~ + changes + file-store=~ + *ford-cache:fusion + == + =/ [res=vase nub=state:ford:fusion] (build-file:ford /lib/foo/hoon) + %+ expect-eq + res + !> *mime +:: +++ test-fasbuc ^- tang + =/ changes + %- my + :~ [/mar/mime/hoon &+hoon+mar-mime] + [/mar/html/hoon &+hoon+mar-html] + [/lib/foo/hoon &+hoon+'/$ foo %mime %html\0a*foo'] + == + =/ ford + %: ford:fusion + bud + *ankh:clay + deletes=~ + changes + file-store=~ + *ford-cache:fusion + == + =/ [res=vase nub=state:ford:fusion] (build-file:ford /lib/foo/hoon) + %+ expect-eq + res + !> '' +:: +++ test-gen-hello ^- tang + =/ ford + %: ford:fusion + bud + *ankh:clay + deletes=~ + changes=(my [/gen/hello/hoon &+hoon+gen-hello]~) file-store=~ *ford-cache:fusion == @@ -116,37 +225,21 @@ :: %+ expect-eq !> (~(gas in *(set path)) /gen/hello/hoon ~) - !> dez:(~(got by vases.cache.nub) /gen/hello/hoon) + !> dez:(~(got by files.cache.nub) /gen/hello/hoon) == :: -++ test-strandio-lib ^- tang - =/ =ankh:clay - :- fil=~ - %- ~(gas by *(map @tas ankh:clay)) - :~ :+ %lib fil=~ - %- ~(gas by *(map @tas ankh:clay)) - :~ :+ %strandio fil=~ - %- ~(gas by *(map @tas ankh:clay)) - :~ :+ %hoon fil=`[*lobe:clay hoon+!>(strandio-lib)] dir=~ - == - :: - :+ %strand fil=~ - %- ~(gas by *(map @tas ankh:clay)) - :~ :+ %hoon fil=`[*lobe:clay hoon+!>(strand-lib)] dir=~ - == == - :: - :+ %sur fil=~ - %- ~(gas by *(map @tas ankh:clay)) - :~ :+ %spider fil=~ - %- ~(gas by *(map @tas ankh:clay)) - :~ :+ %hoon fil=`[*lobe:clay hoon+!>(spider-sur)] dir=~ - == == == +++ test-lib-strandio ^- tang =/ ford %: ford:fusion bud - ankh + *ankh:clay deletes=~ - changes=~ + ^= changes + %- my + :~ [/lib/strand/hoon &+hoon+lib-strand] + [/lib/strandio/hoon &+hoon+lib-strandio] + [/sur/spider/hoon &+hoon+sur-spider] + == file-store=~ *ford-cache:fusion == @@ -161,7 +254,7 @@ /lib/strand/hoon /sur/spider/hoon == - !> dez:(~(got by vases.cache.nub) /lib/strandio/hoon) + !> dez:(~(got by files.cache.nub) /lib/strandio/hoon) == :: :: |utilities: helper functions for testing diff --git a/pkg/arvo/tests/sys/vane/eyre.hoon b/pkg/arvo/tests/sys/vane/eyre.hoon index 0bc7c271ed..0b83d3a843 100644 --- a/pkg/arvo/tests/sys/vane/eyre.hoon +++ b/pkg/arvo/tests/sys/vane/eyre.hoon @@ -2360,7 +2360,6 @@ ++ mash !! ++ pact !! ++ vale |=(=noun !>(;;(json noun))) - ++ volt !! -- :: ?> =(%j view) diff --git a/pkg/arvo/tests/sys/zuse/format.hoon b/pkg/arvo/tests/sys/zuse/format.hoon index 25d1918e99..8719ab241e 100644 --- a/pkg/arvo/tests/sys/zuse/format.hoon +++ b/pkg/arvo/tests/sys/zuse/format.hoon @@ -170,11 +170,15 @@ %+ expect-eq !> [%o (molt props)] !> (pairs props) + :: sect - stored as integer number of seconds since the unix epoch + %+ expect-eq + !> [%n '1'] + !> (sect ~1970.1.1..0.0.1) :: time - stored as integer number of milliseconds since the unix epoch :: %+ expect-eq - !> [%n '1000'] - !> (time ~1970.1.1..0.0.1) + !> [%n '1000'] + !> (time ~1970.1.1..0.0.1) :: ship - store ship identity as a string :: %+ expect-eq diff --git a/pkg/arvo/tests/sys/zuse/ordered-map.hoon b/pkg/arvo/tests/sys/zuse/ordered-map.hoon index ad9f24a90f..261b8f0851 100644 --- a/pkg/arvo/tests/sys/zuse/ordered-map.hoon +++ b/pkg/arvo/tests/sys/zuse/ordered-map.hoon @@ -129,6 +129,30 @@ !> -.b == :: +++ test-ordered-map-traverse-delete-all ^- tang + ;: weld + =/ q ((ordered-map ,@ ,~) lte) + =/ o (gas:q ~ ~[1/~ 2/~ 3/~]) + =/ b ((traverse:q ,~) o ~ |=([~ key=@ ~] [~ %| ~])) + %+ expect-eq + !> [~ ~] + !> b + :: + =/ c + :~ [[2.127 1] ~] [[2.127 2] ~] [[2.127 3] ~] + [[2.127 7] ~] [[2.127 8] ~] [[2.127 9] ~] + == + =/ compare + |= [[aa=@ ab=@] [ba=@ bb=@]] + ?:((lth aa ba) %.y ?:((gth aa ba) %.n (lte ab bb))) + =/ q ((ordered-map ,[@ @] ,~) compare) + =/ o (gas:q ~ c) + =/ b ((traverse:q ,~) o ~ |=([~ key=[@ @] ~] [~ %| ~])) + %+ expect-eq + !> [~ ~] + !> b + == +:: ++ test-ordered-map-uni ^- tang :: =/ a=(tree [@ud @tas]) (gas:atom-map ~ (scag 4 test-items)) diff --git a/pkg/interface/.eslintrc.js b/pkg/interface/.eslintrc.js index 0f63724ca8..fba1ccabcc 100644 --- a/pkg/interface/.eslintrc.js +++ b/pkg/interface/.eslintrc.js @@ -1,186 +1,3 @@ -const env = { - "browser": true, - "es6": true, - "node": true -}; - -const rules = { - "array-bracket-spacing": ["error", "never"], - "arrow-parens": [ - "error", - "as-needed", - { - "requireForBlockBody": true - } - ], - "arrow-spacing": "error", - "block-spacing": ["error", "always"], - "brace-style": ["error", "1tbs"], - "camelcase": [ - "error", - { - "properties": "never" - } - ], - "comma-dangle": ["error", "never"], - "eol-last": ["error", "always"], - "func-name-matching": "error", - "indent": [ - "off", - 2, - { - "ArrayExpression": "off", - "SwitchCase": 1, - "CallExpression": { - "arguments": "off" - }, - "FunctionDeclaration": { - "parameters": "off" - }, - "FunctionExpression": { - "parameters": "off" - }, - "MemberExpression": "off", - "ObjectExpression": "off", - "ImportDeclaration": "off" - } - ], - "handle-callback-err": "off", - "linebreak-style": ["error", "unix"], - "max-lines": [ - "error", - { - "max": 300, - "skipBlankLines": true, - "skipComments": true - } - ], - "max-lines-per-function": [ - "warn", - { - "skipBlankLines": true, - "skipComments": true - } - ], - "max-statements-per-line": [ - "error", - { - "max": 1 - } - ], - "new-cap": [ - "error", - { - "newIsCap": true, - "capIsNew": false - } - ], - "new-parens": "error", - "no-buffer-constructor": "error", - "no-console": "off", - "no-extra-semi": "off", - "no-fallthrough": "off", - "no-func-assign": "off", - "no-implicit-coercion": "error", - "no-multi-assign": "error", - "no-multiple-empty-lines": [ - "error", - { - "max": 1 - } - ], - "no-nested-ternary": "error", - "no-param-reassign": "off", - "no-return-assign": "error", - "no-return-await": "off", - "no-shadow-restricted-names": "error", - "no-tabs": "error", - "no-trailing-spaces": "error", - "no-unused-vars": [ - "error", - { - "vars": "all", - "args": "none", - "ignoreRestSiblings": false - } - ], - "no-use-before-define": [ - "error", - { - "functions": false, - "classes": false - } - ], - "no-useless-escape": "off", - "no-var": "error", - "nonblock-statement-body-position": ["error", "below"], - "object-curly-spacing": ["error", "always"], - "padded-blocks": ["error", "never"], - "prefer-arrow-callback": "error", - "prefer-const": [ - "error", - { - "destructuring": "all", - "ignoreReadBeforeAssign": true - } - ], - "prefer-template": "off", - "quotes": ["error", "single"], - "semi": ["error", "always"], - "spaced-comment": [ - "error", - "always", - { - "exceptions": ["!"] - } - ], - "space-before-blocks": "error", - "unicode-bom": ["error", "never"], - "valid-jsdoc": "error", - "wrap-iife": ["error", "inside"], - "react/jsx-closing-bracket-location": 1, - "react/jsx-tag-spacing": 1, - "react/jsx-max-props-per-line": ["error", { "maximum": 2, "when": "multiline" }], - "react/prop-types": 0 -}; - module.exports = { - "env": env, - "extends": [ - "plugin:react/recommended", - "eslint:recommended", - ], - "settings": { - "react": { - "version": "^16.5.2" - } - }, - "parser": "babel-eslint", - "parserOptions": { - "ecmaVersion": 10, - "requireConfigFile": false, - "sourceType": "module" - }, - "root": true, - "rules": rules, - "overrides": [ - { - "files": ["**/*.ts", "**/*.tsx"], - "env": env, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { "jsx": true }, - "ecmaVersion": 10, - "requireConfigFile": false, - "sourceType": "module" - }, - "plugins": ["@typescript-eslint"], - "rules": rules - } - ] -}; + extends: "@urbit" +}; \ No newline at end of file diff --git a/pkg/interface/config/webpack.dev.js b/pkg/interface/config/webpack.dev.js index 1e6c527643..66bf83ce9d 100644 --- a/pkg/interface/config/webpack.dev.js +++ b/pkg/interface/config/webpack.dev.js @@ -64,6 +64,12 @@ if(urbitrc.URL) { return '/index.js' } }, + '/~landscape/js/serviceworker.js': { + target: 'http://localhost:9000', + pathRewrite: (req, path) => { + return '/serviceworker.js' + } + }, '**': { changeOrigin: true, target: urbitrc.URL, @@ -78,7 +84,8 @@ if(urbitrc.URL) { module.exports = { mode: 'development', entry: { - app: './src/index.js' + app: './src/index.js', + serviceworker: './src/serviceworker.js' }, module: { rules: [ @@ -120,11 +127,11 @@ module.exports = { plugins: [ new UrbitShipPlugin(urbitrc), new webpack.DefinePlugin({ - 'process.env.TUTORIAL_HOST': JSON.stringify('~hastuc-dibtux'), + 'process.env.TUTORIAL_HOST': JSON.stringify('~difmex-passed'), 'process.env.TUTORIAL_GROUP': JSON.stringify('beginner-island'), - 'process.env.TUTORIAL_CHAT': JSON.stringify('chat-1704'), - 'process.env.TUTORIAL_BOOK': JSON.stringify('book-9695'), - 'process.env.TUTORIAL_LINKS': JSON.stringify('link-2827'), + 'process.env.TUTORIAL_CHAT': JSON.stringify('introduce-yourself-7010'), + 'process.env.TUTORIAL_BOOK': JSON.stringify('guides-9684'), + 'process.env.TUTORIAL_LINKS': JSON.stringify('community-articles-2143'), }) // new CleanWebpackPlugin(), @@ -135,10 +142,13 @@ module.exports = { ], watch: true, output: { - filename: 'index.js', - chunkFilename: 'index.js', + filename: (pathData) => { + return pathData.chunk.name === 'app' ? 'index.js' : '[name].js'; + }, + chunkFilename: '[name].js', path: path.resolve(__dirname, '../dist'), - publicPath: '/' + publicPath: '/', + globalObject: 'this' }, optimization: { minimize: false, diff --git a/pkg/interface/config/webpack.prod.js b/pkg/interface/config/webpack.prod.js index c2b1612700..8d9f527090 100644 --- a/pkg/interface/config/webpack.prod.js +++ b/pkg/interface/config/webpack.prod.js @@ -3,11 +3,15 @@ const path = require('path'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const MomentLocalesPlugin = require('moment-locales-webpack-plugin'); const webpack = require('webpack'); +const { execSync } = require('child_process'); + +const GIT_DESC = execSync('git describe --always', { encoding: 'utf8' }).trim(); module.exports = { mode: 'production', entry: { - app: './src/index.js' + app: './src/index.js', + serviceworker: './src/serviceworker.js' }, module: { rules: [ @@ -56,12 +60,12 @@ module.exports = { new CleanWebpackPlugin(), new webpack.DefinePlugin({ 'process.env.LANDSCAPE_STREAM': JSON.stringify(process.env.LANDSCAPE_STREAM), - 'process.env.LANDSCAPE_SHORTHASH': JSON.stringify(process.env.LANDSCAPE_SHORTHASH), - 'process.env.TUTORIAL_HOST': JSON.stringify('~hastuc-dibtux'), + 'process.env.LANDSCAPE_SHORTHASH': JSON.stringify(GIT_DESC), + 'process.env.TUTORIAL_HOST': JSON.stringify('~difmex-passed'), 'process.env.TUTORIAL_GROUP': JSON.stringify('beginner-island'), - 'process.env.TUTORIAL_CHAT': JSON.stringify('chat-8401'), - 'process.env.TUTORIAL_BOOK': JSON.stringify('notebook-9148'), - 'process.env.TUTORIAL_LINKS': JSON.stringify('links-4353'), + 'process.env.TUTORIAL_CHAT': JSON.stringify('introduce-yourself-7010'), + 'process.env.TUTORIAL_BOOK': JSON.stringify('guides-9684'), + 'process.env.TUTORIAL_LINKS': JSON.stringify('community-articles-2143'), }), // new HtmlWebpackPlugin({ // title: 'Hot Module Replacement', @@ -69,7 +73,9 @@ module.exports = { // }), ], output: { - filename: 'index.[contenthash].js', + filename: (pathData) => { + return pathData.chunk.name === 'app' ? 'index.[contenthash].js' : '[name].js'; + }, path: path.resolve(__dirname, '../../arvo/app/landscape/js/bundle'), publicPath: '/' }, diff --git a/pkg/interface/package-lock.json b/pkg/interface/package-lock.json index 8eecc5bbf0..b63ac3536b 100644 --- a/pkg/interface/package-lock.json +++ b/pkg/interface/package-lock.json @@ -1165,6 +1165,67 @@ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, + "@eslint/eslintrc": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz", + "integrity": "sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.1.1", + "espree": "^7.3.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.2.1", + "js-yaml": "^3.13.1", + "lodash": "^4.17.20", + "minimatch": "^3.0.4", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + } + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", + "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.4", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", + "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", + "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.4", + "fastq": "^1.6.0" + } + }, "@reach/auto-id": { "version": "0.10.5", "resolved": "https://registry.npmjs.org/@reach/auto-id/-/auto-id-0.10.5.tgz", @@ -1378,9 +1439,9 @@ "integrity": "sha512-kBzJueOoGDVF2knGt+Kf5ylvil6+V1qn8/RqAj1S6wUTnfUfAMRzDp4LQI2MxLI8Is0OG3XCErVSOUImU6R3lg==" }, "@tlon/indigo-react": { - "version": "1.2.17", - "resolved": "https://registry.npmjs.org/@tlon/indigo-react/-/indigo-react-1.2.17.tgz", - "integrity": "sha512-D53HDLbqkRX3nY5zcXv8DRHw7FhsCGYfY3xa8CbaFfhFupdXBHi96UURi9Qq3sBc4FHgnPj45eJflji7Yj3gYg==", + "version": "1.2.19", + "resolved": "https://registry.npmjs.org/@tlon/indigo-react/-/indigo-react-1.2.19.tgz", + "integrity": "sha512-lcHtPIbKeXVDvqd9dkCswB++CLRB2TsYFoegRU5VX3A886R+larJP81CzmoAwmZiJL3OnwypRklyfAv41F6W2w==", "requires": { "@reach/menu-button": "^0.10.5", "react": "^16.13.1", @@ -1403,12 +1464,6 @@ "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==", "dev": true }, - "@types/eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", - "dev": true - }, "@types/glob": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", @@ -1596,14 +1651,16 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz", - "integrity": "sha512-PQg0emRtzZFWq6PxBcdxRH3QIQiyFO3WCVpRL3fgj5oQS3CDs3AeAKfv4DxNhzn8ITdNJGJ4D3Qw8eAJf3lXeQ==", + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.15.0.tgz", + "integrity": "sha512-DJgdGZW+8CFUTz5C/dnn4ONcUm2h2T0itWD85Ob5/V27Ndie8hUoX5HKyGssvR8sUMkAIlUc/AMK67Lqa3kBIQ==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "3.10.1", + "@typescript-eslint/experimental-utils": "4.15.0", + "@typescript-eslint/scope-manager": "4.15.0", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", + "lodash": "^4.17.15", "regexpp": "^3.0.0", "semver": "^7.3.2", "tsutils": "^3.17.1" @@ -1621,53 +1678,70 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz", - "integrity": "sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==", + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.15.0.tgz", + "integrity": "sha512-V4vaDWvxA2zgesg4KPgEGiomWEBpJXvY4ZX34Y3qxK8LUm5I87L+qGIOTd9tHZOARXNRt9pLbblSKiYBlGMawg==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/types": "3.10.1", - "@typescript-eslint/typescript-estree": "3.10.1", + "@typescript-eslint/scope-manager": "4.15.0", + "@typescript-eslint/types": "4.15.0", + "@typescript-eslint/typescript-estree": "4.15.0", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" } }, - "@typescript-eslint/parser": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.10.1.tgz", - "integrity": "sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw==", + "@typescript-eslint/scope-manager": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.15.0.tgz", + "integrity": "sha512-CSNBZnCC2jEA/a+pR9Ljh8Y+5TY5qgbPz7ICEk9WCpSEgT6Pi7H2RIjxfrrbUXvotd6ta+i27sssKEH8Azm75g==", "dev": true, "requires": { - "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "3.10.1", - "@typescript-eslint/types": "3.10.1", - "@typescript-eslint/typescript-estree": "3.10.1", - "eslint-visitor-keys": "^1.1.0" + "@typescript-eslint/types": "4.15.0", + "@typescript-eslint/visitor-keys": "4.15.0" } }, "@typescript-eslint/types": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", - "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==", + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.15.0.tgz", + "integrity": "sha512-su4RHkJhS+iFwyqyXHcS8EGPlUVoC+XREfy5daivjLur9JP8GhvTmDipuRpcujtGC4M+GYhUOJCPDE3rC5NJrg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz", - "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==", + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.15.0.tgz", + "integrity": "sha512-jG6xTmcNbi6xzZq0SdWh7wQ9cMb2pqXaUp6bUZOMsIlu5aOlxGxgE/t6L/gPybybQGvdguajXGkZKSndZJpksA==", "dev": true, "requires": { - "@typescript-eslint/types": "3.10.1", - "@typescript-eslint/visitor-keys": "3.10.1", + "@typescript-eslint/types": "4.15.0", + "@typescript-eslint/visitor-keys": "4.15.0", "debug": "^4.1.1", - "glob": "^7.1.6", + "globby": "^11.0.1", "is-glob": "^4.0.1", - "lodash": "^4.17.15", "semver": "^7.3.2", "tsutils": "^3.17.1" }, "dependencies": { + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "globby": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.2.tgz", + "integrity": "sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, "semver": { "version": "7.3.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", @@ -1680,14 +1754,72 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", - "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==", + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.15.0.tgz", + "integrity": "sha512-RnDtJwOwFucWFAMjG3ghCG/ikImFJFEg20DI7mn4pHEx3vC48lIAoyjhffvfHmErRDboUPC7p9Z2il4CLb7qxA==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.1.0" + "@typescript-eslint/types": "4.15.0", + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + } } }, + "@urbit/api": { + "version": "file:../npm/api", + "requires": { + "@babel/runtime": "^7.12.5", + "@types/lodash": "^4.14.168", + "@urbit/eslint-config": "^1.0.0", + "big-integer": "^1.6.48", + "lodash": "^4.17.20" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.12.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", + "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@types/lodash": { + "version": "4.14.168", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz", + "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==" + }, + "@urbit/eslint-config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@urbit/eslint-config/-/eslint-config-1.0.0.tgz", + "integrity": "sha512-Xmzb6MvM7KorlPJEq/hURZZ4BHSVy/7CoQXWogsBSTv5MOZnMqwNKw6yt24k2AO/2UpHwjGptimaNLqFfesJbw==" + }, + "big-integer": { + "version": "1.6.48", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", + "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==" + }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + } + } + }, + "@urbit/eslint-config": { + "version": "file:../npm/eslint-config", + "dev": true + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -1925,23 +2057,6 @@ "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", "dev": true }, - "ansi-escapes": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", - "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", - "dev": true, - "requires": { - "type-fest": "^0.11.0" - }, - "dependencies": { - "type-fest": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", - "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", - "dev": true - } - } - }, "ansi-html": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", @@ -2103,9 +2218,9 @@ "dev": true }, "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, "async": { @@ -2149,6 +2264,13 @@ "url": "0.10.3", "uuid": "3.3.2", "xml2js": "0.4.19" + }, + "dependencies": { + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + } } }, "babel-eslint": { @@ -2804,12 +2926,6 @@ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==" }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, "chokidar": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz", @@ -2906,21 +3022,6 @@ "del": "^4.1.1" } }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-width": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", - "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", - "dev": true - }, "cliui": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", @@ -3583,6 +3684,15 @@ "randombytes": "^2.0.0" } }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -3609,9 +3719,9 @@ } }, "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "requires": { "esutils": "^2.0.2" @@ -3839,6 +3949,23 @@ } } }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + }, + "dependencies": { + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + } + } + }, "entities": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", @@ -3904,80 +4031,99 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", - "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.19.0.tgz", + "integrity": "sha512-CGlMgJY56JZ9ZSYhJuhow61lMPPjUzWmChFya71Z/jilVos7mR/jPgaEfVGgMBY5DshbKdG8Ezb8FDCHcoMEMg==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", + "@eslint/eslintrc": "^0.3.0", "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", "debug": "^4.0.1", "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", + "enquirer": "^2.3.5", + "eslint-scope": "^5.1.1", + "eslint-utils": "^2.1.0", + "eslint-visitor-keys": "^2.0.0", + "espree": "^7.3.1", + "esquery": "^1.2.0", "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", + "file-entry-cache": "^6.0.0", "functional-red-black-tree": "^1.0.1", "glob-parent": "^5.0.0", "globals": "^12.1.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", "is-glob": "^4.0.0", "js-yaml": "^3.13.1", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", + "levn": "^0.4.1", + "lodash": "^4.17.20", "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", - "optionator": "^0.8.3", + "optionator": "^0.9.1", "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", + "regexpp": "^3.1.0", + "semver": "^7.2.1", + "strip-ansi": "^6.0.0", + "strip-json-comments": "^3.1.0", + "table": "^6.0.4", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } + "color-convert": "^2.0.1" } }, - "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { - "eslint-visitor-keys": "^1.1.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + }, "globals": { "version": "12.4.0", "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", @@ -3987,40 +4133,43 @@ "type-fest": "^0.8.1" } }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", "dev": true }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", "dev": true, "requires": { - "shebang-regex": "^1.0.0" + "lru-cache": "^6.0.0" } }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "isexe": "^2.0.0" + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" } } } @@ -4042,17 +4191,6 @@ "prop-types": "^15.7.2", "resolve": "^1.18.1", "string.prototype.matchall": "^4.0.2" - }, - "dependencies": { - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - } } }, "eslint-scope": { @@ -4081,14 +4219,14 @@ "dev": true }, "espree": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", - "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", + "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", "dev": true, "requires": { - "acorn": "^7.1.1", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.1.0" + "acorn": "^7.4.0", + "acorn-jsx": "^5.3.1", + "eslint-visitor-keys": "^1.3.0" } }, "esprima": { @@ -4098,9 +4236,9 @@ "dev": true }, "esquery": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", - "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -4396,17 +4534,6 @@ } } }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -4483,6 +4610,32 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "fast-glob": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", + "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + }, + "dependencies": { + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + } + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4494,6 +4647,15 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fastq": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.10.1.tgz", + "integrity": "sha512-AWuv6Ery3pM+dY7LYS8YIaCiQvUaos9OB1RyNgaOWnaX+Tik7Onvcsf8x8c+YtDeT0maYLniBip2hox5KtEXXA==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, "faye-websocket": { "version": "0.11.3", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", @@ -4509,22 +4671,13 @@ "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", "dev": true }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.0.tgz", + "integrity": "sha512-fqoO76jZ3ZnYrXLDRxBR1YvOvc0k844kcOg40bgsPrE25LAb/PDqTY+ho64Xh2c8ZXgIKldchCFHczG2UVRcWA==", "dev": true, "requires": { - "flat-cache": "^2.0.1" + "flat-cache": "^3.0.4" } }, "file-loader": { @@ -4657,20 +4810,19 @@ } }, "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", "dev": true, "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" + "flatted": "^3.1.0", + "rimraf": "^3.0.2" }, "dependencies": { "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" @@ -4679,9 +4831,9 @@ } }, "flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz", + "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==", "dev": true }, "flush-write-stream": { @@ -5375,9 +5527,9 @@ "dev": true }, "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", "dev": true }, "immer": { @@ -5488,87 +5640,6 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, - "inquirer": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", - "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-width": "^3.0.0", - "external-editor": "^3.0.3", - "figures": "^3.0.0", - "lodash": "^4.17.19", - "mute-stream": "0.0.8", - "run-async": "^2.4.0", - "rxjs": "^6.6.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "internal-ip": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", @@ -5580,33 +5651,25 @@ } }, "internal-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz", - "integrity": "sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", "dev": true, "requires": { - "es-abstract": "^1.17.0-next.1", + "get-intrinsic": "^1.1.0", "has": "^1.0.3", - "side-channel": "^1.0.2" + "side-channel": "^1.0.4" }, "dependencies": { - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", "dev": true, "requires": { - "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" + "has-symbols": "^1.0.1" } } } @@ -5979,13 +6042,13 @@ } }, "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" } }, "loader-runner": { @@ -6181,6 +6244,12 @@ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", "dev": true }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -6329,12 +6398,6 @@ "mime-db": "1.45.0" } }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true - }, "min-document": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", @@ -6486,12 +6549,6 @@ "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", "dev": true }, - "mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true - }, "nan": { "version": "2.14.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", @@ -6642,6 +6699,14 @@ "requires": { "punycode": "1.3.2", "querystring": "0.2.0" + }, + "dependencies": { + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + } } } } @@ -6861,15 +6926,6 @@ "wrappy": "1" } }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "requires": { - "mimic-fn": "^2.1.0" - } - }, "opn": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", @@ -6880,17 +6936,17 @@ } }, "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", "dev": true, "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" } }, "original": { @@ -6908,12 +6964,6 @@ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", "dev": true }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -7132,6 +7182,12 @@ } } }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, "pbkdf2": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", @@ -7273,9 +7329,9 @@ "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==" }, "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, "pretty-error": { @@ -7402,9 +7458,9 @@ "dev": true }, "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.1.tgz", + "integrity": "sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==" }, "querystring-es3": { "version": "0.2.1", @@ -7418,6 +7474,12 @@ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", "dev": true }, + "queue-microtask": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.2.tgz", + "integrity": "sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg==", + "dev": true + }, "ramda": { "version": "0.27.1", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.1.tgz", @@ -7920,6 +7982,12 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -8010,16 +8078,6 @@ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "dev": true }, - "restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - } - }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -8032,6 +8090,12 @@ "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", "dev": true }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -8051,11 +8115,14 @@ "inherits": "^2.0.1" } }, - "run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } }, "run-queue": { "version": "1.0.3", @@ -8066,23 +8133,6 @@ "aproba": "^1.1.1" } }, - "rxjs": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", - "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -8405,20 +8455,38 @@ "dev": true }, "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" }, "dependencies": { - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true } } @@ -9017,39 +9085,34 @@ "integrity": "sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ==" }, "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/table/-/table-6.0.7.tgz", + "integrity": "sha512-rxZevLGTUzWna/qBLObOe16kB2RTnnbhciwgPbMMlazz1yZGVEgnZK762xyVdVznhqxrfCeBMmMkgOOaPwjH7g==", "dev": true, "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" + "ajv": "^7.0.2", + "lodash": "^4.17.20", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.0" }, "dependencies": { - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "ajv": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.1.0.tgz", + "integrity": "sha512-svS9uILze/cXbH0z2myCK2Brqprx/+JJYK5pHicT/GQiBfzzhUVAIT6MwqJg8y4xV/zoGsUeuPuwtoiKSGE15g==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true } } }, @@ -9184,12 +9247,6 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true - }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -9251,15 +9308,6 @@ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - }, "to-arraybuffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", @@ -9338,9 +9386,9 @@ "integrity": "sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==" }, "tsutils": { - "version": "3.19.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.19.1.tgz", - "integrity": "sha512-GEdoBf5XI324lu7ycad7s6laADfnAqCw6wLGI+knxvw9vsIYBaJfYdmeCEG3FMMUiSm3OGgNb+m6utsWf5h9Vw==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.20.0.tgz", + "integrity": "sha512-RYbuQuvkhuqVeXweWT3tJLKOEJ/UUw9GjNEZGWdrLLlM+611o1gwLHBpxoFJKKl25fLprp2eVthtKs5JOrNeXg==", "dev": true, "requires": { "tslib": "^1.8.1" @@ -9361,12 +9409,12 @@ "dev": true }, "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "requires": { - "prelude-ls": "~1.1.2" + "prelude-ls": "^1.2.1" } }, "type-fest": { @@ -9587,6 +9635,13 @@ "requires": { "punycode": "1.3.2", "querystring": "0.2.0" + }, + "dependencies": { + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + } } }, "url-parse": { @@ -10345,6 +10400,14 @@ "requires": { "punycode": "1.3.2", "querystring": "0.2.0" + }, + "dependencies": { + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + } } } } @@ -10407,6 +10470,66 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, + "workbox-cacheable-response": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.0.2.tgz", + "integrity": "sha512-OrgFiYWkmFXDIbNRYSu+fchcfoZqyJ4yZbdc8WKUjr9v/MghKHfR9u7UI077xBkjno5J3YNpbwx73/no3HkrzA==", + "requires": { + "workbox-core": "^6.0.2" + } + }, + "workbox-core": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-6.0.2.tgz", + "integrity": "sha512-Ksl6qeikGb+BOCILoCUJGxwlEQOeeqdpOnpOr9UDt3NtacPYbfYBmpYpKArw5DFWK+5geBsFqgUUlXThlCYfKQ==" + }, + "workbox-expiration": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.0.2.tgz", + "integrity": "sha512-6+nbR18cklAdI3BPT675ytftXPwnVbXGR8mPWNWTJtl5y2urRYv56ZOJLD7FBFVkZ8EjWiRhNP/A0fkxgdKtWQ==", + "requires": { + "workbox-core": "^6.0.2" + } + }, + "workbox-precaching": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.0.2.tgz", + "integrity": "sha512-sqKWL2emzmGnfJpna+9RjUkUiqQO++AKfwljCbgkHg8wBbVLy/rnui3eelKgAI7D8R31LJFfiZkY/kXmwkjtlQ==", + "requires": { + "workbox-core": "^6.0.2", + "workbox-routing": "^6.0.2", + "workbox-strategies": "^6.0.2" + } + }, + "workbox-recipes": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.0.2.tgz", + "integrity": "sha512-ewZIHO4jYE6bnEeUIYS6joQy3l+MydpOsVr2F6EpE8ps++z1ScbSdLtJU+yu6WuO3lH44HFZLeFxYQqYm50QAA==", + "requires": { + "workbox-cacheable-response": "^6.0.2", + "workbox-core": "^6.0.2", + "workbox-expiration": "^6.0.2", + "workbox-precaching": "^6.0.2", + "workbox-routing": "^6.0.2", + "workbox-strategies": "^6.0.2" + } + }, + "workbox-routing": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.0.2.tgz", + "integrity": "sha512-iQ9ch3fL1YpztDLfHNURaHQ0ispgPCdzWmZZhtSHUyy/+YkTlIiDVTbOQCIpHIrWlKQiim6X3K2ItIy1FW9+wA==", + "requires": { + "workbox-core": "^6.0.2" + } + }, + "workbox-strategies": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.0.2.tgz", + "integrity": "sha512-HjLnYCVS60U7OKhl5NIq8NAQXrotJQRDakmIONnRlQIlP2If/kAiQSUP3QCHMq4EeXGiF+/CdlR1/bhYBHZzZg==", + "requires": { + "workbox-core": "^6.0.2" + } + }, "worker-farm": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", @@ -10458,15 +10581,6 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, "ws": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", diff --git a/pkg/interface/package.json b/pkg/interface/package.json index c0cb24c391..c7db782f95 100644 --- a/pkg/interface/package.json +++ b/pkg/interface/package.json @@ -10,8 +10,9 @@ "@reach/tabs": "^0.10.5", "@tlon/indigo-dark": "^1.0.6", "@tlon/indigo-light": "^1.0.6", - "@tlon/indigo-react": "1.2.17", + "@tlon/indigo-react": "^1.2.19", "@tlon/sigil-js": "^1.4.3", + "@urbit/api": "file:../npm/api", "aws-sdk": "^2.830.0", "big-integer": "^1.6.48", "classnames": "^2.2.6", @@ -28,6 +29,7 @@ "normalize-wheel": "1.0.1", "oembed-parser": "^1.4.5", "prop-types": "^15.7.2", + "querystring": "^0.2.0", "react": "^16.14.0", "react-codemirror2": "^6.0.1", "react-dom": "^16.14.0", @@ -46,6 +48,10 @@ "urbit-ob": "^5.0.1", "xterm": "^4.10.0", "xterm-addon-fit": "^0.5.0", + "workbox-core": "^6.0.2", + "workbox-precaching": "^6.0.2", + "workbox-recipes": "^6.0.2", + "workbox-routing": "^6.0.2", "yup": "^0.29.3", "zustand": "^3.3.1" }, @@ -65,15 +71,15 @@ "@types/styled-components": "^5.1.7", "@types/styled-system": "^5.1.10", "@types/yup": "^0.29.11", - "@typescript-eslint/eslint-plugin": "^3.10.1", - "@typescript-eslint/parser": "^3.10.1", + "@typescript-eslint/eslint-plugin": "^4.15.0", + "@urbit/eslint-config": "file:../npm/eslint-config", "babel-eslint": "^10.1.0", "babel-loader": "^8.2.2", "babel-plugin-lodash": "^3.3.4", "babel-plugin-root-import": "^6.6.0", "clean-webpack-plugin": "^3.0.0", "cross-env": "^7.0.3", - "eslint": "^6.8.0", + "eslint": "^7.19.0", "eslint-plugin-react": "^7.22.0", "file-loader": "^6.2.0", "html-webpack-plugin": "^4.5.1", @@ -87,12 +93,12 @@ "webpack-dev-server": "^3.11.2" }, "scripts": { - "lint": "eslint ./src/**/*.{js,ts,tsx}", + "lint": "eslint ./src/**/*.{ts,tsx}", "lint-file": "eslint", "tsc": "tsc", "tsc:watch": "tsc --watch", "build:dev": "cross-env NODE_ENV=development webpack --config config/webpack.dev.js", - "build:prod": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js", + "build:prod": "cd ../npm/api && npm i && cd ../../interface && cross-env NODE_ENV=production webpack --config config/webpack.prod.js", "start": "webpack-dev-server --config config/webpack.dev.js", "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/pkg/interface/src/index.js b/pkg/interface/src/index.js index a056a0cb28..ded85a8f58 100644 --- a/pkg/interface/src/index.js +++ b/pkg/interface/src/index.js @@ -1,6 +1,8 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; +import './register-sw'; + import App from './views/App'; ReactDOM.render(, document.getElementById('root')); diff --git a/pkg/interface/src/logic/api/base.ts b/pkg/interface/src/logic/api/base.ts index 085c4cc326..d1122b4345 100644 --- a/pkg/interface/src/logic/api/base.ts +++ b/pkg/interface/src/logic/api/base.ts @@ -1,6 +1,5 @@ -import _ from "lodash"; -import { uuid } from "../lib/util"; -import { Patp, Path } from "~/types/noun"; +import _ from 'lodash'; +import { Patp, Path } from '@urbit/api'; import BaseStore from '../store/base'; export default class BaseApi { @@ -26,8 +25,8 @@ export default class BaseApi { data: event, from: { ship, - path, - }, + path + } }); }, (qui) => { @@ -50,14 +49,17 @@ export default class BaseApi { appl, mark, data, - (json) => { resolve(json); }, - (err) => { reject(err); } + (json) => { + resolve(json); +}, + (err) => { + reject(err); +} ); }); } scry(app: string, path: Path): Promise { - console.log(path); return fetch(`/~/scry/${app}${path}.json`).then(r => r.json() as Promise); } @@ -69,5 +71,4 @@ export default class BaseApi { return res.json(); } - } diff --git a/pkg/interface/src/logic/api/contacts.ts b/pkg/interface/src/logic/api/contacts.ts index ca1f0a338a..ae6a6972a0 100644 --- a/pkg/interface/src/logic/api/contacts.ts +++ b/pkg/interface/src/logic/api/contacts.ts @@ -1,8 +1,7 @@ import BaseApi from './base'; import { StoreState } from '../store/type'; -import { Patp, Path, Enc } from '~/types/noun'; -import { Contact, ContactEdit } from '~/types/contact-update'; -import { GroupPolicy, Resource } from '~/types/group-update'; +import { Patp } from '@urbit/api'; +import { ContactEdit } from '@urbit/api/contacts'; export default class ContactsApi extends BaseApi { add(ship: Patp, contact: any) { @@ -31,7 +30,7 @@ export default class ContactsApi extends BaseApi { ship, 'edit-field': editField, timestamp: Date.now() - }, + } }); } @@ -62,7 +61,7 @@ export default class ContactsApi extends BaseApi { return this.action( 'contact-push-hook', 'contact-share', - { share: recipient }, + { share: recipient } ); } @@ -85,7 +84,7 @@ export default class ContactsApi extends BaseApi { } private storeAction(action: any): Promise { - return this.action('contact-store', 'contact-update', action) + return this.action('contact-store', 'contact-update', action); } private viewAction(threadName: string, action: any) { diff --git a/pkg/interface/src/logic/api/gcp.ts b/pkg/interface/src/logic/api/gcp.ts new file mode 100644 index 0000000000..c6b955b22d --- /dev/null +++ b/pkg/interface/src/logic/api/gcp.ts @@ -0,0 +1,24 @@ +import BaseApi from './base'; +import {StoreState} from '../store/type'; +import {GcpToken} from '../types/gcp-state'; + + +export default class GcpApi extends BaseApi { + isConfigured() { + return this.spider('noun', 'json', 'gcp-is-configured', {}) + .then((data) => { + this.store.handleEvent({ + data + }); + }); + } + + getToken() { + return this.spider('noun', 'gcp-token', 'gcp-get-token', {}) + .then((token) => { + this.store.handleEvent({ + data: token + }); + }); + } +}; diff --git a/pkg/interface/src/logic/api/global.ts b/pkg/interface/src/logic/api/global.ts index 3264be194c..5d26f0b7c9 100644 --- a/pkg/interface/src/logic/api/global.ts +++ b/pkg/interface/src/logic/api/global.ts @@ -1,4 +1,4 @@ -import { Patp } from '~/types/noun'; +import { Patp } from '@urbit/api'; import BaseApi from './base'; import { StoreState } from '../store/type'; import GlobalStore from '../store/store'; @@ -10,7 +10,8 @@ import GroupsApi from './groups'; import LaunchApi from './launch'; import GraphApi from './graph'; import S3Api from './s3'; -import {HarkApi} from './hark'; +import GcpApi from './gcp'; +import { HarkApi } from './hark'; import SettingsApi from './settings'; import TermApi from './term'; @@ -21,6 +22,7 @@ export default class GlobalApi extends BaseApi { contacts = new ContactsApi(this.ship, this.channel, this.store); groups = new GroupsApi(this.ship, this.channel, this.store); launch = new LaunchApi(this.ship, this.channel, this.store); + gcp = new GcpApi(this.ship, this.channel, this.store); s3 = new S3Api(this.ship, this.channel, this.store); graph = new GraphApi(this.ship, this.channel, this.store); hark = new HarkApi(this.ship, this.channel, this.store); diff --git a/pkg/interface/src/logic/api/graph.ts b/pkg/interface/src/logic/api/graph.ts index 0606a2c1e8..3c9b71b43a 100644 --- a/pkg/interface/src/logic/api/graph.ts +++ b/pkg/interface/src/logic/api/graph.ts @@ -1,14 +1,14 @@ import BaseApi from './base'; import { StoreState } from '../store/type'; -import { Patp, Path, PatpNoSig } from '~/types/noun'; +import { Patp, Path } from '@urbit/api'; import _ from 'lodash'; -import {makeResource, resourceFromPath} from '../lib/group'; -import {GroupPolicy, Enc, Post, NodeMap, Content, Resource} from '~/types'; +import { makeResource, resourceFromPath } from '../lib/group'; +import { GroupPolicy, Enc, Post, Content } from '@urbit/api'; import { numToUd, unixToDa, decToUd, deSig, resourceAsPath } from '~/logic/lib/util'; export const createBlankNodeWithChildPost = ( - parentIndex: string = '', - childIndex: string = '', + parentIndex = '', + childIndex = '', contents: Content[] ) => { const date = unixToDa(Date.now()).toString(); @@ -37,11 +37,11 @@ export const createBlankNodeWithChildPost = ( signatures: [] }, children: childGraph - }; + }; }; function markPending(nodes: any) { - _.forEach(nodes, node => { + _.forEach(nodes, (node) => { node.post.author = deSig(node.post.author); node.post.pending = true; markPending(node.children || {}); @@ -50,8 +50,8 @@ function markPending(nodes: any) { export const createPost = ( contents: Content[], - parentIndex: string = '', - childIndex:string = 'DATE_PLACEHOLDER' + parentIndex = '', + childIndex = 'DATE_PLACEHOLDER' ) => { if (childIndex === 'DATE_PLACEHOLDER') { childIndex = unixToDa(Date.now()).toString(); @@ -80,11 +80,10 @@ function moduleToMark(mod: string): string | undefined { } export default class GraphApi extends BaseApi { - joiningGraphs = new Set(); private storeAction(action: any): Promise { - return this.action('graph-store', 'graph-update', action) + return this.action('graph-store', 'graph-update', action); } private viewAction(threadName: string, action: any) { @@ -106,12 +105,12 @@ export default class GraphApi extends BaseApi { const resource = makeResource(`~${window.ship}`, name); return this.viewAction('graph-create', { - "create": { + 'create': { resource, title, description, associated, - "module": mod, + 'module': mod, mark: moduleToMark(mod) } }); @@ -127,12 +126,12 @@ export default class GraphApi extends BaseApi { const resource = makeResource(`~${window.ship}`, name); return this.viewAction('graph-create', { - "create": { + 'create': { resource, title, description, associated: { policy }, - "module": mod, + 'module': mod, mark: moduleToMark(mod) } }); @@ -148,9 +147,9 @@ export default class GraphApi extends BaseApi { return this.viewAction('graph-join', { join: { resource, - ship, + ship } - }).then(res => { + }).then((res) => { this.joiningGraphs.delete(rid); return res; }); @@ -159,7 +158,7 @@ export default class GraphApi extends BaseApi { deleteGraph(name: string) { const resource = makeResource(`~${window.ship}`, name); return this.viewAction('graph-delete', { - "delete": { + 'delete': { resource } }); @@ -168,7 +167,7 @@ export default class GraphApi extends BaseApi { leaveGraph(ship: Patp, name: string) { const resource = makeResource(ship, name); return this.viewAction('graph-leave', { - "leave": { + 'leave': { resource } }); @@ -192,6 +191,7 @@ export default class GraphApi extends BaseApi { }); } + addGraph(ship: Patp, name: string, graph: any, mark: any) { return this.storeAction({ 'add-graph': { @@ -203,7 +203,7 @@ export default class GraphApi extends BaseApi { } addPost(ship: Patp, name: string, post: Post) { - let nodes = {}; + const nodes = {}; nodes[post.index] = { post, children: null @@ -212,7 +212,7 @@ export default class GraphApi extends BaseApi { } addNode(ship: Patp, name: string, node: Object) { - let nodes = {}; + const nodes = {}; nodes[node.post.index] = node; return this.addNodes(ship, name, nodes); @@ -226,12 +226,37 @@ export default class GraphApi extends BaseApi { } }; - const promise = this.hookAction(ship, action); + const pendingPromise = this.spider( + 'graph-update', + 'graph-view-action', + 'graph-add-nodes', + action + ); + markPending(action['add-nodes'].nodes); - action['add-nodes'].resource.ship = action['add-nodes'].resource.ship.slice(1); - console.log(action); - this.store.handleEvent({ data: { 'graph-update': action } }); - return promise; + action['add-nodes'].resource.ship = + action['add-nodes'].resource.ship.slice(1); + + this.store.handleEvent({ data: { + 'graph-update': action + } }); + + return pendingPromise; + /* TODO: stop lying to our users about pending states + return pendingPromise.then((pendingHashes) => { + for (let index in action['add-nodes'].nodes) { + action['add-nodes'].nodes[index].post.hash = + pendingHashes['pending-indices'][index] || null; + } + + this.store.handleEvent({ data: { + 'graph-update': { + 'pending-indices': pendingHashes['pending-indices'], + ...action + } + } }); + }); + */ } removeNodes(ship: Patp, name: string, indices: string[]) { @@ -300,7 +325,6 @@ export default class GraphApi extends BaseApi { this.store.handleEvent({ data }); } - getGraphSubset(ship: string, resource: string, start: string, end: string) { return this.scry( 'graph-store', diff --git a/pkg/interface/src/logic/api/groups.ts b/pkg/interface/src/logic/api/groups.ts index de2f4b488f..f0bab91f0b 100644 --- a/pkg/interface/src/logic/api/groups.ts +++ b/pkg/interface/src/logic/api/groups.ts @@ -1,14 +1,14 @@ import BaseApi from './base'; import { StoreState } from '../store/type'; -import { Path, Patp, Enc } from '~/types/noun'; +import { Path, Patp, Enc } from '@urbit/api'; import { GroupAction, GroupPolicy, Resource, Tag, - GroupPolicyDiff, -} from '~/types/group-update'; -import {makeResource} from '../lib/group'; + GroupPolicyDiff +} from '@urbit/api/groups'; +import { makeResource } from '../lib/group'; export default class GroupsApi extends BaseApi { remove(resource: Resource, ships: Patp[]) { @@ -38,7 +38,7 @@ export default class GroupsApi extends BaseApi { join(ship: string, name: string) { const resource = makeResource(ship, name); - return this.viewAction({ join: { resource, ship }}); + return this.viewAction({ join: { resource, ship } }); } create(name: string, policy: Enc, title: string, description: string) { @@ -76,7 +76,6 @@ export default class GroupsApi extends BaseApi { description } }); - } private proxyAction(action: GroupAction) { @@ -93,6 +92,5 @@ export default class GroupsApi extends BaseApi { private viewAction(action: any) { return this.action('group-view', 'group-view-action', action); - } } diff --git a/pkg/interface/src/logic/api/hark.ts b/pkg/interface/src/logic/api/hark.ts index 4dadeb1690..b807201a1a 100644 --- a/pkg/interface/src/logic/api/hark.ts +++ b/pkg/interface/src/logic/api/hark.ts @@ -1,24 +1,23 @@ -import BaseApi from "./base"; -import { StoreState } from "../store/type"; -import { dateToDa, decToUd } from "../lib/util"; -import {NotifIndex, IndexedNotification, Association, GraphNotifDescription} from "~/types"; +import BaseApi from './base'; +import { StoreState } from '../store/type'; +import { dateToDa, decToUd } from '../lib/util'; +import { NotifIndex, IndexedNotification, Association, GraphNotifDescription } from '@urbit/api'; import { BigInteger } from 'big-integer'; -import {getParentIndex} from "../lib/notification"; +import { getParentIndex } from '../lib/notification'; export class HarkApi extends BaseApi { private harkAction(action: any): Promise { - return this.action("hark-store", "hark-action", action); + return this.action('hark-store', 'hark-action', action); } private graphHookAction(action: any) { - return this.action("hark-graph-hook", "hark-graph-hook-action", action); + return this.action('hark-graph-hook', 'hark-graph-hook-action', action); } private groupHookAction(action: any) { - return this.action("hark-group-hook", "hark-group-hook-action", action); + return this.action('hark-group-hook', 'hark-group-hook-action', action); } - private actOnNotification(frond: string, intTime: BigInteger, index: NotifIndex) { const time = decToUd(intTime.toString()); return this.harkAction({ @@ -74,12 +73,10 @@ export class HarkApi extends BaseApi { module: association.metadata.module, description, index: parent - } }, + } } }); } - - markEachAsRead(association: Association, parent: string, child: string, description: GraphNotifDescription, mod: string) { return this.harkAction({ 'read-each': { @@ -116,7 +113,7 @@ export class HarkApi extends BaseApi { mute(notif: IndexedNotification) { if('graph' in notif.index && 'graph' in notif.notification.contents) { const { index } = notif; - const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph) + const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph); if(!parentIndex) { return Promise.resolve(); } @@ -132,7 +129,7 @@ export class HarkApi extends BaseApi { unmute(notif: IndexedNotification) { if('graph' in notif.index && 'graph' in notif.notification.contents) { const { index } = notif; - const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph) + const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph); if(!parentIndex) { return Promise.resolve(); } @@ -147,7 +144,7 @@ export class HarkApi extends BaseApi { ignoreGroup(group: string) { return this.groupHookAction({ ignore: group - }) + }); } ignoreGraph(graph: string, index: string) { @@ -156,13 +153,13 @@ export class HarkApi extends BaseApi { graph, index } - }) + }); } listenGroup(group: string) { return this.groupHookAction({ listen: group - }) + }); } listenGraph(graph: string, index: string) { @@ -171,7 +168,7 @@ export class HarkApi extends BaseApi { graph, index } - }) + }); } async getMore(): Promise { @@ -183,16 +180,16 @@ export class HarkApi extends BaseApi { async getSubset(offset:number, count:number, isArchive: boolean) { const where = isArchive ? 'archive' : 'inbox'; - const data = await this.scry("hark-store", `/recent/${where}/${offset}/${count}`); + const data = await this.scry('hark-store', `/recent/${where}/${offset}/${count}`); this.store.handleEvent({ data }); } async getTimeSubset(start?: Date, end?: Date) { - const s = start ? dateToDa(start) : "-"; - const e = end ? dateToDa(end) : "-"; - const result = await this.scry("hark-hook", `/recent/${s}/${e}`); + const s = start ? dateToDa(start) : '-'; + const e = end ? dateToDa(end) : '-'; + const result = await this.scry('hark-hook', `/recent/${s}/${e}`); this.store.handleEvent({ - data: result, + data: result }); } } diff --git a/pkg/interface/src/logic/api/invite.ts b/pkg/interface/src/logic/api/invite.ts index 89a7307688..dc390d12d1 100644 --- a/pkg/interface/src/logic/api/invite.ts +++ b/pkg/interface/src/logic/api/invite.ts @@ -1,6 +1,6 @@ -import BaseApi from "./base"; -import { StoreState } from "../store/type"; -import { Serial, Path } from "~/types/noun"; +import BaseApi from './base'; +import { StoreState } from '../store/type'; +import { Serial, Path } from '@urbit/api'; export default class InviteApi extends BaseApi { accept(app: string, uid: Serial) { diff --git a/pkg/interface/src/logic/api/launch.ts b/pkg/interface/src/logic/api/launch.ts index ce0a09a1c0..3bba3b3cc0 100644 --- a/pkg/interface/src/logic/api/launch.ts +++ b/pkg/interface/src/logic/api/launch.ts @@ -2,7 +2,7 @@ import BaseApi from './base'; import { StoreState } from '../store/type'; export default class LaunchApi extends BaseApi { - add(name: string, tile = { basic : { title: '', linkedUrl: '', iconUrl: '' }}) { + add(name: string, tile = { basic : { title: '', linkedUrl: '', iconUrl: '' } }) { return this.launchAction({ add: { name, tile } }); } @@ -15,7 +15,7 @@ export default class LaunchApi extends BaseApi { } changeIsShown(name: string, isShown = true) { - return this.launchAction({ 'change-is-shown': { name, isShown }}); + return this.launchAction({ 'change-is-shown': { name, isShown } }); } weather(location: string) { diff --git a/pkg/interface/src/logic/api/local.ts b/pkg/interface/src/logic/api/local.ts index 8446f27ce3..e5bad49a3f 100644 --- a/pkg/interface/src/logic/api/local.ts +++ b/pkg/interface/src/logic/api/local.ts @@ -1,9 +1,9 @@ -import BaseApi from "./base"; -import { StoreState } from "../store/type"; +import BaseApi from './base'; +import { StoreState } from '../store/type'; export default class LocalApi extends BaseApi { getBaseHash() { - this.scry('file-server', '/clay/base/hash').then(baseHash => { + this.scry('file-server', '/clay/base/hash').then((baseHash) => { this.store.handleEvent({ data: { local: { baseHash } } }); }); } @@ -11,5 +11,4 @@ export default class LocalApi extends BaseApi { dehydrate() { this.store.dehydrate(); } - } diff --git a/pkg/interface/src/logic/api/metadata.ts b/pkg/interface/src/logic/api/metadata.ts index c4e20ae2f1..c2d388dfc3 100644 --- a/pkg/interface/src/logic/api/metadata.ts +++ b/pkg/interface/src/logic/api/metadata.ts @@ -1,12 +1,10 @@ import BaseApi from './base'; import { StoreState } from '../store/type'; -import { Path, Patp, Association, Metadata, MetadataUpdatePreview } from '~/types'; -import {uxToHex} from '../lib/util'; +import { Path, Patp, Association, Metadata, MetadataUpdatePreview } from '@urbit/api'; +import { uxToHex } from '../lib/util'; export default class MetadataApi extends BaseApi { - - metadataAdd(appName: string, resource: Path, group: Path, title: string, description: string, dateCreated: string, color: string, moduleName: string) { const creator = `~${this.ship}`; return this.metadataAction({ @@ -44,9 +42,9 @@ export default class MetadataApi extends BaseApi { } update(association: Association, newMetadata: Partial) { - const metadata = {...association.metadata, ...newMetadata }; + const metadata = { ...association.metadata, ...newMetadata }; metadata.color = uxToHex(metadata.color); - return this.metadataAction({ + return this.metadataAction({ add: { group: association.group, resource: { @@ -69,10 +67,10 @@ export default class MetadataApi extends BaseApi { } done = true; tempChannel.delete(); - reject(new Error("offline")) + reject(new Error('offline')); }, 15000); - tempChannel.subscribe(window.ship, "metadata-pull-hook", `/preview${group}`, + tempChannel.subscribe(window.ship, 'metadata-pull-hook', `/preview${group}`, (err) => { console.error(err); reject(err); @@ -88,24 +86,22 @@ export default class MetadataApi extends BaseApi { } else { done = true; tempChannel.delete(); - reject(new Error("no-permissions")); + reject(new Error('no-permissions')); } }, (quit) => { tempChannel.delete(); if(!done) { - reject(new Error("offline")) + reject(new Error('offline')); } }, (a) => { console.log(a); } ); - }) + }); } - - private metadataAction(data) { return this.action('metadata-push-hook', 'metadata-update', data); } diff --git a/pkg/interface/src/logic/api/s3.ts b/pkg/interface/src/logic/api/s3.ts index 834cc6ffe1..1d775ef775 100644 --- a/pkg/interface/src/logic/api/s3.ts +++ b/pkg/interface/src/logic/api/s3.ts @@ -1,10 +1,8 @@ import BaseApi from './base'; import { StoreState } from '../store/type'; -import {S3Update} from '../../types/s3-update'; - +import { S3Update } from '../../types/s3-update'; export default class S3Api extends BaseApi { - setCurrentBucket(bucket: string) { return this.s3Action({ 'set-current-bucket': bucket }); } @@ -32,6 +30,5 @@ export default class S3Api extends BaseApi { private s3Action(data: any) { return this.action('s3-store', 's3-action', data); } - } diff --git a/pkg/interface/src/logic/api/settings.ts b/pkg/interface/src/logic/api/settings.ts index cd0cd736ff..9a5812ed98 100644 --- a/pkg/interface/src/logic/api/settings.ts +++ b/pkg/interface/src/logic/api/settings.ts @@ -1,13 +1,9 @@ import BaseApi from './base'; import { StoreState } from '../store/type'; -import { - SettingsUpdate, - SettingsData, - Key, +import { Key, Value, - Bucket, -} from '~/types/settings'; - + Bucket +} from '@urbit/api/settings'; export default class SettingsApi extends BaseApi { private storeAction(action: SettingsEvent): Promise { @@ -15,60 +11,62 @@ export default class SettingsApi extends BaseApi { } putBucket(key: Key, bucket: Bucket) { - this.storeAction({ - "put-bucket": { - "bucket-key": key, - "bucket": bucket, + return this.storeAction({ + 'put-bucket': { + 'bucket-key': key, + 'bucket': bucket } }); } delBucket(key: Key) { - this.storeAction({ - "del-bucket": { - "bucket-key": key, + return this.storeAction({ + 'del-bucket': { + 'bucket-key': key } }); } putEntry(buc: Key, key: Key, val: Value) { return this.storeAction({ - "put-entry": { - "bucket-key": buc, - "entry-key": key, - "value": val, + 'put-entry': { + 'bucket-key': buc, + 'entry-key': key, + 'value': val } }); } delEntry(buc: Key, key: Key) { - this.storeAction({ - "put-entry": { - "bucket-key": buc, - "entry-key": key, + return this.storeAction({ + 'put-entry': { + 'bucket-key': buc, + 'entry-key': key } }); } async getAll() { - const data = await this.scry("settings-store", "/all"); - this.store.handleEvent({data: {"settings-data": data.all}}); + const { all } = await this.scry("settings-store", "/all"); + this.store.handleEvent({data: + {"settings-data": { all } } + }); } async getBucket(bucket: Key) { const data = await this.scry('settings-store', `/bucket/${bucket}`); - this.store.handleEvent({data: {"settings-data": { - "bucket-key": bucket, - "bucket": data.bucket, - }}}); + this.store.handleEvent({ data: { 'settings-data': { + 'bucket-key': bucket, + 'bucket': data.bucket + } } }); } async getEntry(bucket: Key, entry: Key) { const data = await this.scry('settings-store', `/entry/${bucket}/${entry}`); - this.store.handleEvent({data: {"settings-data": { - "bucket-key": bucket, - "entry-key": entry, - "entry": data.entry, - }}}); + this.store.handleEvent({ data: { 'settings-data': { + 'bucket-key': bucket, + 'entry-key': entry, + 'entry': data.entry + } } }); } } diff --git a/pkg/interface/src/logic/lib/BigIntOrderedMap.ts b/pkg/interface/src/logic/lib/BigIntOrderedMap.ts index 069d520020..21fc1c8cac 100644 --- a/pkg/interface/src/logic/lib/BigIntOrderedMap.ts +++ b/pkg/interface/src/logic/lib/BigIntOrderedMap.ts @@ -1,4 +1,4 @@ -import bigInt, { BigInteger } from "big-integer"; +import bigInt, { BigInteger } from 'big-integer'; interface NonemptyNode { n: [BigInteger, V]; @@ -14,7 +14,7 @@ type MapNode = NonemptyNode | null; */ export class BigIntOrderedMap implements Iterable<[BigInteger, V]> { private root: MapNode = null; - size: number = 0; + size = 0; constructor(initial: [BigInteger, V][] = []) { initial.forEach(([key, val]) => { @@ -48,13 +48,12 @@ export class BigIntOrderedMap implements Iterable<[BigInteger, V]> { * Put an item by a key */ set(key: BigInteger, value: V): void { - const inner = (node: MapNode) => { if (!node) { return { n: [key, value], l: null, - r: null, + r: null }; } const [k] = node.n; @@ -62,22 +61,22 @@ export class BigIntOrderedMap implements Iterable<[BigInteger, V]> { this.size--; return { ...node, - n: [k, value], + n: [k, value] }; } if (key.gt(k)) { const l = inner(node.l); if (!l) { - throw new Error("invariant violation"); + throw new Error('invariant violation'); } return { ...node, - l, + l }; } const r = inner(node.r); if (!r) { - throw new Error("invariant violation"); + throw new Error('invariant violation'); } return { ...node, r }; @@ -133,8 +132,8 @@ export class BigIntOrderedMap implements Iterable<[BigInteger, V]> { bool, { ...node, - l, - }, + l + } ]; } @@ -143,8 +142,8 @@ export class BigIntOrderedMap implements Iterable<[BigInteger, V]> { bool, { ...node, - r, - }, + r + } ]; }; const [ret, newRoot] = inner(this.root); @@ -165,12 +164,12 @@ export class BigIntOrderedMap implements Iterable<[BigInteger, V]> { } return { ...node.l, - r: inner(node.r), + r: inner(node.r) }; }; return inner(nod); } - + peekLargest(): [BigInteger, V] | undefined { const inner = (node: MapNode) => { if(!node) { @@ -180,7 +179,7 @@ export class BigIntOrderedMap implements Iterable<[BigInteger, V]> { return inner(node.l); } return node.n; - } + }; return inner(this.root); } @@ -193,7 +192,7 @@ export class BigIntOrderedMap implements Iterable<[BigInteger, V]> { return inner(node.r); } return node.n; - } + }; return inner(this.root); } @@ -208,7 +207,7 @@ export class BigIntOrderedMap implements Iterable<[BigInteger, V]> { } [Symbol.iterator](): IterableIterator<[BigInteger, V]> { - let result: [BigInteger, V][] = []; + const result: [BigInteger, V][] = []; const inner = (node: MapNode) => { if (!node) { return; @@ -227,7 +226,7 @@ export class BigIntOrderedMap implements Iterable<[BigInteger, V]> { return { value: result[idx++], done: false }; } return { done: true, value: null }; - }, + } }; } } diff --git a/pkg/interface/src/logic/lib/GcpClient.ts b/pkg/interface/src/logic/lib/GcpClient.ts new file mode 100644 index 0000000000..b03c7fd0a6 --- /dev/null +++ b/pkg/interface/src/logic/lib/GcpClient.ts @@ -0,0 +1,67 @@ +// Very simple GCP Storage client. +// +// It's designed to match a subset of the S3 client upload API. The upload +// function on S3 returns a ManagedUpload, which has a promise() method on +// it. We don't care about any of the other methods on ManagedUpload, so we +// just do the work in its promise() method. +// +import querystring from 'querystring'; +import { + StorageAcl, + StorageClient, + StorageUpload, + UploadParams, + UploadResult +} from './StorageClient'; + + +const ENDPOINT = 'storage.googleapis.com'; + +class GcpUpload implements StorageUpload { + #params: UploadParams; + #accessKey: string; + + constructor(params: UploadParams, accessKey: string) { + this.#params = params; + this.#accessKey = accessKey; + } + + async promise(): UploadResult { + const {Bucket, Key, ContentType, Body} = this.#params; + const urlParams = { + uploadType: 'media', + name: Key, + predefinedAcl: 'publicRead' + }; + const url = `https://${ENDPOINT}/upload/storage/v1/b/${Bucket}/o?` + + querystring.stringify(urlParams); + const headers = new Headers(); + headers.append('Authorization', `Bearer ${this.#accessKey}`); + headers.append('Content-Type', ContentType); + const response = await fetch(url, { + method: 'POST', + mode: 'cors', + cache: 'default', + headers, + referrerPolicy: 'no-referrer', + body: Body + }); + if (!response.ok) { + console.error('GcpClient server error', await response.json()); + throw new Error(`GcpClient: response ${response.status}`); + } + return {Location: `https://${ENDPOINT}/${Bucket}/${Key}`}; + } +} + +export default class GcpClient implements StorageClient { + #accessKey: string; + + constructor(accessKey: string) { + this.#accessKey = accessKey; + } + + upload(params: UploadParams): StorageUpload { + return new GcpUpload(params, this.#accessKey); + } +} diff --git a/pkg/interface/src/logic/lib/OrderedMap.ts b/pkg/interface/src/logic/lib/OrderedMap.ts index d66346ff61..91ec52b783 100644 --- a/pkg/interface/src/logic/lib/OrderedMap.ts +++ b/pkg/interface/src/logic/lib/OrderedMap.ts @@ -1,7 +1,6 @@ export class OrderedMap extends Map implements Iterable<[number, V]> { - [Symbol.iterator](): IterableIterator<[number, V]> { const sorted = Array.from(super[Symbol.iterator]()).sort( ([a], [b]) => b - a @@ -15,7 +14,7 @@ export class OrderedMap extends Map } else { return { done: true, value: null }; } - }, + } }; } } diff --git a/pkg/interface/src/logic/lib/StorageClient.ts b/pkg/interface/src/logic/lib/StorageClient.ts new file mode 100644 index 0000000000..31e12f8233 --- /dev/null +++ b/pkg/interface/src/logic/lib/StorageClient.ts @@ -0,0 +1,32 @@ +// Defines a StorageClient interface interoperable between S3 and GCP Storage. +// + + +// XX kind of gross. S3 needs 'public-read', GCP needs 'publicRead'. +// Rather than write a wrapper around S3, we offer this field here, which +// should always be passed, and will be replaced by 'publicRead' in the +// GCP client. +export enum StorageAcl { + PublicRead = 'public-read' +}; + +export interface UploadParams { + Bucket: string; // the bucket to upload the object to + Key: string; // the desired location within the bucket + ContentType: string; // the object's mime-type + ACL: StorageAcl; // ACL, always 'public-read' + Body: File; // the object itself +}; + +export interface UploadResult { + Location: string; +}; + +// Extra layer of indirection used by S3 client. +export interface StorageUpload { + promise(): Promise; +}; + +export interface StorageClient { + upload(params: UploadParams): StorageUpload; +}; diff --git a/pkg/interface/src/logic/lib/bigInt.ts b/pkg/interface/src/logic/lib/bigInt.ts index ecc6dda0af..01da28b455 100644 --- a/pkg/interface/src/logic/lib/bigInt.ts +++ b/pkg/interface/src/logic/lib/bigInt.ts @@ -1,4 +1,4 @@ -import bigInt, { BigInteger } from "big-integer"; +import bigInt, { BigInteger } from 'big-integer'; export function max(a: BigInteger, b: BigInteger) { return a.gt(b) ? a : b; diff --git a/pkg/interface/src/logic/lib/gcpManager.ts b/pkg/interface/src/logic/lib/gcpManager.ts new file mode 100644 index 0000000000..69f157c2c5 --- /dev/null +++ b/pkg/interface/src/logic/lib/gcpManager.ts @@ -0,0 +1,142 @@ +// Singleton that manages GCP token state. +// +// To use: +// +// 1. call configure with a GlobalApi and GlobalStore. +// 2. call start() to start the token refresh loop. +// +// If the ship does not have GCP storage configured, we don't try to get +// a token, but we keep checking at regular intervals to see if it gets +// configured. If GCP storage is configured, we try to invoke the GCP +// get-token thread on the ship until it gives us an access token. Once +// we have a token, we refresh it every hour or so according to its +// intrinsic expiry. +// +// +import GlobalApi from '../api/global'; +import GlobalStore from '../store/store'; + + +class GcpManager { + #api: GlobalApi | null = null; + #store: GlobalStore | null = null; + + configure(api: GlobalApi, store: GlobalStore) { + this.#api = api; + this.#store = store; + } + + #running = false; + #timeoutId: number | null = null; + + start() { + if (this.#running) { + console.warn('GcpManager already running'); + return; + } + if (!this.#api || !this.#store) { + console.error('GcpManager must have api and store set'); + return; + } + this.#running = true; + this.refreshLoop(); + } + + stop() { + if (!this.#running) { + console.warn('GcpManager already stopped'); + console.assert(this.#timeoutId === null); + return; + } + this.#running = false; + if (this.#timeoutId !== null) { + clearTimeout(this.#timeoutId); + this.#timeoutId = null; + } + } + + restart() { + if (this.#running) { + this.stop(); + } + this.start(); + } + + #consecutiveFailures: number = 0; + + private isConfigured() { + return this.#store.state.storage.gcp.configured; + } + + private refreshLoop() { + if (!this.isConfigured()) { + this.#api.gcp.isConfigured() + .then(() => { + if (this.isConfigured() === undefined) { + throw new Error("can't check whether GCP is configured?"); + } + if (this.isConfigured()) { + this.refreshLoop(); + } else { + this.refreshAfter(10_000); + } + }) + .catch((reason) => { + console.error('GcpManager failure; stopping.', reason); + this.stop(); + }); + return; + } + this.#api.gcp.getToken() + .then(() => { + const token = this.#store.state.storage.gcp?.token; + if (token) { + this.#consecutiveFailures = 0; + const interval = this.refreshInterval(token.expiresIn); + console.log('GcpManager got token; refreshing after', interval); + this.refreshAfter(interval); + } else { + throw new Error('thread succeeded, but returned no token?'); + } + }) + .catch((reason) => { + this.#consecutiveFailures++; + console.warn('GcpManager token refresh failed; retrying with backoff'); + this.refreshAfter(this.backoffInterval()); + }); + } + + private refreshAfter(durationMs) { + if (!this.#running) { + return; + } + if (this.#timeoutId !== null) { + console.warn('GcpManager already has a timeout set'); + return; + } + this.#timeoutId = setTimeout(() => { + this.#timeoutId = null; + this.refreshLoop(); + }, durationMs); + } + + private refreshInterval(expiresIn: number) { + // Give ourselves a minute for processing delays, but never refresh sooner + // than 30 minutes from now. (The expiry window should be about an hour.) + return Math.max(30 * 60_000, expiresIn - 60_000); + } + + private backoffInterval() { + // exponential backoff. + const slotMs = 5_000; + const maxSlot = 60; // 5 minutes + const backoffSlots = + Math.floor(Math.random() * Math.min(maxSlot, this.#consecutiveFailures)); + return slotMs * backoffSlots; + } +} + +const instance = new GcpManager(); +Object.freeze(instance); + +export default instance; diff --git a/pkg/interface/src/logic/lib/group.ts b/pkg/interface/src/logic/lib/group.ts index edc525d89b..2eb81e015b 100644 --- a/pkg/interface/src/logic/lib/group.ts +++ b/pkg/interface/src/logic/lib/group.ts @@ -1,7 +1,7 @@ -import _ from "lodash"; -import { roleTags, RoleTags, Group, Resource } from "~/types/group-update"; -import { PatpNoSig, Path } from "~/types/noun"; -import {deSig} from "./util"; +import _ from 'lodash'; +import { roleTags, RoleTags, Group, Resource } from '@urbit/api/groups'; +import { PatpNoSig, Path } from '@urbit/api'; +import { deSig } from './util'; export function roleForShip( group: Group, @@ -14,7 +14,7 @@ export function roleForShip( } export function resourceFromPath(path: Path): Resource { - const [, , ship, name] = path.split("/"); + const [, , ship, name] = path.split('/'); return { ship, name }; } @@ -25,7 +25,7 @@ export function makeResource(ship: string, name: string) { export function isWriter(group: Group, resource: string) { const writers: Set | undefined = _.get( group, - ["tags", "graph", resource, "writers"], + ['tags', 'graph', resource, 'writers'], undefined ); const admins = group?.tags?.role?.admin ?? new Set(); @@ -36,18 +36,18 @@ export function isWriter(group: Group, resource: string) { } } -export function isChannelAdmin(group: Group, resource: string, ship: string = `~${window.ship}`) { +export function isChannelAdmin(group: Group, resource: string, ship = `~${window.ship}`) { const role = roleForShip(group, ship.slice(1)); return ( isHost(resource, ship) || - role === "admin" || - role === "moderator" + role === 'admin' || + role === 'moderator' ); } -export function isHost(resource: string, ship: string = `~${window.ship}`) { - const [, , host] = resource.split("/"); +export function isHost(resource: string, ship = `~${window.ship}`) { + const [, , host] = resource.split('/'); return ship === host; } diff --git a/pkg/interface/src/logic/lib/hark.ts b/pkg/interface/src/logic/lib/hark.ts index ee5ecd7618..b65a09603d 100644 --- a/pkg/interface/src/logic/lib/hark.ts +++ b/pkg/interface/src/logic/lib/hark.ts @@ -1,6 +1,6 @@ -import bigInt, { BigInteger } from "big-integer"; -import f from "lodash/fp"; -import { Unreads } from "~/types"; +import bigInt, { BigInteger } from 'big-integer'; +import f from 'lodash/fp'; +import { Unreads } from '@urbit/api'; export function getLastSeen( unreads: Unreads, @@ -8,10 +8,10 @@ export function getLastSeen( index: string ): BigInteger | undefined { const lastSeenIdx = unreads.graph?.[path]?.[index]?.unreads; - if (!(typeof lastSeenIdx === "string")) { + if (!(typeof lastSeenIdx === 'string')) { return bigInt.zero; } - return f.flow(f.split("/"), f.last, (x) => (!!x ? bigInt(x) : undefined))( + return f.flow(f.split('/'), f.last, x => (x ? bigInt(x) : undefined))( lastSeenIdx ); } diff --git a/pkg/interface/src/logic/lib/idling.ts b/pkg/interface/src/logic/lib/idling.ts new file mode 100644 index 0000000000..9744e68dee --- /dev/null +++ b/pkg/interface/src/logic/lib/idling.ts @@ -0,0 +1,23 @@ +import { useState, useEffect } from 'react'; + +export function useIdlingState() { + const [idling, setIdling] = useState(false); + + useEffect(() => { + function blur() { + setIdling(true); + } + function focus() { + setIdling(false); + } + window.addEventListener('blur', blur); + window.addEventListener('focus', focus); + + return () => { + window.removeEventListener('blur', blur); + window.removeEventListener('focus', focus); + } + }, []); + + return idling; +} diff --git a/pkg/interface/src/logic/lib/migrateSettings.ts b/pkg/interface/src/logic/lib/migrateSettings.ts new file mode 100644 index 0000000000..bcfe44bfcb --- /dev/null +++ b/pkg/interface/src/logic/lib/migrateSettings.ts @@ -0,0 +1,72 @@ +import useLocalState, { LocalState } from "~/logic/state/local"; +import useSettingsState from "~/logic/state/settings"; +import GlobalApi from "../api/global"; +import { BackgroundConfig, RemoteContentPolicy } from "~/types"; + +const getBackgroundString = (bg: BackgroundConfig) => { + if (bg?.type === "url") { + return bg.url; + } else if (bg?.type === "color") { + return bg.color; + } else { + return ""; + } +}; + +export function useMigrateSettings(api: GlobalApi) { + const local = useLocalState(); + const { display, remoteContentPolicy, calm } = useSettingsState(); + + return async () => { + if (!localStorage?.has("localReducer")) { + return; + } + + let promises: Promise[] = []; + + if (local.hideAvatars !== calm.hideAvatars) { + promises.push( + api.settings.putEntry("calm", "hideAvatars", local.hideAvatars) + ); + } + + if (local.hideNicknames !== calm.hideNicknames) { + promises.push( + api.settings.putEntry("calm", "hideNicknames", local.hideNicknames) + ); + } + + if ( + local?.background?.type && + display.background !== getBackgroundString(local.background) + ) { + promises.push( + api.settings.putEntry( + "display", + "background", + getBackgroundString(local.background) + ) + ); + promises.push( + api.settings.putEntry( + "display", + "backgroundType", + local.background?.type + ) + ); + } + + Object.keys(local.remoteContentPolicy).forEach((_key) => { + const key = _key as keyof RemoteContentPolicy; + const localVal = local.remoteContentPolicy[key]; + if (localVal !== remoteContentPolicy[key]) { + promises.push( + api.settings.putEntry("remoteContentPolicy", key, localVal) + ); + } + }); + + await Promise.all(promises); + localStorage.removeItem("localReducer"); + }; +} diff --git a/pkg/interface/src/logic/lib/notification.ts b/pkg/interface/src/logic/lib/notification.ts index 87288d83d2..bd2979f409 100644 --- a/pkg/interface/src/logic/lib/notification.ts +++ b/pkg/interface/src/logic/lib/notification.ts @@ -1,19 +1,19 @@ -import { GraphNotifIndex, GraphNotificationContents } from "~/types"; +import { GraphNotifIndex, GraphNotificationContents } from '@urbit/api'; export function getParentIndex( idx: GraphNotifIndex, contents: GraphNotificationContents ) { - const origIndex = contents[0].index.slice(1).split("/"); - const ret = (i: string[]) => `/${i.join("/")}`; + const origIndex = contents[0].index.slice(1).split('/'); + const ret = (i: string[]) => `/${i.join('/')}`; switch (idx.description) { - case "link": - return "/"; - case "comment": + case 'link': + return '/'; + case 'comment': return ret(origIndex.slice(0, 1)); - case "note": - return "/"; - case "mention": + case 'note': + return '/'; + case 'mention': return undefined; default: return undefined; diff --git a/pkg/interface/src/logic/lib/omnibox.js b/pkg/interface/src/logic/lib/omnibox.js index eabb8017ec..439b5a5e34 100644 --- a/pkg/interface/src/logic/lib/omnibox.js +++ b/pkg/interface/src/logic/lib/omnibox.js @@ -1,7 +1,7 @@ import { cite } from '~/logic/lib/util'; import { isChannelAdmin } from '~/logic/lib/group'; - const indexes = new Map([ +const makeIndexes = () => new Map([ ['ships', []], ['commands', []], ['subscriptions', []], @@ -70,18 +70,27 @@ const appIndex = function (apps) { return applications; }; -const otherIndex = function() { +const otherIndex = function(config) { const other = []; - other.push(result('My Channels', '/~landscape/home', 'home', null)); - other.push(result('Notifications', '/~notifications', 'inbox', null)); - other.push(result('Profile and Settings', `/~profile/~${window.ship}`, 'profile', null)); - other.push(result('Messages', '/~landscape/messages', 'messages', null)); - other.push(result('Log Out', '/~/logout', 'logout', null)); + const idx = { + mychannel: result('My Channels', '/~landscape/home', 'home', null), + updates: result('Notifications', '/~notifications', 'inbox', null), + profile: result('Profile', `/~profile/~${window.ship}`, 'profile', null), + messages: result('Messages', '/~landscape/messages', 'messages', null), + logout: result('Log Out', '/~/logout', 'logout', null) + }; + other.push(result('Tutorial', '/?tutorial=true', 'tutorial', null)); + for(let cat of config.categories) { + if(idx[cat]) { + other.push(idx[cat]); + } + } return other; }; -export default function index(contacts, associations, apps, currentGroup, groups) { +export default function index(contacts, associations, apps, currentGroup, groups, hide) { + const indexes = makeIndexes(); indexes.set('ships', shipIndex(contacts)); // all metadata from all apps is indexed // into subscriptions and landscape @@ -141,7 +150,7 @@ export default function index(contacts, associations, apps, currentGroup, groups indexes.set('subscriptions', subscriptions); indexes.set('groups', landscape); indexes.set('apps', appIndex(apps)); - indexes.set('other', otherIndex()); + indexes.set('other', otherIndex(hide)); return indexes; }; diff --git a/pkg/interface/src/logic/lib/platform.ts b/pkg/interface/src/logic/lib/platform.ts new file mode 100644 index 0000000000..3243cbf110 --- /dev/null +++ b/pkg/interface/src/logic/lib/platform.ts @@ -0,0 +1,6 @@ + +const ua = window.navigator.userAgent; + +export const IS_IOS = ua.includes('iPhone'); + +console.log(IS_IOS); diff --git a/pkg/interface/src/logic/lib/post.ts b/pkg/interface/src/logic/lib/post.ts index 1a311de55c..397563283a 100644 --- a/pkg/interface/src/logic/lib/post.ts +++ b/pkg/interface/src/logic/lib/post.ts @@ -1,4 +1,4 @@ -import { Post, GraphNode } from "~/types"; +import { Post, GraphNode } from '@urbit/api'; export const buntPost = (): Post => ({ author: '', @@ -10,7 +10,7 @@ export const buntPost = (): Post => ({ }); export function makeNodeMap(posts: Post[]): Record { - let nodes = {}; + const nodes = {}; posts.forEach((p) => { nodes[p.index] = { children: { empty: null }, post: p }; }); diff --git a/pkg/interface/src/logic/lib/publish.ts b/pkg/interface/src/logic/lib/publish.ts index 10c64aecf3..ce616b3cf5 100644 --- a/pkg/interface/src/logic/lib/publish.ts +++ b/pkg/interface/src/logic/lib/publish.ts @@ -1,8 +1,8 @@ -import { Post, GraphNode, TextContent, Graph, NodeMap } from "~/types"; +import { Post, GraphNode, TextContent, Graph, NodeMap } from '@urbit/api'; import { buntPost } from '~/logic/lib/post'; -import { unixToDa } from "~/logic/lib/util"; -import {BigIntOrderedMap} from "./BigIntOrderedMap"; -import bigInt, {BigInteger} from 'big-integer'; +import { unixToDa } from '~/logic/lib/util'; +import { BigIntOrderedMap } from './BigIntOrderedMap'; +import bigInt, { BigInteger } from 'big-integer'; export function newPost( title: string, @@ -12,20 +12,20 @@ export function newPost( const nowDa = unixToDa(now); const root: Post = { author: `~${window.ship}`, - index: "/" + nowDa.toString(), - "time-sent": now, + index: '/' + nowDa.toString(), + 'time-sent': now, contents: [], hash: null, - signatures: [], + signatures: [] }; - const revContainer: Post = { ...root, index: root.index + "/1" }; - const commentsContainer = { ...root, index: root.index + "/2" }; + const revContainer: Post = { ...root, index: root.index + '/1' }; + const commentsContainer = { ...root, index: root.index + '/2' }; const firstRevision: Post = { ...revContainer, - index: revContainer.index + "/1", - contents: [{ text: title }, { text: body }], + index: revContainer.index + '/1', + contents: [{ text: title }, { text: body }] }; const nodes = { @@ -37,16 +37,16 @@ export function newPost( children: { 1: { post: firstRevision, - children: null, - }, - }, + children: null + } + } }, 2: { post: commentsContainer, children: null - }, - }, - }, + } + } + } }; return [nowDa, nodes]; @@ -57,15 +57,15 @@ export function editPost(rev: number, noteId: BigInteger, title: string, body: s const newRev: Post = { author: `~${window.ship}`, index: `/${noteId.toString()}/1/${rev}`, - "time-sent": now, + 'time-sent': now, contents: [{ text: title }, { text: body }], hash: null, - signatures: [], + signatures: [] }; const nodes = { [newRev.index]: { post: newRev, - children: null + children: null } }; @@ -74,7 +74,7 @@ export function editPost(rev: number, noteId: BigInteger, title: string, body: s export function getLatestRevision(node: GraphNode): [number, string, string, Post] { const revs = node.children.get(bigInt(1)); - const empty = [1, "", "", buntPost()] as [number, string, string, Post]; + const empty = [1, '', '', buntPost()] as [number, string, string, Post]; if(!revs) { return empty; } @@ -98,17 +98,16 @@ export function getLatestCommentRevision(node: GraphNode): [number, Post] { return [revNum.toJSNumber(), rev.post]; } - export function getComments(node: GraphNode): GraphNode { const comments = node.children.get(bigInt(2)); if(!comments) { - return { post: buntPost(), children: new BigIntOrderedMap() } + return { post: buntPost(), children: new BigIntOrderedMap() }; } return comments; } export function getSnippet(body: string) { const start = body.slice(0, body.indexOf('\n', 2)); - return (start === body || start.startsWith("![")) ? start : `${start}...`; + return (start === body || start.startsWith('![')) ? start : `${start}...`; } diff --git a/pkg/interface/src/logic/lib/relativePosition.tsx b/pkg/interface/src/logic/lib/relativePosition.tsx index 8ad1618e53..b2c4b751e5 100644 --- a/pkg/interface/src/logic/lib/relativePosition.tsx +++ b/pkg/interface/src/logic/lib/relativePosition.tsx @@ -1,16 +1,16 @@ -import _ from "lodash"; +import _ from 'lodash'; -export const alignY = ["top", "bottom"] as const; +export const alignY = ['top', 'bottom'] as const; export type AlignY = typeof alignY[number]; -export const alignX = ["left", "right"] as const; +export const alignX = ['left', 'right'] as const; export type AlignX = typeof alignX[number]; export function getRelativePosition( relativeTo: HTMLElement | null, alignX: AlignX | AlignX[], alignY: AlignY | AlignY[], - offsetX: number = 0, - offsetY: number = 0 + offsetX = 0, + offsetY = 0 ) { const rect = relativeTo?.getBoundingClientRect(); if (!rect) { @@ -20,7 +20,7 @@ export function getRelativePosition( top: rect.top - offsetY, left: rect.left - offsetX, bottom: document.documentElement.clientHeight - rect.bottom - offsetY, - right: document.documentElement.clientWidth - rect.right - offsetX, + right: document.documentElement.clientWidth - rect.right - offsetX }; const alignXArr = _.isArray(alignX) ? alignX : [alignX]; const alignYArr = _.isArray(alignY) ? alignY : [alignY]; @@ -34,7 +34,7 @@ export function getRelativePosition( [...Array(idx), `${bounds[a]}px`], acc[a] || [], (a, b) => a || b || null - ), + ) }), {} ), @@ -46,10 +46,10 @@ export function getRelativePosition( [...Array(idx), `${bounds[a]}px`], acc[a] || [], (a, b) => a || b || null - ), + ) }), {} - ), + ) } as Record; } diff --git a/pkg/interface/src/logic/lib/s3.js b/pkg/interface/src/logic/lib/s3.js deleted file mode 100644 index 64bda11be7..0000000000 --- a/pkg/interface/src/logic/lib/s3.js +++ /dev/null @@ -1,41 +0,0 @@ -import S3 from 'aws-sdk/clients/s3'; - -export default class S3Client { - constructor() { - this.s3 = null; - - this.endpoint = ''; - this.accessKeyId = ''; - this.secretAccesskey = ''; - } - - setCredentials(endpoint, accessKeyId, secretAccessKey) { - this.endpoint = endpoint; - this.accessKeyId = accessKeyId; - this.secretAccessKey = secretAccessKey; - - this.s3 = new S3({ - endpoint: endpoint, - credentials: { - accessKeyId: this.accessKeyId, - secretAccessKey: this.secretAccessKey - } - }); - } - - async upload(bucket, filename, buffer) { - const params = { - Bucket: bucket, - Key: filename, - Body: buffer, - ACL: 'public-read', - ContentType: buffer.type - }; - - if(!this.s3) { - throw new Error('S3 not initialized'); - } - return this.s3.upload(params).promise(); - } -} - diff --git a/pkg/interface/src/logic/lib/tokenizeMessage.js b/pkg/interface/src/logic/lib/tokenizeMessage.js index dc1ffeffc4..85a5b5234c 100644 --- a/pkg/interface/src/logic/lib/tokenizeMessage.js +++ b/pkg/interface/src/logic/lib/tokenizeMessage.js @@ -1,6 +1,6 @@ import urbitOb from 'urbit-ob'; -const URL_REGEX = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source)); +const URL_REGEX = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+\w)/.source)); const isUrl = (string) => { try { @@ -52,16 +52,13 @@ const tokenizeMessage = (text) => { } messages.push({ url: str }); message = []; - } else if (urbitOb.isValidPatp(str.replace(/[^a-z\-\~]/g, '')) && !isInCodeBlock) { + } else if(urbitOb.isValidPatp(str) && !isInCodeBlock) { if (message.length > 0) { // If we're in the middle of a message, add it to the stack and reset messages.push({ text: message.join(' ') }); message = []; } - messages.push({ mention: str.replace(/[^a-z\-\~]/g, '') }); - if (str.replace(/[a-z\-\~]/g, '').length > 0) { - messages.push({ text: str.replace(/[a-z\-\~]/g, '') }); - } + messages.push({ mention: str }); message = []; } else { diff --git a/pkg/interface/src/logic/lib/tutorialModal.ts b/pkg/interface/src/logic/lib/tutorialModal.ts index ef1d753b1e..507e9b5e15 100644 --- a/pkg/interface/src/logic/lib/tutorialModal.ts +++ b/pkg/interface/src/logic/lib/tutorialModal.ts @@ -1,6 +1,6 @@ -import { TutorialProgress, Associations } from "~/types"; -import { AlignX, AlignY } from "~/logic/lib/relativePosition"; -import { Direction } from "~/views/components/Triangle"; +import { TutorialProgress, Associations } from '@urbit/api'; +import { AlignX, AlignY } from '~/logic/lib/relativePosition'; +import { Direction } from '~/views/components/Triangle'; export const MODAL_WIDTH = 256; export const MODAL_HEIGHT = 256; @@ -12,6 +12,7 @@ export const TUTORIAL_GROUP = process.env.TUTORIAL_GROUP!; export const TUTORIAL_CHAT = process.env.TUTORIAL_CHAT!; export const TUTORIAL_BOOK = process.env.TUTORIAL_BOOK!; export const TUTORIAL_LINKS = process.env.TUTORIAL_LINKS!; +export const TUTORIAL_GROUP_RESOURCE = `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}` ; interface StepDetail { title: string; @@ -26,7 +27,7 @@ interface StepDetail { export function hasTutorialGroup(props: { associations: Associations }) { return ( - `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}` in props.associations.groups + TUTORIAL_GROUP_RESOURCE in props.associations.groups ); } @@ -43,7 +44,7 @@ export const getTrianglePosition = (dir: Direction) => { return { top: midY, left: '-32px' - } + }; case 'North': return { top: '-32px', @@ -55,117 +56,117 @@ export const getTrianglePosition = (dir: Direction) => { left: midX }; } -} +}; export const progressDetails: Record = { hidden: {} as any, exit: {} as any, done: { - title: "End", + title: 'End', description: - "This tutorial is finished. Would you like to leave Beginner Island?", - url: "/", - alignX: "right", - alignY: "top", + 'This tutorial is finished. Would you like to leave Beginner Island?', + url: '/', + alignX: 'right', + alignY: 'top', offsetX: MODAL_WIDTH + 8, - offsetY: 0, + offsetY: 0 }, start: { - title: "New Group added", + title: 'New Group added', description: - "We just added you to the Beginner island group to show you around. This group is public, but other groups can be private", - url: "/", - alignX: "right", - alignY: "top", - arrow: "West", + 'We just added you to the Beginner island group to show you around. This group is public, but other groups can be private', + url: '/', + alignX: 'right', + alignY: 'top', + arrow: 'West', offsetX: MODAL_WIDTH + 24, - offsetY: 64, + offsetY: 64 }, - "group-desc": { - title: "What's a group", + 'group-desc': { + title: 'What\'s a group', description: - "A group contains members and tends to be centered around a topic or multiple topics.", + 'A group contains members and tends to be centered around a topic or multiple topics.', url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`, - alignX: "left", - alignY: "top", - arrow: "East", + alignX: 'left', + alignY: 'top', + arrow: 'East', offsetX: MODAL_WIDTH + 24, - offsetY: MODAL_HEIGHT / 2 - 8, + offsetY: 80, }, channels: { - title: "Channels", + title: 'Channels', description: - "Inside a group you have three types of Channels: Chat, Collection, or Notebook. Mix and match these depending on your group context!", + 'Inside a group you have three types of Channels: Chat, Collection, or Notebook. Mix and match these depending on your group context!', url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`, - alignY: "top", - alignX: "right", - arrow: "West", + alignY: 'top', + alignX: 'right', + arrow: 'West', offsetX: MODAL_WIDTH + 24, - offsetY: -8, + offsetY: -8 }, chat: { - title: "Chat", + title: 'Chat', description: - "Chat channels are for messaging within your group. Direct Messages are also supported, and are accessible from the “DMs” tile on the homescreen", + 'Chat channels are for messaging within your group. Direct Messages can be accessed from Messages in the top right', url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/chat/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}`, - alignY: "top", - arrow: "North", - alignX: "right", + alignY: 'top', + arrow: 'North', + alignX: 'right', offsetY: -56, - offsetX: -8, + offsetX: -8 }, link: { - title: "Collection", + title: 'Collection', description: - "A collection is where you can share and view links, images, and other media within your group. Every item in a Collection can have it’s own comment thread.", + 'A collection is where you can share and view links, images, and other media within your group. Every item in a Collection can have it’s own comment thread.', url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/link/ship/${TUTORIAL_HOST}/${TUTORIAL_LINKS}`, - alignY: "top", - alignX: "right", - arrow: "North", + alignY: 'top', + alignX: 'right', + arrow: 'North', offsetX: -8, - offsetY: -56, + offsetY: -56 }, publish: { - title: "Notebook", + title: 'Notebook', description: - "Notebooks are for creating long-form content within your group. Use markdown to create rich posts with headers, lists and images.", + 'Notebooks are for creating long-form content within your group. Use markdown to create rich posts with headers, lists and images.', url: `/~landscape/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}/resource/publish/ship/${TUTORIAL_HOST}/${TUTORIAL_BOOK}`, - alignY: "top", - alignX: "right", - arrow: "North", + alignY: 'top', + alignX: 'right', + arrow: 'North', offsetX: -8, - offsetY: -56, + offsetY: -56 }, notifications: { - title: "Notifications", - description: "You will get updates from subscribed channels and mentions here. You can access Notifications through Leap.", + title: 'Notifications', + description: 'You will get updates from subscribed channels and mentions here. You can access Notifications through Leap.', url: '/~notifications', - alignY: "top", - alignX: "left", - arrow: "North", - offsetX: (MODAL_WIDTH / 2) - 16, - offsetY: -48, + alignY: 'top', + alignX: 'left', + arrow: 'North', + offsetX: 0, + offsetY: -48 }, profile: { - title: "Profile", + title: 'Profile', description: - "Your profile is customizable and can be shared with other ships. Enter as much or as little information as you’d like.", + 'Your profile is customizable and can be shared with other ships. Enter as much or as little information as you’d like.', url: `/~profile/~${window.ship}`, - alignY: "top", - alignX: "right", - arrow: "South", + alignY: 'top', + alignX: 'right', + arrow: 'South', offsetX: -300 + MODAL_WIDTH / 2, - offsetY: -120 + MODAL_HEIGHT / 2, + offsetY: -4, }, leap: { - title: "Leap", + title: 'Leap', description: - "Leap allows you to go to a specific channel, message, collection, profile or group simply by typing in a command or selecting a shortcut from the dropdown menu.", + 'Leap allows you to go to a specific channel, message, collection, profile or group simply by typing in a command or selecting a shortcut from the dropdown menu.', url: `/~profile/~${window.ship}`, alignY: "top", alignX: "left", arrow: "North", - offsetX: 0.3 *MODAL_HEIGHT, + offsetX: 76, offsetY: -48, }, }; diff --git a/pkg/interface/src/logic/lib/useDrag.ts b/pkg/interface/src/logic/lib/useDrag.ts index e2aa496a88..fb17928e96 100644 --- a/pkg/interface/src/logic/lib/useDrag.ts +++ b/pkg/interface/src/logic/lib/useDrag.ts @@ -1,4 +1,4 @@ -import { useState, useCallback, useMemo, useEffect } from "react"; +import { useState, useCallback, useMemo, useEffect } from 'react'; function validateDragEvent(e: DragEvent): FileList | File[] | true | null { const files: File[] = []; @@ -8,8 +8,8 @@ function validateDragEvent(e: DragEvent): FileList | File[] | true | null { } if (e.dataTransfer?.items) { Array.from(e.dataTransfer.items || []) - .filter((i) => i.kind === 'file') - .forEach(f => { + .filter(i => i.kind === 'file') + .forEach((f) => { valid = true; // Valid if file exists, but on DragOver, won't reveal its contents for security const data = f.getAsFile(); if (data) { @@ -89,14 +89,14 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi document.body.addEventListener('mouseout', mouseleave); return () => { document.body.removeEventListener('mouseout', mouseleave); - } + }; }, []); const bind = { onDragLeave, onDragOver, onDrop, - onDragEnter, + onDragEnter }; return { bind, dragging }; diff --git a/pkg/interface/src/logic/lib/useDropdown.ts b/pkg/interface/src/logic/lib/useDropdown.ts index 0e0b37e0dc..98a7359f32 100644 --- a/pkg/interface/src/logic/lib/useDropdown.ts +++ b/pkg/interface/src/logic/lib/useDropdown.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo, useCallback } from "react"; +import { useState, useEffect, useMemo, useCallback } from 'react'; export function useDropdown( candidates: C[], @@ -12,10 +12,10 @@ export function useDropdown( (s: string) => { const exactMatch = isExact(s); const exact = exactMatch ? [exactMatch] : []; - const opts = [...new Set([...exact, ...candidates.filter((c) => searchPred(s, c))])]; + const opts = [...new Set([...exact, ...candidates.filter(c => searchPred(s, c))])]; setOptions(opts); if (selected) { - const idx = opts.findIndex((c) => key(c) === key(selected)); + const idx = opts.findIndex(c => key(c) === key(selected)); if (idx < 0) { setSelected(undefined); } @@ -29,9 +29,11 @@ export function useDropdown( const select = (idx: number) => { setSelected(options[idx]); }; - if(!selected) { select(0); return false; } + if(!selected) { + select(0); return false; +} - const idx = options.findIndex((c) => key(c) === key(selected)); + const idx = options.findIndex(c => key(c) === key(selected)); if ( idx === -1 || (options.length - 1 <= idx && !backward) @@ -55,6 +57,6 @@ export function useDropdown( back, search, selected, - options, + options }; } diff --git a/pkg/interface/src/logic/lib/useHashLink.ts b/pkg/interface/src/logic/lib/useHashLink.ts index 77c91be2e2..7d93ce5d42 100644 --- a/pkg/interface/src/logic/lib/useHashLink.ts +++ b/pkg/interface/src/logic/lib/useHashLink.ts @@ -1,6 +1,5 @@ import { useEffect } from 'react'; -import {useLocation} from "react-router-dom"; - +import { useLocation } from 'react-router-dom'; export function useHashLink() { const location = useLocation(); @@ -10,8 +9,5 @@ export function useHashLink() { return; } document.querySelector(location.hash)?.scrollIntoView({ behavior: 'smooth', block: 'start' }); - }, [location.hash]); - - } diff --git a/pkg/interface/src/logic/lib/useLazyScroll.ts b/pkg/interface/src/logic/lib/useLazyScroll.ts index bd1c31a328..03a3ce432d 100644 --- a/pkg/interface/src/logic/lib/useLazyScroll.ts +++ b/pkg/interface/src/logic/lib/useLazyScroll.ts @@ -1,6 +1,6 @@ -import { useEffect, RefObject, useRef, useState } from "react"; -import _ from "lodash"; -import usePreviousValue from "./usePreviousValue"; +import { useEffect, RefObject, useRef, useState } from 'react'; +import _ from 'lodash'; +import usePreviousValue from './usePreviousValue'; export function distanceToBottom(el: HTMLElement) { const { scrollTop, scrollHeight, clientHeight } = el; @@ -40,7 +40,6 @@ export function useLazyScroll( } }, [count]); - useEffect(() => { if (!ref.current) { return; @@ -54,13 +53,12 @@ export function useLazyScroll( loadUntil(el); }; - ref.current.addEventListener("scroll", onScroll, { passive: true }); + ref.current.addEventListener('scroll', onScroll, { passive: true }); return () => { - ref.current?.removeEventListener("scroll", onScroll); + ref.current?.removeEventListener('scroll', onScroll); }; }, [ref?.current, count]); - return { isDone, isLoading }; } diff --git a/pkg/interface/src/logic/lib/useLocalStorageState.ts b/pkg/interface/src/logic/lib/useLocalStorageState.ts index b81214a677..13de71afe3 100644 --- a/pkg/interface/src/logic/lib/useLocalStorageState.ts +++ b/pkg/interface/src/logic/lib/useLocalStorageState.ts @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect } from "react"; +import { useState, useCallback, useEffect } from 'react'; function retrieve(key: string, initial: T): T { const s = localStorage.getItem(key); @@ -25,7 +25,7 @@ export function useLocalStorageState(key: string, initial: T) { const setState = useCallback( (s: SetState) => { - const updated = typeof s === "function" ? s(state) : s; + const updated = typeof s === 'function' ? s(state) : s; _setState(updated); localStorage.setItem(key, JSON.stringify(updated)); }, diff --git a/pkg/interface/src/logic/lib/useModal.tsx b/pkg/interface/src/logic/lib/useModal.tsx index c7cb9be7d4..3d19343cd7 100644 --- a/pkg/interface/src/logic/lib/useModal.tsx +++ b/pkg/interface/src/logic/lib/useModal.tsx @@ -5,15 +5,15 @@ import React, { SyntheticEvent, useMemo, useEffect, - useRef, -} from "react"; + useRef +} from 'react'; -import { Box } from "@tlon/indigo-react"; -import { useOutsideClick } from "./useOutsideClick"; -import { ModalOverlay } from "~/views/components/ModalOverlay"; -import {Portal} from "~/views/components/Portal"; -import {ModalPortal} from "~/views/components/ModalPortal"; -import {PropFunc} from "~/types"; +import { Box } from '@tlon/indigo-react'; +import { useOutsideClick } from './useOutsideClick'; +import { ModalOverlay } from '~/views/components/ModalOverlay'; +import { Portal } from '~/views/components/Portal'; +import { ModalPortal } from '~/views/components/ModalPortal'; +import { PropFunc } from '@urbit/api'; type ModalFunc = (dismiss: () => void) => JSX.Element; interface UseModalProps { @@ -42,7 +42,7 @@ export function useModal(props: UseModalProps & PropFunc): UseModalR () => !modalShown ? null - : typeof modal === "function" + : typeof modal === 'function' ? modal(dismiss) : modal, [modalShown, modal, dismiss] @@ -59,7 +59,7 @@ export function useModal(props: UseModalProps & PropFunc): UseModalR bg="white" borderRadius={2} border={[0, 1]} - borderColor={["washedGray", "washedGray"]} + borderColor={['washedGray', 'washedGray']} display="flex" alignItems="stretch" flexDirection="column" @@ -76,6 +76,6 @@ export function useModal(props: UseModalProps & PropFunc): UseModalR return { showModal, - modal: modalComponent, + modal: modalComponent }; } diff --git a/pkg/interface/src/logic/lib/useOutsideClick.ts b/pkg/interface/src/logic/lib/useOutsideClick.ts index 4d4f95ca6a..481fa2a904 100644 --- a/pkg/interface/src/logic/lib/useOutsideClick.ts +++ b/pkg/interface/src/logic/lib/useOutsideClick.ts @@ -1,8 +1,8 @@ -import { useEffect, RefObject } from "react"; +import { useEffect, RefObject } from 'react'; export function useOutsideClick( ref: RefObject, - onClick: () => void, + onClick: () => void ) { useEffect(() => { function handleClick(event: MouseEvent) { @@ -16,17 +16,16 @@ export function useOutsideClick( } function handleKeyDown(ev) { - if(ev.key === "Escape") { + if(ev.key === 'Escape') { onClick(); - } } - document.addEventListener("mousedown", handleClick); - document.addEventListener("keydown", handleKeyDown); + document.addEventListener('mousedown', handleClick); + document.addEventListener('keydown', handleKeyDown); return () => { - document.removeEventListener("mousedown", handleClick); - document.removeEventListener("keydown", handleKeyDown); + document.removeEventListener('mousedown', handleClick); + document.removeEventListener('keydown', handleKeyDown); }; }, [ref.current, onClick]); } diff --git a/pkg/interface/src/logic/lib/usePreviousValue.ts b/pkg/interface/src/logic/lib/usePreviousValue.ts index e240978108..109f9ffa86 100644 --- a/pkg/interface/src/logic/lib/usePreviousValue.ts +++ b/pkg/interface/src/logic/lib/usePreviousValue.ts @@ -1,7 +1,5 @@ -import { useRef } from "react"; -import { Primitive } from "~/types"; - - +import { useRef } from 'react'; +import { Primitive } from '@urbit/api'; export default function usePreviousValue(value: T): T { const prev = useRef(null); diff --git a/pkg/interface/src/logic/lib/useQuery.ts b/pkg/interface/src/logic/lib/useQuery.ts index 0957ca0988..735060a2da 100644 --- a/pkg/interface/src/logic/lib/useQuery.ts +++ b/pkg/interface/src/logic/lib/useQuery.ts @@ -1,5 +1,5 @@ -import { useMemo, useCallback } from "react"; -import { useLocation } from "react-router-dom"; +import { useMemo, useCallback } from 'react'; +import { useLocation } from 'react-router-dom'; import _ from 'lodash'; export function useQuery() { @@ -25,6 +25,6 @@ export function useQuery() { return { query, - appendQuery, + appendQuery }; } diff --git a/pkg/interface/src/logic/lib/useStatelessAsyncClickable.ts b/pkg/interface/src/logic/lib/useStatelessAsyncClickable.ts index b92fdaa03c..154f903baa 100644 --- a/pkg/interface/src/logic/lib/useStatelessAsyncClickable.ts +++ b/pkg/interface/src/logic/lib/useStatelessAsyncClickable.ts @@ -1,23 +1,23 @@ -import { MouseEvent, useCallback, useState, useEffect } from "react"; -export type AsyncClickableState = "waiting" | "error" | "loading" | "success"; +import { MouseEvent, useCallback, useState, useEffect } from 'react'; +export type AsyncClickableState = 'waiting' | 'error' | 'loading' | 'success'; export function useStatelessAsyncClickable( onClick: (e: MouseEvent) => Promise, name: string ) { - const [state, setState] = useState("waiting"); + const [state, setState] = useState('waiting'); const handleClick = useCallback( async (e: MouseEvent) => { try { - setState("loading"); + setState('loading'); await onClick(e); - setState("success"); + setState('success'); } catch (e) { console.error(e); - setState("error"); + setState('error'); } finally { setTimeout(() => { - setState("waiting"); + setState('waiting'); }, 3000); } }, @@ -26,7 +26,7 @@ export function useStatelessAsyncClickable( // When name changes, reset button useEffect(() => { - setState("waiting"); + setState('waiting'); }, [name]); return { buttonState: state, onClick: handleClick }; diff --git a/pkg/interface/src/logic/lib/useS3.ts b/pkg/interface/src/logic/lib/useStorage.ts similarity index 52% rename from pkg/interface/src/logic/lib/useS3.ts rename to pkg/interface/src/logic/lib/useStorage.ts index 0717d52f7f..7e7438d11f 100644 --- a/pkg/interface/src/logic/lib/useS3.ts +++ b/pkg/interface/src/logic/lib/useStorage.ts @@ -1,9 +1,16 @@ -import { useCallback, useMemo, useEffect, useRef, useState } from "react"; -import { S3State } from "../../types/s3-update"; -import S3 from "aws-sdk/clients/s3"; -import { dateToDa, deSig } from "./util"; +import { useCallback, useMemo, useEffect, useRef, useState } from 'react'; +import { + GcpState, + S3State, + StorageState +} from '../../types'; +import S3 from 'aws-sdk/clients/s3'; +import GcpClient from './GcpClient'; +import { StorageClient, StorageAcl } from './StorageClient'; +import { dateToDa, deSig } from './util'; -export interface IuseS3 { + +export interface IuseStorage { canUpload: boolean; upload: (file: File, bucket: string) => Promise; uploadDefault: (file: File) => Promise; @@ -11,31 +18,43 @@ export interface IuseS3 { promptUpload: () => Promise; } -const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => { +const useStorage = ({gcp, s3}: StorageState, + { accept = '*' } = { accept: '*' }): IuseStorage => { const [uploading, setUploading] = useState(false); - const client = useRef(null); + const client = useRef(null); useEffect(() => { - if (!s3.credentials) { - return; + // prefer GCP if available, else use S3. + if (gcp.token !== undefined) { + client.current = new GcpClient(gcp.token.accessKey); + } else { + // XX ships currently always have S3 credentials, but the fields are all + // set to '' if they are not configured. + if (!s3.credentials || + !s3.credentials.accessKeyId || + !s3.credentials.secretAccessKey) { + return; + } + client.current = new S3({ + credentials: s3.credentials, + endpoint: s3.credentials.endpoint + }); } - client.current = new S3({ - credentials: s3.credentials, - endpoint: s3.credentials.endpoint - }); - }, [s3.credentials]); + }, [gcp.token, s3.credentials]); const canUpload = useMemo( () => - (client && s3.credentials && s3.configuration.currentBucket !== "") || false, - [s3.credentials, s3.configuration.currentBucket, client] + ((gcp.token || (s3.credentials && s3.credentials.accessKeyId && + s3.credentials.secretAccessKey)) && + s3.configuration.currentBucket !== '') || false, + [s3.credentials, gcp.token, s3.configuration.currentBucket] ); const upload = useCallback( async (file: File, bucket: string) => { - if (!client.current) { - throw new Error("S3 not ready"); + if (client.current === null) { + throw new Error('Storage not ready'); } const fileParts = file.name.split('.'); @@ -47,8 +66,8 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => { Bucket: bucket, Key: `${window.ship}/${timestamp}-${fileName}.${fileExtension}`, Body: file, - ACL: "public-read", - ContentType: file.type, + ACL: StorageAcl.PublicRead, + ContentType: file.type }; setUploading(true); @@ -63,11 +82,11 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => { ); const uploadDefault = useCallback(async (file: File) => { - if (s3.configuration.currentBucket === "") { - throw new Error("current bucket not set"); + if (s3.configuration.currentBucket === '') { + throw new Error('current bucket not set'); } return upload(file, s3.configuration.currentBucket); - }, [s3]); + }, [s3, upload]); const promptUpload = useCallback( () => { @@ -84,16 +103,15 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => { } uploadDefault(files[0]).then(resolve); document.body.removeChild(fileSelector); - }) + }); document.body.appendChild(fileSelector); fileSelector.click(); - }) - + }); }, [uploadDefault] ); - return { canUpload, upload, uploadDefault, uploading, promptUpload }; + return {canUpload, upload, uploadDefault, uploading, promptUpload}; }; -export default useS3; \ No newline at end of file +export default useStorage; diff --git a/pkg/interface/src/logic/lib/useWaitForProps.ts b/pkg/interface/src/logic/lib/useWaitForProps.ts index 80e92e92a8..8a81b0b0ae 100644 --- a/pkg/interface/src/logic/lib/useWaitForProps.ts +++ b/pkg/interface/src/logic/lib/useWaitForProps.ts @@ -1,12 +1,11 @@ import { useState, useEffect, useCallback } from 'react'; - -export function useWaitForProps

(props: P, timeout: number = 0) { +export function useWaitForProps

(props: P, timeout = 0) { const [resolve, setResolve] = useState<() => void>(() => () => {}); const [ready, setReady] = useState<(p: P) => boolean | undefined>(); useEffect(() => { - if (typeof ready === "function" && ready(props)) { + if (typeof ready === 'function' && ready(props)) { resolve(); } }, [props, ready, resolve]); @@ -26,7 +25,7 @@ export function useWaitForProps

(props: P, timeout: number = 0) { setResolve(() => resolve); if(timeout > 0) { setTimeout(() => { - reject(new Error("Timed out")); + reject(new Error('Timed out')); }, timeout); } }); diff --git a/pkg/interface/src/logic/lib/util.ts b/pkg/interface/src/logic/lib/util.ts index 670c83d430..d15be08cb3 100644 --- a/pkg/interface/src/logic/lib/util.ts +++ b/pkg/interface/src/logic/lib/util.ts @@ -3,25 +3,25 @@ import _ from "lodash"; import f, { memoize } from "lodash/fp"; import bigInt, { BigInteger } from "big-integer"; import { Contact } from '~/types'; -import useLocalState from '../state/local'; +import useSettingsState from '../state/settings'; export const MOBILE_BROWSER_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i; export const MOMENT_CALENDAR_DATE = { - sameDay: "[Today]", - nextDay: "[Tomorrow]", - nextWeek: "dddd", - lastDay: "[Yesterday]", - lastWeek: "[Last] dddd", - sameElse: "~YYYY.M.D", + sameDay: '[Today]', + nextDay: '[Tomorrow]', + nextWeek: 'dddd', + lastDay: '[Yesterday]', + lastWeek: '[Last] dddd', + sameElse: '~YYYY.M.D' }; export const getModuleIcon = (mod: string) => { - if (mod === "link") { - return "Collection"; + if (mod === 'link') { + return 'Collection'; } return _.capitalize(mod); -} +}; export function wait(ms: number) { return new Promise((resolve, reject) => { @@ -37,8 +37,8 @@ export function parentPath(path: string) { return _.dropRight(path.split('/'), 1).join('/'); } -const DA_UNIX_EPOCH = bigInt("170141184475152167957503069145530368000"); // `@ud` ~1970.1.1 -const DA_SECOND = bigInt("18446744073709551616"); // `@ud` ~s1 +const DA_UNIX_EPOCH = bigInt('170141184475152167957503069145530368000'); // `@ud` ~1970.1.1 +const DA_SECOND = bigInt('18446744073709551616'); // `@ud` ~s1 export function daToUnix(da: BigInteger) { // ported from +time:enjs:format in hoon.hoon const offset = DA_SECOND.divide(bigInt(2000)); @@ -59,20 +59,20 @@ export function makePatDa(patda: string) { } export function udToDec(ud: string): string { - return ud.replace(/\./g, ""); + return ud.replace(/\./g, ''); } export function decToUd(str: string): string { return _.trimStart( f.flow( - f.split(""), + f.split(''), f.reverse, f.chunk(3), - f.map(f.flow(f.reverse, f.join(""))), + f.map(f.flow(f.reverse, f.join(''))), f.reverse, - f.join(".") + f.join('.') )(str), - "0." + '0.' ); } @@ -86,12 +86,12 @@ export function clamp(x: number, min: number, max: number) { // color is a #000000 color export function adjustHex(color: string, amount: number): string { return f.flow( - f.split(""), + f.split(''), f.chunk(2), // get RGB channels - f.map((c) => parseInt(c.join(""), 16)), // as hex - f.map((c) => clamp(c + amount, 0, 255).toString(16)), // adjust - f.join(""), - (res) => `#${res}` //format + f.map(c => parseInt(c.join(''), 16)), // as hex + f.map(c => clamp(c + amount, 0, 255).toString(16)), // adjust + f.join(''), + res => `#${res}` // format )(color.slice(1)); } @@ -101,12 +101,12 @@ export function resourceAsPath(resource: any) { } export function uuid() { - let str = "0v"; - str += Math.ceil(Math.random() * 8) + "."; + let str = '0v'; + str += Math.ceil(Math.random() * 8) + '.'; for (let i = 0; i < 5; i++) { let _str = Math.ceil(Math.random() * 10000000).toString(32); - _str = ("00000" + _str).substr(-5, 5); - str += _str + "."; + _str = ('00000' + _str).substr(-5, 5); + str += _str + '.'; } return str.slice(0, -1); @@ -120,11 +120,11 @@ export function uuid() { */ export function daToDate(st: string) { const dub = function (n: string) { - return parseInt(n) < 10 ? "0" + parseInt(n) : n.toString(); + return parseInt(n) < 10 ? '0' + parseInt(n) : n.toString(); }; - const da = st.split(".."); - const bigEnd = da[0].split("."); - const lilEnd = da[1].split("."); + const da = st.split('..'); + const bigEnd = da[0].split('.'); + const lilEnd = da[1].split('.'); const ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub( lilEnd[0] )}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`; @@ -138,9 +138,9 @@ export function daToDate(st: string) { ~2018.7.17..23.15.09..5be5 // urbit @da */ -export function dateToDa(d: Date, mil: boolean = false) { +export function dateToDa(d: Date, mil = false) { const fil = function (n: number) { - return n >= 10 ? n : "0" + n; + return n >= 10 ? n : '0' + n; }; return ( `~${d.getUTCFullYear()}.` + @@ -149,7 +149,7 @@ export function dateToDa(d: Date, mil: boolean = false) { `${fil(d.getUTCHours())}.` + `${fil(d.getUTCMinutes())}.` + `${fil(d.getUTCSeconds())}` + - `${mil ? "..0000" : ""}` + `${mil ? '..0000' : ''}` ); } @@ -157,16 +157,16 @@ export function deSig(ship: string) { if (!ship) { return null; } - return ship.replace("~", ""); + return ship.replace('~', ''); } export function uxToHex(ux: string) { - if (ux.length > 2 && ux.substr(0, 2) === "0x") { - const value = ux.substr(2).replace(".", "").padStart(6, "0"); + if (ux.length > 2 && ux.substr(0, 2) === '0x') { + const value = ux.substr(2).replace('.', '').padStart(6, '0'); return value; } - const value = ux.replace(".", "").padStart(6, "0"); + const value = ux.replace('.', '').padStart(6, '0'); return value; } @@ -187,13 +187,13 @@ export function writeText(str: string) { let success = false; function listener(e) { - e.clipboardData.setData("text/plain", str); + e.clipboardData.setData('text/plain', str); e.preventDefault(); success = true; } - document.addEventListener("copy", listener); - document.execCommand("copy"); - document.removeEventListener("copy", listener); + document.addEventListener('copy', listener); + document.execCommand('copy'); + document.removeEventListener('copy', listener); document?.getSelection()?.removeAllRanges(); @@ -206,21 +206,21 @@ export function writeText(str: string) { // trim patps to match dojo, chat-cli export function cite(ship: string) { let patp = ship, - shortened = ""; - if (patp === null || patp === "") { + shortened = ''; + if (patp === null || patp === '') { return null; } - if (patp.startsWith("~")) { + if (patp.startsWith('~')) { patp = patp.substr(1); } // comet if (patp.length === 56) { - shortened = "~" + patp.slice(0, 6) + "_" + patp.slice(50, 56); + shortened = '~' + patp.slice(0, 6) + '_' + patp.slice(50, 56); return shortened; } // moon if (patp.length === 27) { - shortened = "~" + patp.slice(14, 20) + "^" + patp.slice(21, 27); + shortened = '~' + patp.slice(14, 20) + '^' + patp.slice(21, 27); return shortened; } return `~${patp}`; @@ -232,7 +232,6 @@ export function alphabeticalOrder(a: string, b: string) { export function lengthOrder(a: string, b: string) { return b.length - a.length; - } // TODO: deprecated @@ -244,13 +243,13 @@ export function alphabetiseAssociations(associations: any) { let bName = b.substr(1); if (associations[a].metadata && associations[a].metadata.title) { aName = - associations[a].metadata.title !== "" + associations[a].metadata.title !== '' ? associations[a].metadata.title : a.substr(1); } if (associations[b].metadata && associations[b].metadata.title) { bName = - associations[b].metadata.title !== "" + associations[b].metadata.title !== '' ? associations[b].metadata.title : b.substr(1); } @@ -266,41 +265,42 @@ export function alphabetiseAssociations(associations: any) { // for example, 'some Chars!' becomes '~.some.~43.hars~21.' // export function stringToTa(str: string) { - let out = ""; + let out = ''; for (let i = 0; i < str.length; i++) { const char = str[i]; - let add = ""; + let add = ''; switch (char) { - case " ": - add = "."; + case ' ': + add = '.'; break; - case ".": - add = "~."; + case '.': + add = '~.'; break; - case "~": - add = "~~"; + case '~': + add = '~~'; break; default: const charCode = str.charCodeAt(i); if ( (charCode >= 97 && charCode <= 122) || // a-z (charCode >= 48 && charCode <= 57) || // 0-9 - char === "-" + char === '-' ) { add = char; } else { // TODO behavior for unicode doesn't match +wood's, // but we can probably get away with that for now. - add = "~" + charCode.toString(16) + "."; + add = '~' + charCode.toString(16) + '.'; } } out = out + add; } - return "~." + out; + return '~.' + out; } export function amOwnerOfGroup(groupPath: string) { - if (!groupPath) return false; + if (!groupPath) +return false; const groupOwner = /(\/~)?\/~([a-z-]{3,})\/.*/.exec(groupPath)?.[2]; return window.ship === groupOwner; } @@ -308,18 +308,18 @@ export function amOwnerOfGroup(groupPath: string) { export function getContactDetails(contact: any) { const member = !contact; contact = contact || { - nickname: "", + nickname: '', avatar: null, - color: "0x0", + color: '0x0' }; - const nickname = contact.nickname || ""; - const color = uxToHex(contact.color || "0x0"); + const nickname = contact.nickname || ''; + const color = uxToHex(contact.color || '0x0'); const avatar = contact.avatar || null; return { nickname, color, member, avatar }; } export function stringToSymbol(str: string) { - let result = ""; + let result = ''; for (let i = 0; i < str.length; i++) { const n = str.charCodeAt(i); if ((n >= 97 && n <= 122) || (n >= 48 && n <= 57)) { @@ -327,19 +327,17 @@ export function stringToSymbol(str: string) { } else if (n >= 65 && n <= 90) { result += String.fromCharCode(n + 32); } else { - result += "-"; + result += '-'; } } - result = result.replace(/^[\-\d]+|\-+/g, "-"); - result = result.replace(/^\-+|\-+$/g, ""); - if (result === "") { + result = result.replace(/^[\-\d]+|\-+/g, '-'); + result = result.replace(/^\-+|\-+$/g, ''); + if (result === '') { return dateToDa(new Date()); } return result; } - - /** * Formats a numbers as a `@ud` inserting dot where needed */ @@ -351,23 +349,24 @@ export function numToUd(num: number) { f.reverse, f.map(s => s.join('')), f.join('.') - )(num.toString()) + )(num.toString()); } -export function usePreventWindowUnload(shouldPreventDefault: boolean, message = "You have unsaved changes. Are you sure you want to exit?") { +export function usePreventWindowUnload(shouldPreventDefault: boolean, message = 'You have unsaved changes. Are you sure you want to exit?') { useEffect(() => { - if (!shouldPreventDefault) return; - const handleBeforeUnload = event => { + if (!shouldPreventDefault) +return; + const handleBeforeUnload = (event) => { event.preventDefault(); return message; - } - window.addEventListener("beforeunload", handleBeforeUnload); + }; + window.addEventListener('beforeunload', handleBeforeUnload); window.onbeforeunload = handleBeforeUnload; return () => { - window.removeEventListener("beforeunload", handleBeforeUnload); + window.removeEventListener('beforeunload', handleBeforeUnload); // @ts-ignore window.onbeforeunload = undefined; - } + }; }, [shouldPreventDefault]); } @@ -377,7 +376,7 @@ export function pluralize(text: string, isPlural = false, vowel = false) { // Hide is an optional second parameter for when this function is used in class components export function useShowNickname(contact: Contact | null, hide?: boolean): boolean { - const hideNicknames = typeof hide !== 'undefined' ? hide : useLocalState(state => state.hideNicknames); + const hideNicknames = typeof hide !== 'undefined' ? hide : useSettingsState(state => state.calm.hideNicknames); return !!(contact && contact.nickname && !hideNicknames); } @@ -406,7 +405,6 @@ export function getItemTitle(association: Association) { return cite(`~${name.slice(4)}`); } return cite(ship); - } - return association.metadata.title || association.resource -}; + return association.metadata.title || association.resource; +} diff --git a/pkg/interface/src/logic/lib/virtualContext.tsx b/pkg/interface/src/logic/lib/virtualContext.tsx new file mode 100644 index 0000000000..fb22350c86 --- /dev/null +++ b/pkg/interface/src/logic/lib/virtualContext.tsx @@ -0,0 +1,47 @@ +import React, { + useContext, + useState, + useCallback, + useLayoutEffect, +} from "react"; + +export interface VirtualContextProps { + save: () => void; + restore: () => void; +} +const fallback: VirtualContextProps = { + save: () => {}, + restore: () => {}, +}; + +export const VirtualContext = React.createContext(fallback); + +export function useVirtual() { + return useContext(VirtualContext); +} + +export const withVirtual =

(Component: React.ComponentType

) => + React.forwardRef((props: P, ref) => ( + + {(context) => } + + )); + +export function useVirtualResizeState(s: boolean) { + const [state, _setState] = useState(s); + const { save, restore } = useVirtual(); + + const setState = useCallback( + (sta: boolean) => { + save(); + _setState(sta); + }, + [_setState, save] + ); + + useLayoutEffect(() => { + restore(); + }, [state]); + + return [state, setState] as const; +} diff --git a/pkg/interface/src/logic/lib/workspace.ts b/pkg/interface/src/logic/lib/workspace.ts index e17b81e7ba..f13df61956 100644 --- a/pkg/interface/src/logic/lib/workspace.ts +++ b/pkg/interface/src/logic/lib/workspace.ts @@ -1,24 +1,24 @@ -import { Associations, Workspace } from "~/types"; +import { Associations, Workspace } from '@urbit/api'; export function getTitleFromWorkspace( associations: Associations, workspace: Workspace ) { switch (workspace.type) { - case "home": - return "My Channels"; - case "messages": - return "Messages"; - case "group": + case 'home': + return 'My Channels'; + case 'messages': + return 'Messages'; + case 'group': const association = associations.groups[workspace.group]; - return association?.metadata?.title || ""; + return association?.metadata?.title || ''; } } export function getGroupFromWorkspace( workspace: Workspace ): string | undefined { - if (workspace.type === "group") { + if (workspace.type === 'group') { return workspace.group; } diff --git a/pkg/interface/src/logic/reducers/contact-update.ts b/pkg/interface/src/logic/reducers/contact-update.ts index 932852eced..21b83a0044 100644 --- a/pkg/interface/src/logic/reducers/contact-update.ts +++ b/pkg/interface/src/logic/reducers/contact-update.ts @@ -1,8 +1,8 @@ import _ from 'lodash'; import { StoreState } from '../../store/type'; import { Cage } from '~/types/cage'; -import { ContactUpdate } from '~/types/contact-update'; -import {resourceAsPath} from '../lib/util'; +import { ContactUpdate } from '@urbit/api/contacts'; +import { resourceAsPath } from '../lib/util'; type ContactState = Pick; @@ -78,4 +78,3 @@ const setPublic = (json: ContactUpdate, state: S) => { state.isContactPublic = data; }; - diff --git a/pkg/interface/src/logic/reducers/gcp-reducer.ts b/pkg/interface/src/logic/reducers/gcp-reducer.ts new file mode 100644 index 0000000000..2900df976d --- /dev/null +++ b/pkg/interface/src/logic/reducers/gcp-reducer.ts @@ -0,0 +1,37 @@ +import _ from 'lodash'; +import {StoreState} from '../store/type'; +import {GcpToken} from '../../types/gcp-state'; + +type GcpState = Pick; + +export default class GcpReducer{ + reduce(json: Cage, state: S) { + this.reduceConfigured(json, state); + this.reduceToken(json, state); + } + + reduceConfigured(json, state) { + let data = json['gcp-configured']; + if (data !== undefined) { + state.storage.gcp.configured = data; + } + } + + reduceToken(json: Cage, state: S) { + let data = json['gcp-token']; + if (data) { + this.setToken(data, state); + } + } + + setToken(data: any, state: S) { + if (this.isToken(data)) { + state.storage.gcp.token = data; + } + } + + isToken(token: any): token is GcpToken { + return (typeof(token.accessKey) === 'string' && + typeof(token.expiresIn) === 'number'); + } +} diff --git a/pkg/interface/src/logic/reducers/graph-update.js b/pkg/interface/src/logic/reducers/graph-update.js index c1fe98fbea..1bf12d8a16 100644 --- a/pkg/interface/src/logic/reducers/graph-update.js +++ b/pkg/interface/src/logic/reducers/graph-update.js @@ -4,6 +4,7 @@ import bigInt, { BigInteger } from "big-integer"; export const GraphReducer = (json, state) => { const data = _.get(json, 'graph-update', false); + if (data) { keys(data, state); addGraph(data, state); @@ -55,6 +56,8 @@ const addGraph = (json, state) => { let resource = data.resource.ship + '/' + data.resource.name; state.graphs[resource] = new BigIntOrderedMap(); + state.graphTimesentMap[resource] = {}; + for (let idx in data.graph) { let item = data.graph[idx]; @@ -95,7 +98,7 @@ const mapifyChildren = (children) => { }; const addNodes = (json, state) => { - const _addNode = (graph, index, node) => { + const _addNode = (graph, index, node, resource) => { // set child of graph if (index.length === 1) { graph.set(index[0], node); @@ -113,6 +116,48 @@ const addNodes = (json, state) => { return graph; }; + const _remove = (graph, index) => { + if (index.length === 1) { + graph.delete(index[0]); + } else { + const child = graph.get(index[0]); + if (child) { + child.children = _remove(child.children, index.slice(1)); + graph.set(index[0], child); + } + } + + return graph; + }; + + const _killByFuzzyTimestamp = (graph, resource, timestamp) => { + if (state.graphTimesentMap[resource][timestamp]) { + let index = state.graphTimesentMap[resource][timestamp]; + + if (index.split('/').length === 0) { return; } + let indexArr = index.split('/').slice(1).map((ind) => { + return bigInt(ind); + }); + + graph = _remove(graph, indexArr); + delete state.graphTimesentMap[resource][timestamp]; + } + + return graph; + }; + + const _removePending = (graph, post, resource) => { + if (!post.hash) { + return graph; + } + + graph = _killByFuzzyTimestamp(graph, resource, post['time-sent']); + graph = _killByFuzzyTimestamp(graph, resource, post['time-sent'] - 1); + graph = _killByFuzzyTimestamp(graph, resource, post['time-sent'] + 1); + + return graph; + }; + const data = _.get(json, 'add-nodes', false); if (data) { if (!('graphs' in state)) { return; } @@ -121,40 +166,66 @@ const addNodes = (json, state) => { if (!(resource in state.graphs)) { state.graphs[resource] = new BigIntOrderedMap(); } + + if (!(resource in state.graphTimesentMap)) { + state.graphTimesentMap[resource] = {}; + } + state.graphKeys.add(resource); + + let indices = Array.from(Object.keys(data.nodes)); - for (let index in data.nodes) { + indices.sort((a, b) => { + let aArr = a.split('/'); + let bArr = b.split('/'); + return bArr.length < aArr.length; + }); + + let graph = state.graphs[resource]; + + indices.forEach((index) => { let node = data.nodes[index]; + graph = _removePending(graph, node.post, resource); + if (index.split('/').length === 0) { return; } - - index = index.split('/').slice(1).map((ind) => { + let indexArr = index.split('/').slice(1).map((ind) => { return bigInt(ind); }); - if (index.length === 0) { return; } + if (indexArr.length === 0) { return; } + + if (node.post.pending) { + state.graphTimesentMap[resource][node.post['time-sent']] = index; + } node.children = mapifyChildren(node?.children || {}); - - - state.graphs[resource] = _addNode( - state.graphs[resource], - index, + + graph = _addNode( + graph, + indexArr, node ); - } + + }); + + state.graphs[resource] = graph; } }; + const removeNodes = (json, state) => { const _remove = (graph, index) => { if (index.length === 1) { graph.delete(index[0]); } else { const child = graph.get(index[0]); - _remove(child.children, index.slice(1)); - graph.set(index[0], child); + if (child) { + _remove(child.children, index.slice(1)); + graph.set(index[0], child); + } } }; + const data = _.get(json, 'remove-nodes', false); if (data) { const { ship, name } = data.resource; diff --git a/pkg/interface/src/logic/reducers/group-update.ts b/pkg/interface/src/logic/reducers/group-update.ts index 169b30d56c..49a620edf2 100644 --- a/pkg/interface/src/logic/reducers/group-update.ts +++ b/pkg/interface/src/logic/reducers/group-update.ts @@ -10,9 +10,9 @@ import { OpenPolicyDiff, OpenPolicy, InvitePolicyDiff, - InvitePolicy, -} from '~/types/group-update'; -import { Enc, PatpNoSig } from '~/types/noun'; + InvitePolicy +} from '@urbit/api/groups'; +import { Enc, PatpNoSig } from '@urbit/api'; import { resourceAsPath } from '../lib/util'; type GroupState = Pick; @@ -23,7 +23,7 @@ function decodeGroup(group: Enc): Group { ...group, members, tags: decodeTags(group.tags), - policy: decodePolicy(group.policy), + policy: decodePolicy(group.policy) }; return res; } @@ -35,7 +35,7 @@ function decodePolicy(policy: Enc): GroupPolicy { } else { const { open } = policy; return { - open: { banned: new Set(open.banned), banRanks: new Set(open.banRanks) }, + open: { banned: new Set(open.banned), banRanks: new Set(open.banRanks) } }; } } @@ -61,6 +61,7 @@ export default class GroupReducer { reduce(json: Cage, state: S) { const data = json.groupUpdate; if (data) { + console.log(data); this.initial(data, state); this.addMembers(data, state); this.addTag(data, state); @@ -98,7 +99,7 @@ export default class GroupReducer { members: new Set(), tags: { role: { admin: new Set([window.ship]) } }, policy: decodePolicy(policy), - hidden, + hidden }; } } @@ -116,6 +117,12 @@ export default class GroupReducer { const resourcePath = resourceAsPath(resource); for (const member of ships) { state.groups[resourcePath].members.add(member); + if ( + 'invite' in state.groups[resourcePath].policy && + state.groups[resourcePath].policy.invite.pending.has(member) + ) { + state.groups[resourcePath].policy.invite.pending.delete(member) + } } } } @@ -189,7 +196,6 @@ export default class GroupReducer { } } - private inviteChangePolicy(diff: InvitePolicyDiff, policy: InvitePolicy) { if ('addInvites' in diff) { const { addInvites } = diff; diff --git a/pkg/interface/src/logic/reducers/group-view.ts b/pkg/interface/src/logic/reducers/group-view.ts index 92be7c0f5f..dd8763c6d6 100644 --- a/pkg/interface/src/logic/reducers/group-view.ts +++ b/pkg/interface/src/logic/reducers/group-view.ts @@ -1,25 +1,24 @@ -import { resourceAsPath } from "~/logic/lib/util"; - +import { resourceAsPath } from '~/logic/lib/util'; const initial = (json: any, state: any) => { const data = json.initial; if(data) { state.pendingJoin = data; } -} +}; const progress = (json: any, state: any) => { const data = json.progress; if(data) { const { progress, resource } = data; - state.pendingJoin = {...state.pendingJoin, [resource]: progress }; + state.pendingJoin = { ...state.pendingJoin, [resource]: progress }; if(progress === 'done') { setTimeout(() => { delete state.pendingJoin[resource]; }, 10000); } } -} +}; export const GroupViewReducer = (json: any, state: any) => { const data = json['group-view-update']; @@ -27,4 +26,4 @@ export const GroupViewReducer = (json: any, state: any) => { progress(data, state); initial(data, state); } -} +}; diff --git a/pkg/interface/src/logic/reducers/hark-update.ts b/pkg/interface/src/logic/reducers/hark-update.ts index 6a415aaf11..c2dce2959c 100644 --- a/pkg/interface/src/logic/reducers/hark-update.ts +++ b/pkg/interface/src/logic/reducers/hark-update.ts @@ -3,22 +3,21 @@ import { NotifIndex, NotificationGraphConfig, GroupNotificationsConfig, - UnreadStats, -} from "~/types"; -import { makePatDa } from "~/logic/lib/util"; -import _ from "lodash"; -import {StoreState} from "../store/type"; + UnreadStats +} from '@urbit/api'; +import { makePatDa } from '~/logic/lib/util'; +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) => { - const data = _.get(json, "harkUpdate", false); + const data = _.get(json, 'harkUpdate', false); if (data) { reduce(data, state); } - const graphHookData = _.get(json, "hark-graph-hook-update", false); + const graphHookData = _.get(json, 'hark-graph-hook-update', false); if (graphHookData) { graphInitial(graphHookData, state); graphIgnore(graphHookData, state); @@ -26,7 +25,7 @@ export const HarkReducer = (json: any, state: HarkState) => { graphWatchSelf(graphHookData, state); graphMentions(graphHookData, state); } - const groupHookData = _.get(json, "hark-group-hook-update", false); + const groupHookData = _.get(json, 'hark-group-hook-update', false); if (groupHookData) { groupInitial(groupHookData, state); groupListen(groupHookData, state); @@ -35,31 +34,31 @@ export const HarkReducer = (json: any, state: HarkState) => { }; function groupInitial(json: any, state: HarkState) { - const data = _.get(json, "initial", false); + const data = _.get(json, 'initial', false); if (data) { state.notificationsGroupConfig = data; } } function graphInitial(json: any, state: HarkState) { - const data = _.get(json, "initial", false); + const data = _.get(json, 'initial', false); if (data) { state.notificationsGraphConfig = data; } } function graphListen(json: any, state: HarkState) { - const data = _.get(json, "listen", false); + const data = _.get(json, 'listen', false); if (data) { state.notificationsGraphConfig.watching = [ ...state.notificationsGraphConfig.watching, - data, + data ]; } } function graphIgnore(json: any, state: HarkState) { - const data = _.get(json, "ignore", false); + const data = _.get(json, 'ignore', false); if (data) { state.notificationsGraphConfig.watching = state.notificationsGraphConfig.watching.filter( ({ graph, index }) => !(graph === data.graph && index === data.index) @@ -68,30 +67,30 @@ function graphIgnore(json: any, state: HarkState) { } function groupListen(json: any, state: HarkState) { - const data = _.get(json, "listen", false); + const data = _.get(json, 'listen', false); if (data) { state.notificationsGroupConfig = [...state.notificationsGroupConfig, data]; } } function groupIgnore(json: any, state: HarkState) { - const data = _.get(json, "ignore", false); + const data = _.get(json, 'ignore', false); if (data) { state.notificationsGroupConfig = state.notificationsGroupConfig.filter( - (n) => n !== data + n => n !== data ); } } function graphMentions(json: any, state: HarkState) { - const data = _.get(json, "set-mentions", undefined); + const data = _.get(json, 'set-mentions', undefined); if (!_.isUndefined(data)) { state.notificationsGraphConfig.mentions = data; } } function graphWatchSelf(json: any, state: HarkState) { - const data = _.get(json, "set-watch-on-self", undefined); + const data = _.get(json, 'set-watch-on-self', undefined); if (!_.isUndefined(data)) { state.notificationsGraphConfig.watchOnSelf = data; } @@ -139,14 +138,14 @@ function seenIndex(json: any, state: HarkState) { function readEach(json: any, state: HarkState) { const data = _.get(json, 'read-each'); if(data) { - updateUnreads(state, data.index, u => u.delete(data.target)) + updateUnreads(state, data.index, u => u.delete(data.target)); } } function readSince(json: any, state: HarkState) { const data = _.get(json, 'read-count'); if(data) { - updateUnreadCount(state, data, () => 0) + updateUnreadCount(state, data, () => 0); } } @@ -160,7 +159,7 @@ function unreadSince(json: any, state: HarkState) { function unreadEach(json: any, state: HarkState) { const data = _.get(json, 'unread-each'); if(data) { - updateUnreads(state, data.index, us => us.add(data.target)) + updateUnreads(state, data.index, us => us.add(data.target)); } } @@ -183,15 +182,15 @@ function unreads(json: any, state: HarkState) { } } -function clearState(state){ - let initialState = { +function clearState(state) { + const initialState = { notifications: new BigIntOrderedMap(), archivedNotifications: new BigIntOrderedMap(), notificationsGroupConfig: [], notificationsGraphConfig: { watchOnSelf: false, mentions: false, - watching: [], + watching: [] }, unreads: { graph: {}, @@ -200,7 +199,7 @@ function clearState(state){ notificationsCount: 0 }; - Object.keys(initialState).forEach(key => { + Object.keys(initialState).forEach((key) => { state[key] = initialState[key]; }); } @@ -211,7 +210,7 @@ function updateUnreadCount(state: HarkState, index: NotifIndex, count: (c: numbe } const property = [index.graph.graph, index.graph.index, 'unreads']; const curr = _.get(state.unreads.graph, property, 0); - const newCount = count(curr) + const newCount = count(curr); _.set(state.unreads.graph, property, newCount); } @@ -226,7 +225,6 @@ function updateUnreads(state: HarkState, index: NotifIndex, f: (us: Set) _.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], unreads); } - function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'notifications' | 'unreads' | 'last', f: (x: number) => number) { if(statField === 'notifications') { state.notificationsCount = f(state.notificationsCount); @@ -235,19 +233,19 @@ function updateNotificationStats(state: HarkState, index: NotifIndex, statField: const curr = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, statField], 0); _.set(state.unreads.graph, [index.graph.graph, index.graph.index, statField], f(curr)); } else if('group' in index) { - const curr = _.get(state.unreads.group, [index.group.group, statField], 0); - _.set(state.unreads.group, [index.group.group, statField], f(curr)); + const curr = _.get(state.unreads.group, [index.group, statField], 0); + _.set(state.unreads.group, [index.group, statField], f(curr)); } } function added(json: any, state: HarkState) { - const data = _.get(json, "added", false); + const data = _.get(json, 'added', false); if (data) { const { index, notification } = data; const time = makePatDa(data.time); const timebox = state.notifications.get(time) || []; - const arrIdx = timebox.findIndex((idxNotif) => + const arrIdx = timebox.findIndex(idxNotif => notifIdxEqual(index, idxNotif.index) ); if (arrIdx !== -1) { @@ -264,14 +262,14 @@ function added(json: any, state: HarkState) { } const dnd = (json: any, state: HarkState) => { - const data = _.get(json, "set-dnd", undefined); + const data = _.get(json, 'set-dnd', undefined); if (!_.isUndefined(data)) { state.doNotDisturb = data; } }; const timebox = (json: any, state: HarkState) => { - const data = _.get(json, "timebox", false); + const data = _.get(json, 'timebox', false); if (data) { const time = makePatDa(data.time); if (!data.archive) { @@ -281,21 +279,21 @@ const timebox = (json: any, state: HarkState) => { }; function more(json: any, state: HarkState) { - const data = _.get(json, "more", false); + const data = _.get(json, 'more', false); if (data) { - _.forEach(data, (d) => reduce(d, state)); + _.forEach(data, d => reduce(d, state)); } } function notifIdxEqual(a: NotifIndex, b: NotifIndex) { - if ("graph" in a && "graph" in b) { + if ('graph' in a && 'graph' in b) { return ( a.graph.graph === b.graph.graph && a.graph.group === b.graph.group && a.graph.module === b.graph.module && a.graph.description === b.graph.description ); - } else if ("group" in a && "group" in b) { + } else if ('group' in a && 'group' in b) { return ( a.group.group === b.group.group && a.group.description === b.group.description @@ -313,14 +311,14 @@ function setRead( const patDa = makePatDa(time); const timebox = state.notifications.get(patDa); if (_.isNull(timebox)) { - console.warn("Modifying nonexistent timebox"); + console.warn('Modifying nonexistent timebox'); return; } - const arrIdx = timebox.findIndex((idxNotif) => + const arrIdx = timebox.findIndex(idxNotif => notifIdxEqual(index, idxNotif.index) ); if (arrIdx === -1) { - console.warn("Modifying nonexistent index"); + console.warn('Modifying nonexistent index'); return; } timebox[arrIdx].notification.read = read; @@ -328,7 +326,7 @@ function setRead( } function read(json: any, state: HarkState) { - const data = _.get(json, "read-note", false); + const data = _.get(json, 'read-note', false); if (data) { const { time, index } = data; updateNotificationStats(state, index, 'notifications', x => x-1); @@ -337,7 +335,7 @@ function read(json: any, state: HarkState) { } function unread(json: any, state: HarkState) { - const data = _.get(json, "unread-note", false); + const data = _.get(json, 'unread-note', false); if (data) { const { time, index } = data; updateNotificationStats(state, index, 'notifications', x => x+1); @@ -346,16 +344,16 @@ function unread(json: any, state: HarkState) { } function archive(json: any, state: HarkState) { - const data = _.get(json, "archive", false); + const data = _.get(json, 'archive', false); if (data) { const { index } = data; const time = makePatDa(data.time); const timebox = state.notifications.get(time); if (!timebox) { - console.warn("Modifying nonexistent timebox"); + console.warn('Modifying nonexistent timebox'); return; } - const [archived, unarchived] = _.partition(timebox, (idxNotif) => + const [archived, unarchived] = _.partition(timebox, idxNotif => notifIdxEqual(index, idxNotif.index) ); if(unarchived.length === 0) { @@ -365,6 +363,6 @@ function archive(json: any, state: HarkState) { state.notifications.set(time, unarchived); } const newlyRead = archived.filter(x => !x.notification.read).length; - updateNotificationStats(state, index, 'notifications', (x) => x - newlyRead); + updateNotificationStats(state, index, 'notifications', x => x - newlyRead); } } diff --git a/pkg/interface/src/logic/reducers/invite-update.ts b/pkg/interface/src/logic/reducers/invite-update.ts index 30fb61042b..c4f472be08 100644 --- a/pkg/interface/src/logic/reducers/invite-update.ts +++ b/pkg/interface/src/logic/reducers/invite-update.ts @@ -1,10 +1,9 @@ import _ from 'lodash'; import { StoreState } from '../../store/type'; import { Cage } from '~/types/cage'; -import { InviteUpdate } from '~/types/invite-update'; - -type InviteState = Pick; +import { InviteUpdate } from '@urbit/api/invite'; +type InviteState = Pick; export default class InviteReducer { reduce(json: Cage, state: S) { diff --git a/pkg/interface/src/logic/reducers/launch-update.ts b/pkg/interface/src/logic/reducers/launch-update.ts index d05145d788..56a00efcea 100644 --- a/pkg/interface/src/logic/reducers/launch-update.ts +++ b/pkg/interface/src/logic/reducers/launch-update.ts @@ -51,12 +51,11 @@ export default class LaunchReducer { changeIsShown(json: LaunchUpdate, state: S) { const data = _.get(json, 'changeIsShown', false); if (data) { - let tile = state.launch.tiles[data.name]; + const tile = state.launch.tiles[data.name]; console.log(tile); if (tile) { tile.isShown = data.isShown; } } } - } diff --git a/pkg/interface/src/logic/reducers/metadata-update.ts b/pkg/interface/src/logic/reducers/metadata-update.ts index 00070b87e3..41e6d79855 100644 --- a/pkg/interface/src/logic/reducers/metadata-update.ts +++ b/pkg/interface/src/logic/reducers/metadata-update.ts @@ -2,14 +2,14 @@ import _ from 'lodash'; import { StoreState } from '../../store/type'; -import { MetadataUpdate } from '~/types/metadata-update'; +import { MetadataUpdate } from '@urbit/api/metadata'; import { Cage } from '~/types/cage'; type MetadataState = Pick; export default class MetadataReducer { reduce(json: Cage, state: S) { - let data = json['metadata-update'] + const data = json['metadata-update']; if (data) { console.log(data); this.associations(data, state); @@ -29,13 +29,13 @@ export default class MetadataReducer { } associations(json: MetadataUpdate, state: S) { - let data = _.get(json, 'associations', false); + const data = _.get(json, 'associations', false); if (data) { - let metadata = state.associations; + const metadata = state.associations; Object.keys(data).forEach((key) => { - let val = data[key]; - let appName = val['app-name']; - let rid = val.resource; + const val = data[key]; + const appName = val['app-name']; + const rid = val.resource; if (!(appName in metadata)) { metadata[appName] = {}; } @@ -50,11 +50,11 @@ export default class MetadataReducer { } add(json: MetadataUpdate, state: S) { - let data = _.get(json, 'add', false); + const data = _.get(json, 'add', false); if (data) { - let metadata = state.associations; - let appName = data['app-name']; - let appPath = data.resource; + const metadata = state.associations; + const appName = data['app-name']; + const appPath = data.resource; if (!(appName in metadata)) { metadata[appName] = {}; @@ -69,11 +69,11 @@ export default class MetadataReducer { } update(json: MetadataUpdate, state: S) { - let data = _.get(json, 'update-metadata', false); + const data = _.get(json, 'update-metadata', false); if (data) { - let metadata = state.associations; - let appName = data['app-name']; - let rid = data.resource; + const metadata = state.associations; + const appName = data['app-name']; + const rid = data.resource; if (!(appName in metadata)) { metadata[appName] = {}; @@ -88,11 +88,11 @@ export default class MetadataReducer { } remove(json: MetadataUpdate, state: S) { - let data = _.get(json, 'remove', false); + const data = _.get(json, 'remove', false); if (data) { - let metadata = state.associations; - let appName = data['app-name']; - let rid = data.resource; + const metadata = state.associations; + const appName = data['app-name']; + const rid = data.resource; if (appName in metadata && rid in metadata[appName]) { delete metadata[appName][rid]; diff --git a/pkg/interface/src/logic/reducers/s3-update.ts b/pkg/interface/src/logic/reducers/s3-update.ts index d05f741b39..ba224cd9bf 100644 --- a/pkg/interface/src/logic/reducers/s3-update.ts +++ b/pkg/interface/src/logic/reducers/s3-update.ts @@ -23,14 +23,14 @@ export default class S3Reducer { credentials(json: S3Update, state: S) { const data = _.get(json, 'credentials', false); if (data) { - state.s3.credentials = data; + state.storage.s3.credentials = data; } } configuration(json: S3Update, state: S) { const data = _.get(json, 'configuration', false); if (data) { - state.s3.configuration = { + state.storage.s3.configuration = { buckets: new Set(data.buckets), currentBucket: data.currentBucket }; @@ -39,44 +39,44 @@ export default class S3Reducer { currentBucket(json: S3Update, state: S) { const data = _.get(json, 'setCurrentBucket', false); - if (data && state.s3) { - state.s3.configuration.currentBucket = data; + if (data && state.storage.s3) { + state.storage.s3.configuration.currentBucket = data; } } addBucket(json: S3Update, state: S) { const data = _.get(json, 'addBucket', false); if (data) { - state.s3.configuration.buckets = - state.s3.configuration.buckets.add(data); + state.storage.s3.configuration.buckets = + state.storage.s3.configuration.buckets.add(data); } } removeBucket(json: S3Update, state: S) { const data = _.get(json, 'removeBucket', false); if (data) { - state.s3.configuration.buckets.delete(data); + state.storage.s3.configuration.buckets.delete(data); } } endpoint(json: S3Update, state: S) { const data = _.get(json, 'setEndpoint', false); - if (data && state.s3.credentials) { - state.s3.credentials.endpoint = data; + if (data && state.storage.s3.credentials) { + state.storage.s3.credentials.endpoint = data; } } accessKeyId(json: S3Update , state: S) { const data = _.get(json, 'setAccessKeyId', false); - if (data && state.s3.credentials) { - state.s3.credentials.accessKeyId = data; + if (data && state.storage.s3.credentials) { + state.storage.s3.credentials.accessKeyId = data; } } secretAccessKey(json: S3Update, state: S) { const data = _.get(json, 'setSecretAccessKey', false); - if (data && state.s3.credentials) { - state.s3.credentials.secretAccessKey = data; + if (data && state.storage.s3.credentials) { + state.storage.s3.credentials.secretAccessKey = data; } } } diff --git a/pkg/interface/src/logic/reducers/settings-update.ts b/pkg/interface/src/logic/reducers/settings-update.ts index 9716cbb85c..8e3eacc78b 100644 --- a/pkg/interface/src/logic/reducers/settings-update.ts +++ b/pkg/interface/src/logic/reducers/settings-update.ts @@ -1,77 +1,83 @@ import _ from 'lodash'; -import { StoreState } from '../../store/type'; -import { - SettingsUpdate, -} from '~/types/settings'; +import { SettingsUpdate } from '~/types/settings'; +import useSettingsState, { SettingsStateZus } from "~/logic/state/settings"; +import produce from 'immer'; -type SettingsState = Pick; - -export default class SettingsReducer{ - reduce(json: Cage, state: S) { - let data = json["settings-event"]; - if (data) { - this.putBucket(data, state); - this.delBucket(data, state); - this.putEntry(data, state); - this.delEntry(data, state); - } - data = json["settings-data"]; - if (data) { - this.getAll(data, state); - this.getBucket(data, state); - this.getEntry(data, state); - } +export default class SettingsStateZusettingsReducer{ + reduce(json: any) { + const old = useSettingsState.getState(); + const newState = produce(old, state => { + let data = json["settings-event"]; + if (data) { + console.log(data); + this.putBucket(data, state); + this.delBucket(data, state); + this.putEntry(data, state); + this.delEntry(data, state); + } + data = json["settings-data"]; + if (data) { + console.log(data); + this.getAll(data, state); + this.getBucket(data, state); + this.getEntry(data, state); + } + }); + useSettingsState.setState(newState); } - putBucket(json: SettingsUpdate, state: S) { + putBucket(json: SettingsUpdate, state: SettingsStateZus) { const data = _.get(json, 'put-bucket', false); if (data) { - state.settings[data["bucket-key"]] = data.bucket; + state[data["bucket-key"]] = data.bucket; } } - delBucket(json: SettingsUpdate, state: S) { + delBucket(json: SettingsUpdate, state: SettingsStateZus) { const data = _.get(json, 'del-bucket', false); if (data) { - delete state.settings[data["bucket-key"]]; + delete settings[data['bucket-key']]; } } - putEntry(json: SettingsUpdate, state: S) { + putEntry(json: SettingsUpdate, state: SettingsStateZus) { const data = _.get(json, 'put-entry', false); if (data) { - if (!state.settings[data["bucket-key"]]) { - state.settings[data["bucket-key"]] = {}; + if (!state[data["bucket-key"]]) { + state[data["bucket-key"]] = {}; } - state.settings[data["bucket-key"]][data["entry-key"]] = data.value; + state[data["bucket-key"]][data["entry-key"]] = data.value; } } - delEntry(json: SettingsUpdate, state: S) { + delEntry(json: SettingsUpdate, state: SettingsStateZus) { const data = _.get(json, 'del-entry', false); if (data) { - delete state.settings[data["bucket-key"]][data["entry-key"]]; + delete state[data["bucket-key"]][data["entry-key"]]; } } - getAll(json: any, state: S) { - state.settings = json; + getAll(json: any, state: SettingsStateZus) { + const data = _.get(json, 'all'); + if(data) { + _.mergeWith(state, data, (obj, src) => _.isArray(src) ? src : undefined) + } } - getBucket(json: any, state: S) { + getBucket(json: any, state: SettingsStateZus) { const key = _.get(json, 'bucket-key', false); const bucket = _.get(json, 'bucket', false); if (key && bucket) { - state.settings[key] = bucket; + state[key] = bucket; } } - getEntry(json: any, state: S) { + getEntry(json: any, state: SettingsStateZus) { const bucketKey = _.get(json, 'bucket-key', false); const entryKey = _.get(json, 'entry-key', false); const entry = _.get(json, 'entry', false); if (bucketKey && entryKey && entry) { - state.settings[bucketKey][entryKey] = entry; + state[bucketKey][entryKey] = entry; } } } diff --git a/pkg/interface/src/logic/state/local.tsx b/pkg/interface/src/logic/state/local.tsx index 64e68a0ca2..c5a187d11b 100644 --- a/pkg/interface/src/logic/state/local.tsx +++ b/pkg/interface/src/logic/state/local.tsx @@ -1,20 +1,24 @@ -import React, { ReactNode } from "react"; +import React, { ReactNode } from 'react'; import f from 'lodash/fp'; import create, { State } from 'zustand'; import { persist } from 'zustand/middleware'; import produce from 'immer'; -import { BackgroundConfig, RemoteContentPolicy, TutorialProgress, tutorialProgress } from "~/types/local-update"; +import { BackgroundConfig, RemoteContentPolicy, TutorialProgress, tutorialProgress, LeapCategories } from "~/types/local-update"; -export interface LocalState extends State { +export interface LocalState { + theme: "light" | "dark" | "auto"; hideAvatars: boolean; hideNicknames: boolean; remoteContentPolicy: RemoteContentPolicy; tutorialProgress: TutorialProgress; + hideGroups: boolean; + hideUtilities: boolean; tutorialRef: HTMLElement | null, hideTutorial: () => void; nextTutStep: () => void; prevTutStep: () => void; + hideLeapCats: LeapCategories[]; setTutorialRef: (el: HTMLElement | null) => void; dark: boolean; background: BackgroundConfig; @@ -23,31 +27,38 @@ export interface LocalState extends State { toggleOmnibox: () => void; set: (fn: (state: LocalState) => void) => void }; -export const selectLocalState = + +type LocalStateZus = LocalState & State; + +export const selectLocalState = (keys: K[]) => f.pick(keys); -const useLocalState = create(persist((set, get) => ({ +const useLocalState = create(persist((set, get) => ({ dark: false, background: undefined, + theme: "auto", hideAvatars: false, hideNicknames: false, + hideLeapCats: [], + hideGroups: false, + hideUtilities: false, tutorialProgress: 'hidden', tutorialRef: null, - setTutorialRef: (el: HTMLElement | null) => set(produce(state => { + setTutorialRef: (el: HTMLElement | null) => set(produce((state) => { state.tutorialRef = el; })), - hideTutorial: () => set(produce(state => { + hideTutorial: () => set(produce((state) => { state.tutorialProgress = 'hidden'; state.tutorialRef = null; })), - nextTutStep: () => set(produce(state => { - const currIdx = tutorialProgress.findIndex(p => p === state.tutorialProgress) + nextTutStep: () => set(produce((state) => { + const currIdx = tutorialProgress.findIndex(p => p === state.tutorialProgress); if(currIdx < tutorialProgress.length) { state.tutorialProgress = tutorialProgress[currIdx + 1]; } })), - prevTutStep: () => set(produce(state => { - const currIdx = tutorialProgress.findIndex(p => p === state.tutorialProgress) + prevTutStep: () => set(produce((state) => { + const currIdx = tutorialProgress.findIndex(p => p === state.tutorialProgress); if(currIdx > 0) { state.tutorialProgress = tutorialProgress[currIdx - 1]; } @@ -56,11 +67,11 @@ const useLocalState = create(persist((set, get) => ({ imageShown: true, audioShown: true, videoShown: true, - oembedShown: true, + oembedShown: true }, omniboxShown: false, suspendedFocus: undefined, - toggleOmnibox: () => set(produce(state => { + toggleOmnibox: () => set(produce((state) => { state.omniboxShown = !state.omniboxShown; if (typeof state.suspendedFocus?.focus === 'function') { state.suspendedFocus.focus(); @@ -86,7 +97,7 @@ function withLocalState(Component: any, stateMemb (object, key) => ({ ...object, [key]: state[key] }), {} ) ): useLocalState(); - return + return ; }); } diff --git a/pkg/interface/src/logic/state/settings.tsx b/pkg/interface/src/logic/state/settings.tsx new file mode 100644 index 0000000000..2c68be37fd --- /dev/null +++ b/pkg/interface/src/logic/state/settings.tsx @@ -0,0 +1,82 @@ +import React, { ReactNode } from "react"; +import f from 'lodash/fp'; +import create, { State } from 'zustand'; +import { persist } from 'zustand/middleware'; +import produce from 'immer'; +import { BackgroundConfig, RemoteContentPolicy, TutorialProgress, tutorialProgress, LeapCategories, leapCategories } from "~/types/local-update"; + + +export interface SettingsState { + display: { + backgroundType: 'none' | 'url' | 'color'; + background?: string; + dark: boolean; + theme: "light" | "dark" | "auto"; + }; + calm: { + hideNicknames: boolean; + hideAvatars: boolean; + hideUnreads: boolean; + hideGroups: boolean; + hideUtilities: boolean; + }; + remoteContentPolicy: RemoteContentPolicy; + leap: { + categories: LeapCategories[]; + }; + tutorial: { + seen: boolean; + joined?: number; + }; + set: (fn: (state: SettingsState) => void) => void +}; + +export type SettingsStateZus = SettingsState & State; + +export const selectSettingsState = +(keys: K[]) => f.pick(keys); + +export const selectCalmState = (s: SettingsState) => s.calm; + +export const selectDisplayState = (s: SettingsState) => s.display; + +const useSettingsState = create((set) => ({ + display: { + backgroundType: 'none', + background: undefined, + dark: false, + theme: "auto" + }, + calm: { + hideNicknames: false, + hideAvatars: false, + hideUnreads: false, + hideGroups: false, + hideUtilities: false + }, + remoteContentPolicy: { + imageShown: true, + oembedShown: true, + audioShown: true, + videoShown: true + }, + leap: { + categories: leapCategories, + }, + tutorial: { + seen: false, + joined: undefined + }, + set: (fn: (state: SettingsState) => void) => set(produce(fn)) +})); + +function withSettingsState(Component: any, stateMemberKeys?: S[]) { + return React.forwardRef((props: Omit, ref) => { + const localState = stateMemberKeys + ? useSettingsState(selectSettingsState(stateMemberKeys)) + : useSettingsState(); + return + }); +} + +export { useSettingsState as default, withSettingsState }; diff --git a/pkg/interface/src/logic/store/base.ts b/pkg/interface/src/logic/store/base.ts index 9a48ebd80e..faeacca1e9 100644 --- a/pkg/interface/src/logic/store/base.ts +++ b/pkg/interface/src/logic/store/base.ts @@ -19,7 +19,7 @@ export default class BaseStore { clear() { this.handleEvent({ - data: { clear: true }, + data: { clear: true } }); } @@ -30,7 +30,7 @@ export default class BaseStore { return; } - if ("clear" in json && json.clear) { + if ('clear' in json && json.clear) { this.setState(this.initialState()); return; } diff --git a/pkg/interface/src/logic/store/store.ts b/pkg/interface/src/logic/store/store.ts index b2ab60c6c8..599054b87c 100644 --- a/pkg/interface/src/logic/store/store.ts +++ b/pkg/interface/src/logic/store/store.ts @@ -6,7 +6,7 @@ import MetadataReducer from '../reducers/metadata-update'; import LocalReducer from '../reducers/local'; import { StoreState } from './type'; -import { Timebox } from '~/types'; +import { Timebox } from '@urbit/api'; import { Cage } from '~/types/cage'; import S3Reducer from '../reducers/s3-update'; import { GraphReducer } from '../reducers/graph-update'; @@ -16,10 +16,10 @@ import GroupReducer from '../reducers/group-update'; import LaunchReducer from '../reducers/launch-update'; import ConnectionReducer from '../reducers/connection'; import SettingsReducer from '../reducers/settings-update'; -import {OrderedMap} from '../lib/OrderedMap'; +import GcpReducer from '../reducers/gcp-reducer'; +import { OrderedMap } from '../lib/OrderedMap'; import { BigIntOrderedMap } from '../lib/BigIntOrderedMap'; -import {GroupViewReducer} from '../reducers/group-view'; - +import { GroupViewReducer } from '../reducers/group-view'; export default class GlobalStore extends BaseStore { inviteReducer = new InviteReducer(); @@ -30,6 +30,7 @@ export default class GlobalStore extends BaseStore { launchReducer = new LaunchReducer(); connReducer = new ConnectionReducer(); settingsReducer = new SettingsReducer(); + gcpReducer = new GcpReducer(); pastActions: Record = {} @@ -58,7 +59,7 @@ export default class GlobalStore extends BaseStore { invites: {}, associations: { groups: {}, - graph: {}, + graph: {} }, groups: {}, groupKeys: new Set(), @@ -67,16 +68,19 @@ export default class GlobalStore extends BaseStore { launch: { firstTime: false, tileOrdering: [], - tiles: {}, + tiles: {} }, weather: {}, userLocation: null, - s3: { - configuration: { - buckets: new Set(), - currentBucket: '' + storage: { + gcp: {}, + s3: { + configuration: { + buckets: new Set(), + currentBucket: '' + }, + credentials: null }, - credentials: null }, isContactPublic: false, contacts: {}, @@ -87,7 +91,7 @@ export default class GlobalStore extends BaseStore { notificationsGraphConfig: { watchOnSelf: false, mentions: false, - watching: [], + watching: [] }, unreads: { graph: {}, @@ -96,6 +100,7 @@ export default class GlobalStore extends BaseStore { notificationsCount: 0, settings: {}, pendingJoin: {}, + graphTimesentMap: {} }; } @@ -115,7 +120,8 @@ export default class GlobalStore extends BaseStore { GraphReducer(data, this.state); HarkReducer(data, this.state); ContactReducer(data, this.state); - this.settingsReducer.reduce(data, this.state); + this.settingsReducer.reduce(data); + this.gcpReducer.reduce(data, this.state); GroupViewReducer(data, this.state); } } diff --git a/pkg/interface/src/logic/store/type.ts b/pkg/interface/src/logic/store/type.ts index 96b68f8a2d..7c847c5c40 100644 --- a/pkg/interface/src/logic/store/type.ts +++ b/pkg/interface/src/logic/store/type.ts @@ -1,20 +1,20 @@ -import { Path } from '~/types/noun'; -import { Invites } from '~/types/invite-update'; -import { Associations } from '~/types/metadata-update'; -import { Rolodex } from '~/types/contact-update'; -import { Groups } from '~/types/group-update'; -import { S3State } from '~/types/s3-update'; +import { Path } from '@urbit/api'; +import { Invites } from '@urbit/api/invite'; +import { Associations } from '@urbit/api/metadata'; +import { Rolodex } from '@urbit/api/contacts'; +import { Groups } from '@urbit/api/groups'; +import { StorageState } from '~/types/storage-state'; import { LaunchState, WeatherState } from '~/types/launch-update'; import { ConnectionStatus } from '~/types/connection'; -import {Graphs} from '~/types/graph-update'; +import { Graphs } from '@urbit/api/graph'; import { Notifications, - NotificationGraphConfig, + NotificationGraphConfig, GroupNotificationsConfig, Unreads, JoinRequests, Patp -} from "~/types"; +} from '@urbit/api'; export interface StoreState { // local state @@ -31,11 +31,10 @@ export interface StoreState { groups: Groups; groupKeys: Set; nackedContacts: Set - s3: S3State; + storage: StorageState; graphs: Graphs; graphKeys: Set; - // App specific states // launch state launch: LaunchState; diff --git a/pkg/interface/src/logic/subscription/base.ts b/pkg/interface/src/logic/subscription/base.ts index eb4b3b3afa..debf665415 100644 --- a/pkg/interface/src/logic/subscription/base.ts +++ b/pkg/interface/src/logic/subscription/base.ts @@ -1,6 +1,6 @@ -import BaseStore from "../store/base"; -import BaseApi from "../api/base"; -import { Path } from "~/types/noun"; +import BaseStore from '../store/base'; +import BaseApi from '../api/base'; +import { Path } from '@urbit/api'; export default class BaseSubscription { private errorCount = 0; @@ -19,24 +19,24 @@ export default class BaseSubscription { // Exists to allow subclasses to hook restart() { - this.handleEvent({ data: { connection: 'reconnecting' }}); + this.handleEvent({ data: { connection: 'reconnecting' } }); this.start(); } onChannelOpen(e: any) { this.errorCount = 0; - this.handleEvent({ data: { connection: 'connected' }}); + this.handleEvent({ data: { connection: 'connected' } }); } onChannelError(err) { console.error('event source error: ', err); this.errorCount++; if(this.errorCount >= 5) { - console.error("bailing out, too many retries"); - this.handleEvent({ data: { connection: 'disconnected' }}); + console.error('bailing out, too many retries'); + this.handleEvent({ data: { connection: 'disconnected' } }); return; } - this.handleEvent({ data: { connection: 'reconnecting' }}); + this.handleEvent({ data: { connection: 'reconnecting' } }); setTimeout(() => { this.restart(); }, Math.pow(2,this.errorCount - 1) * 750); diff --git a/pkg/interface/src/logic/subscription/global.ts b/pkg/interface/src/logic/subscription/global.ts index a434c41e00..5b444f06df 100644 --- a/pkg/interface/src/logic/subscription/global.ts +++ b/pkg/interface/src/logic/subscription/global.ts @@ -1,9 +1,8 @@ import BaseSubscription from './base'; import { StoreState } from '../store/type'; -import { Path } from '~/types/noun'; +import { Path } from '@urbit/api'; import _ from 'lodash'; - /** * Path to subscribe on and app to subscribe to */ @@ -68,7 +67,7 @@ export default class GlobalSubscription extends BaseSubscription { } stopApp(app: AppName) { - this.openSubscriptions[app].map(id => this.unsubscribe(id)) + this.openSubscriptions[app].map(id => this.unsubscribe(id)); this.openSubscriptions[app] = []; } } diff --git a/pkg/interface/src/register-sw.js b/pkg/interface/src/register-sw.js new file mode 100644 index 0000000000..d4a2ed09c2 --- /dev/null +++ b/pkg/interface/src/register-sw.js @@ -0,0 +1,10 @@ + + +if ("serviceWorker" in navigator) { + window.addEventListener("load", () => { + navigator.serviceWorker.register("/~landscape/js/bundle/serviceworker.js", { + scope: "/", + }).then(reg => { + }); + }); +} diff --git a/pkg/interface/src/serviceworker.js b/pkg/interface/src/serviceworker.js new file mode 100644 index 0000000000..76858dba63 --- /dev/null +++ b/pkg/interface/src/serviceworker.js @@ -0,0 +1,82 @@ +import { registerRoute } from 'workbox-routing'; +import { + NetworkFirst, + StaleWhileRevalidate, + CacheFirst, +} from 'workbox-strategies'; + +// Used for filtering matches based on status code, header, or both +import { CacheableResponsePlugin } from 'workbox-cacheable-response'; +// Used to limit entries in cache, remove entries after a certain period of time +import { ExpirationPlugin } from 'workbox-expiration'; + + +// generate a different sw for every build, to bust cache properly +const hash = process.env.LANDSCAPE_SHORTHASH; + +self.addEventListener("install", ev => { + self.skipWaiting(); +}); + +self.addEventListener('activate', ev => { + ev.waitUntil(clients.claim()); +}); + +// Cache page navigations (html) with a Network First strategy +registerRoute( + // Check to see if the request is a navigation to a new page + ({ request }) => request.mode === 'navigate', + // Use a Network First caching strategy + new NetworkFirst({ + // Put all cached files in a cache named 'pages' + cacheName: 'pages', + plugins: [ + // Ensure that only requests that result in a 200 status are cached + new CacheableResponsePlugin({ + statuses: [200], + }), + ], + }), +); + +// Cache CSS, JS, and Web Worker requests with a Stale While Revalidate strategy +registerRoute( + // Check to see if the request's destination is style for stylesheets, script for JavaScript, or worker for web worker + ({ request }) => + request.destination === 'style' || + request.destination === 'script' || + request.destination === 'worker', + // Use a Stale While Revalidate caching strategy + new StaleWhileRevalidate({ + // Put all cached files in a cache named 'assets' + cacheName: 'assets', + plugins: [ + // Ensure that only requests that result in a 200 status are cached + new CacheableResponsePlugin({ + statuses: [200], + }), + ], + }), +); + +// Cache images with a Cache First strategy +registerRoute( + // Check to see if the request's destination is style for an image + ({ request }) => request.destination === 'image', + // Use a Cache First caching strategy + new CacheFirst({ + // Put all cached files in a cache named 'images' + cacheName: 'images', + plugins: [ + // Ensure that only requests that result in a 200 status are cached + new CacheableResponsePlugin({ + statuses: [200], + }), + // Don't cache more than 50 items, and expire them after 30 days + new ExpirationPlugin({ + maxEntries: 50, + maxAgeSeconds: 60 * 60 * 24 * 30, // 30 Days + }), + ], + }), +); diff --git a/pkg/interface/src/types/cage.ts b/pkg/interface/src/types/cage.ts index 266188543f..044fb46212 100644 --- a/pkg/interface/src/types/cage.ts +++ b/pkg/interface/src/types/cage.ts @@ -1,21 +1,17 @@ -import { ContactUpdate } from "./contact-update"; -import { InviteUpdate } from "./invite-update"; -import { LocalUpdate } from "./local-update"; -import { MetadataUpdate } from "./metadata-update"; -import { GroupUpdate } from "./group-update"; -import { LaunchUpdate, WeatherState } from "./launch-update"; -import { ConnectionStatus } from "./connection"; -import { SettingsUpdate } from "./settings"; +import { LocalUpdate } from './local-update'; +import { LaunchUpdate, WeatherState } from './launch-update'; +import { ConnectionStatus } from './connection'; +import { ContactUpdate, GroupUpdate, InviteUpdate, MetadataUpdate } from '@urbit/api'; +import { SettingsUpdate } from '@urbit/api/settings'; interface MarksToTypes { readonly json: any; - readonly "contact-update": ContactUpdate; - readonly "invite-update": InviteUpdate; - readonly "metadata-update": MetadataUpdate; + readonly 'contact-update': ContactUpdate; + readonly 'invite-update': InviteUpdate; + readonly 'metadata-update': MetadataUpdate; readonly groupUpdate: GroupUpdate; - readonly "launch-update": LaunchUpdate; - readonly "link-listen-update": LinkListenUpdate; - readonly "settings-event": SettingsUpdate; + readonly 'launch-update': LaunchUpdate; + readonly 'settings-event': SettingsUpdate; // not really marks but w/e readonly 'local': LocalUpdate; readonly 'weather': WeatherState | {}; diff --git a/pkg/interface/src/types/connection.ts b/pkg/interface/src/types/connection.ts index 6f624238e8..38b06b09aa 100644 --- a/pkg/interface/src/types/connection.ts +++ b/pkg/interface/src/types/connection.ts @@ -1,2 +1 @@ - export type ConnectionStatus = 'reconnecting' | 'disconnected' | 'connected'; diff --git a/pkg/interface/src/types/contact-update.ts b/pkg/interface/src/types/contact-update.ts deleted file mode 100644 index e48afc739c..0000000000 --- a/pkg/interface/src/types/contact-update.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Path, Patp } from "./noun"; - -export type ContactUpdate = - | ContactUpdateCreate - | ContactUpdateDelete - | ContactUpdateAdd - | ContactUpdateRemove - | ContactUpdateEdit - | ContactUpdateInitial - | ContactUpdateContacts; - -interface ContactUpdateCreate { - create: Path; -} - -interface ContactUpdateDelete { - delete: Path; -} - -interface ContactUpdateAdd { - add: { - path: Path; - ship: Patp; - contact: Contact; - }; -} - -interface ContactUpdateRemove { - remove: { - path: Path; - ship: Patp; - }; -} - -interface ContactUpdateEdit { - edit: { - path: Path; - ship: Patp; - "edit-field": ContactEdit; - }; -} - -interface ContactUpdateInitial { - initial: Rolodex; -} - -interface ContactUpdateContacts { - contacts: { - path: Path; - contacts: Contacts; - }; -} - -// - -type ContactAvatar = ContactAvatarUrl | ContactAvatarOcts; - -export type Rolodex = { - [p in Path]: Contacts; -}; - -export type Contacts = { - [p in Patp]: Contact; -}; - -interface ContactAvatarUrl { - url: string; -} - -interface ContactAvatarOcts { - octs: string; -} -export interface Contact { - nickname: string; - email: string; - phone: string; - website: string; - notes: string; - color: string; - avatar: string | null; -} - -export type ContactEdit = { - [k in keyof Contact]: Contact[k]; -}; diff --git a/pkg/interface/src/types/gcp-state.ts b/pkg/interface/src/types/gcp-state.ts new file mode 100644 index 0000000000..cdbcbc1c97 --- /dev/null +++ b/pkg/interface/src/types/gcp-state.ts @@ -0,0 +1,9 @@ +export interface GcpToken { + accessKey: string; + expiresIn: number; +}; + +export interface GcpState { + configured?: boolean; + token?: GcpToken +}; diff --git a/pkg/interface/src/types/global.ts b/pkg/interface/src/types/global.ts index d43a77b1b7..4294f574f2 100644 --- a/pkg/interface/src/types/global.ts +++ b/pkg/interface/src/types/global.ts @@ -1,4 +1,4 @@ -import { PatpNoSig } from "./noun"; +import { PatpNoSig } from '@urbit/api'; declare global { interface Window { diff --git a/pkg/interface/src/types/index.ts b/pkg/interface/src/types/index.ts index 5c1d81b0b9..40d45c7ff9 100644 --- a/pkg/interface/src/types/index.ts +++ b/pkg/interface/src/types/index.ts @@ -1,16 +1,10 @@ export * from './cage'; export * from './connection'; -export * from './contact-update'; export * from './global'; -export * from './group-update'; -export * from './group-view'; -export * from './graph-update'; -export * from './hark-update'; -export * from './invite-update'; export * from './launch-update'; export * from './local-update'; -export * from './metadata-update'; -export * from './noun'; +export * from './storage-state'; +export * from './gcp-state'; export * from './s3-update'; export * from './workspace'; export * from './util'; diff --git a/pkg/interface/src/types/launch-update.ts b/pkg/interface/src/types/launch-update.ts index 0999878455..879c3620b7 100644 --- a/pkg/interface/src/types/launch-update.ts +++ b/pkg/interface/src/types/launch-update.ts @@ -1,11 +1,9 @@ - export type LaunchUpdate = LaunchUpdateInitial | LaunchUpdateFirstTime | LaunchUpdateOrder | LaunchUpdateIsShown; - interface LaunchUpdateInitial { initial: LaunchState; } diff --git a/pkg/interface/src/types/local-update.ts b/pkg/interface/src/types/local-update.ts index 5849931fe2..9813729f37 100644 --- a/pkg/interface/src/types/local-update.ts +++ b/pkg/interface/src/types/local-update.ts @@ -1,5 +1,9 @@ export const tutorialProgress = ['hidden', 'start', 'group-desc', 'channels', 'chat', 'link', 'publish', 'profile', 'leap', 'notifications', 'done', 'exit'] as const; +export const leapCategories = ["mychannel", "messages", "updates", "profile", "logout"] as const; + +export type LeapCategories = typeof leapCategories[number]; + export type TutorialProgress = typeof tutorialProgress[number]; interface LocalUpdateSetDark { setDark: boolean; diff --git a/pkg/interface/src/types/s3-update.ts b/pkg/interface/src/types/s3-update.ts index 3dfa516229..88763b7d66 100644 --- a/pkg/interface/src/types/s3-update.ts +++ b/pkg/interface/src/types/s3-update.ts @@ -1,6 +1,4 @@ - - - export interface S3Credentials { +export interface S3Credentials { endpoint: string; accessKeyId: string; secretAccessKey: string; @@ -51,7 +49,6 @@ interface S3UpdateSecretAccessKey { setSecretAccessKey: string; } - export type S3Update = S3UpdateCredentials | S3UpdateConfiguration diff --git a/pkg/interface/src/types/storage-state.ts b/pkg/interface/src/types/storage-state.ts new file mode 100644 index 0000000000..61bf612ada --- /dev/null +++ b/pkg/interface/src/types/storage-state.ts @@ -0,0 +1,8 @@ +import {GcpState} from './gcp-state'; +import {S3State} from './s3-update'; + + +export interface StorageState { + gcp: GcpState; + s3: S3State; +}; diff --git a/pkg/interface/src/types/util.ts b/pkg/interface/src/types/util.ts index 2f3d390e5d..f247978caf 100644 --- a/pkg/interface/src/types/util.ts +++ b/pkg/interface/src/types/util.ts @@ -1,4 +1,4 @@ -import { Icon } from "@tlon/indigo-react"; +import { Icon } from '@tlon/indigo-react'; export type PropFunc any> = Parameters[0]; export type Primitive = string | number | undefined | symbol | null | boolean; diff --git a/pkg/interface/src/types/workspace.ts b/pkg/interface/src/types/workspace.ts index 69da82ed3f..e8238e7251 100644 --- a/pkg/interface/src/types/workspace.ts +++ b/pkg/interface/src/types/workspace.ts @@ -1,5 +1,3 @@ - - interface GroupWorkspace { type: 'group'; group: string; diff --git a/pkg/interface/src/views/App.js b/pkg/interface/src/views/App.js index 3e1650dc34..77b89f0428 100644 --- a/pkg/interface/src/views/App.js +++ b/pkg/interface/src/views/App.js @@ -27,20 +27,22 @@ import GlobalSubscription from '~/logic/subscription/global'; import GlobalApi from '~/logic/api/global'; import { uxToHex } from '~/logic/lib/util'; import { foregroundFromBackground } from '~/logic/lib/sigil'; +import gcpManager from '~/logic/lib/gcpManager'; import { withLocalState } from '~/logic/state/local'; +import { withSettingsState } from '~/logic/state/settings'; -const Root = styled.div` +const Root = withSettingsState(styled.div` font-family: ${p => p.theme.fonts.sans}; height: 100%; width: 100%; padding: 0; margin: 0; - ${p => p.background?.type === 'url' ? ` - background-image: url('${p.background?.url}'); + ${p => p.display.backgroundType === 'url' ? ` + background-image: url('${p.display.background}'); background-size: cover; - ` : p.background?.type === 'color' ? ` - background-color: ${p.background.color}; + ` : p.display.backgroundType === 'color' ? ` + background-color: ${p.display.background}; ` : `background-color: ${p.theme.colors.white};` } display: flex; @@ -64,10 +66,9 @@ const Root = styled.div` border-radius: 1rem; border: 0px solid transparent; } -`; +`, ['display']); const StatusBarWithRouter = withRouter(StatusBar); - class App extends React.Component { constructor(props) { super(props); @@ -78,6 +79,7 @@ class App extends React.Component { this.appChannel = new window.channel(); this.api = new GlobalApi(this.ship, this.appChannel, this.store); + gcpManager.configure(this.api, this.store); this.subscription = new GlobalSubscription(this.store, this.api, this.appChannel); @@ -97,6 +99,7 @@ class App extends React.Component { this.api.local.getBaseHash(); this.api.settings.getAll(); this.store.rehydrate(); + gcpManager.start(); Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => { e.preventDefault(); e.stopImmediatePropagation(); @@ -116,8 +119,8 @@ class App extends React.Component { faviconString() { let background = '#ffffff'; - if (this.state.contacts.hasOwnProperty('/~/default')) { - background = `#${uxToHex(this.state.contacts['/~/default'][window.ship].color)}`; + if (this.state.contacts.hasOwnProperty(`~${window.ship}`)) { + background = `#${uxToHex(this.state.contacts[`~${window.ship}`].color)}`; } const foreground = foregroundFromBackground(background); const svg = sigiljs({ @@ -134,13 +137,14 @@ class App extends React.Component { const { state, props } = this; const associations = state.associations ? state.associations : { contacts: {} }; - const theme = props.dark ? dark : light; - const background = this.props.background; + const theme = + ((props.dark && props?.display?.theme == "auto") || + props?.display?.theme == "dark" + ) ? dark : light; const notificationsCount = state.notificationsCount || 0; const doNotDisturb = state.doNotDisturb || false; const ourContact = this.state.contacts[`~${this.ship}`] || null; - return ( @@ -148,7 +152,7 @@ class App extends React.Component { ? : null} - + @@ -195,5 +199,4 @@ class App extends React.Component { } } -export default withLocalState(process.env.NODE_ENV === 'production' ? App : hot(App)); - +export default withSettingsState(withLocalState(process.env.NODE_ENV === 'production' ? App : hot(App)), ['display']); diff --git a/pkg/interface/src/views/apps/chat/ChatResource.tsx b/pkg/interface/src/views/apps/chat/ChatResource.tsx index 6b47a44091..ba9daa6312 100644 --- a/pkg/interface/src/views/apps/chat/ChatResource.tsx +++ b/pkg/interface/src/views/apps/chat/ChatResource.tsx @@ -3,7 +3,7 @@ import { RouteComponentProps } from 'react-router-dom'; import { Col } from '@tlon/indigo-react'; import _ from 'lodash'; -import { Association } from '~/types/metadata-update'; +import { Association } from '@urbit/api/metadata'; import { StoreState } from '~/logic/store/type'; import { useFileDrag } from '~/logic/lib/useDrag'; import ChatWindow from './components/ChatWindow'; @@ -13,7 +13,6 @@ import { ShareProfile } from '~/views/apps/chat/components/ShareProfile'; 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, resourceFromPath } from '~/logic/lib/group'; import './css/custom.css'; @@ -29,6 +28,7 @@ export function ChatResource(props: ChatResourceProps) { const groupPath = props.association.group; const group = props.groups[groupPath]; const contacts = props.contacts; + const graphPath = station.slice(7); const graph = props.graphs[station.slice(7)]; const isChatMissing = !props.graphKeys.has(station.slice(7)); const unreadCount = props.unreads.graph?.[station]?.['/']?.unreads || 0; @@ -38,7 +38,7 @@ export function ChatResource(props: ChatResourceProps) { const canWrite = isWriter(group, station); useEffect(() => { - const count = Math.min(50, unreadCount + 15); + const count = 100 + unreadCount; props.api.graph.getNewest(owner, name, count); }, [station]); @@ -114,7 +114,6 @@ export function ChatResource(props: ChatResourceProps) { } else { setShowBanner(false); } - } else { const groupShared = await props.api.contacts.fetchIsAllowed( `~${window.ship}`, @@ -127,14 +126,13 @@ export function ChatResource(props: ChatResourceProps) { setHasLoadedAllowed(true); })(); - - }, [groupPath]); + }, [groupPath, group]); if(!graph) { return ; } - var modifiedContacts = { ...contacts }; + const modifiedContacts = { ...contacts }; delete modifiedContacts[`~${window.ship}`]; return ( @@ -148,9 +146,10 @@ export function ChatResource(props: ChatResourceProps) { setShowBanner={setShowBanner} group={group} groupPath={groupPath} - /> + /> {dragging && } { inCodeMode: false }, async () => { const output = await props.api.graph.eval(text); - const contents: Content[] = [{ code: { output, expression: text }}]; + const contents: Content[] = [{ code: { output, expression: text } }]; const post = createPost(contents); props.api.graph.addPost(ship, name, post); }); return; } - const post = createPost(tokenizeMessage((text))) + const post = createPost(tokenizeMessage((text))); props.deleteMessage(); - props.api.graph.addPost(ship,name, post); + props.api.graph.addPost(ship, name, post); } uploadSuccess(url) { @@ -110,7 +111,7 @@ class ChatInput extends Component { if (!this.props.canUpload) { return; } - Array.from(files).forEach(file => { + Array.from(files).forEach((file) => { this.props.uploadDefault(file) .then(this.uploadSuccess) .catch(this.uploadError); @@ -128,9 +129,16 @@ class ChatInput extends Component { const avatar = ( props.ourContact && - ((props.ourContact.avatar !== null) && !props.hideAvatars) + ((props.ourContact?.avatar) && !props.hideAvatars) ) - ? + ? : { width="16" height="16" onClick={() => this.props.promptUpload().then(this.uploadSuccess)} - /> + /> : null } @@ -200,4 +208,4 @@ class ChatInput extends Component { } } -export default withLocalState(withS3(ChatInput, {accept: 'image/*'}), ['hideAvatars']); +export default withLocalState(withStorage(ChatInput, { accept: 'image/*' }), ['hideAvatars']); diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index 6252383ddb..2d4ca543aa 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx @@ -1,12 +1,15 @@ +/* eslint-disable max-lines-per-function */ import React, { useState, useEffect, useRef, Component, - PureComponent + PureComponent, + useCallback } from 'react'; import moment from 'moment'; import _ from 'lodash'; +import VisibilitySensor from 'react-visibility-sensor'; import { Box, Row, Text, Rule, BaseImage } from '@tlon/indigo-react'; import { Sigil } from '~/logic/lib/sigil'; import OverlaySigil from '~/views/components/OverlaySigil'; @@ -15,48 +18,86 @@ import { cite, writeText, useShowNickname, + useHideAvatar, useHovering } from '~/logic/lib/util'; -import { Group, Association, Contacts, Post, Groups, Associations } from '~/types'; +import { + Group, + Association, + Contacts, + Post, + Groups, + Associations +} from '~/types'; import TextContent from './content/text'; import CodeContent from './content/code'; import RemoteContent from '~/views/components/RemoteContent'; import { Mention } from '~/views/components/MentionText'; import styled from 'styled-components'; import useLocalState from '~/logic/state/local'; +import useSettingsState, {selectCalmState} from "~/logic/state/settings"; +import Timestamp from '~/views/components/Timestamp'; +import {useIdlingState} from '~/logic/lib/idling'; export const DATESTAMP_FORMAT = '[~]YYYY.M.D'; -export const UnreadMarker = React.forwardRef(({ dayBreak, when }, ref) => ( - - - - New messages below - - - - -)); +interface DayBreakProps { + when: string; + shimTop?: boolean; +} -export const DayBreak = ({ when }) => ( - - +export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => ( + + + {moment(when).calendar(null, { sameElse: DATESTAMP_FORMAT })} + ); +export const UnreadMarker = React.forwardRef(({ dayBreak, when, api, association }, ref) => { + const [visible, setVisible] = useState(false); + const idling = useIdlingState(); + const dismiss = useCallback(() => { + api.hark.markCountAsRead(association, '/', 'message'); + }, [api, association]); + + useEffect(() => { + if(visible && !idling) { + dismiss(); + } + }, [visible, idling]); + + return ( + + + + + New messages below + + + + +)}); + interface ChatMessageProps { - measure(element): void; msg: Post; previousMsg?: Post; nextMsg?: Post; @@ -66,16 +107,18 @@ interface ChatMessageProps { contacts: Contacts; className?: string; isPending: boolean; - style?: any; + style?: unknown; scrollWindow: HTMLDivElement; isLastMessage?: boolean; unreadMarkerRef: React.RefObject; - history: any; - api: any; + history: unknown; + api: GlobalApi; highlighted?: boolean; + renderSigil?: boolean; + innerRef: (el: HTMLDivElement | null) => void; } -export default class ChatMessage extends Component { +class ChatMessage extends Component { private divRef: React.RefObject; constructor(props) { @@ -84,9 +127,6 @@ export default class ChatMessage extends Component { } componentDidMount() { - if (this.divRef.current) { - this.props.measure(this.divRef.current); - } } render() { @@ -101,7 +141,6 @@ export default class ChatMessage extends Component { className = '', isPending, style, - measure, scrollWindow, isLastMessage, unreadMarkerRef, @@ -113,25 +152,27 @@ export default class ChatMessage extends Component { associations } = this.props; - const renderSigil = Boolean( - (nextMsg && msg.author !== nextMsg.author) || !nextMsg || msg.number === 1 - ); + let { renderSigil } = this.props; + + if (renderSigil === undefined) { + renderSigil = Boolean( + (nextMsg && msg.author !== nextMsg.author) || + !nextMsg || + msg.number === 1 + ); + } + const dayBreak = nextMsg && new Date(msg['time-sent']).getDate() !== new Date(nextMsg['time-sent']).getDate(); - const containerClass = `${ - renderSigil ? 'cf pl2 lh-copy' : 'items-top cf hide-child' - } ${isPending ? 'o-40' : ''} ${className}`; + const containerClass = `${isPending ? 'o-40' : ''} ${className}`; const timestamp = moment .unix(msg['time-sent'] / 1000) - .format(renderSigil ? 'hh:mm a' : 'hh:mm'); + .format(renderSigil ? 'h:mm A' : 'h:mm'); - const reboundMeasure = (event) => { - return measure(this.divRef.current); - }; const messageProps = { msg, @@ -139,7 +180,6 @@ export default class ChatMessage extends Component { contacts, association, group, - measure: reboundMeasure.bind(this), style, containerClass, isPending, @@ -158,36 +198,29 @@ export default class ChatMessage extends Component { return ( - {dayBreak && !isLastRead ? : null} + {dayBreak && !isLastRead ? ( + + ) : null} {renderSigil ? ( - + <> + + + ) : ( - + )} - + {isLastRead ? ( { } } -interface MessageProps { - msg: Post; - timestamp: string; - group: Group; - association: Association; - contacts: Contacts; - containerClass: string; - isPending: boolean; - style: any; - measure(element): void; - scrollWindow: HTMLDivElement; - associations: Associations; - groups: Groups; -} +export default React.forwardRef((props, ref) => ); -export const MessageWithSigil = (props) => { - const { - msg, - timestamp, - contacts, - association, - associations, - groups, - group, - measure, - api, - history, - scrollWindow, - fontSize - } = props; +export const MessageAuthor = ({ + timestamp, + contacts, + msg, + group, + api, + associations, + groups, + history, + scrollWindow, + ...rest +}) => { + const osDark = useLocalState((state) => state.dark); - const dark = useLocalState((state) => state.dark); + const theme = useSettingsState(s => s.display.theme); + const dark = theme === 'dark' || (theme === 'auto' && osDark) const datestamp = moment .unix(msg['time-sent'] / 1000) .format(DATESTAMP_FORMAT); - const contact = `~${msg.author}` in contacts ? contacts[`~${msg.author}`] : false; + const contact = + `~${msg.author}` in contacts ? contacts[`~${msg.author}`] : false; const showNickname = useShowNickname(contact); + const { hideAvatars } = useSettingsState(selectCalmState); const shipName = showNickname ? contact.nickname : cite(msg.author); const copyNotice = 'Copied'; const color = contact @@ -270,15 +291,17 @@ export const MessageWithSigil = (props) => { }; const timer = setTimeout(() => resetDisplay(), 800); return () => clearTimeout(timer); - }, [displayName]); + }, [shipName, displayName]); const img = - contact && contact.avatar !== null ? ( + contact?.avatar && !hideAvatars ? ( ) : ( { padding={2} /> ); - return ( - <> + { setShowOverlay(true); }} - className='fl v-top pt1' height={16} - pr={3} + pr={2} pl={2} + cursor='pointer' position='relative' > {showOverlay && ( { > { writeText(`~${msg.author}`); showCopyNotice(); @@ -342,169 +365,122 @@ export const MessageWithSigil = (props) => { > {displayName} - + {timestamp} {datestamp} - - {msg.contents.map((c, i) => ( - - ))} - - + ); }; -const ContentBox = styled(Box)` - & > :first-child { - margin-left: 0px; - } -`; - -export const MessageWithoutSigil = ({ +export const Message = ({ timestamp, contacts, msg, - measure, group, api, associations, groups, - scrollWindow + scrollWindow, + timestampHover, + ...rest }) => { const { hovering, bind } = useHovering(); return ( - <> - - {timestamp} - - - {msg.contents.map((c, i) => ( - - ))} - - - ); -}; - -export const MessageContent = ({ - content, - contacts, - api, - associations, - groups, - measure, - scrollWindow, - fontSize, - group -}) => { - if ('code' in content) { - return ; - } else if ('url' in content) { - return ( - - + + {timestampHover ? ( + + {timestamp} + + ) : ( + <> + )} + + {msg.contents.map((content, i) => { + switch (Object.keys(content)[0]) { + case 'text': + return ( + + ); + case 'code': + return ; + case 'url': + return ( + + + + ); + case 'mention': + const first = (i) => (i === 0); + return ( + + ); + default: + return null; + } + })} - ); - } else if ('text' in content) { - return ( - ); - } else if ('mention' in content) { - return ( - - ); - } else { - return null; - } + + ); }; export const MessagePlaceholder = ({ diff --git a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx index 8db2935a50..15668ae614 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx @@ -1,22 +1,25 @@ -import React, { Component } from "react"; -import { RouteComponentProps } from "react-router-dom"; -import _ from "lodash"; +import React, { Component } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import _ from 'lodash'; import bigInt, { BigInteger } from 'big-integer'; import { Col } from '@tlon/indigo-react'; +import { + Patp, + Contacts, + Association, + Associations, + Group, + Groups, + Graph +} from '@urbit/api'; -import GlobalApi from "~/logic/api/global"; -import { Patp, Path } from "~/types/noun"; -import { Contacts } from "~/types/contact-update"; -import { Association, Associations } from "~/types/metadata-update"; -import { Group, Groups } from "~/types/group-update"; -import { Envelope, IMessage } from "~/types/chat-update"; -import { Graph } from "~/types"; +import GlobalApi from '~/logic/api/global'; -import VirtualScroller from "~/views/components/VirtualScroller"; +import VirtualScroller from '~/views/components/VirtualScroller'; import ChatMessage, { MessagePlaceholder } from './ChatMessage'; -import { UnreadNotice } from "./unread-notice"; +import { UnreadNotice } from './unread-notice'; const INITIAL_LOAD = 20; const DEFAULT_BACKLOG_SIZE = 100; @@ -38,7 +41,7 @@ type ChatWindowProps = RouteComponentProps<{ scrollTo?: number; associations: Associations; groups: Groups; -} +}; interface ChatWindowState { fetchPending: boolean; @@ -47,12 +50,18 @@ interface ChatWindowState { unreadIndex: BigInteger; } -export default class ChatWindow extends Component { +const virtScrollerStyle = { height: '100%' }; + +export default class ChatWindow extends Component< + ChatWindowProps, + ChatWindowState +> { private virtualList: VirtualScroller | null; private unreadMarkerRef: React.RefObject; private prevSize = 0; private loadedNewest = false; private loadedOldest = false; + private fetchPending = false; INITIALIZATION_MAX_TIME = 100; @@ -66,14 +75,11 @@ export default class ChatWindow extends Component { - if(this.props.scrollTo) { + if (this.props.scrollTo) { this.scrollToUnread(); } - this.setState({ initialized: true }); + }, this.INITIALIZATION_MAX_TIME); } - componentWillUnmount() { - window.removeEventListener('blur', this.handleWindowBlur); - window.removeEventListener('focus', this.handleWindowFocus); - } - calculateUnreadIndex() { const { graph, unreadCount } = this.props; const unreadIndex = graph.keys()[unreadCount]; - if(!unreadIndex || unreadCount === 0) { + if (!unreadIndex || unreadCount === 0) { this.setState({ unreadIndex: bigInt.zero }); @@ -110,7 +108,7 @@ export default class ChatWindow extends Component prevProps.unreadCount && this.state.idle) { + if (unreadCount > prevProps.unreadCount) { this.calculateUnreadIndex(); } - - if(this.prevSize !== graph.size) { - if(this.state.unreadIndex.eq(bigInt.zero)) { - this.calculateUnreadIndex(); - this.scrollToUnread(); - } - this.prevSize = graph.size; - this.virtualList?.calculateVisibleItems(); - this.stayLockedIfActive(); - } - if (station !== prevProps.station) { this.virtualList?.resetScroll(); this.calculateUnreadIndex(); @@ -161,11 +148,11 @@ export default class ChatWindow extends Component { + setActive = () => { + if(this.state.idle) { + this.setState({ idle: false }); + } + } + + fetchMessages = async (newer: boolean): Promise => { const { api, station, graph } = this.props; - - if ( this.state.fetchPending && !force) { - return new Promise((resolve, reject) => {}); + if(this.fetchPending) { + return false; } + - this.setState({ fetchPending: true }); + this.fetchPending = true; - const [,, ship, name] = station.split('/'); + const [, , ship, name] = station.split('/'); const currSize = graph.size; - if(newer && !this.loadedNewest) { + if (newer) { const [index] = graph.peekLargest()!; - await api.graph.getYoungerSiblings(ship,name, 20, `/${index.toString()}`) - if(currSize === graph.size) { - console.log('loaded all newest'); - this.loadedNewest = true; - } - } else if(!newer && !this.loadedOldest) { + await api.graph.getYoungerSiblings( + ship, + name, + 100, + `/${index.toString()}` + ); + } else { const [index] = graph.peekSmallest()!; - await api.graph.getOlderSiblings(ship,name, 20, `/${index.toString()}`) + await api.graph.getOlderSiblings(ship, name, 100, `/${index.toString()}`); this.calculateUnreadIndex(); - if(currSize === graph.size) { - console.log('loaded all oldest'); - this.loadedOldest = true; - } } - this.setState({ fetchPending: false }); - + this.fetchPending = false; + return currSize === graph.size; } - onScroll({ scrollTop, scrollHeight, windowHeight }) { + onScroll = ({ scrollTop, scrollHeight, windowHeight }) => { if (!this.state.idle && scrollTop > IDLE_THRESHOLD) { this.setState({ idle: true }); } - - this.dismissIfLineVisible(); } - dismissIfLineVisible() { - if (this.props.unreadCount === 0) return; - if (!this.unreadMarkerRef.current || !this.virtualList?.window) return; - const parent = this.unreadMarkerRef.current.parentElement?.parentElement; - if (!parent) return; - const { scrollTop, scrollHeight, offsetHeight } = this.virtualList.window; - if ( - (scrollHeight - parent.offsetTop > scrollTop) - && (scrollHeight - parent.offsetTop < scrollTop + offsetHeight) - ) { - this.dismissUnread(); + + renderer = React.forwardRef(({ index, scrollWindow }, ref) => { + const { + api, + association, + group, + contacts, + graph, + history, + groups, + associations + } = this.props; + const { unreadMarkerRef } = this; + const messageProps = { + association, + group, + contacts, + unreadMarkerRef, + history, + api, + groups, + associations + }; + const msg = graph.get(index)?.post; + if (!msg) return null; + if (!this.state.initialized) { + return ( + + ); } - } + const isPending: boolean = 'pending' in msg && Boolean(msg.pending); + const isLastMessage = index.eq( + graph.peekLargest()?.[0] ?? bigInt.zero + ); + const highlighted = false; // this.state.unreadIndex.eq(index); + const keys = graph.keys().reverse(); + const graphIdx = keys.findIndex((idx) => idx.eq(index)); + const prevIdx = keys[graphIdx + 1]; + const nextIdx = keys[graphIdx - 1]; + const isLastRead: boolean = this.state.unreadIndex.eq(index); + const props = { + highlighted, + scrollWindow, + isPending, + isLastRead, + isLastMessage, + msg, + ...messageProps + }; + return ( + + ); + }); render() { const { @@ -241,68 +278,56 @@ export default class ChatWindow extends Component + {this.virtualList = list}} - origin="bottom" - style={{ height: '100%' }} - onStartReached={() => { - this.setState({ idle: false }); - this.dismissUnread(); + ref={(list) => { + this.virtualList = list; }} - onScroll={this.onScroll.bind(this)} + offset={unreadCount} + origin='bottom' + style={virtScrollerStyle} + onStartReached={this.setActive} + onScroll={this.onScroll} data={graph} size={graph.size} - renderer={({ index, measure, scrollWindow }) => { - const msg = graph.get(index)?.post; - if (!msg) return null; - if (!this.state.initialized) { - return ; - } - const isPending: boolean = 'pending' in msg && Boolean(msg.pending); - const isLastMessage = index.eq(graph.peekLargest()?.[0] ?? bigInt.zero); - const highlighted = bigInt(this.props.scrollTo || -1).eq(index); - const graphIdx = keys.findIndex(idx => idx.eq(index)); - const prevIdx = keys[graphIdx+1]; - const nextIdx = keys[graphIdx-1]; - - - const isLastRead: boolean = this.state.unreadIndex.eq(index); - const props = { measure, highlighted, scrollWindow, isPending, isLastRead, isLastMessage, msg, ...messageProps }; - return ( - - ); - }} - loadRows={(newer) => { - this.fetchMessages(newer); - }} + pendingSize={pendingSize} + id={association.resource} + averageHeight={22} + renderer={this.renderer} + loadRows={this.fetchMessages} /> ); } } - diff --git a/pkg/interface/src/views/apps/chat/components/content/text.js b/pkg/interface/src/views/apps/chat/components/content/text.js index f450689436..2962d7a942 100644 --- a/pkg/interface/src/views/apps/chat/components/content/text.js +++ b/pkg/interface/src/views/apps/chat/components/content/text.js @@ -4,7 +4,8 @@ import ReactMarkdown from 'react-markdown'; import RemarkDisableTokenizers from 'remark-disable-tokenizers'; import urbitOb from 'urbit-ob'; import { Text } from '@tlon/indigo-react'; -import { GroupLink } from "~/views/components/GroupLink"; +import { GroupLink } from '~/views/components/GroupLink'; +import { Row } from '@tlon/indigo-react'; const DISABLED_BLOCK_TOKENS = [ 'indentedCode', @@ -26,44 +27,72 @@ const DISABLED_INLINE_TOKENS = [ ]; const renderers = { - inlineCode: ({language, value}) => { - return {value} + inlineCode: ({ language, value }) => { + return ( + + {value} + + ); + }, + blockquote: ({ children }) => { + return ( + + {children} + + ) }, paragraph: ({ children }) => { - return ({children}); + return ( + + {children} + + ); }, - code: ({language, value}) => { - return - {value} - + code: ({ language, value }) => { + return ( + + {value} + + ); } }; -const MessageMarkdown = React.memo(props => { +const MessageMarkdown = React.memo((props) => { const { source, ...rest } = props; const blockCode = source.split('```'); - const codeLines = blockCode.map(codes => codes.split("\n")); + const codeLines = blockCode.map((codes) => codes.split('\n')); const lines = codeLines.reduce((acc, val, i) => { - if((i % 2) === 1) { + if (i % 2 === 1) { return [...acc, `\`\`\`${val.join('\n')}\`\`\``]; } else { return [...acc, ...val]; } }, []); - return lines.map((line, i) => ( <> - { i !== 0 &&
} + {i !== 0 && } { renderers={renderers} allowNode={(node, index, parent) => { if ( - node.type === 'blockquote' - && parent.type === 'root' - && node.children.length - && node.children[0].type === 'paragraph' - && node.children[0].position.start.offset < 2 + node.type === 'blockquote' && + parent.type === 'root' && + node.children.length && + node.children[0].type === 'paragraph' && + node.children[0].position.start.offset < 2 ) { - node.children[0].children[0].value = '>' + node.children[0].children[0].value; + node.children[0].children[0].value = + '>' + node.children[0].children[0].value; return false; } return true; }} - plugins={[[RemarkDisableTokenizers, { - block: DISABLED_BLOCK_TOKENS, - inline: DISABLED_INLINE_TOKENS - }]]} - /> + plugins={[ + [ + RemarkDisableTokenizers, + { + block: DISABLED_BLOCK_TOKENS, + inline: DISABLED_INLINE_TOKENS + } + ] + ]} + /> - )) + )); }); export default function TextContent(props) { @@ -98,16 +133,16 @@ export default function TextContent(props) { const group = content.text.match( /([~][/])?(~[a-z]{3,6})(-[a-z]{6})?([/])(([a-z0-9-])+([/-])?)+/ ); - const isGroupLink = ((group !== null) // matched possible chatroom - && (group[2].length > 2) // possible ship? - && (urbitOb.isValidPatp(group[2]) // valid patp? - && (group[0] === content.text))) // entire message is room name? + const isGroupLink = + group !== null && // matched possible chatroom + group[2].length > 2 && // possible ship? + urbitOb.isValidPatp(group[2]) && // valid patp? + group[0] === content.text; // entire message is room name? - if(isGroupLink) { + if (isGroupLink) { const resource = `/ship/${content.text}`; return ( + ); diff --git a/pkg/interface/src/views/apps/chat/components/unread-notice.js b/pkg/interface/src/views/apps/chat/components/unread-notice.js index f66c0eb528..ff81a7b30b 100644 --- a/pkg/interface/src/views/apps/chat/components/unread-notice.js +++ b/pkg/interface/src/views/apps/chat/components/unread-notice.js @@ -1,6 +1,9 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import moment from 'moment'; import { Box, Text } from '@tlon/indigo-react'; +import VisibilitySensor from 'react-visibility-sensor'; + +import Timestamp from '~/views/components/Timestamp'; export const UnreadNotice = (props) => { const { unreadCount, unreadMsg, dismissUnread, onClick } = props; @@ -9,6 +12,8 @@ export const UnreadNotice = (props) => { return null; } + const stamp = moment.unix(unreadMsg.post['time-sent'] / 1000); + let datestamp = moment.unix(unreadMsg.post['time-sent'] / 1000).format('YYYY.M.D'); const timestamp = moment.unix(unreadMsg.post['time-sent'] / 1000).format('HH:mm'); @@ -34,14 +39,9 @@ export const UnreadNotice = (props) => { borderRadius='1' border='1' borderColor='blue'> - + {unreadCount} new message{unreadCount > 1 ? 's' : ''} since{' '} - {datestamp && ( - <> - ~{datestamp} at{' '} - - )} - {timestamp} + { writeText(props.baseHash); @@ -66,7 +67,9 @@ export default function LaunchApp(props) { }, 2000); }} > - {hashText || props.baseHash} + + {hashText || props.baseHash} +
); @@ -82,12 +85,15 @@ export default function LaunchApp(props) { } }, [query]); + const { hideUtilities } = useSettingsState(selectCalmState); const { tutorialProgress, nextTutStep } = useLocalState(tutSelector); + let { hideGroups } = useLocalState(tutSelector); + !hideGroups ? { hideGroups } = useSettingsState(selectCalmState) : null; const waiter = useWaitForProps(props); const { modal, showModal } = useModal({ - position: 'relative', + position: 'relative', maxWidth: '350px', modal: (dismiss) => { const onDismiss = (e) => { @@ -99,10 +105,11 @@ export default function LaunchApp(props) { e.stopPropagation(); if(!hasTutorialGroup(props)) { await props.api.groups.join(TUTORIAL_HOST, TUTORIAL_GROUP); + await props.api.settings.putEntry('tutorial', 'joined', Date.now()); await waiter(hasTutorialGroup); await Promise.all( [TUTORIAL_BOOK, TUTORIAL_CHAT, TUTORIAL_LINKS].map(graph => - props.api.graph.join(TUTORIAL_HOST, graph))); + props.api.graph.joinGraph(TUTORIAL_HOST, graph))); await waiter(p => { return `/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}` in p.associations.graph && @@ -113,26 +120,39 @@ export default function LaunchApp(props) { nextTutStep(); dismiss(); } - return ( - - - - - Welcome - - You have been invited to use Landscape, an interface to chat - and interact with communities -
- Would you like a tour of Landscape? -
- - - - Yes - - - - )} + return exitingTut ? ( + + + + You can always restart the tutorial by typing "tutorial" in Leap + + + + + + ) : ( + + + + + Welcome + + You have been invited to use Landscape, an interface to chat + and interact with communities +
+ Would you like a tour of Landscape? +
+ + + + Yes + + + + )} }); const hasLoaded = useMemo(() => Object.keys(props.contacts).length > 0, [props.contacts]); @@ -158,6 +178,7 @@ export default function LaunchApp(props) { p={2} pt={0} > + {!hideUtilities && <> - - - - + + + + } + {!hideGroups && + () + }
{hashBox} diff --git a/pkg/interface/src/views/apps/launch/components/Groups.tsx b/pkg/interface/src/views/apps/launch/components/Groups.tsx index 229536ec38..798ab983cc 100644 --- a/pkg/interface/src/views/apps/launch/components/Groups.tsx +++ b/pkg/interface/src/views/apps/launch/components/Groups.tsx @@ -1,22 +1,27 @@ -import React, {useRef} from "react"; -import { Box, Text, Col } from "@tlon/indigo-react"; -import f from "lodash/fp"; -import _ from "lodash"; +import React, { useRef } from 'react'; +import { Box, Text, Col } from '@tlon/indigo-react'; +import f from 'lodash/fp'; +import _ from 'lodash'; +import moment from 'moment'; -import { Associations, Association, Unreads, UnreadStats } from "~/types"; -import { alphabeticalOrder } from "~/logic/lib/util"; -import { getUnreadCount, getNotificationCount } from "~/logic/lib/hark"; -import Tile from "../components/tiles/tile"; -import { useTutorialModal } from "~/views/components/useTutorialModal"; -import {TUTORIAL_HOST, TUTORIAL_GROUP} from "~/logic/lib/tutorialModal"; +import { Associations, Association, Unreads, UnreadStats } from '@urbit/api'; +import { alphabeticalOrder } from '~/logic/lib/util'; +import { getUnreadCount, getNotificationCount } from '~/logic/lib/hark'; +import Tile from '../components/tiles/tile'; +import { useTutorialModal } from '~/views/components/useTutorialModal'; +import { TUTORIAL_HOST, TUTORIAL_GROUP, TUTORIAL_GROUP_RESOURCE } from '~/logic/lib/tutorialModal'; +import useSettingsState, { selectCalmState, SettingsState } from '~/logic/state/settings'; interface GroupsProps { associations: Associations; } const sortGroupsAlph = (a: Association, b: Association) => - alphabeticalOrder(a.metadata.title, b.metadata.title); - + a.group === TUTORIAL_GROUP_RESOURCE + ? -1 + : b.group === TUTORIAL_GROUP_RESOURCE + ? 1 + : alphabeticalOrder(a.metadata.title, b.metadata.title); const getGraphUnreads = (associations: Associations, unreads: Unreads) => (path: string) => f.flow( @@ -34,12 +39,11 @@ const getGraphNotifications = (associations: Associations, unreads: Unreads) => f.reduce(f.add, 0) )(associations.graph); - export default function Groups(props: GroupsProps & Parameters[0]) { const { associations, unreads, inbox, ...boxProps } = props; const groups = Object.values(associations?.groups || {}) - .filter((e) => e?.group in props.groups) + .filter(e => e?.group in props.groups) .sort(sortGroupsAlph); const graphUnreads = getGraphUnreads(associations || {}, unreads); const graphNotifications = getGraphNotifications(associations || {}, unreads); @@ -48,7 +52,7 @@ export default function Groups(props: GroupsProps & Parameters[0]) { <> {groups.map((group, index) => { const path = group?.group; - const unreadCount = graphUnreads(path) + const unreadCount = graphUnreads(path); const notCount = graphNotifications(path); return ( @@ -73,6 +77,7 @@ interface GroupProps { unreads: number; first: boolean; } +const selectJoined = (s: SettingsState) => s.tutorial.joined; function Group(props: GroupProps) { const { path, title, unreads, updates, first = false } = props; const anchorRef = useRef(null); @@ -80,21 +85,26 @@ function Group(props: GroupProps) { useTutorialModal( 'start', isTutorialGroup, - anchorRef.current + anchorRef ); + const { hideUnreads } = useSettingsState(selectCalmState) + const joined = useSettingsState(selectJoined); return ( {title} - - {unreads > 0 && - ({unreads} unread{unreads !== 1 && 's'} ) + {!hideUnreads && ( + {isTutorialGroup && joined && + ({Math.floor(moment.duration(moment(joined).add(14, 'days').diff(moment())).as('days'))} days remaining) } {updates > 0 && ({updates} update{updates !== 1 && 's'} ) } + {unreads > 0 && + ({unreads}) + } - + )} ); diff --git a/pkg/interface/src/views/apps/launch/components/ModalButton.tsx b/pkg/interface/src/views/apps/launch/components/ModalButton.tsx index eea2d3b6e7..190b746a06 100644 --- a/pkg/interface/src/views/apps/launch/components/ModalButton.tsx +++ b/pkg/interface/src/views/apps/launch/components/ModalButton.tsx @@ -1,38 +1,33 @@ -import React from "react" -import { Box, Button, Icon, Text } from "@tlon/indigo-react" -import {useModal} from "~/logic/lib/useModal"; +import React from 'react'; +import { Row, Button, Icon, Text } from '@tlon/indigo-react'; +import { useModal } from '~/logic/lib/useModal'; const ModalButton = (props) => { - const { - children, - icon, - text, - bg, - color, - ...rest - } = props; + const { children, icon, text, bg, color, ...rest } = props; const { modal, showModal } = useModal({ modal: props.children }); - return ( <> {modal} ); -} +}; export default ModalButton; diff --git a/pkg/interface/src/views/apps/launch/components/tiles/custom.js b/pkg/interface/src/views/apps/launch/components/tiles/custom.js index 47403b3487..7ede9f3239 100644 --- a/pkg/interface/src/views/apps/launch/components/tiles/custom.js +++ b/pkg/interface/src/views/apps/launch/components/tiles/custom.js @@ -17,7 +17,6 @@ export default class CustomTile extends React.PureComponent { > {}; @@ -34,20 +33,20 @@ export function LinkResource(props: LinkResourceProps) { associations, graphKeys, unreads, - s3, + graphTimesentMap, + storage, history } = props; - const rid = association.resource; + const rid = association.resource; const relativePath = (p: string) => `${baseUrl}/resource/link${rid}${p}`; - const [, , ship, name] = rid.split("/"); + const [, , ship, name] = rid.split('/'); const resourcePath = `${ship.slice(1)}/${name}`; const resource = associations.graph[rid] ? associations.graph[rid] : { metadata: {} }; - const contactDetails = contacts[resource?.group] || {}; const group = groups[resource?.group] || {}; const graph = graphs[resourcePath] || null; @@ -58,7 +57,7 @@ export function LinkResource(props: LinkResourceProps) { const resourceUrl = `${baseUrl}/resource/link${rid}`; if (!graph) { - return

; + return
; } return ( @@ -66,11 +65,12 @@ export function LinkResource(props: LinkResourceProps) { { return ( @@ -86,7 +87,7 @@ export function LinkResource(props: LinkResourceProps) { }} /> { const index = bigInt(props.match.params.index); const editCommentId = props.match.params.commentId || null; @@ -95,18 +96,15 @@ export function LinkResource(props: LinkResourceProps) { return
Malformed URL
; } - const node = !!graph ? graph.get(index) : null; + const node = graph ? graph.get(index) : null; if (!node) { return Not found; } - - const contact = contactDetails[node.post.author]; - return ( - {"<- Back"} + {'<- Back'} (); - const fetchLinks = useCallback( - async (newer: boolean) => { - /* stubbed, should we generalize the display of graphs in virtualscroller? */ - }, [] - ); - useEffect(() => { - const list = virtualList?.current; - if(!list) return; - list.calculateVisibleItems(); - }, [graph.size]); +const style = { + height: "100%", + width: "100%", + display: "flex", + flexDirection: "column", + alignItems: "center", +}; - const first = graph.peekLargest()?.[0]; - const [,,ship, name] = association.resource.split('/'); - const canWrite = isWriter(props.group, association.resource) +export class LinkWindow extends Component { + fetchLinks = async () => true; - const style = useMemo(() => - ({ - height: "100%", - width: "100%", - display: 'flex', - flexDirection: 'column', - alignItems: 'center' - }), []); + canWrite() { + const { group, association } = this.props; + return isWriter(group, association.resource); + } - if (!first) { - return ( - - { canWrite ? ( - + renderItem = ({ index, scrollWindow }) => { + const { props } = this; + const { association, graph, api } = props; + const [, , ship, name] = association.resource.split("/"); + const node = graph.get(index); + const first = graph.peekLargest()?.[0]; + const post = node?.post; + if (!node || !post) { + return null; + } + const linkProps = { + ...props, + node, + }; + if (this.canWrite() && index.eq(first ?? bigInt.zero)) { + return ( + + + + + + + ); + } + return ; + }; + + render() { + const { graph, api, association, storage, pendingSize } = this.props; + const first = graph.peekLargest()?.[0]; + const [, , ship, name] = association.resource.split("/"); + if (!first) { + return ( + + {this.canWrite() ? ( + ) : ( - There are no links here yet. You do not have permission to post to this collection. - ) - } + + There are no links here yet. You do not have permission to post to + this collection. + + )} + + ); + } + + return ( + + ); } - - return ( - (virtualList.current = l ?? undefined)} - origin="top" - style={style} - onStartReached={() => {}} - onScroll={() => {}} - data={graph} - size={graph.size} - renderer={({ index, measure, scrollWindow }) => { - const node = graph.get(index); - const post = node?.post; - if (!node || !post) return null; - const linkProps = { - ...props, - node, - measure, - }; - if(canWrite && index.eq(first ?? bigInt.zero)) { - return ( - - - - - - - ) - } - return ; - }} - loadRows={fetchLinks} - /> - ); } diff --git a/pkg/interface/src/views/apps/links/components/LinkItem.tsx b/pkg/interface/src/views/apps/links/components/LinkItem.tsx index 8149fc580d..65f26a12ea 100644 --- a/pkg/interface/src/views/apps/links/components/LinkItem.tsx +++ b/pkg/interface/src/views/apps/links/components/LinkItem.tsx @@ -1,12 +1,12 @@ -import React, { useState, useEffect, useRef, useCallback } from 'react'; +import React, { useState, useEffect, useRef, useCallback, ReactElement } from 'react'; import { Link } from 'react-router-dom'; + import { Row, Col, Anchor, Box, Text, Icon, Action } from '@tlon/indigo-react'; +import { GraphNode, Group, Rolodex, Unreads } from '@urbit/api'; import { writeText } from '~/logic/lib/util'; import Author from '~/views/components/Author'; - import { roleForShip } from '~/logic/lib/group'; -import { Contacts, GraphNode, Group, Rolodex, Unreads } from '~/types'; import GlobalApi from '~/logic/api/global'; import { Dropdown } from '~/views/components/Dropdown'; import RemoteContent from '~/views/components/RemoteContent'; @@ -19,10 +19,9 @@ interface LinkItemProps { path: string; contacts: Rolodex; unreads: Unreads; - measure: (el: any) => void; } -export const LinkItem = (props: LinkItemProps) => { +export const LinkItem = (props: LinkItemProps): ReactElement => { const { node, resource, @@ -30,7 +29,6 @@ export const LinkItem = (props: LinkItemProps) => { group, path, contacts, - measure, ...rest } = props; @@ -46,7 +44,7 @@ export const LinkItem = (props: LinkItemProps) => { // FF will only update on next tick setTimeout(() => { console.log(remoteRef.current); - if(document.activeElement instanceof HTMLIFrameElement + if(document.activeElement instanceof HTMLIFrameElement && remoteRef?.current?.containerRef?.contains(document.activeElement)) { markRead(); } @@ -55,8 +53,7 @@ export const LinkItem = (props: LinkItemProps) => { window.addEventListener('blur', onBlur); return () => { window.removeEventListener('blur', onBlur); - } - + }; }, [markRead]); const URLparser = new RegExp( @@ -68,6 +65,7 @@ export const LinkItem = (props: LinkItemProps) => { const size = node.children ? node.children.size : 0; const contents = node.post.contents; const hostname = URLparser.exec(contents[1].url) ? URLparser.exec(contents[1].url)[4] : null; + const href = URLparser.exec(contents[1].url) ? contents[1].url : `http://${contents[1].url}` const baseUrl = props.baseUrl || `/~404/${resource}`; @@ -94,18 +92,15 @@ export const LinkItem = (props: LinkItemProps) => { const commColor = (props.unreads.graph?.[appPath]?.[`/${index}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray'; const isUnread = props.unreads.graph?.[appPath]?.['/']?.unreads?.has(node.post.index); - - - const onMeasure = useCallback(() => { - ref.current && measure(ref.current); - }, [ref.current, measure]) - - useEffect(() => { - onMeasure(); - }, [onMeasure]); - return ( - + { overflow="hidden" onClick={markRead} > + {contents[0].text} { remoteRef.current = r}} - url={contents[1].url} + ref={r => { remoteRef.current = r }} + renderUrl={false} + url={href} text={contents[0].text} unfold={true} - onLoad={onMeasure} style={{ alignSelf: 'center' }} oembedProps={{ p: 2, @@ -143,9 +139,10 @@ export const LinkItem = (props: LinkItemProps) => { alignSelf: 'center', style: { textOverflow: 'ellipsis', whiteSpace: 'pre', width: '100%' }, p: 2 - }} /> + }} + /> - + {hostname} @@ -165,7 +162,9 @@ export const LinkItem = (props: LinkItemProps) => { > - + {node.children.size} @@ -189,7 +188,7 @@ export const LinkItem = (props: LinkItemProps) => { } } - > + > diff --git a/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx b/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx index 2eaba45559..1e34bb5494 100644 --- a/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx +++ b/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx @@ -1,22 +1,23 @@ -import { BaseInput, Box, Button, LoadingSpinner, Text } from "@tlon/indigo-react"; -import React, { useCallback, useState } from "react"; -import GlobalApi from "~/logic/api/global"; -import { useFileDrag } from "~/logic/lib/useDrag"; -import useS3 from "~/logic/lib/useS3"; -import { S3State } from "~/types"; -import SubmitDragger from "~/views/components/SubmitDragger"; -import { createPost } from "~/logic/api/graph"; -import { hasProvider } from "oembed-parser"; +import { BaseInput, Box, Button, LoadingSpinner, Text } from '@tlon/indigo-react'; +import React, { useCallback, useState } from 'react'; +import GlobalApi from '~/logic/api/global'; +import { useFileDrag } from '~/logic/lib/useDrag'; +import useStorage from '~/logic/lib/useStorage'; +import { StorageState } from '~/types'; +import SubmitDragger from '~/views/components/SubmitDragger'; +import { createPost } from '~/logic/api/graph'; +import { hasProvider } from 'oembed-parser'; interface LinkSubmitProps { api: GlobalApi; - s3: S3State; + storage: StorageState; name: string; ship: string; -}; +} const LinkSubmit = (props: LinkSubmitProps) => { - let { canUpload, uploadDefault, uploading, promptUpload } = useS3(props.s3); + const { canUpload, uploadDefault, uploading, promptUpload } = + useStorage(props.storage); const [submitFocused, setSubmitFocused] = useState(false); const [urlFocused, setUrlFocused] = useState(false); @@ -100,7 +101,7 @@ const LinkSubmit = (props: LinkSubmitProps) => { const onLinkChange = (linkValue: string) => { setLinkValueHook(linkValue); - const link = validateLink(linkValue) + const link = validateLink(linkValue); setLinkValid(link); }; @@ -133,7 +134,7 @@ const LinkSubmit = (props: LinkSubmitProps) => { px={2} pt={2} style={{ pointerEvents: 'none' }} - >{canUpload + >{canUpload ? <> Drop or{' '} { zIndex={9} alignItems="center" justifyContent="center" - > + > } {dragging && } @@ -223,4 +224,4 @@ const LinkSubmit = (props: LinkSubmitProps) => { ); }; -export default LinkSubmit; \ No newline at end of file +export default LinkSubmit; diff --git a/pkg/interface/src/views/apps/links/css/custom.css b/pkg/interface/src/views/apps/links/css/custom.css index 6c6906fd23..3b81b54227 100644 --- a/pkg/interface/src/views/apps/links/css/custom.css +++ b/pkg/interface/src/views/apps/links/css/custom.css @@ -16,23 +16,4 @@ left: 0; width: 100%; height: 100%; -} - -/* responsive */ -@media all and (max-width: 34.375em) { - .dn-s { - display: none; - } - .flex-basis-100-s, .flex-basis-full-s { - flex-basis: 100%; - } -} - -@media all and (min-width: 34.375em) { - .db-ns { - display: block; - } - .flex-basis-30-ns { - flex-basis: 30vw; - } } \ No newline at end of file diff --git a/pkg/interface/src/views/apps/notifications/graph.tsx b/pkg/interface/src/views/apps/notifications/graph.tsx index 7517b0b505..6092359b00 100644 --- a/pkg/interface/src/views/apps/notifications/graph.tsx +++ b/pkg/interface/src/views/apps/notifications/graph.tsx @@ -1,31 +1,27 @@ -import React, { ReactNode, useCallback } from "react"; -import moment from "moment"; -import { Row, Box, Col, Text, Anchor, Icon, Action } from "@tlon/indigo-react"; -import { Link, useHistory } from "react-router-dom"; -import _ from "lodash"; +import React, { ReactNode, useCallback } from 'react'; +import moment from 'moment'; +import { Row, Box, Col, Text, Anchor, Icon, Action } from '@tlon/indigo-react'; +import { Link, useHistory } from 'react-router-dom'; +import _ from 'lodash'; import { - Post, GraphNotifIndex, GraphNotificationContents, Associations, - Content, Rolodex, - Groups, -} from "~/types"; -import { Header } from "./header"; -import { cite, deSig, pluralize } from "~/logic/lib/util"; -import { Sigil } from "~/logic/lib/sigil"; -import RichText from "~/views/components/RichText"; -import GlobalApi from "~/logic/api/global"; -import ReactMarkdown from "react-markdown"; -import { getSnippet } from "~/logic/lib/publish"; -import styled from "styled-components"; -import {MentionText} from "~/views/components/MentionText"; -import ChatMessage, {MessageWithoutSigil} from "../chat/components/ChatMessage"; + Groups +} from '~/types'; +import { Header } from './header'; +import { cite, deSig, pluralize, useShowNickname } from '~/logic/lib/util'; +import Author from '~/views/components/Author'; +import GlobalApi from '~/logic/api/global'; +import { getSnippet } from '~/logic/lib/publish'; +import styled from 'styled-components'; +import { MentionText } from '~/views/components/MentionText'; +import ChatMessage from '../chat/components/ChatMessage'; function getGraphModuleIcon(module: string) { - if (module === "link") { - return "Collection"; + if (module === 'link') { + return 'Collection'; } return _.capitalize(module); } @@ -34,81 +30,89 @@ const FilterBox = styled(Box)` background: linear-gradient( to bottom, transparent, - ${(p) => p.theme.colors.white} + ${p => p.theme.colors.white} ); `; -function describeNotification(description: string, plural: boolean) { +function describeNotification(description: string, plural: boolean): string { switch (description) { - case "link": - return `added ${pluralize("new link", plural)} to`; - case "comment": - return `left ${pluralize("comment", plural)} on`; - case "edit-comment": - return `updated ${pluralize("comment", plural)} on`; - case "note": - return `posted ${pluralize("note", plural)} to`; - case "edit-note": - return `updated ${pluralize("note", plural)} in`; - case "mention": - return "mentioned you on"; - case "message": - return `sent ${pluralize("message", plural)} to`; + case 'link': + return `added ${pluralize('new link', plural)} to`; + case 'comment': + return `left ${pluralize('comment', plural)} on`; + case 'edit-comment': + return `updated ${pluralize('comment', plural)} on`; + case 'note': + return `posted ${pluralize('note', plural)} to`; + case 'edit-note': + return `updated ${pluralize('note', plural)} in`; + case 'mention': + return 'mentioned you on'; + case 'message': + return `sent ${pluralize('message', plural)} to`; default: return description; } } const GraphUrl = ({ url, title }) => ( - - - + + + {title} ); -const GraphNodeContent = ({ group, post, contacts, mod, description, index, remoteContentPolicy }) => { +const GraphNodeContent = ({ + group, + post, + contacts, + mod, + description, + index, + remoteContentPolicy +}) => { const { contents } = post; - const idx = index.slice(1).split("/"); - if (mod === "link") { + const idx = index.slice(1).split('/'); + if (mod === 'link') { if (idx.length === 1) { const [{ text }, { url }] = contents; return ; } else if (idx.length === 3) { - return + return ( + + ); } return null; } - if (mod === "publish") { - if (idx[1] === "2") { - return - } else if (idx[1] === "1") { + if (mod === 'publish') { + if (idx[1] === '2') { + return ( + + ); + } else if (idx[1] === '1') { const [{ text: header }, { text: body }] = contents; const snippet = getSnippet(body); return ( - + {header} - - {snippet} + + {snippet} @@ -116,49 +120,56 @@ const GraphNodeContent = ({ group, post, contacts, mod, description, index, remo } } - if(mod === 'chat') { + if (mod === 'chat') { return ( - {}} - group={group} - contacts={contacts} - groups={{}} - associations={{ graph: {}, groups: {}}} - msg={post} - fontSize='0' - pt='2' - /> - ); - + width='100%' + flexShrink={0} + flexGrow={1} + flexWrap='wrap' + marginLeft='-32px' + > + + + ); } return null; }; -function getNodeUrl(mod: string, hidden: boolean, groupPath: string, graph: string, index: string) { +function getNodeUrl( + mod: string, + hidden: boolean, + groupPath: string, + graph: string, + index: string +) { if (hidden && mod === 'chat') { groupPath = '/messages'; } else if (hidden) { groupPath = '/home'; } const graphUrl = `/~landscape${groupPath}/resource/${mod}${graph}`; - const idx = index.slice(1).split("/"); - if (mod === "publish") { + const idx = index.slice(1).split('/'); + if (mod === 'publish') { const [noteId] = idx; return `${graphUrl}/note/${noteId}`; - } else if (mod === "link") { + } else if (mod === 'link') { const [linkId] = idx; return `${graphUrl}/${linkId}`; } else if (mod === 'chat') { return graphUrl; } - return ""; + return ''; } const GraphNode = ({ post, @@ -174,60 +185,42 @@ const GraphNode = ({ read, onRead, showContact = false, - remoteContentPolicy }) => { - const { contents } = post; author = deSig(author); const history = useHistory(); - const img = showContact ? ( - - ) : ; - - const groupContacts = contacts[groupPath] ?? {}; - const nodeUrl = getNodeUrl(mod, group?.hidden, groupPath, graph, index); const onClick = useCallback(() => { - if(!read) { + if (!read) { onRead(); } history.push(nodeUrl); }, [read, onRead]); + const showNickname = useShowNickname(contacts?.[`~${author}`]); + const nickname = (contacts?.[`~${author}`]?.nickname && showNickname) ? contacts[`~${author}`].nickname : cite(author); return ( - - {img} - - {showContact && - - {cite(author)} - - - {moment(time).format("HH:mm")} - - } - + + + {showContact && ( + + )} + @@ -249,7 +242,7 @@ export function GraphNotification(props: { }) { const { contents, index, read, time, api, timebox, groups } = props; - const authors = _.map(contents, "author"); + const authors = _.map(contents, 'author'); const { graph, group } = index; const icon = getGraphModuleIcon(index.module); const desc = describeNotification(index.description, contents.length !== 1); @@ -259,10 +252,10 @@ export function GraphNotification(props: { return; } - return api.hark["read"](timebox, { graph: index }); + return api.hark['read'](timebox, { graph: index }); }, [api, timebox, index, read]); -return ( + return ( <>
- + {_.map(contents, (content, idx) => ( ) => ( ); -function Author(props: { patp: string; contacts: Contacts; last?: boolean }) { - const contact: Contact | undefined = props.contacts?.[props.patp]; +function Author(props: { patp: string; contacts: Contacts; last?: boolean }): ReactElement { + const contact: Contact | undefined = props.contacts?.[`~${props.patp}`]; const showNickname = useShowNickname(contact); const name = contact?.nickname || `~${props.patp}`; @@ -20,7 +23,7 @@ function Author(props: { patp: string; contacts: Contacts; last?: boolean }) { return ( {name} - {!props.last && ", "} + {!props.last && ', '} ); } @@ -36,9 +39,8 @@ export function Header(props: { time: number; read: boolean; associations: Associations; -} & PropFunc ) { - const { description, channel, group, moduleIcon, read } = props; - const contacts = props.contacts[group] || {}; +} & PropFunc ): ReactElement { + const { description, channel, contacts, moduleIcon, read } = props; const authors = _.uniq(props.authors); @@ -50,17 +52,17 @@ export function Header(props: { const last = lent - 1 === parseInt(idx, 10); return ; }), - (auths) => ( + auths => ( {auths} {authors.length > 3 && - ` and ${authors.length - 3} other${authors.length === 4 ? "" : "s"}`} + ` and ${authors.length - 3} other${authors.length === 4 ? '' : 's'}`} ) )(authors); - const time = moment(props.time).format("HH:mm"); + const time = moment(props.time).format('HH:mm'); const groupTitle = props.associations.groups?.[props.group]?.metadata?.title; @@ -84,8 +86,8 @@ export function Header(props: { {authorDesc} {description} - {!!moduleIcon && } - {!!channel && {channelTitle}} + {Boolean(moduleIcon) && } + {Boolean(channel) && {channelTitle}} {groupTitle && <> @@ -93,9 +95,7 @@ export function Header(props: { } - - {time} - + ); } diff --git a/pkg/interface/src/views/apps/notifications/inbox.tsx b/pkg/interface/src/views/apps/notifications/inbox.tsx index a5d5cf3822..2f05b736e8 100644 --- a/pkg/interface/src/views/apps/notifications/inbox.tsx +++ b/pkg/interface/src/views/apps/notifications/inbox.tsx @@ -1,22 +1,28 @@ -import React, { useEffect, useCallback, useRef, useState } from "react"; -import f from "lodash/fp"; -import _ from "lodash"; -import { Icon, Col, Center, Row, Box, Text, Anchor, Rule, LoadingSpinner } from "@tlon/indigo-react"; -import moment from "moment"; -import { Notifications, Rolodex, Timebox, IndexedNotification, Groups, joinProgress, JoinRequests, GroupNotificationsConfig, NotificationGraphConfig } from "~/types"; -import { MOMENT_CALENDAR_DATE, daToUnix, resourceAsPath } from "~/logic/lib/util"; -import { BigInteger } from "big-integer"; -import GlobalApi from "~/logic/api/global"; -import { Notification } from "./notification"; -import { Associations } from "~/types"; -import { InviteItem } from '~/views/components/Invite'; -import { useWaitForProps } from "~/logic/lib/useWaitForProps"; -import { useHistory } from "react-router-dom"; -import {useModal} from "~/logic/lib/useModal"; -import {JoinGroup} from "~/views/landscape/components/JoinGroup"; -import {JoiningStatus} from "./joining"; -import {Invites} from "./invites"; -import {useLazyScroll} from "~/logic/lib/useLazyScroll"; +import React, { useEffect, useCallback, useRef } from 'react'; +import f from 'lodash/fp'; +import _ from 'lodash'; +import moment from 'moment'; +import { BigInteger } from 'big-integer'; + +import { Col, Center, Box, Text, LoadingSpinner } from '@tlon/indigo-react'; +import { + Associations, + Notifications, + Rolodex, + Timebox, + IndexedNotification, + Groups, + JoinRequests, + GroupNotificationsConfig, + NotificationGraphConfig, + Invites as InviteType +} from '@urbit/api'; + +import { MOMENT_CALENDAR_DATE, daToUnix } from '~/logic/lib/util'; +import GlobalApi from '~/logic/api/global'; +import { Notification } from './notification'; +import { Invites } from './invites'; +import { useLazyScroll } from '~/logic/lib/useLazyScroll'; type DatedTimebox = [BigInteger, Timebox]; @@ -25,12 +31,12 @@ function filterNotification(associations: Associations, groups: string[]) { return () => true; } return (n: IndexedNotification) => { - if ("graph" in n.index) { + if ('graph' in n.index) { const { group } = n.index.graph; - return groups.findIndex((g) => group === g) !== -1; - } else if ("group" in n.index) { + return groups.findIndex(g => group === g) !== -1; + } else if ('group' in n.index) { const { group } = n.index.group; - return groups.findIndex((g) => group === g) !== -1; + return groups.findIndex(g => group === g) !== -1; } return true; }; @@ -46,7 +52,7 @@ export default function Inbox(props: { associations: Associations; contacts: Rolodex; filter: string[]; - invites: any; + invites: InviteType; pendingJoin: JoinRequests; notificationsGroupConfig: GroupNotificationsConfig; notificationsGraphConfig: NotificationGraphConfig; @@ -70,30 +76,30 @@ export default function Inbox(props: { const calendar = { ...MOMENT_CALENDAR_DATE, sameDay: function (now) { if (this.subtract(6, 'hours').isBefore(now)) { - return "[Earlier Today]"; + return '[Earlier Today]'; } else { return MOMENT_CALENDAR_DATE.sameDay; } } }; - let notificationsByDay = f.flow( + const notificationsByDay = f.flow( f.map(([date, nots]) => [ date, - nots.filter(filterNotification(associations, props.filter)), + nots.filter(filterNotification(associations, props.filter)) ]), f.groupBy(([d]) => { const date = moment(daToUnix(d)); if (moment().subtract(6, 'hours').isBefore(date)) { return 'latest'; } else { - return date.format("YYYYMMDD"); + return date.format('YYYYMMDD'); } - }), + }) )(notifications); const notificationsByDayMap = new Map( - Object.keys(notificationsByDay).map(timebox => { + Object.keys(notificationsByDay).map((timebox) => { return [timebox, notificationsByDay[timebox]]; }) ); @@ -105,13 +111,12 @@ export default function Inbox(props: { }, [api]); const { isDone, isLoading } = useLazyScroll( - scrollRef, + scrollRef, 0.2, _.flatten(notifications).length, loadMore ); - return ( @@ -123,7 +128,7 @@ export default function Inbox(props: { label={day === 'latest' ? 'Today' : moment(day).calendar(null, calendar)} timeboxes={timeboxes} contacts={props.contacts} - archive={!!props.showArchive} + archive={Boolean(props.showArchive)} associations={props.associations} api={api} groups={props.groups} @@ -142,7 +147,7 @@ export default function Inbox(props: { )} - + ); } @@ -167,9 +172,8 @@ function DaySection({ associations, api, groupConfig, - graphConfig, + graphConfig }) { - const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0); if (lent === 0 || timeboxes.length === 0) { return null; diff --git a/pkg/interface/src/views/apps/notifications/invites.tsx b/pkg/interface/src/views/apps/notifications/invites.tsx index a856af68d5..829a383bc7 100644 --- a/pkg/interface/src/views/apps/notifications/invites.tsx +++ b/pkg/interface/src/views/apps/notifications/invites.tsx @@ -1,15 +1,12 @@ -import React, { useCallback, useState } from "react"; +import React, { ReactElement } from 'react'; import _ from 'lodash'; -import { Box, Row, Col } from "@tlon/indigo-react"; -import GlobalApi from "~/logic/api/global"; -import { Invites as IInvites, Associations, Invite, JoinRequests, Groups, Contacts, AppInvites, JoinProgress } from "~/types"; -import { resourceAsPath, alphabeticalOrder } from "~/logic/lib/util"; -import { useHistory } from "react-router-dom"; -import { useWaitForProps } from "~/logic/lib/useWaitForProps"; -import InviteItem from "~/views/components/Invite"; -import {JoiningStatus} from "./joining"; -import {useModal} from "~/logic/lib/useModal"; -import {JoinGroup} from "~/views/landscape/components/JoinGroup"; + +import { Col } from '@tlon/indigo-react'; +import { Invites as IInvites, Associations, Invite, JoinRequests, Groups, Contacts, AppInvites, JoinProgress } from '@urbit/api'; + +import GlobalApi from '~/logic/api/global'; +import { resourceAsPath, alphabeticalOrder } from '~/logic/lib/util'; +import InviteItem from '~/views/components/Invite'; interface InvitesProps { api: GlobalApi; @@ -26,50 +23,18 @@ interface InviteRef { invite: Invite; } -export function Invites(props: InvitesProps) { +export function Invites(props: InvitesProps): ReactElement { const { api, invites, pendingJoin } = props; - const [selected, setSelected] = useState<[string, string, Invite] | undefined>() - - const acceptInvite = ( - app: string, - uid: string, - invite: Invite - ) => async () => { - setSelected([app, uid, invite]); - showModal(); - }; - - const declineInvite = useCallback( - (app: string, uid: string) => () => api.invite.decline(app, uid), - [api] - ); - - const { modal, showModal } = useModal({ modal: () => { - const [app, uid, invite] = selected!; - const autojoin = `~${invite.resource.ship}/${invite.resource.name}`; - return ( - - )}}); const inviteArr: InviteRef[] = _.reduce(invites, (acc: InviteRef[], val: AppInvites, app: string) => { const appInvites = _.reduce(val, (invs: InviteRef[], invite: Invite, uid: string) => { return [...invs, { invite, uid, app }]; - }, []); return [...acc, ...appInvites]; }, []); - const invitesAndStatus: { [rid: string]: JoinProgress | InviteRef } = - {..._.keyBy(inviteArr, ({ invite }) => resourceAsPath(invite.resource)), ...props.pendingJoin }; - - + const invitesAndStatus: { [rid: string]: JoinProgress | InviteRef } = + { ..._.keyBy(inviteArr, ({ invite }) => resourceAsPath(invite.resource)), ...props.pendingJoin }; return ( { + .map((resource) => { const inviteOrStatus = invitesAndStatus[resource]; if(typeof inviteOrStatus === 'string') { return ( @@ -93,10 +58,10 @@ export function Invites(props: InvitesProps) { groups={props.groups} associations={props.associations} resource={resource} - pendingJoin={pendingJoin} - api={api} /> - ) - + pendingJoin={pendingJoin} + api={api} + /> + ); } else { const { app, uid, invite } = inviteOrStatus; console.log(inviteOrStatus); @@ -107,13 +72,13 @@ export function Invites(props: InvitesProps) { invite={invite} app={app} uid={uid} - pendingJoin={pendingJoin} + pendingJoin={pendingJoin} resource={resource} contacts={props.contacts} groups={props.groups} associations={props.associations} /> - ) + ); } })} diff --git a/pkg/interface/src/views/apps/notifications/joining.tsx b/pkg/interface/src/views/apps/notifications/joining.tsx index 7b2295fb20..5763712055 100644 --- a/pkg/interface/src/views/apps/notifications/joining.tsx +++ b/pkg/interface/src/views/apps/notifications/joining.tsx @@ -1,35 +1,32 @@ -import React, { useState, useEffect } from "react"; -import { Col, Row, Text, SegmentedProgressBar, Box } from "@tlon/indigo-react"; -import GlobalApi from "~/logic/api/global"; +import React from 'react'; +import { Row, Text, SegmentedProgressBar, Box } from '@tlon/indigo-react'; import { JoinProgress, joinProgress, - MetadataUpdatePreview, - joinError, -} from "~/types"; -import { clamp } from "~/logic/lib/util"; + joinError +} from '@urbit/api'; interface JoiningStatusProps { status: JoinProgress; } const description: string[] = [ - "Attempting to contact host", - "Retrieving data", - "Finished join", - "Unable to join, you do not have the correct permissions", - "Internal error, please file an issue", + 'Attempting to contact host', + 'Retrieving data', + 'Finished join', + 'Unable to join, you do not have the correct permissions', + 'Internal error, please file an issue' ]; export function JoiningStatus(props: JoiningStatusProps) { const { status } = props; const current = joinProgress.indexOf(status); - const desc = description?.[current] || ""; + const desc = description?.[current] || ''; const isError = joinError.indexOf(status as any) !== -1; return ( - + {desc} diff --git a/pkg/interface/src/views/apps/notifications/metadata.tsx b/pkg/interface/src/views/apps/notifications/metadata.tsx index b5643f3591..20d5ac0dcf 100644 --- a/pkg/interface/src/views/apps/notifications/metadata.tsx +++ b/pkg/interface/src/views/apps/notifications/metadata.tsx @@ -1,8 +1,8 @@ -import React from "react"; -import { Box } from "@tlon/indigo-react"; +import React from 'react'; +import { Box } from '@tlon/indigo-react'; -import { MetadataBody, NotificationProps } from "./types"; -import { Header } from "./header"; +import { MetadataBody, NotificationProps } from './types'; +import { Header } from './header'; function getInvolvedUsers(body: MetadataBody) { return []; @@ -10,22 +10,22 @@ function getInvolvedUsers(body: MetadataBody) { function getDescription(body: MetadataBody) { const b = body.metadata; - if ("new" in b) { - return "created"; - } else if ("changedTitle" in b) { - return "changed the title to"; - } else if ("changedDescription" in b) { - return "changed the description to"; - } else if ("changedColor" in b) { - return "changed the color to"; - } else if ("deleted" in b) { - return "deleted"; + if ('new' in b) { + return 'created'; + } else if ('changedTitle' in b) { + return 'changed the title to'; + } else if ('changedDescription' in b) { + return 'changed the description to'; + } else if ('changedColor' in b) { + return 'changed the color to'; + } else if ('deleted' in b) { + return 'deleted'; } else { - throw new Error("bad metadata frond"); + throw new Error('bad metadata frond'); } } -export function MetadataNotification(props: NotificationProps<"metadata">) { +export function MetadataNotification(props: NotificationProps<'metadata'>) { const { unread } = props; const description = getDescription(unread.unreads[0].body); diff --git a/pkg/interface/src/views/apps/notifications/notification.tsx b/pkg/interface/src/views/apps/notifications/notification.tsx index cc55d38145..8c550257d1 100644 --- a/pkg/interface/src/views/apps/notifications/notification.tsx +++ b/pkg/interface/src/views/apps/notifications/notification.tsx @@ -1,6 +1,6 @@ -import React, { ReactNode, useCallback, useMemo, useState } from "react"; -import { Row, Box } from "@tlon/indigo-react"; -import _ from "lodash"; +import React, { ReactNode, useCallback, useMemo, useState } from 'react'; +import { Row, Box } from '@tlon/indigo-react'; +import _ from 'lodash'; import { GraphNotificationContents, IndexedNotification, @@ -9,15 +9,15 @@ import { GroupNotificationsConfig, Groups, Associations, - Contacts, -} from "~/types"; -import GlobalApi from "~/logic/api/global"; -import { getParentIndex } from "~/logic/lib/notification"; -import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction"; -import { GroupNotification } from "./group"; -import { GraphNotification } from "./graph"; -import { BigInteger } from "big-integer"; -import { useHovering } from "~/logic/lib/util"; + Contacts +} from '@urbit/api'; +import GlobalApi from '~/logic/api/global'; +import { getParentIndex } from '~/logic/lib/notification'; +import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction'; +import { GroupNotification } from './group'; +import { GraphNotification } from './graph'; +import { BigInteger } from 'big-integer'; +import { useHovering } from '~/logic/lib/util'; interface NotificationProps { notification: IndexedNotification; @@ -37,7 +37,7 @@ function getMuted( graphs: NotificationGraphConfig ) { const { index, notification } = idxNotif; - if ("graph" in idxNotif.index) { + if ('graph' in idxNotif.index) { const { graph } = idxNotif.index.graph; if(!('graph' in notification.contents)) { throw new Error(); @@ -46,11 +46,11 @@ function getMuted( return _.findIndex( graphs?.watching || [], - (g) => g.graph === graph && g.index === parent + g => g.graph === graph && g.index === parent ) === -1; } - if ("group" in index) { - return _.findIndex(groups || [], (g) => g === index.group.group) === -1; + if ('group' in index) { + return _.findIndex(groups || [], g => g === index.group.group) === -1; } return false; } @@ -77,13 +77,13 @@ function NotificationWrapper(props: { ); const onChangeMute = useCallback(async () => { - const func = isMuted ? "unmute" : "mute"; + const func = isMuted ? 'unmute' : 'mute'; return api.hark[func](notif); }, [notif, api, isMuted]); const { hovering, bind } = useHovering(); - const changeMuteDesc = isMuted ? "Unmute" : "Mute"; + const changeMuteDesc = isMuted ? 'Unmute' : 'Mute'; return ( ); - if ("graph" in notification.index) { + if ('graph' in notification.index) { const index = notification.index.graph; const c: GraphNotificationContents = (contents as any).graph; @@ -147,7 +147,7 @@ export function Notification(props: NotificationProps) { ); } - if ("group" in notification.index) { + if ('group' in notification.index) { const index = notification.index.group; const c: GroupNotificationContents = (contents as any).group; return ( diff --git a/pkg/interface/src/views/apps/notifications/notifications.tsx b/pkg/interface/src/views/apps/notifications/notifications.tsx index 87e5764c25..592e458c60 100644 --- a/pkg/interface/src/views/apps/notifications/notifications.tsx +++ b/pkg/interface/src/views/apps/notifications/notifications.tsx @@ -1,25 +1,24 @@ -import React, { useCallback, useState, useRef } from "react"; +import React, { useCallback, useState, useRef, ReactElement } from 'react'; import _ from 'lodash'; -import { Box, Col, Text, Row } from "@tlon/indigo-react"; -import { Link, Switch, Route } from "react-router-dom"; -import Helmet from "react-helmet"; +import { Link, Switch, Route } from 'react-router-dom'; +import Helmet from 'react-helmet'; -import { Body } from "~/views/components/Body"; -import { PropFunc } from "~/types/util"; -import Inbox from "./inbox"; -import NotificationPreferences from "./preferences"; -import { Dropdown } from "~/views/components/Dropdown"; -import { Formik } from "formik"; -import { FormikOnBlur } from "~/views/components/FormikOnBlur"; -import GroupSearch from "~/views/components/GroupSearch"; -import {useTutorialModal} from "~/views/components/useTutorialModal"; +import { Box, Col, Text, Row } from '@tlon/indigo-react'; -const baseUrl = "/~notifications"; +import { Body } from '~/views/components/Body'; +import { PropFunc } from '~/types/util'; +import Inbox from './inbox'; +import { Dropdown } from '~/views/components/Dropdown'; +import { FormikOnBlur } from '~/views/components/FormikOnBlur'; +import GroupSearch from '~/views/components/GroupSearch'; +import { useTutorialModal } from '~/views/components/useTutorialModal'; + +const baseUrl = '/~notifications'; const HeaderLink = React.forwardRef(( props: PropFunc & { view?: string; current: string }, ref -) => { +): ReactElement => { const { current, view, ...textProps } = props; const to = view ? `${baseUrl}/${view}` : baseUrl; const active = view ? current === view : !current; @@ -35,7 +34,7 @@ interface NotificationFilter { groups: string[]; } -export default function NotificationsScreen(props: any) { +export default function NotificationsScreen(props: any): ReactElement { const relativePath = (p: string) => baseUrl + p; const [filter, setFilter] = useState({ groups: [] }); @@ -43,20 +42,20 @@ export default function NotificationsScreen(props: any) { setFilter({ groups }); }; const onReadAll = useCallback(() => { - props.api.hark.readAll() + props.api.hark.readAll(); }, []); const groupFilterDesc = filter.groups.length === 0 - ? "All" + ? 'All' : filter.groups - .map((g) => props.associations?.groups?.[g]?.metadata?.title) - .join(", "); + .map(g => props.associations?.groups?.[g]?.metadata?.title) + .join(', '); const anchorRef = useRef(null); - useTutorialModal('notifications', true, anchorRef.current); + useTutorialModal('notifications', true, anchorRef); return ( { const { view } = routeProps.match.params; return ( @@ -75,21 +74,11 @@ export default function NotificationsScreen(props: any) { borderBottom="1" borderBottomColor="washedGray" > - Updates - - - - Inbox - - - - - Preferences - - - + + Notifications + justifyContent="space-between" + > - {view === "preferences" && ( - - )} {!view && } diff --git a/pkg/interface/src/views/apps/notifications/preferences.tsx b/pkg/interface/src/views/apps/notifications/preferences.tsx deleted file mode 100644 index 8d3f900b43..0000000000 --- a/pkg/interface/src/views/apps/notifications/preferences.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React, { useCallback } from "react"; - -import { Box, Col, ManagedCheckboxField as Checkbox } from "@tlon/indigo-react"; -import { Formik, Form, FormikHelpers } from "formik"; -import * as Yup from "yup"; -import _ from "lodash"; -import { AsyncButton } from "~/views/components/AsyncButton"; -import { FormikOnBlur } from "~/views/components/FormikOnBlur"; -import { NotificationGraphConfig } from "~/types"; -import GlobalApi from "~/logic/api/global"; - -interface FormSchema { - mentions: boolean; - dnd: boolean; - watchOnSelf: boolean; - watching: string[]; -} - -interface NotificationPreferencesProps { - graphConfig: NotificationGraphConfig; - dnd: boolean; - api: GlobalApi; -} - -export default function NotificationPreferences( - props: NotificationPreferencesProps -) { - const { graphConfig, api, dnd } = props; - - const initialValues: FormSchema = { - mentions: graphConfig.mentions, - watchOnSelf: graphConfig.watchOnSelf, - dnd, - watching: graphConfig.watching, - }; - - const onSubmit = useCallback( - async (values: FormSchema, actions: FormikHelpers) => { - console.log(values); - try { - let promises: Promise[] = []; - if (values.mentions !== graphConfig.mentions) { - promises.push(api.hark.setMentions(values.mentions)); - } - if (values.watchOnSelf !== graphConfig.watchOnSelf) { - promises.push(api.hark.setWatchOnSelf(values.watchOnSelf)); - } - if (values.dnd !== dnd && !_.isUndefined(values.dnd)) { - promises.push(api.hark.setDoNotDisturb(values.dnd)) - } - - await Promise.all(promises); - actions.setStatus({ success: null }); - actions.resetForm({ values: initialValues }); - } catch (e) { - console.error(e); - actions.setStatus({ error: e.message }); - } - }, - [api, graphConfig] - ); - - return ( - -
- - - - - - -
- ); -} diff --git a/pkg/interface/src/views/apps/profile/components/EditProfile.tsx b/pkg/interface/src/views/apps/profile/components/EditProfile.tsx index d43cfbaad2..b36dbe2631 100644 --- a/pkg/interface/src/views/apps/profile/components/EditProfile.tsx +++ b/pkg/interface/src/views/apps/profile/components/EditProfile.tsx @@ -1,30 +1,32 @@ -import React from "react"; -import * as Yup from "yup"; +import React, { ReactElement, useRef, useState } from 'react'; +import * as Yup from 'yup'; import _ from 'lodash'; +import { Formik } from 'formik'; +import { useHistory } from 'react-router-dom'; import { ManagedForm as Form, ManagedTextInputField as Input, ManagedCheckboxField as Checkbox, - Center, Col, - Box, Text, Row, - Button, -} from "@tlon/indigo-react"; -import { Formik, FormikHelpers } from "formik"; -import { useHistory } from "react-router-dom"; - -import { uxToHex } from "~/logic/lib/util"; -import { Sigil } from "~/logic/lib/sigil"; -import { AsyncButton } from "~/views/components/AsyncButton"; -import { ColorInput } from "~/views/components/ColorInput"; -import { ImageInput } from "~/views/components/ImageInput"; -import { MarkdownField } from "~/views/apps/publish/components/MarkdownField"; -import { resourceFromPath } from "~/logic/lib/group"; -import GroupSearch from "~/views/components/GroupSearch"; + Button +} from '@tlon/indigo-react'; +import { uxToHex } from '~/logic/lib/util'; +import { AsyncButton } from '~/views/components/AsyncButton'; +import { ColorInput } from '~/views/components/ColorInput'; +import { ImageInput } from '~/views/components/ImageInput'; +import { MarkdownField } from '~/views/apps/publish/components/MarkdownField'; +import { resourceFromPath } from '~/logic/lib/group'; +import GroupSearch from '~/views/components/GroupSearch'; +import { + ProfileHeader, + ProfileControls, + ProfileStatus, + ProfileImages +} from './Profile'; const formSchema = Yup.object({ nickname: Yup.string(), @@ -38,64 +40,96 @@ const emptyContact = { bio: '', status: '', color: '0', - avatar: null, - cover: null, + avatar: '', + cover: '', groups: [], 'last-updated': 0, isPublic: false }; +export function ProfileHeaderImageEdit(props: any): ReactElement { + const { contact, storage, setFieldValue, handleHideCover } = { ...props }; + const [editCover, setEditCover] = useState(false); + const [removedCoverLabel, setRemovedCoverLabel] = useState('Remove Header'); + const handleClear = (e) => { + e.preventDefault(); + handleHideCover(true); + setFieldValue('cover', ''); + setRemovedCoverLabel('Header Removed'); + }; + + return ( + <> + {contact?.cover ? ( +
+ {editCover ? ( + + ) : ( + + + + + )} +
+ ) : ( + + )} + + ); +} + +export function EditProfile(props: any): ReactElement { + const { contact, storage, ship, api, isPublic } = props; + const [hideCover, setHideCover] = useState(false); + + const handleHideCover = (value) => { + setHideCover(value); + }; -export function EditProfile(props: any) { - const { contact, ship, api, isPublic } = props; const history = useHistory(); if (contact) { contact.isPublic = isPublic; } const onSubmit = async (values: any, actions: any) => { - console.log(values); try { await Object.keys(values).reduce((acc, key) => { - console.log(key); - const newValue = key !== "color" ? values[key] : uxToHex(values[key]); - + const newValue = key !== 'color' ? values[key] : uxToHex(values[key]); if (newValue !== contact[key]) { - if (key === "isPublic") { - return acc.then(() => - api.contacts.setPublic(newValue) - ); + if (key === 'isPublic') { + return acc.then(() => api.contacts.setPublic(newValue)); } else if (key === 'groups') { - const toRemove: string[] = _.difference(contact?.groups || [], newValue); - console.log(toRemove); - const toAdd: string[] = _.difference(newValue, contact?.groups || []); - console.log(toAdd); - let promises: Promise[] = []; - + const toRemove: string[] = _.difference( + contact?.groups || [], + newValue + ); + const toAdd: string[] = _.difference( + newValue, + contact?.groups || [] + ); + const promises: Promise[] = []; promises.concat( - toRemove.map(e => - api.contacts.edit(ship, {'remove-group': resourceFromPath(e) }) + toRemove.map((e) => + api.contacts.edit(ship, { 'remove-group': resourceFromPath(e) }) ) ); promises.concat( - toAdd.map(e => - api.contacts.edit(ship, {'add-group': resourceFromPath(e) }) + toAdd.map((e) => + api.contacts.edit(ship, { 'add-group': resourceFromPath(e) }) ) ); return acc.then(() => Promise.all(promises)); - - } else if ( - key !== "last-updated" && - key !== "isPublic" - ) { - return acc.then(() => - api.contacts.edit(ship, { [key]: newValue }) - ); + } else if (key !== 'last-updated' && key !== 'isPublic') { + return acc.then(() => api.contacts.edit(ship, { [key]: newValue })); } } return acc; }, Promise.resolve()); - //actions.setStatus({ success: null }); + // actions.setStatus({ success: null }); history.push(`/~profile/${ship}`); } catch (e) { console.error(e); @@ -110,28 +144,78 @@ export function EditProfile(props: any) { initialValues={contact || emptyContact} onSubmit={onSubmit} > -
- - - Description - - - - - - - - - - - - - - - Submit - - - - + {({ setFieldValue }) => ( +
+ + + + + { + history.push(`/~profile/${ship}`); + }} + > + Cancel + + + + + + + + + + + + + + + + + + + Description + + + + + + Submit + + + )} + + ); } diff --git a/pkg/interface/src/views/apps/profile/components/Profile.tsx b/pkg/interface/src/views/apps/profile/components/Profile.tsx index 0072b591bc..e46bde16fa 100644 --- a/pkg/interface/src/views/apps/profile/components/Profile.tsx +++ b/pkg/interface/src/views/apps/profile/components/Profile.tsx @@ -1,128 +1,205 @@ -import React, { useEffect, useRef, useState } from "react"; -import { Sigil } from "~/logic/lib/sigil"; +import React, { ReactElement, useEffect, useRef, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { Center, Box, Row, BaseImage, Text } from '@tlon/indigo-react'; +import RichText from '~/views/components/RichText'; +import useSettingsState, { selectCalmState } from '~/logic/state/settings'; +import { Sigil } from '~/logic/lib/sigil'; import { ViewProfile } from './ViewProfile'; import { EditProfile } from './EditProfile'; import { SetStatusBarModal } from '~/views/components/SetStatusBarModal'; +import { uxToHex } from '~/logic/lib/util'; +import { useTutorialModal } from '~/views/components/useTutorialModal'; -import { uxToHex } from "~/logic/lib/util"; -import { - Center, - Box, - Row, - BaseImage, - StatelessTextInput as Input, - Button, - Text -} from "@tlon/indigo-react"; -import RichText from '~/views/components/RichText' -import useLocalState from "~/logic/state/local"; -import { useHistory } from "react-router-dom"; -import {useTutorialModal} from "~/views/components/useTutorialModal"; +export function ProfileHeader(props: any): ReactElement { + return ( + + {props.children} + + ); +} +export function ProfileImages(props: any): ReactElement { + const { hideAvatars } = useSettingsState(selectCalmState); + const { contact, hideCover, ship } = { ...props }; + const hexColor = contact?.color ? `#${uxToHex(contact.color)}` : '#000000'; -export function Profile(props: any) { - const { hideAvatars } = useLocalState(({ hideAvatars }) => ({ - hideAvatars - })); + const anchorRef = useRef(null) + + useTutorialModal('profile', ship === `~${window.ship}`, anchorRef); + + const cover = + contact?.cover && !hideCover ? ( + + ) : ( + + ); + + const image = + !hideAvatars && contact?.avatar ? ( + + ) : ( + + ); + + return ( + <> + + {cover} +
+ {props.children} +
+
+ + {image} + + + ); +} + +export function ProfileControls(props: any): ReactElement { + return ( + + {props.children} + + ); +} + +export function ProfileStatus(props: any): ReactElement { + const { contact } = { ...props }; + return ( + + {contact?.status ?? ''} + + ); +} + +export function ProfileActions(props: any): ReactElement { + const { ship, isPublic, contact, api } = { ...props }; const history = useHistory(); + return ( + + {ship === `~${window.ship}` ? ( + <> + { + history.push(`/~profile/${ship}/edit`); + }} + > + Edit {isPublic ? 'Public' : 'Private'} Profile + + + + ) : ( + <> + history.push(`/~landscape/dm/${ship.substring(1)}`)} + > + Message + + + )} + + ); +} + +export function Profile(props: any): ReactElement { + const history = useHistory(); + + const { contact, nackedContacts, hasLoaded, isPublic, isEdit, ship } = props; + const nacked = nackedContacts.has(ship); + const formRef = useRef(null); + + useEffect(() => { + if (hasLoaded && !contact && !nacked) { + props.api.contacts.retrieve(ship); + } + }, [hasLoaded, contact]); + + const anchorRef = useRef(null); if (!props.ship) { return null; } - const { contact, nackedContacts, hasLoaded, isPublic, isEdit, ship } = props; - const nacked = nackedContacts.has(ship); - - const [statusModal, showStatusModal] = useState(false); - - - useEffect(() => { - if(hasLoaded && !contact && !nacked) { - props.api.contacts.retrieve(ship); - } - }, [hasLoaded, contact]) - - - const hexColor = contact?.color ? `#${uxToHex(contact.color)}` : "#000000"; - const cover = (contact?.cover) - ? - : ; - - const image = (!hideAvatars && contact?.avatar) - ? - : ; - - const anchorRef = useRef(null); - - useTutorialModal('profile', ship === `~${window.ship}`, anchorRef.current); return ( -
- - - - - {ship === `~${window.ship}` ? ( - <> - { history.push(`/~profile/${ship}/edit`) }}> - Edit Profile - - - - ) : null} - - {contact?.status ?? ""} - - - {cover} - - -
- - {image} - -
-
+
+ { isEdit ? ( + isPublic={isPublic} + /> ) : ( - ) } + )}
); diff --git a/pkg/interface/src/views/apps/profile/components/SetStatus.tsx b/pkg/interface/src/views/apps/profile/components/SetStatus.tsx index 3795727880..e894ccb192 100644 --- a/pkg/interface/src/views/apps/profile/components/SetStatus.tsx +++ b/pkg/interface/src/views/apps/profile/components/SetStatus.tsx @@ -3,14 +3,13 @@ import React, { useCallback, useEffect, ChangeEvent -} from "react"; +} from 'react'; import { Row, Button, - StatelessTextInput as Input, -} from "@tlon/indigo-react"; - + StatelessTextInput as Input +} from '@tlon/indigo-react'; export function SetStatus(props: any) { const { contact, ship, api, callback } = props; @@ -23,11 +22,11 @@ export function SetStatus(props: any) { ); useEffect(() => { - setStatus(!!contact ? contact.status : ''); + setStatus(contact ? contact.status : ''); }, [contact]); const editStatus = () => { - api.contacts.edit(ship, {status: _status}); + api.contacts.edit(ship, { status: _status }); if (callback) { callback(); @@ -53,7 +52,8 @@ export function SetStatus(props: any) { color="white" ml={2} width="25%" - onClick={editStatus}> + onClick={editStatus} + > Set Status diff --git a/pkg/interface/src/views/apps/profile/components/ViewProfile.tsx b/pkg/interface/src/views/apps/profile/components/ViewProfile.tsx index cd639b1354..523306bb18 100644 --- a/pkg/interface/src/views/apps/profile/components/ViewProfile.tsx +++ b/pkg/interface/src/views/apps/profile/components/ViewProfile.tsx @@ -1,69 +1,66 @@ -import React, {useEffect, useState} from "react"; +import React from 'react'; import _ from 'lodash'; -import { Sigil } from "~/logic/lib/sigil"; - +import { useHistory } from 'react-router-dom'; +import { Center, Box, Text, Row, Col } from '@tlon/indigo-react'; +import RichText from '~/views/components/RichText'; +import useSettingsState, { selectCalmState } from '~/logic/state/settings'; +import { Sigil } from '~/logic/lib/sigil'; +import { GroupLink } from '~/views/components/GroupLink'; +import { lengthOrder } from '~/logic/lib/util'; +import useLocalState from '~/logic/state/local'; import { - Center, - Box, - Text, - Row, - Button, - Col, - LoadingSpinner -} from "@tlon/indigo-react"; -import { AsyncButton } from "~/views/components/AsyncButton"; -import RichText from "~/views/components/RichText"; -import { useHistory } from "react-router-dom"; -import {GroupSummary} from "~/views/landscape/components/GroupSummary"; -import {MetadataUpdatePreview} from "~/types"; -import {GroupLink} from "~/views/components/GroupLink"; -import {lengthOrder} from "~/logic/lib/util"; -import useLocalState from "~/logic/state/local"; - + ProfileHeader, + ProfileControls, + ProfileActions, + ProfileStatus, + ProfileImages +} from './Profile'; export function ViewProfile(props: any) { const history = useHistory(); - const { hideNicknames } = useLocalState(({ hideNicknames }) => ({ - hideNicknames - })); + const { hideNicknames } = useSettingsState(selectCalmState); const { api, contact, nacked, isPublic, ship, associations, groups } = props; return ( <> - -
+ + + + + + + + +
- {((!hideNicknames && contact?.nickname) ? contact.nickname : "")} + {!hideNicknames && contact?.nickname ? contact.nickname : ''}
- -
- {ship} + +
+ + {ship} +
- -
+ +
- {(contact?.bio ? contact.bio : "")} + {contact?.bio ? contact.bio : ''} -
- - { (contact?.groups || []).length > 0 && ( - +
+ + {(contact?.groups || []).length > 0 && ( + Pinned Groups - { contact?.groups.sort(lengthOrder).map(g => ( + {contact?.groups.sort(lengthOrder).map((g) => ( ))} - + )} - { (nacked || (!isPublic && ship === `~${window.ship}`)) ? ( + {nacked || (!isPublic && ship === `~${window.ship}`) ? ( -
- {ship} - remains private + borderColor='washedGray' + > +
+ + {ship} + + remains private
- ) : null - } + ) : null} ); } - diff --git a/pkg/interface/src/views/apps/profile/profile.tsx b/pkg/interface/src/views/apps/profile/profile.tsx index 977a0975e6..dee3dc4d84 100644 --- a/pkg/interface/src/views/apps/profile/profile.tsx +++ b/pkg/interface/src/views/apps/profile/profile.tsx @@ -1,66 +1,62 @@ -import React from "react"; -import { Route, Link } from "react-router-dom"; +import React from 'react'; +import { Route, Link } from 'react-router-dom'; import Helmet from 'react-helmet'; -import { Box, Text, Row, Col, Icon, BaseImage } from "@tlon/indigo-react"; - -import { uxToHex } from "~/logic/lib/util"; +import { Box } from '@tlon/indigo-react'; import { Profile } from "./components/Profile"; -import useLocalState from "~/logic/state/local"; export default function ProfileScreen(props: any) { const { dark } = props; - const hideAvatars = useLocalState(state => state.hideAvatars); return ( <> - - { props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Profile - - { - const ship = match.params.ship; - const isEdit = match.url.includes('edit'); - const isPublic = props.isContactPublic; - const contact = props.contacts?.[ship]; - const sigilColor = contact?.color - ? `#${uxToHex(contact.color)}` - : dark - ? "#FFFFFF" - : "#000000"; + + + {props.notificationsCount + ? `(${String(props.notificationsCount)}) ` + : ''} + Landscape - Profile + + + { + const ship = match.params.ship; + const isEdit = match.url.includes('edit'); + const isPublic = props.isContactPublic; + const contact = props.contacts?.[ship]; - return ( - - - - + return ( + + + + + - - ); - }} - /> + ); + }} + /> ); } diff --git a/pkg/interface/src/views/apps/publish/PublishResource.tsx b/pkg/interface/src/views/apps/publish/PublishResource.tsx index cd9f077c1c..56821cda87 100644 --- a/pkg/interface/src/views/apps/publish/PublishResource.tsx +++ b/pkg/interface/src/views/apps/publish/PublishResource.tsx @@ -1,11 +1,11 @@ -import React from "react"; -import { Box } from "@tlon/indigo-react"; +import React from 'react'; +import { Box } from '@tlon/indigo-react'; -import GlobalApi from "~/logic/api/global"; -import { StoreState } from "~/logic/store/type"; -import { Association } from "~/types"; -import { RouteComponentProps } from "react-router-dom"; -import { NotebookRoutes } from "./components/NotebookRoutes"; +import GlobalApi from '~/logic/api/global'; +import { StoreState } from '~/logic/store/type'; +import { Association } from '@urbit/api'; +import { RouteComponentProps } from 'react-router-dom'; +import { NotebookRoutes } from './components/NotebookRoutes'; type PublishResourceProps = StoreState & { association: Association; @@ -16,8 +16,7 @@ type PublishResourceProps = StoreState & { export function PublishResource(props: PublishResourceProps) { const { association, api, baseUrl, notebooks } = props; const rid = association.resource; - const [, , ship, book] = rid.split("/"); - const notebookContacts = props.contacts[association.group]; + const [, , ship, book] = rid.split('/'); return ( @@ -29,7 +28,6 @@ export function PublishResource(props: PublishResourceProps) { groups={props.groups} associations={props.associations} association={association} - notebookContacts={notebookContacts} rootUrl={baseUrl} baseUrl={`${baseUrl}/resource/publish/ship/${ship}/${book}`} history={props.history} @@ -37,7 +35,7 @@ export function PublishResource(props: PublishResourceProps) { location={props.location} unreads={props.unreads} graphs={props.graphs} - s3={props.s3} + storage={props.storage} /> ); diff --git a/pkg/interface/src/views/apps/publish/components/EditPost.tsx b/pkg/interface/src/views/apps/publish/components/EditPost.tsx index 0f6ed4dcc0..58047fc745 100644 --- a/pkg/interface/src/views/apps/publish/components/EditPost.tsx +++ b/pkg/interface/src/views/apps/publish/components/EditPost.tsx @@ -1,42 +1,46 @@ -import React from "react"; +import React, { ReactElement } from 'react'; import _ from 'lodash'; -import { PostFormSchema, PostForm } from "./NoteForm"; -import { FormikHelpers } from "formik"; -import GlobalApi from "~/logic/api/global"; -import { RouteComponentProps, useLocation } from "react-router-dom"; -import { GraphNode, TextContent, Association, S3State } from "~/types"; -import { getLatestRevision, editPost } from "~/logic/lib/publish"; -import {useWaitForProps} from "~/logic/lib/useWaitForProps"; +import { FormikHelpers } from 'formik'; +import { RouteComponentProps, useLocation } from 'react-router-dom'; + +import { GraphNode } from '@urbit/api'; + +import { PostFormSchema, PostForm } from './NoteForm'; +import GlobalApi from '~/logic/api/global'; +import { getLatestRevision, editPost } from '~/logic/lib/publish'; +import { useWaitForProps } from '~/logic/lib/useWaitForProps'; +import { StorageState } from '~/types'; + interface EditPostProps { ship: string; noteId: number; note: GraphNode; api: GlobalApi; book: string; - s3: S3State; + storage: StorageState; } -export function EditPost(props: EditPostProps & RouteComponentProps) { - const { note, book, noteId, api, ship, history, s3 } = props; +export function EditPost(props: EditPostProps & RouteComponentProps): ReactElement { + const { note, book, noteId, api, ship, history, storage } = props; const [revNum, title, body] = getLatestRevision(note); const location = useLocation(); const waiter = useWaitForProps(props); const initial: PostFormSchema = { title, - body, + body }; const onSubmit = async ( values: PostFormSchema, actions: FormikHelpers - ) => { + ): Promise => { const { title, body } = values; try { const newRev = revNum + 1; const nodes = editPost(newRev, noteId, title, body); await api.graph.addNodes(ship, book, nodes); - await waiter(p => { + await waiter((p) => { const [rev] = getLatestRevision(p.note); return rev === newRev; }); @@ -44,7 +48,7 @@ export function EditPost(props: EditPostProps & RouteComponentProps) { history.push(noteUrl); } catch (e) { console.error(e); - actions.setStatus({ error: "Failed to edit notebook" }); + actions.setStatus({ error: 'Failed to edit notebook' }); } }; @@ -54,7 +58,7 @@ export function EditPost(props: EditPostProps & RouteComponentProps) { cancel history={history} onSubmit={onSubmit} - s3={s3} + storage={storage} submitLabel="Update" loadingText="Updating..." /> diff --git a/pkg/interface/src/views/apps/publish/components/MarkdownEditor.tsx b/pkg/interface/src/views/apps/publish/components/MarkdownEditor.tsx index 32f372f5d8..95bcf55247 100644 --- a/pkg/interface/src/views/apps/publish/components/MarkdownEditor.tsx +++ b/pkg/interface/src/views/apps/publish/components/MarkdownEditor.tsx @@ -1,26 +1,26 @@ -import React, { createRef, useCallback, useRef } from "react"; -import { IUnControlledCodeMirror, UnControlled as CodeEditor } from "react-codemirror2"; +import React, { createRef, useCallback, useRef } from 'react'; +import { IUnControlledCodeMirror, UnControlled as CodeEditor } from 'react-codemirror2'; import { useFormikContext } from 'formik'; import { Prompt } from 'react-router-dom'; import { Editor } from 'codemirror'; -import { MOBILE_BROWSER_REGEX, usePreventWindowUnload } from "~/logic/lib/util"; -import { PropFunc } from "~/types/util"; -import CodeMirror from "codemirror"; +import { MOBILE_BROWSER_REGEX, usePreventWindowUnload } from '~/logic/lib/util'; +import { PropFunc } from '~/types/util'; +import CodeMirror from 'codemirror'; -import "codemirror/mode/markdown/markdown"; -import "codemirror/addon/display/placeholder"; -import "codemirror/addon/edit/continuelist"; +import 'codemirror/mode/markdown/markdown'; +import 'codemirror/addon/display/placeholder'; +import 'codemirror/addon/edit/continuelist'; -import "codemirror/lib/codemirror.css"; -import { Box } from "@tlon/indigo-react"; -import { useFileDrag } from "~/logic/lib/useDrag"; -import SubmitDragger from "~/views/components/SubmitDragger"; -import useS3 from "~/logic/lib/useS3"; -import { S3State } from "~/types"; +import 'codemirror/lib/codemirror.css'; +import { Box } from '@tlon/indigo-react'; +import { useFileDrag } from '~/logic/lib/useDrag'; +import SubmitDragger from '~/views/components/SubmitDragger'; +import useStorage from '~/logic/lib/useStorage'; +import { StorageState } from '~/types'; const MARKDOWN_CONFIG = { - name: "markdown", + name: 'markdown' }; interface MarkdownEditorProps { @@ -28,7 +28,7 @@ interface MarkdownEditorProps { value: string; onChange: (s: string) => void; onBlur?: (e: any) => void; - s3: S3State; + storage: StorageState; } const PromptIfDirty = () => { @@ -49,12 +49,12 @@ export function MarkdownEditor( const options = { mode: MARKDOWN_CONFIG, - theme: "tlon", + theme: 'tlon', lineNumbers: false, lineWrapping: true, - scrollbarStyle: "native", + scrollbarStyle: 'native', // cursorHeight: 0.85, - placeholder: placeholder || "", + placeholder: placeholder || '', extraKeys: { 'Enter': 'newlineAndIndentContinueMarkdownList' } }; @@ -74,7 +74,7 @@ export function MarkdownEditor( [onBlur] ); - const { uploadDefault, canUpload } = useS3(props.s3); + const { uploadDefault, canUpload } = useStorage(props.storage); const onFileDrag = useCallback( async (files: FileList | File[], e: DragEvent) => { @@ -84,7 +84,7 @@ export function MarkdownEditor( const codeMirror: Editor = editor.current.editor; const doc = codeMirror.getDoc(); - Array.from(files).forEach(async file => { + Array.from(files).forEach(async (file) => { const placeholder = `![Uploading ${file.name}](...)`; doc.setValue(doc.getValue() + placeholder); const url = await uploadDefault(file); diff --git a/pkg/interface/src/views/apps/publish/components/MarkdownField.tsx b/pkg/interface/src/views/apps/publish/components/MarkdownField.tsx index 750a649784..0a1a1125a5 100644 --- a/pkg/interface/src/views/apps/publish/components/MarkdownField.tsx +++ b/pkg/interface/src/views/apps/publish/components/MarkdownField.tsx @@ -1,25 +1,25 @@ -import React, { useCallback } from "react"; -import _ from "lodash"; -import { Box, ErrorLabel } from "@tlon/indigo-react"; -import { useField } from "formik"; -import { MarkdownEditor } from "./MarkdownEditor"; +import React, { useCallback } from 'react'; +import _ from 'lodash'; +import { Box, ErrorLabel } from '@tlon/indigo-react'; +import { useField } from 'formik'; +import { MarkdownEditor } from './MarkdownEditor'; export const MarkdownField = ({ id, - s3, + storage, ...rest }: { id: string } & Parameters[0]) => { const [{ value, onBlur }, { error, touched }, { setValue }] = useField(id); const handleBlur = useCallback( (e: any) => { - _.set(e, "target.id", id); + _.set(e, 'target.id', id); onBlur && onBlur(e); }, [onBlur, id] ); - const hasError = !!(error && touched); + const hasError = Boolean(error && touched); return ( - + {error} diff --git a/pkg/interface/src/views/apps/publish/components/MetadataForm.tsx b/pkg/interface/src/views/apps/publish/components/MetadataForm.tsx index c586321a11..7a3bb39ae4 100644 --- a/pkg/interface/src/views/apps/publish/components/MetadataForm.tsx +++ b/pkg/interface/src/views/apps/publish/components/MetadataForm.tsx @@ -12,10 +12,10 @@ import { import { Formik, Form, useFormikContext, FormikHelpers } from "formik"; import GlobalApi from "~/logic/api/global"; import { Notebook } from "~/types/publish-update"; -import { Contacts } from "~/types/contact-update"; +import { Contacts } from "@urbit/api/contacts"; import { FormError } from "~/views/components/FormError"; import { RouteComponentProps, useHistory } from "react-router-dom"; -import {Association} from "~/types"; +import {Association} from "@urbit/api"; import { uxToHex } from "~/logic/lib/util"; interface MetadataFormProps { diff --git a/pkg/interface/src/views/apps/publish/components/Note.tsx b/pkg/interface/src/views/apps/publish/components/Note.tsx index d336650fa0..ba1c8dc538 100644 --- a/pkg/interface/src/views/apps/publish/components/Note.tsx +++ b/pkg/interface/src/views/apps/publish/components/Note.tsx @@ -1,16 +1,16 @@ -import React, { useState, useEffect } from "react"; -import { Box, Text, Col } from "@tlon/indigo-react"; -import ReactMarkdown from "react-markdown"; +import React, { useState, useEffect } from 'react'; +import { Box, Text, Col, Anchor } from '@tlon/indigo-react'; +import ReactMarkdown from 'react-markdown'; import bigInt from 'big-integer'; -import { Link, RouteComponentProps } from "react-router-dom"; -import { Spinner } from "~/views/components/Spinner"; -import { Comments } from "~/views/components/Comments"; -import { NoteNavigation } from "./NoteNavigation"; -import GlobalApi from "~/logic/api/global"; +import { Link, RouteComponentProps } from 'react-router-dom'; +import { Spinner } from '~/views/components/Spinner'; +import { Comments } from '~/views/components/Comments'; +import { NoteNavigation } from './NoteNavigation'; +import GlobalApi from '~/logic/api/global'; import { getLatestRevision, getComments } from '~/logic/lib/publish'; -import Author from "~/views/components/Author"; -import { Contacts, GraphNode, Graph, Association, Unreads, Group } from "~/types"; +import Author from '~/views/components/Author'; +import { Contacts, GraphNode, Graph, Association, Unreads, Group } from '@urbit/api'; interface NoteProps { ship: string; @@ -32,14 +32,21 @@ export function Note(props: NoteProps & RouteComponentProps) { const { notebook, note, contacts, ship, book, api, rootUrl, baseUrl, group } = props; const editCommentId = props.match.params.commentId; + const renderers = { + link: ({ href, children }) => { + return ( + {children} + ) + } + }; + const deletePost = async () => { setDeleting(true); - const indices = [note.post.index] + const indices = [note.post.index]; await api.graph.removeNodes(ship, book, indices); props.history.push(rootUrl); }; - const comments = getComments(note); const [revNum, title, body, post] = getLatestRevision(note); const index = note.post.index.split('/'); @@ -49,8 +56,6 @@ export function Note(props: NoteProps & RouteComponentProps) { api.hark.markEachAsRead(props.association, '/',`/${index[1]}/1/1`, 'note', 'publish'); }, [props.association, props.note]); - - let adminLinks: JSX.Element | null = null; if (window.ship === note?.post?.author) { adminLinks = ( @@ -67,7 +72,7 @@ export function Note(props: NoteProps & RouteComponentProps) { color="red" ml={2} onClick={deletePost} - style={{ cursor: "pointer" }} + style={{ cursor: 'pointer' }} > Delete @@ -96,21 +101,21 @@ export function Note(props: NoteProps & RouteComponentProps) { ref={windowRef} > - {"<- Notebook Index"} + {'<- Notebook Index'} - {title || ""} + {title || ''} {adminLinks} - - + + Promise; submitLabel: string; loadingText: string; - s3: S3State; + storage: StorageState; } const formSchema = Yup.object({ @@ -35,7 +35,7 @@ export interface PostFormSchema { } export function PostForm(props: PostFormProps) { - const { initial, onSubmit, submitLabel, loadingText, s3, cancel, history } = props; + const { initial, onSubmit, submitLabel, loadingText, storage, cancel, history } = props; return ( @@ -63,10 +63,11 @@ export function PostForm(props: PostFormProps) { onClick={() => { history.goBack(); }} - type="button">Cancel} + type="button" + >Cancel} - + diff --git a/pkg/interface/src/views/apps/publish/components/NoteNavigation.tsx b/pkg/interface/src/views/apps/publish/components/NoteNavigation.tsx index b3633df3fd..2ea84118d0 100644 --- a/pkg/interface/src/views/apps/publish/components/NoteNavigation.tsx +++ b/pkg/interface/src/views/apps/publish/components/NoteNavigation.tsx @@ -1,32 +1,39 @@ -import React, { Component } from "react"; -import moment from "moment"; -import { Box } from "@tlon/indigo-react"; -import { Link } from "react-router-dom"; -import { Graph, GraphNode } from "~/types"; -import { getLatestRevision } from "~/logic/lib/publish"; -import { BigInteger } from "big-integer"; +import React, { ReactElement } from 'react'; +import moment from 'moment'; +import { Link } from 'react-router-dom'; +import { BigInteger } from 'big-integer'; + +import { Box, Text } from '@tlon/indigo-react'; +import { Graph } from '@urbit/api'; + +import { getLatestRevision } from '~/logic/lib/publish'; +import Timestamp from '~/views/components/Timestamp'; function NavigationItem(props: { url: string; title: string; date: number; prev?: boolean; -}) { - const date = moment(props.date).fromNow(); +}): ReactElement { return ( - - {props.prev ? "Previous" : "Next"} - - {props.title} - {date} + + {props.prev ? 'Previous' : 'Next'} + + {props.title} + ); @@ -53,7 +60,7 @@ interface NoteNavigationProps { baseUrl: string; } -export function NoteNavigation(props: NoteNavigationProps) { +export function NoteNavigation(props: NoteNavigationProps): ReactElement { let nextComponent = ; let prevComponent = ; const { noteId, notebook } = props; @@ -72,13 +79,13 @@ export function NoteNavigation(props: NoteNavigationProps) { if (next && nextId) { const nextUrl = makeNoteUrl(nextId); const [, title, , post] = getLatestRevision(next); - const date = post["time-sent"]; + const date = post['time-sent']; nextComponent = ; } if (prev && prevId) { const prevUrl = makeNoteUrl(prevId); const [, title, , post] = getLatestRevision(prev); - const date = post["time-sent"]; + const date = post['time-sent']; prevComponent = ( ); diff --git a/pkg/interface/src/views/apps/publish/components/NotePreview.tsx b/pkg/interface/src/views/apps/publish/components/NotePreview.tsx index 1820188c46..8036f317f2 100644 --- a/pkg/interface/src/views/apps/publish/components/NotePreview.tsx +++ b/pkg/interface/src/views/apps/publish/components/NotePreview.tsx @@ -4,14 +4,14 @@ import styled from 'styled-components'; import { Col, Row, Box, Text, Icon, Image } from '@tlon/indigo-react'; import Author from '~/views/components/Author'; -import { GraphNode } from '~/types/graph-update'; -import { Contacts, Group } from '~/types'; +import { GraphNode } from '@urbit/api/graph'; +import { Contacts, Group } from '@urbit/api'; import { getComments, getLatestRevision, - getSnippet, -} from "~/logic/lib/publish"; -import {Unreads} from "~/types"; + getSnippet +} from '~/logic/lib/publish'; +import { Unreads } from '@urbit/api'; import GlobalApi from '~/logic/api/global'; import ReactMarkdown from 'react-markdown'; @@ -48,9 +48,14 @@ export function NotePreview(props: NotePreviewProps) { const snippet = getSnippet(body); const commColor = (props.unreads.graph?.[appPath]?.[`/${noteId}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray'; + + const cursorStyle = post.pending ? 'default' : 'pointer'; + return ( - - + + + image: props => ( + + + + ) }} source={snippet} /> diff --git a/pkg/interface/src/views/apps/publish/components/NoteRoutes.tsx b/pkg/interface/src/views/apps/publish/components/NoteRoutes.tsx index 916347038a..f56668c81a 100644 --- a/pkg/interface/src/views/apps/publish/components/NoteRoutes.tsx +++ b/pkg/interface/src/views/apps/publish/components/NoteRoutes.tsx @@ -1,12 +1,13 @@ -import React from "react"; -import { Route, Switch } from "react-router-dom"; +import React from 'react'; +import { Route, Switch } from 'react-router-dom'; -import GlobalApi from "~/logic/api/global"; -import { RouteComponentProps } from "react-router-dom"; -import Note from "./Note"; -import { EditPost } from "./EditPost"; +import GlobalApi from '~/logic/api/global'; +import { RouteComponentProps } from 'react-router-dom'; +import Note from './Note'; +import { EditPost } from './EditPost'; -import { GraphNode, Graph, Contacts, Association, S3State, Group } from "~/types"; +import { GraphNode, Graph, Contacts, Association, Group } from '@urbit/api'; +import { StorageState } from '~/types'; interface NoteRoutesProps { ship: string; @@ -20,7 +21,7 @@ interface NoteRoutesProps { baseUrl?: string; rootUrl?: string; group: Group; - s3: S3State; + storage: StorageState; } export function NoteRoutes(props: NoteRoutesProps & RouteComponentProps) { @@ -32,17 +33,18 @@ export function NoteRoutes(props: NoteRoutesProps & RouteComponentProps) { } + path={relativePath('/edit')} + render={routeProps => } /> + path={relativePath('/:commentId?')} + render={routeProps => + rootUrl={rootUrl} + /> } /> diff --git a/pkg/interface/src/views/apps/publish/components/Notebook.tsx b/pkg/interface/src/views/apps/publish/components/Notebook.tsx index 2063497234..d350d648b4 100644 --- a/pkg/interface/src/views/apps/publish/components/Notebook.tsx +++ b/pkg/interface/src/views/apps/publish/components/Notebook.tsx @@ -1,17 +1,18 @@ -import React from "react"; -import { RouteComponentProps, Link } from "react-router-dom"; -import { NotebookPosts } from "./NotebookPosts"; -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"; +import React, { ReactElement } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; + +import { Col, Box, Text, Row } from '@tlon/indigo-react'; +import { Contacts, Rolodex, Groups, Associations, Graph, Association, Unreads } from '@urbit/api'; + +import { NotebookPosts } from './NotebookPosts'; +import GlobalApi from '~/logic/api/global'; +import { useShowNickname } from '~/logic/lib/util'; interface NotebookProps { api: GlobalApi; ship: string; book: string; graph: Graph; - notebookContacts: Contacts; association: Association; associations: Associations; contacts: Rolodex; @@ -21,11 +22,11 @@ interface NotebookProps { unreads: Unreads; } -export function Notebook(props: NotebookProps & RouteComponentProps) { +export function Notebook(props: NotebookProps & RouteComponentProps): ReactElement { const { ship, book, - notebookContacts, + contacts, groups, association, graph @@ -36,11 +37,9 @@ 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; + const contact = contacts?.[`~${ship}`]; console.log(association.resource); const showNickname = useShowNickname(contact); @@ -61,7 +60,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps) { graph={graph} host={ship} book={book} - contacts={notebookContacts ? notebookContacts : {}} + contacts={contacts} unreads={props.unreads} baseUrl={props.baseUrl} api={props.api} diff --git a/pkg/interface/src/views/apps/publish/components/NotebookPosts.tsx b/pkg/interface/src/views/apps/publish/components/NotebookPosts.tsx index 639cf5330c..53d551e486 100644 --- a/pkg/interface/src/views/apps/publish/components/NotebookPosts.tsx +++ b/pkg/interface/src/views/apps/publish/components/NotebookPosts.tsx @@ -1,7 +1,7 @@ -import React, { Component } from "react"; -import { Col } from "@tlon/indigo-react"; -import { NotePreview } from "./NotePreview"; -import { Contacts, Graph, Unreads, Group } from "~/types"; +import React, { Component } from 'react'; +import { Col } from '@tlon/indigo-react'; +import { NotePreview } from './NotePreview'; +import { Contacts, Graph, Unreads, Group } from '@urbit/api'; interface NotebookPostsProps { contacts: Contacts; @@ -27,7 +27,7 @@ export function NotebookPosts(props: NotebookPostsProps) { host={props.host} book={props.book} unreads={props.unreads} - contact={props.contacts[node.post.author]} + contact={props.contacts[`~${node.post.author}`]} contacts={props.contacts} node={node} baseUrl={props.baseUrl} diff --git a/pkg/interface/src/views/apps/publish/components/NotebookRoutes.tsx b/pkg/interface/src/views/apps/publish/components/NotebookRoutes.tsx index a3d1f5474f..51efad37a6 100644 --- a/pkg/interface/src/views/apps/publish/components/NotebookRoutes.tsx +++ b/pkg/interface/src/views/apps/publish/components/NotebookRoutes.tsx @@ -1,6 +1,6 @@ -import React, { useEffect } from "react"; -import { RouteComponentProps, Route, Switch } from "react-router-dom"; -import GlobalApi from "~/logic/api/global"; +import React, { useEffect } from 'react'; +import { RouteComponentProps, Route, Switch } from 'react-router-dom'; +import GlobalApi from '~/logic/api/global'; import { Association, Associations, @@ -9,23 +9,20 @@ import { Contacts, Rolodex, Unreads, - S3State -} from "~/types"; -import { Center, LoadingSpinner } from "@tlon/indigo-react"; +} from '@urbit/api'; +import { Center, LoadingSpinner } from '@tlon/indigo-react'; +import { StorageState } from '~/types'; import bigInt from 'big-integer'; -import Notebook from "./Notebook"; -import NewPost from "./new-post"; +import Notebook from './Notebook'; +import NewPost from './new-post'; import { NoteRoutes } from './NoteRoutes'; - - interface NotebookRoutesProps { api: GlobalApi; ship: string; book: string; graphs: Graphs; - notebookContacts: Contacts; unreads: Unreads; contacts: Rolodex; groups: Groups; @@ -33,13 +30,13 @@ interface NotebookRoutesProps { rootUrl: string; association: Association; associations: Associations; - s3: S3State; + storage: StorageState; } export function NotebookRoutes( props: NotebookRoutesProps & RouteComponentProps ) { - const { ship, book, api, notebookContacts, baseUrl, rootUrl, groups } = props; + const { ship, book, api, contacts, baseUrl, rootUrl, groups } = props; useEffect(() => { ship && book && api.graph.getGraph(ship, book); @@ -49,7 +46,6 @@ export function NotebookRoutes( const group = groups?.[props.association?.group]; - const relativePath = (path: string) => `${baseUrl}${path}`; return ( @@ -63,15 +59,16 @@ export function NotebookRoutes( return ; + baseUrl={baseUrl} + />; }} /> ( + path={relativePath('/new')} + render={routeProps => ( )} /> { const { noteId } = routeProps.match.params; const noteIdNum = bigInt(noteId); @@ -109,10 +106,10 @@ export function NotebookRoutes( notebook={graph} unreads={props.unreads} noteId={noteIdNum} - contacts={notebookContacts} + contacts={contacts} association={props.association} group={group} - s3={props.s3} + storage={props.storage} {...routeProps} /> ); diff --git a/pkg/interface/src/views/apps/publish/components/Writers.js b/pkg/interface/src/views/apps/publish/components/Writers.js index b6be6c1597..e8ff547af0 100644 --- a/pkg/interface/src/views/apps/publish/components/Writers.js +++ b/pkg/interface/src/views/apps/publish/components/Writers.js @@ -1,16 +1,14 @@ import React, { Component } from 'react'; import { Box, Text } from '@tlon/indigo-react'; import { ShipSearch } from '~/views/components/ShipSearch'; -import { Formik, Form, FormikHelpers } from 'formik'; +import { Formik, Form } from 'formik'; import { resourceFromPath } from '~/logic/lib/group'; import { AsyncButton } from '~/views/components/AsyncButton'; -import { cite } from '~/logic/lib/util'; export class Writers extends Component { render() { const { association, groups, contacts, api } = this.props; - const [,,,name] = association?.resource.split('/'); const resource = resourceFromPath(association?.group); const onSubmit = async (values, actions) => { 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 2b0dd366a2..7ca53bc8f1 100644 --- a/pkg/interface/src/views/apps/publish/components/new-post.tsx +++ b/pkg/interface/src/views/apps/publish/components/new-post.tsx @@ -1,13 +1,14 @@ -import React from "react"; -import { FormikHelpers } from "formik"; -import GlobalApi from "~/logic/api/global"; -import { useWaitForProps } from "~/logic/lib/useWaitForProps"; -import { RouteComponentProps } from "react-router-dom"; -import { PostForm, PostFormSchema } from "./NoteForm"; -import {createPost} from "~/logic/api/graph"; -import {Graph} from "~/types/graph-update"; -import {Association, S3State} from "~/types"; -import {newPost} from "~/logic/lib/publish"; +import React from 'react'; +import { FormikHelpers } from 'formik'; +import GlobalApi from '~/logic/api/global'; +import { useWaitForProps } from '~/logic/lib/useWaitForProps'; +import { RouteComponentProps } from 'react-router-dom'; +import { PostForm, PostFormSchema } from './NoteForm'; +import { createPost } from '~/logic/api/graph'; +import { Graph } from '@urbit/api/graph'; +import { Association } from '@urbit/api'; +import { StorageState } from '~/types'; +import { newPost } from '~/logic/lib/publish'; interface NewPostProps { api: GlobalApi; @@ -16,7 +17,7 @@ interface NewPostProps { graph: Graph; association: Association; baseUrl: string; - s3: S3State; + storage: StorageState; } export default function NewPost(props: NewPostProps & RouteComponentProps) { @@ -30,21 +31,18 @@ export default function NewPost(props: NewPostProps & RouteComponentProps) { ) => { const { title, body } = values; try { - const [noteId, nodes] = newPost(title, body) - await api.graph.addNodes(ship, book, nodes) - await waiter(p => - p.graph.has(noteId) && !p.graph.get(noteId)?.post?.pending - ); - history.push(`${props.baseUrl}/note/${noteId}`); + const [noteId, nodes] = newPost(title, body); + await api.graph.addNodes(ship, book, nodes); + history.push(`${props.baseUrl}`); } catch (e) { console.error(e); - actions.setStatus({ error: "Posting note failed" }); + actions.setStatus({ error: 'Posting note failed' }); } }; const initialValues: PostFormSchema = { - title: "", - body: "", + title: '', + body: '' }; return ( @@ -53,7 +51,7 @@ export default function NewPost(props: NewPostProps & RouteComponentProps) { onSubmit={onSubmit} submitLabel="Publish" loadingText="Posting..." - s3={props.s3} + storage={props.storage} /> ); } diff --git a/pkg/interface/src/views/apps/publish/css/custom.css b/pkg/interface/src/views/apps/publish/css/custom.css index 4ab638b972..920c889471 100644 --- a/pkg/interface/src/views/apps/publish/css/custom.css +++ b/pkg/interface/src/views/apps/publish/css/custom.css @@ -5,41 +5,6 @@ --light-gray: rgba(0,0,0,0.08); } -.bg-welcome-green { - background-color: #ECF6F2; -} - -@media all and (max-width: 34.375em) { - .dn-s { - display: none; - } - .flex-basis-100-s, .flex-basis-full-s { - flex-basis: 100%; - } - .h-100-m-40-s { - height: calc(100% - 40px); - } - .black-s { - color: #000; - } -} - -@media all and (min-width: 34.375em) { - .db-ns { - display: block; - } - .flex-basis-250-ns { - flex-basis: 250px; - } - .h-100-m-40-ns { - height: calc(100% - 40px); - } -} - -.bg-light-green { - background: rgba(42, 167, 121, 0.1); -} - .NotebookButton { border-radius:2px; cursor: pointer; @@ -76,6 +41,8 @@ cursor: text; font-size: 12px; line-height: 20px; + background: inherit; + color: inherit; } .publish .CodeMirror * { @@ -207,57 +174,6 @@ } @media all and (prefers-color-scheme: dark) { - .bg-black-d { - background-color: black; - } - .white-d { - color: white; - } - .gray1-d { - color: #4d4d4d; - } - .gray2-d { - color: #7f7f7f; - } - .gray3-d { - color: #b1b2b3; - } - .gray4-d { - color: #e6e6e6; - } - .bg-gray0-d { - background-color: #333; - } - .bg-gray1-d { - background-color: #4d4d4d; - } - .b--gray0-d { - border-color: #333; - } - .b--gray1-d { - border-color: #4d4d4d; - } - .b--gray2-d { - border-color: #7f7f7f; - } - .b--white-d { - border-color: #fff; - } - .invert-d { - filter: invert(1); - } - .o-60-d { - opacity: .6; - } - a { - color: #fff; - } - .focus-b--white-d:focus { - border-color: #fff; - } - .hover-bg-gray1-d:hover { - background-color: #4d4d4d; - } .options.open { background-color: #4d4d4d; } @@ -266,32 +182,32 @@ } .publish .cm-s-tlon.CodeMirror { background: unset; - color: #fff; + color: inherit; } .publish .cm-s-tlon span.cm-def { - color: white; + color: inherit; } .publish .cm-s-tlon span.cm-variable { - color: white; + color: inherit; } .publish .cm-s-tlon span.cm-variable-2 { - color: white; + color: inherit; } .publish .cm-s-tlon span.cm-variable-3, .publish .cm-s-tlon span.cm-type { - color: white; + color: inherit; } .publish .cm-s-tlon span.cm-property { - color: white; + color: inherit; } .publish .cm-s-tlon span.cm-operator { - color: white; + color: inherit; } @@ -325,7 +241,7 @@ color: black; display: inline-block; padding: 0; - background-color: rgba(255,255,255, 0.3); + background-color: rgba(0,255,255, 0.3); border-radius: 2px; } } diff --git a/pkg/interface/src/views/apps/settings/components/lib/BackButton.tsx b/pkg/interface/src/views/apps/settings/components/lib/BackButton.tsx new file mode 100644 index 0000000000..570618c641 --- /dev/null +++ b/pkg/interface/src/views/apps/settings/components/lib/BackButton.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Text } from '@tlon/indigo-react'; + +export function BackButton(props: {}) { + return ( + + {"<- Back to System Preferences"} + + ); +} diff --git a/pkg/interface/src/views/apps/settings/components/lib/BackgroundPicker.tsx b/pkg/interface/src/views/apps/settings/components/lib/BackgroundPicker.tsx index 53aed1b3e9..b897aca394 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/BackgroundPicker.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/BackgroundPicker.tsx @@ -1,58 +1,65 @@ -import React from "react"; +import React, { ReactElement } from 'react'; + import { Box, + Text, Row, Label, Col, ManagedRadioButtonField as Radio, - ManagedTextInputField as Input, -} from "@tlon/indigo-react"; +} from '@tlon/indigo-react'; -import GlobalApi from "~/logic/api/global"; -import { S3State } from "~/types"; -import { ImageInput } from "~/views/components/ImageInput"; -import {ColorInput} from "~/views/components/ColorInput"; +import GlobalApi from '~/logic/api/global'; +import { ImageInput } from '~/views/components/ImageInput'; +import { ColorInput } from '~/views/components/ColorInput'; +import { StorageState } from '~/types'; -export type BgType = "none" | "url" | "color"; +export type BgType = 'none' | 'url' | 'color'; export function BackgroundPicker({ bgType, bgUrl, api, - s3, + storage }: { bgType: BgType; bgUrl?: string; api: GlobalApi; - s3: S3State; + storage: StorageState; }) { - const rowSpace = { my: 0, alignItems: 'center' }; - const radioProps = { my: 4, mr: 4, name: 'bgType' }; + const colProps = { my: 3, mr: 4, gapY: 1 }; return ( - + - - {bgType === "url" && ( + + + Set an image background - )} + - - {bgType === "color" && ( - - )} + + + Set a hex-based background + + - + ); } diff --git a/pkg/interface/src/views/apps/settings/components/lib/BucketList.tsx b/pkg/interface/src/views/apps/settings/components/lib/BucketList.tsx index 5ffdc270a0..25afa1dff3 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/BucketList.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/BucketList.tsx @@ -1,35 +1,38 @@ -import React, { useCallback } from "react"; +import React, { ReactElement, useCallback, useState } from "react"; +import { Formik, FormikHelpers } from 'formik'; import { ManagedTextInputField as Input, ManagedForm as Form, Box, Button, - Col, Text, Menu, MenuButton, MenuList, MenuItem, + Row, } from "@tlon/indigo-react"; -import { Formik } from "formik"; -import GlobalApi from "~/logic/api/global"; +import GlobalApi from '~/logic/api/global'; export function BucketList({ buckets, selected, - api, + api }: { buckets: Set; selected: string; api: GlobalApi; -}) { +}): ReactElement { const _buckets = Array.from(buckets); + const [adding, setAdding] = useState(false); + const onSubmit = useCallback( - (values: { newBucket: string }) => { + (values: { newBucket: string }, actions: FormikHelpers) => { api.s3.addBucket(values.newBucket); + actions.resetForm({ values: { newBucket: "" } }); }, [api] ); @@ -53,14 +56,14 @@ export function BucketList({ ); return ( - +
- {_buckets.map((bucket) => ( + {_buckets.map(bucket => ( ))} - - + {adding && ( + + )} + + + +
); diff --git a/pkg/interface/src/views/apps/settings/components/lib/CalmPref.tsx b/pkg/interface/src/views/apps/settings/components/lib/CalmPref.tsx new file mode 100644 index 0000000000..c368ccc9e2 --- /dev/null +++ b/pkg/interface/src/views/apps/settings/components/lib/CalmPref.tsx @@ -0,0 +1,147 @@ +import React, {useCallback} from "react"; +import { + Box, + ManagedToggleSwitchField as Toggle, + Button, + Col, + Text, +} from "@tlon/indigo-react"; +import { Formik, Form, FormikHelpers } from "formik"; +import * as Yup from "yup"; +import { BackButton } from "./BackButton"; +import useSettingsState, {selectSettingsState} from "~/logic/state/settings"; +import GlobalApi from "~/logic/api/global"; +import {AsyncButton} from "~/views/components/AsyncButton"; + +interface FormSchema { + hideAvatars: boolean; + hideNicknames: boolean; + hideUnreads: boolean; + hideGroups: boolean; + hideUtilities: boolean; + imageShown: boolean; + audioShown: boolean; + oembedShown: boolean; + videoShown: boolean; +} + +const settingsSel = selectSettingsState(["calm", "remoteContentPolicy"]); + +export function CalmPrefs(props: { + api: GlobalApi; +}) { + const { api } = props; + const { + calm: { + hideAvatars, + hideNicknames, + hideUnreads, + hideGroups, + hideUtilities + }, + remoteContentPolicy: { + imageShown, + videoShown, + oembedShown, + audioShown, + } + } = useSettingsState(settingsSel); + + + const initialValues: FormSchema = { + hideAvatars, + hideNicknames, + hideUnreads, + hideGroups, + hideUtilities, + imageShown, + videoShown, + oembedShown, + audioShown, + }; + + const onSubmit = useCallback(async (v: FormSchema, actions: FormikHelpers) => { + await Promise.all([ + api.settings.putEntry('calm', 'hideAvatars', v.hideAvatars), + api.settings.putEntry('calm', 'hideNicknames', v.hideNicknames), + api.settings.putEntry('calm', 'hideUnreads', v.hideUnreads), + api.settings.putEntry('calm', 'hideGroups', v.hideGroups), + api.settings.putEntry('calm', 'hideUtilities', v.hideUtilities), + api.settings.putEntry('remoteContentPolicy', 'imageShown', v.imageShown), + api.settings.putEntry('remoteContentPolicy', 'videoShown', v.videoShown), + api.settings.putEntry('remoteContentPolicy', 'audioShown', v.audioShown), + api.settings.putEntry('remoteContentPolicy', 'oembedShown', v.oembedShown), + ]); + actions.setStatus({ success: null }); + }, [api]); + + return ( + +
+ + + + + CalmEngine + + + Modulate various elements across Landscape to maximize calmness + + + Home screen + + + + User-set identity + + + Remote Content + + + + + + + Save + + + +
+ ); +} diff --git a/pkg/interface/src/views/apps/settings/components/lib/DisplayForm.tsx b/pkg/interface/src/views/apps/settings/components/lib/DisplayForm.tsx index 1fcc3a1204..a673dc06a9 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/DisplayForm.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/DisplayForm.tsx @@ -1,115 +1,123 @@ -import React from 'react'; +import React from "react"; import { - Box, - ManagedCheckboxField as Checkbox, - Button -} from '@tlon/indigo-react'; -import { Formik, Form } from 'formik'; -import * as Yup from 'yup'; + Col, + Text, + Label, + ManagedRadioButtonField as Radio +} from "@tlon/indigo-react"; +import { Formik, Form } from "formik"; +import * as Yup from "yup"; -import GlobalApi from '~/logic/api/global'; -import { uxToHex } from '~/logic/lib/util'; -import { S3State, BackgroundConfig } from '~/types'; -import { BackgroundPicker, BgType } from './BackgroundPicker'; -import useLocalState, { LocalState } from '~/logic/state/local'; +import GlobalApi from "~/logic/api/global"; +import { uxToHex } from "~/logic/lib/util"; +import { S3State, BackgroundConfig, StorageState } from "~/types"; +import { BackgroundPicker, BgType } from "./BackgroundPicker"; +import useSettingsState, { SettingsState, selectSettingsState } from "~/logic/state/settings"; +import {AsyncButton} from "~/views/components/AsyncButton"; +import { BackButton } from "./BackButton"; const formSchema = Yup.object().shape({ bgType: Yup.string() - .oneOf(['none', 'color', 'url'], 'invalid') - .required('Required'), - bgUrl: Yup.string().url(), - bgColor: Yup.string(), - avatars: Yup.boolean(), - nicknames: Yup.boolean() + .oneOf(["none", "color", "url"], "invalid") + .required("Required"), + background: Yup.string(), + theme: Yup.string() + .oneOf(["light", "dark", "auto"]) + .required("Required") }); interface FormSchema { bgType: BgType; bgColor: string | undefined; bgUrl: string | undefined; - avatars: boolean; - nicknames: boolean; + theme: string; } interface DisplayFormProps { api: GlobalApi; - s3: S3State; + storage: StorageState; } -export default function DisplayForm(props: DisplayFormProps) { - const { api, s3 } = props; +const settingsSel = selectSettingsState(["display"]); + +export default function DisplayForm(props: DisplayFormProps) { + const { api, storage } = props; + + const { + display: { + background, + backgroundType, + theme + } + } = useSettingsState(settingsSel); - const { hideAvatars, hideNicknames, background, set: setLocalState } = useLocalState(); let bgColor, bgUrl; - if (background?.type === 'url') { - bgUrl = background.url; + if (backgroundType === "url") { + bgUrl = background; } - if (background?.type === 'color') { - bgColor = background.color; + if (backgroundType === "color") { + bgColor = background; } - const bgType = background?.type || 'none'; + const bgType = backgroundType || "none"; return ( { - const bgConfig: BackgroundConfig = - values.bgType === 'color' - ? { type: 'color', color: `#${uxToHex(values.bgColor || '0x0')}` } - : values.bgType === 'url' - ? { type: 'url', url: values.bgUrl || '' } - : undefined; + onSubmit={async (values, actions) => { + let promises = [] as Promise[]; + promises.push(api.settings.putEntry('display', 'backgroundType', values.bgType)); + promises.push( + api.settings.putEntry('display', 'background', + values.bgType === "color" + ? `#${uxToHex(values.bgColor || "0x0")}` + : values.bgType === "url" + ? values.bgUrl || "" + : false + )); + + promises.push(api.settings.putEntry('display', 'theme', values.theme)); + await Promise.all(promises); + + actions.setStatus({ success: null }); - setLocalState((state: LocalState) => { - state.background = bgConfig; - state.hideAvatars = values.avatars; - state.hideNicknames = values.nicknames; - }); - actions.setSubmitting(false); }} > - {props => ( + {(props) => (
- - - Display Preferences - + + + + + Display Preferences + + + Customize visual interfaces across your Landscape + + - - - - + +
)}
diff --git a/pkg/interface/src/views/apps/settings/components/lib/LeapSettings.tsx b/pkg/interface/src/views/apps/settings/components/lib/LeapSettings.tsx new file mode 100644 index 0000000000..a4f7992824 --- /dev/null +++ b/pkg/interface/src/views/apps/settings/components/lib/LeapSettings.tsx @@ -0,0 +1,103 @@ +import React, { useCallback } from "react"; +import _ from "lodash"; +import { + Col, + Text, + ManagedToggleSwitchField as Toggle, + ManagedCheckboxField, + BaseInput, +} from "@tlon/indigo-react"; +import { Form, FormikHelpers, useField, useFormikContext } from "formik"; +import { FormikOnBlur } from "~/views/components/FormikOnBlur"; +import { BackButton } from "./BackButton"; +import GlobalApi from "~/logic/api/global"; +import { + NotificationGraphConfig, + LeapCategories, + leapCategories, +} from "~/types"; +import useSettingsState, { selectSettingsState } from "~/logic/state/settings"; +import { ShuffleFields } from "~/views/components/ShuffleFields"; + +const labels: Record = { + mychannel: "My Channel", + updates: "Notifications", + profile: "Profile", + messages: "Messages", + logout: "Log Out", +}; + +interface FormSchema { + categories: { display: boolean; category: LeapCategories }[]; +} + +function CategoryCheckbox(props: { index: number }) { + const { index } = props; + const { values } = useFormikContext(); + const cats = values.categories; + const catNameId = `categories[${index}].category`; + const [field] = useField(catNameId); + + const { category } = cats[index]; + const label = labels[category]; + + return ( + + ); +} + +const settingsSel = selectSettingsState(["leap", "set"]); + +export function LeapSettings(props: { api: GlobalApi; }) { + const { api } = props; + const { leap, set: setSettingsState } = useSettingsState(settingsSel); + const categories = leap.categories as LeapCategories[]; + const missing = _.difference(leapCategories, categories); + console.log(categories); + + const initialValues = { + categories: [ + ...categories.map((cat) => ({ + category: cat, + display: true, + })), + ...missing.map((cat) => ({ category: cat, display: false })), + ], + }; + + const onSubmit = async (values: FormSchema) => { + const result = values.categories.reduce( + (acc, { display, category }) => (display ? [...acc, category] : acc), + [] as LeapCategories[] + ); + await api.settings.putEntry('leap', 'categories', result); + }; + + return ( + <> + + + + + Leap + + + Customize Leap ordering, omit modules or results + + + +
+ + + Customize default Leap sections + + + {(index, helpers) => } + + +
+
+ + + ); +} diff --git a/pkg/interface/src/views/apps/settings/components/lib/NotificationPref.tsx b/pkg/interface/src/views/apps/settings/components/lib/NotificationPref.tsx new file mode 100644 index 0000000000..ae04e14ae9 --- /dev/null +++ b/pkg/interface/src/views/apps/settings/components/lib/NotificationPref.tsx @@ -0,0 +1,90 @@ +import React, { useCallback } from "react"; +import { + Col, + Text, + ManagedToggleSwitchField as Toggle, +} from "@tlon/indigo-react"; +import { Form, FormikHelpers } from "formik"; +import { FormikOnBlur } from "~/views/components/FormikOnBlur"; +import { BackButton } from "./BackButton"; +import GlobalApi from "~/logic/api/global"; +import {NotificationGraphConfig} from "~/types"; + +interface FormSchema { + mentions: boolean; + dnd: boolean; + watchOnSelf: boolean; +} + +export function NotificationPreferences(props: { + api: GlobalApi; + graphConfig: NotificationGraphConfig; + dnd: boolean; +}) { + const { graphConfig, api, dnd } = props; + const initialValues = { + mentions: graphConfig.mentions, + dnd: dnd, + watchOnSelf: graphConfig.watchOnSelf, + }; + + const onSubmit = useCallback(async (values: FormSchema, actions: FormikHelpers) => { + try { + let promises: Promise[] = []; + if (values.mentions !== graphConfig.mentions) { + promises.push(api.hark.setMentions(values.mentions)); + } + if (values.watchOnSelf !== graphConfig.watchOnSelf) { + promises.push(api.hark.setWatchOnSelf(values.watchOnSelf)); + } + if (values.dnd !== dnd && !_.isUndefined(values.dnd)) { + promises.push(api.hark.setDoNotDisturb(values.dnd)) + } + + await Promise.all(promises); + actions.setStatus({ success: null }); + actions.resetForm({ values: initialValues }); + } catch (e) { + console.error(e); + actions.setStatus({ error: e.message }); + } + }, [api]); + + return ( + <> + + + + + Notification Preferences + + + Set notification visibility and default behaviours for groups and + messaging + + + +
+ + + + + + +
+ + + ); +} diff --git a/pkg/interface/src/views/apps/settings/components/lib/RemoteContent.tsx b/pkg/interface/src/views/apps/settings/components/lib/RemoteContent.tsx index 168a441505..6d57395689 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/RemoteContent.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/RemoteContent.tsx @@ -1,20 +1,20 @@ -import React from "react"; +import React from 'react'; import { Box, Button, - ManagedCheckboxField as Checkbox, -} from "@tlon/indigo-react"; -import { Formik, Form } from "formik"; -import * as Yup from "yup"; + ManagedCheckboxField as Checkbox +} from '@tlon/indigo-react'; +import { Formik, Form } from 'formik'; +import * as Yup from 'yup'; -import GlobalApi from "~/logic/api/global"; -import useLocalState from "~/logic/state/local"; +import GlobalApi from '~/logic/api/global'; +import useLocalState from '~/logic/state/local'; const formSchema = Yup.object().shape({ imageShown: Yup.boolean(), audioShown: Yup.boolean(), videoShown: Yup.boolean(), - oembedShown: Yup.boolean(), + oembedShown: Yup.boolean() }); interface FormSchema { @@ -44,17 +44,17 @@ export default function RemoteContentForm(props: RemoteContentFormProps) { imageShown, audioShown, videoShown, - oembedShown, + oembedShown } as FormSchema } onSubmit={(values, actions) => { - setRemoteContentPolicy(state => { + setRemoteContentPolicy((state) => { Object.assign(state.remoteContentPolicy, values); }); actions.setSubmitting(false); }} > - {(props) => ( + {props => (
{ @@ -47,9 +50,10 @@ export default function S3Form(props: S3FormProps) { }, [api, s3] ); + return ( <> - + - - - S3 Credentials - - - - - + + + + + + S3 Storage Setup + + + Store credentials for your S3 object storage buckets on your + Urbit ship, and upload media freely to various modules. + + Learn more + + + + + + + + - - - S3 Buckets - + + + + S3 Buckets + + + Your 'active' bucket will be the one used when Landscape uploads a + file + + - - Security - - - Log out of this session - - - You will be logged out of your Urbit on this browser. + <> + + + + + Security Preferences + + + Manage sessions, login credentials and Landscape access + + + + + Log out of this session + + + {allSessions + ? "You will be logged out of all browsers that have currently logged into your Urbit." + : "You will be logged out of your Urbit on this browser."} + + setAllSessions((s) => !s)} + > + Log out of all sessions +
-
-
- - Log out of all sessions - - - You will be logged out of all browsers that have currently logged into your Urbit. -
- - -
-
-
+ + + ); } diff --git a/pkg/interface/src/views/apps/settings/components/settings.tsx b/pkg/interface/src/views/apps/settings/components/settings.tsx index c199f82910..9deeec6eb3 100644 --- a/pkg/interface/src/views/apps/settings/components/settings.tsx +++ b/pkg/interface/src/views/apps/settings/components/settings.tsx @@ -1,37 +1,97 @@ -import React from 'react'; +import React from "react"; -import { Box } from '@tlon/indigo-react'; +import { Row, Icon, Box, Col, Text } from "@tlon/indigo-react"; -import GlobalApi from '~/logic/api/global'; -import { StoreState } from '~/logic/store/type'; -import DisplayForm from './lib/DisplayForm'; -import S3Form from './lib/S3Form'; -import SecuritySettings from './lib/Security'; -import RemoteContentForm from './lib/RemoteContent'; +import GlobalApi from "~/logic/api/global"; +import { StoreState } from "~/logic/store/type"; +import DisplayForm from "./lib/DisplayForm"; +import S3Form from "./lib/S3Form"; +import SecuritySettings from "./lib/Security"; +import RemoteContentForm from "./lib/RemoteContent"; +import { NotificationPreferences } from "./lib/NotificationPref"; +import { CalmPrefs } from "./lib/CalmPref"; +import { Link } from "react-router-dom"; -type ProfileProps = StoreState & { api: GlobalApi; ship: string }; - -export default function Settings({ - api, - s3 -}: ProfileProps) { +export function SettingsItem(props: { + title: string; + description: string; + to: string; +}) { + const { to, title, description } = props; return ( - - - - - - + + + + + {title} + {description} + + + + ); +} + +export default function Settings(props: {}) { + return ( + + + System Preferences + Configure and customize Landscape + + + + + + + + {/* + + */} + + + ); } diff --git a/pkg/interface/src/views/apps/settings/settings.tsx b/pkg/interface/src/views/apps/settings/settings.tsx index bfe331ecd3..38572086f6 100644 --- a/pkg/interface/src/views/apps/settings/settings.tsx +++ b/pkg/interface/src/views/apps/settings/settings.tsx @@ -1,48 +1,139 @@ -import React from "react"; -import { Route, Link, Switch } from "react-router-dom"; -import Helmet from 'react-helmet'; +import React, { ReactNode } from "react"; +import { useLocation } from "react-router-dom"; +import Helmet from "react-helmet"; -import { Box, Text, Row, Col, Icon, BaseImage } from "@tlon/indigo-react"; +import { Text, Box, Col, Row } from '@tlon/indigo-react'; -import Settings from "./components/settings"; -import useLocalState from "~/logic/state/local"; +import { NotificationPreferences } from "./components/lib/NotificationPref"; +import DisplayForm from "./components/lib/DisplayForm"; +import S3Form from "./components/lib/S3Form"; +import { CalmPrefs } from "./components/lib/CalmPref"; +import SecuritySettings from "./components/lib/Security"; +import { LeapSettings } from "./components/lib/LeapSettings"; +import { useHashLink } from "~/logic/lib/useHashLink"; +import { SidebarItem as BaseSidebarItem } from "~/views/landscape/components/SidebarItem"; +import { PropFunc } from "~/types"; + +export const Skeleton = (props: { children: ReactNode }) => ( + + + {props.children} + + +); + +type ProvSideProps = "to" | "selected"; +type BaseProps = PropFunc; +function SidebarItem(props: { hash: string } & Omit) { + const { hash, icon, text, ...rest } = props; + + const to = `/~settings#${hash}`; + + const location = useLocation(); + const selected = location.hash.slice(1) === hash; + + return ( + + ); +} + +function SettingsItem(props: { children: ReactNode }) { + const { children } = props; + + return ( + + {children} + + ); +} export default function SettingsScreen(props: any) { - const { ship, dark } = props; - const hideAvatars = useLocalState(state => state.hideAvatars); + + const location = useLocation(); + const hash = location.hash.slice(1) + return ( <> Landscape - Settings - { - return ( - - - - - - ); - }} - /> + + + + + System Preferences + + + + + + + + + + + + + {hash === "notifications" && ( + + )} + {hash === "display" && ( + + )} + {hash === "s3" && ( + + )} + {hash === "leap" && ( + + )} + {hash === "calm" && ( + + )} + {hash === "security" && ( + + )} + + + + ); } diff --git a/pkg/interface/src/views/components/AsyncButton.tsx b/pkg/interface/src/views/components/AsyncButton.tsx index a181ef7842..bc1120b12e 100644 --- a/pkg/interface/src/views/components/AsyncButton.tsx +++ b/pkg/interface/src/views/components/AsyncButton.tsx @@ -1,8 +1,8 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect } from 'react'; -import { Button, LoadingSpinner } from "@tlon/indigo-react"; +import { Button, LoadingSpinner } from '@tlon/indigo-react'; -import { useFormikContext } from "formik"; +import { useFormikContext } from 'formik'; export function AsyncButton({ children, @@ -15,11 +15,11 @@ export function AsyncButton({ useEffect(() => { const s = status || {}; let done = false; - if ("success" in s) { + if ('success' in s) { setSuccess(true); onSuccess(); done = true; - } else if ("error" in s) { + } else if ('error' in s) { setSuccess(false); done = true; } @@ -40,13 +40,13 @@ export function AsyncButton({ > {isSubmitting ? ( ) : success === true ? ( - "Done" + 'Done' ) : success === false ? ( - "Errored" + 'Errored' ) : ( children )} diff --git a/pkg/interface/src/views/components/Author.tsx b/pkg/interface/src/views/components/Author.tsx index 8428121e4b..ec5a418457 100644 --- a/pkg/interface/src/views/components/Author.tsx +++ b/pkg/interface/src/views/components/Author.tsx @@ -1,13 +1,18 @@ -import React, { ReactNode, useState, useRef } from 'react'; +import React, { ReactElement, ReactNode, useState } from 'react'; import moment from 'moment'; +import { useHistory } from 'react-router-dom'; + import { Row, Box, BaseImage } from '@tlon/indigo-react'; -import { uxToHex, cite, useShowNickname } from '~/logic/lib/util'; -import { Contacts } from '~/types/contact-update'; +import { Contacts } from '@urbit/api/contacts'; +import { Group } from '@urbit/api'; + +import { uxToHex, cite, useShowNickname, deSig } from '~/logic/lib/util'; +import useSettingsState, {selectCalmState} from "~/logic/state/settings"; +import useLocalState from "~/logic/state/local"; import OverlaySigil from './OverlaySigil'; import { Sigil } from '~/logic/lib/sigil'; -import { Group } from '~/types'; import GlobalApi from '~/logic/api/global'; -import { useHistory } from 'react-router-dom'; +import Timestamp from './Timestamp'; interface AuthorProps { contacts: Contacts; @@ -17,35 +22,43 @@ interface AuthorProps { children?: ReactNode; unread?: boolean; group: Group; - api: GlobalApi; + api?: GlobalApi; } // eslint-disable-next-line max-lines-per-function -export default function Author(props: AuthorProps) { +export default function Author(props: AuthorProps): ReactElement { const { contacts, ship = '', date, showImage, group } = props; const history = useHistory(); + const osDark = useLocalState((state) => state.dark); + + const theme = useSettingsState(s => s.display.theme); + const dark = theme === 'dark' || (theme === 'auto' && osDark) + let contact; if (contacts) { - contact = ship in contacts ? contacts[ship] : null; + contact = `~${deSig(ship)}` in contacts ? contacts[`~${deSig(ship)}`] : null; } - const color = contact?.color ? `#${uxToHex(contact?.color)}` : '#000000'; + const color = contact?.color ? `#${uxToHex(contact?.color)}` : dark ? '#000000' : '#FFFFFF'; const showNickname = useShowNickname(contact); + const { hideAvatars } = useSettingsState(selectCalmState); const name = showNickname ? contact.nickname : cite(ship); - const dateFmt = moment(date).fromNow(); + const stamp = moment(date); const [showOverlay, setShowOverlay] = useState(false); const toggleOverlay = () => { - setShowOverlay((value) => !value); + setShowOverlay(value => !value); }; const img = - contact && contact.avatar !== null ? ( + contact?.avatar && !hideAvatars ? ( ) : ( @@ -82,9 +95,7 @@ export default function Author(props: AuthorProps) { > {name}
- - {dateFmt} - + {props.children} ); diff --git a/pkg/interface/src/views/components/Body.tsx b/pkg/interface/src/views/components/Body.tsx index 7449689fd0..2f28d7c8d2 100644 --- a/pkg/interface/src/views/components/Body.tsx +++ b/pkg/interface/src/views/components/Body.tsx @@ -1,11 +1,11 @@ -import React, { ReactNode } from "react"; +import React, { ReactNode } from 'react'; -import { Box } from "@tlon/indigo-react"; +import { Box } from '@tlon/indigo-react'; export function Body( props: { children: ReactNode } & Parameters[0] ) { - const { children, ...boxProps } = props; + const { children, border, ...boxProps } = props; return ( - {props.children} + {children} ); diff --git a/pkg/interface/src/views/components/ChipInput.tsx b/pkg/interface/src/views/components/ChipInput.tsx index e75934fffb..02471d817d 100644 --- a/pkg/interface/src/views/components/ChipInput.tsx +++ b/pkg/interface/src/views/components/ChipInput.tsx @@ -2,23 +2,23 @@ import React, { useCallback, useState, ReactNode, - SyntheticEvent, useEffect, useRef, -} from "react"; + ReactElement +} from 'react'; +import { useField } from 'formik'; +import Mousetrap from 'mousetrap'; + import { - Box, Label, Row, Col, StatelessTextInput as Input, ErrorLabel -} from "@tlon/indigo-react"; -import { useField } from "formik"; -import Mousetrap from "mousetrap"; -import * as Yup from "yup"; +} from '@tlon/indigo-react'; -function Chip(props: { children: ReactNode }) { + +function Chip(props: { children: ReactNode }): ReactElement { return ( ( + const [{ onBlur, value }, meta, { setValue }] = useField( id ); - const [newChip, setNextChip] = useState(""); + const [newChip, setNextChip] = useState(''); const onChange = useCallback( (e: any) => { setNextChip(e.target.value); @@ -57,7 +57,7 @@ export function ChipInput(props: ChipInputProps) { const addNewChip = useCallback(() => { setValue([...value, newChip]); - setNextChip(""); + setNextChip(''); }, [setValue, value, newChip, setNextChip]); const removeLastChip = useCallback(() => { @@ -70,18 +70,18 @@ export function ChipInput(props: ChipInputProps) { return () => {}; } const mousetrap = Mousetrap(inputRef.current); - mousetrap.bind("backspace", (e) => { + mousetrap.bind('backspace', (e) => { if (newChip.length === 0) { removeLastChip(); return false; } return true; }); - mousetrap.bind("tab", (e) => { + mousetrap.bind('tab', (e) => { addNewChip(); return false; }); - mousetrap.bind("space", (e) => { + mousetrap.bind('space', (e) => { if (props.breakOnSpace) { addNewChip(); return false; @@ -89,9 +89,9 @@ export function ChipInput(props: ChipInputProps) { return true; }); return () => { - mousetrap.unbind("tab"); - mousetrap.unbind("backspace"); - mousetrap.unbind("space"); + mousetrap.unbind('tab'); + mousetrap.unbind('backspace'); + mousetrap.unbind('space'); }; }, [inputRef.current, addNewChip, newChip]); @@ -128,7 +128,7 @@ export function ChipInput(props: ChipInputProps) { py="1" /> - + {meta.error} diff --git a/pkg/interface/src/views/components/ColorInput.tsx b/pkg/interface/src/views/components/ColorInput.tsx index 0ee279db14..0a4036e7c6 100644 --- a/pkg/interface/src/views/components/ColorInput.tsx +++ b/pkg/interface/src/views/components/ColorInput.tsx @@ -1,30 +1,32 @@ -import React from "react"; -import { useField } from "formik"; +import React, { FormEvent, ReactElement } from 'react'; +import { useField } from 'formik'; + import { Col, Label, Row, Box, ErrorLabel, - StatelessTextInput as Input, -} from "@tlon/indigo-react"; + StatelessTextInput as Input +} from '@tlon/indigo-react'; -import { uxToHex, hexToUx } from "~/logic/lib/util"; +import { hexToUx } from '~/logic/lib/util'; type ColorInputProps = Parameters[0] & { id: string; - label: string; + label?: string; + placeholder?: string; disabled?: boolean; }; export function ColorInput(props: ColorInputProps) { - const { id, label, caption, disabled, ...rest } = props; + const { id, placeholder, label, caption, disabled, ...rest } = props; const [{ value, onBlur }, meta, { setValue }] = useField(id); - const hex = value.replace('#', '').replace("0x","").replace(".", ""); - const padded = hex.padStart(6, "0"); + const hex = value.replace('#', '').replace('0x', '').replace('.', ''); + const padded = hex.padStart(6, '0'); - const onChange = (e: any) => { + const onChange = (e: FormEvent) => { let { value: newValue } = e.target as HTMLInputElement; newValue = newValue.replace('#', ''); const valid = newValue.match(/^(\d|[a-f]|[A-F]){0,6}$/); @@ -36,17 +38,17 @@ export function ColorInput(props: ColorInputProps) { setValue(result); }; - return ( - + {caption ? ( - diff --git a/pkg/interface/src/views/components/CommentInput.tsx b/pkg/interface/src/views/components/CommentInput.tsx index 99189efb7f..df61fc1618 100644 --- a/pkg/interface/src/views/components/CommentInput.tsx +++ b/pkg/interface/src/views/components/CommentInput.tsx @@ -1,15 +1,15 @@ -import React from "react"; -import * as Yup from "yup"; -import { Formik, FormikHelpers, Form, useFormikContext } from "formik"; -import { AsyncButton } from "./AsyncButton"; -import { ManagedTextAreaField as TextArea } from "@tlon/indigo-react"; +import React from 'react'; +import * as Yup from 'yup'; +import { Formik, FormikHelpers, Form, useFormikContext } from 'formik'; +import { AsyncButton } from './AsyncButton'; +import { ManagedTextAreaField as TextArea } from '@tlon/indigo-react'; interface FormSchema { comment: string; } const formSchema = Yup.object({ - comment: Yup.string().required("Comment can't be empty"), + comment: Yup.string().required('Comment can\'t be empty') }); interface CommentInputProps { @@ -25,7 +25,7 @@ interface CommentInputProps { const SubmitTextArea = (props) => { const { submitForm } = useFormikContext(); const onKeyDown = (e: KeyboardEvent) => { - if ((e.getModifierState("Control") || e.metaKey) && e.key === "Enter") { + if ((e.getModifierState('Control') || e.metaKey) && e.key === 'Enter') { submitForm(); } }; @@ -33,20 +33,22 @@ const SubmitTextArea = (props) => { }; export default function CommentInput(props: CommentInputProps) { - const initialValues: FormSchema = { comment: props.initial || "" }; - const label = props.label || "Add Comment"; - const loading = props.loadingText || "Commenting..."; + const initialValues: FormSchema = { comment: props.initial || '' }; + const label = props.label || 'Add Comment'; + const loading = props.loadingText || 'Commenting...'; return (
{label} diff --git a/pkg/interface/src/views/components/CommentItem.tsx b/pkg/interface/src/views/components/CommentItem.tsx index 4bd76a8ebe..a18905c7a8 100644 --- a/pkg/interface/src/views/components/CommentItem.tsx +++ b/pkg/interface/src/views/components/CommentItem.tsx @@ -1,13 +1,14 @@ -import React, { useState } from 'react'; -import { Link } from "react-router-dom"; -import { Contacts } from '~/types/contact-update'; -import GlobalApi from '~/logic/api/global'; -import { Box, Row, Text } from '@tlon/indigo-react'; +import React from 'react'; +import { Link } from 'react-router-dom'; import styled from 'styled-components'; + +import { Box, Row, Text } from '@tlon/indigo-react'; +import { Contacts } from '@urbit/api/contacts'; +import { GraphNode } from '@urbit/api/graph'; +import { Group } from '@urbit/api'; + +import GlobalApi from '~/logic/api/global'; import Author from '~/views/components/Author'; -import { GraphNode, TextContent } from '~/types/graph-update'; -import tokenizeMessage from '~/logic/lib/tokenizeMessage'; -import { Group } from '~/types'; import { MentionText } from '~/views/components/MentionText'; import { getLatestCommentRevision } from '~/logic/lib/publish'; @@ -28,9 +29,9 @@ interface CommentItemProps { group: Group; } -export function CommentItem(props: CommentItemProps) { +export function CommentItem(props: CommentItemProps): ReactElement { const { ship, contacts, name, api, comment, group } = props; - const [revNum, post] = getLatestCommentRevision(comment); + const [, post] = getLatestCommentRevision(comment); const disabled = props.pending || window.ship !== post?.author; const onDelete = async () => { @@ -39,7 +40,7 @@ export function CommentItem(props: CommentItemProps) { const commentIndexArray = (comment.post?.index || '/').split('/'); const commentIndex = commentIndexArray[commentIndexArray.length - 1]; - const updateUrl = `${props.baseUrl}/${commentIndex}` + const updateUrl = `${props.baseUrl}/${commentIndex}`; return ( diff --git a/pkg/interface/src/views/components/Comments.tsx b/pkg/interface/src/views/components/Comments.tsx index 51297020bb..6c6d135133 100644 --- a/pkg/interface/src/views/components/Comments.tsx +++ b/pkg/interface/src/views/components/Comments.tsx @@ -3,10 +3,10 @@ import bigInt from 'big-integer'; import { Col } from '@tlon/indigo-react'; import { CommentItem } from './CommentItem'; import CommentInput from './CommentInput'; -import { Contacts } from '~/types/contact-update'; +import { Contacts } from '@urbit/api/contacts'; import GlobalApi from '~/logic/api/global'; import { FormikHelpers } from 'formik'; -import { Group, GraphNode, Association } from '~/types'; +import { Group, GraphNode, Association } from '@urbit/api'; import { createPost, createBlankNodeWithChildPost } from '~/logic/api/graph'; import { getLatestCommentRevision } from '~/logic/lib/publish'; import tokenizeMessage from '~/logic/lib/tokenizeMessage'; diff --git a/pkg/interface/src/views/components/Dropdown.tsx b/pkg/interface/src/views/components/Dropdown.tsx index deb3ac96d0..75c17b32a6 100644 --- a/pkg/interface/src/views/components/Dropdown.tsx +++ b/pkg/interface/src/views/components/Dropdown.tsx @@ -4,20 +4,25 @@ import React, { useRef, useEffect, useCallback, -} from "react"; -import styled from "styled-components"; -import _ from "lodash"; -import { Box, Col } from "@tlon/indigo-react"; -import { useOutsideClick } from "~/logic/lib/useOutsideClick"; -import { useLocation } from "react-router-dom"; -import { Portal } from "./Portal"; -import { getRelativePosition, AlignY, AlignX } from "~/logic/lib/relativePosition"; + ReactElement +} from 'react'; +import styled from 'styled-components'; +import _ from 'lodash'; +import { useLocation } from 'react-router-dom'; + +import { Box } from '@tlon/indigo-react'; + +import { useOutsideClick } from '~/logic/lib/useOutsideClick'; +import { Portal } from './Portal'; +import { getRelativePosition, AlignY, AlignX } from '~/logic/lib/relativePosition'; interface DropdownProps { children: ReactNode; options: ReactNode; alignY: AlignY | AlignY[]; alignX: AlignX | AlignX[]; + offsetX?: number; + offsetY?: number; width?: string; dropWidth?: string; } @@ -33,8 +38,8 @@ const DropdownOptions = styled(Box)` transition-timing-function: ease; `; -export function Dropdown(props: DropdownProps) { - const { children, options } = props; +export function Dropdown(props: DropdownProps): ReactElement { + const { children, options, offsetX = 0, offsetY = 0 } = props; const dropdownRef = useRef(null); const anchorRef = useRef(null); const { pathname } = useLocation(); @@ -42,7 +47,7 @@ export function Dropdown(props: DropdownProps) { const [coords, setCoords] = useState({}); const updatePos = useCallback(() => { - const newCoords = getRelativePosition(anchorRef.current, props.alignX, props.alignY); + const newCoords = getRelativePosition(anchorRef.current, props.alignX, props.alignY, offsetX, offsetY); if(newCoords) { setCoords(newCoords); } @@ -81,14 +86,14 @@ export function Dropdown(props: DropdownProps) { }, []); return ( - + {children} {open && ( { // check if entry is exact match @@ -36,14 +37,14 @@ interface DropdownSearchExtraProps { disabled?: boolean; placeholder?: string; onChange?: (e: ChangeEvent) => void; - onBlur?: (e: any) => void; + onBlur?: (e: FocusEvent) => void; onFocus?: (e: FocusEvent) => void; } type DropdownSearchProps = PropFunc & DropdownSearchExtraProps; -export function DropdownSearch(props: DropdownSearchProps) { +export function DropdownSearch(props: DropdownSearchProps): ReactElement { const textarea = useRef(); const { candidates, @@ -54,13 +55,13 @@ export function DropdownSearch(props: DropdownSearchProps) { renderCandidate, disabled, placeholder, - onFocus = () => {}, - onChange = () => {}, - onBlur = () => {}, + onFocus = (): void => {}, + onChange = (): void => {}, + onBlur = (): void => {}, ...rest } = props; - const [query, setQuery] = useState(""); + const [query, setQuery] = useState(''); const exact = useCallback( (s: string) => { return isExact ? isExact(s) : undefined; @@ -77,7 +78,7 @@ export function DropdownSearch(props: DropdownSearchProps) { const handleSelect = useCallback( (c: C) => { - setQuery(""); + setQuery(''); onSelect(c); }, [setQuery, onSelect] @@ -96,14 +97,14 @@ export function DropdownSearch(props: DropdownSearchProps) { } const mousetrap = Mousetrap(textarea.current); - mousetrap.bind(["down", "tab"], next); - mousetrap.bind(["up", "shift+tab"], back); - mousetrap.bind("enter", onEnter); + mousetrap.bind(['down', 'tab'], next); + mousetrap.bind(['up', 'shift+tab'], back); + mousetrap.bind('enter', onEnter); return () => { - mousetrap.unbind(["down", "tab"]); - mousetrap.unbind(["up", "shift+tab"]); - mousetrap.unbind("enter"); + mousetrap.unbind(['down', 'tab']); + mousetrap.unbind(['up', 'shift+tab']); + mousetrap.unbind('enter'); }; }, [textarea.current, next, back, onEnter]); @@ -113,7 +114,7 @@ export function DropdownSearch(props: DropdownSearchProps) { search(e.target.value); setQuery(e.target.value); }, - [setQuery] + [search, onChange] ); const dropdown = useMemo(() => { diff --git a/pkg/interface/src/views/components/ErrorBoundary.tsx b/pkg/interface/src/views/components/ErrorBoundary.tsx index 5d36f7ab99..c166dfe6b9 100644 --- a/pkg/interface/src/views/components/ErrorBoundary.tsx +++ b/pkg/interface/src/views/components/ErrorBoundary.tsx @@ -13,7 +13,7 @@ class ErrorBoundary extends Component< history.listen((location, action) => { if (this.state.error) { this.setState({ - error: undefined, + error: undefined }); } }); @@ -26,7 +26,7 @@ class ErrorBoundary extends Component< render() { if (this.state.error) { - return () + return (); } return this.props.children; } diff --git a/pkg/interface/src/views/components/FormError.tsx b/pkg/interface/src/views/components/FormError.tsx index 6aa5cca1c4..ed10100a86 100644 --- a/pkg/interface/src/views/components/FormError.tsx +++ b/pkg/interface/src/views/components/FormError.tsx @@ -1,16 +1,16 @@ -import React from "react"; -import { useFormikContext } from "formik"; -import { ErrorLabel } from "@tlon/indigo-react"; -import {PropFunc} from "~/types/util"; +import React from 'react'; +import { useFormikContext } from 'formik'; +import { ErrorLabel } from '@tlon/indigo-react'; +import { PropFunc } from '~/types/util'; export function FormError(props: { message?: string } & PropFunc) { const { status } = useFormikContext(); const { message, ...rest } = props; - let s = status || {}; + const s = status || {}; const contents = message || s?.error; return ( - {contents} + {contents} ); } diff --git a/pkg/interface/src/views/components/FormSubmit.tsx b/pkg/interface/src/views/components/FormSubmit.tsx index ae481de856..6851c8618d 100644 --- a/pkg/interface/src/views/components/FormSubmit.tsx +++ b/pkg/interface/src/views/components/FormSubmit.tsx @@ -1,13 +1,13 @@ -import React, { useCallback, ReactNode } from "react"; -import { useFormikContext } from "formik"; -import { Row, Button } from "@tlon/indigo-react"; -import { AsyncButton } from "./AsyncButton"; +import React, { useCallback, ReactNode, ReactElement } 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) { +export function FormSubmit(props: FormSubmitProps): ReactElement { const { children } = props; const { initialValues, values, dirty, resetForm, isSubmitting } = useFormikContext(); @@ -19,7 +19,6 @@ export function FormSubmit(props: FormSubmitProps) { resetForm({ errors: {}, touched: {}, values: initialValues, status: {} }); }, [resetForm, initialValues]); - return ( void; detailed?: boolean; } & PropFunc -) { - const { resource, api, associations, groups, measure, ...rest } = props; +): ReactElement { + const { resource, api, associations, groups, ...rest } = props; const name = resource.slice(6); const [preview, setPreview] = useState(null); const joined = resource in props.associations.groups; + const { save, restore } = useVirtual(); + const { modal, showModal } = useModal({ modal: joined && preview ? ( @@ -31,7 +35,7 @@ export function GroupLink( ) : ( @@ -41,21 +45,24 @@ export function GroupLink( api={api} autojoin={name} /> - ), + ) }); useEffect(() => { (async () => { - setPreview(await api.metadata.preview(resource)); + const prev = await api.metadata.preview(resource); + save(); + setPreview(prev); })(); return () => { + save(); setPreview(null); }; }, [resource]); useLayoutEffect(() => { - measure(); + restore(); }, [preview]); return ( @@ -69,20 +76,15 @@ export function GroupLink( pr="2" onClick={showModal} cursor='pointer' + opacity={preview ? '1' : '0.6'} > - {preview ? ( - <> - - - - {preview.metadata.title} - - {preview.members} members - - - ) : ( - {name} - )} + + + + {preview ? preview.metadata.title : name} + + {preview ? `${preview.members} members` : "Fetching member count"} + ); diff --git a/pkg/interface/src/views/components/GroupSearch.tsx b/pkg/interface/src/views/components/GroupSearch.tsx index 4cc75b781d..fe887e17e7 100644 --- a/pkg/interface/src/views/components/GroupSearch.tsx +++ b/pkg/interface/src/views/components/GroupSearch.tsx @@ -1,4 +1,8 @@ -import React, { useMemo, useState } from 'react'; +import React, { ReactElement, useMemo, useState } from 'react'; +import { useFormikContext, FieldArray } from 'formik'; +import _ from 'lodash'; +import styled from 'styled-components'; + import { Box, Text, @@ -8,15 +12,12 @@ import { Icon, ErrorLabel } from '@tlon/indigo-react'; -import _ from 'lodash'; -import { useField, useFormikContext, FieldArray } from 'formik'; -import styled from 'styled-components'; +import { Groups } from '@urbit/api'; +import { Associations, Association } from '@urbit/api/metadata'; + import { roleForShip } from '~/logic/lib/group'; - import { DropdownSearch } from './DropdownSearch'; -import { Groups } from '~/types'; -import { Associations, Association } from '~/types/metadata-update'; interface GroupSearchProps { disabled?: boolean; @@ -36,10 +37,11 @@ const CandidateBox = styled(Box)<{ selected: boolean }>` } `; -const Candidate = ({ title, selected, onClick }) => ( +const Candidate = ({ title, selected, onClick }): ReactElement => ( void -) { +): ReactElement { const { title } = a.metadata; const onClick = () => { @@ -68,7 +70,7 @@ type FormValues = { [id in I]: string[]; }; -export function GroupSearch>(props: GroupSearchProps) { +export function GroupSearch>(props: GroupSearchProps): ReactElement { const { id, caption, label } = props; const { values, @@ -76,7 +78,7 @@ export function GroupSearch>(props: Gr errors, initialValues, setFieldValue, - setFieldTouched, + setFieldTouched } = useFormikContext(); const [inputIdx, setInputIdx] = useState(initialValues[id].length); const name = `${id}[${inputIdx}]`; @@ -144,7 +146,7 @@ export function GroupSearch>(props: Gr disabled={props.maxLength ? value.length >= props.maxLength : false} renderCandidate={renderCandidate} search={(s: string, a: Association) => - a.metadata.title.toLowerCase().startsWith(s.toLowerCase()) + a.metadata.title.toLowerCase().includes(s.toLowerCase()) } getKey={(a: Association) => a.group} onSelect={onSelect} @@ -177,7 +179,8 @@ export function GroupSearch>(props: Gr ); - }} /> + }} + /> ); } diff --git a/pkg/interface/src/views/components/HoverBox.tsx b/pkg/interface/src/views/components/HoverBox.tsx index d678c1e4d2..8159a68e3b 100644 --- a/pkg/interface/src/views/components/HoverBox.tsx +++ b/pkg/interface/src/views/components/HoverBox.tsx @@ -1,19 +1,19 @@ -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"; +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; bgActive: string; } export const HoverBox = styled(Box)` - background-color: ${(p) => + background-color: ${p => p.selected ? p.theme.colors[p.bgActive] : p.theme.colors[p.bg]}; pointer: cursor; &:hover { - background-color: ${(p) => p.theme.colors[p.bgActive]}; + background-color: ${p => p.theme.colors[p.bgActive]}; } `; diff --git a/pkg/interface/src/views/components/IconRadio.tsx b/pkg/interface/src/views/components/IconRadio.tsx index 912329e8f3..e58e76a803 100644 --- a/pkg/interface/src/views/components/IconRadio.tsx +++ b/pkg/interface/src/views/components/IconRadio.tsx @@ -1,15 +1,15 @@ -import React, { useCallback, useMemo } from "react"; -import styled from "styled-components"; +import React, { useCallback, useMemo } from 'react'; +import styled from 'styled-components'; +import { useField } from 'formik'; + import { Icon, Box, Row, BaseLabel, - Indicator, Col, - Label, -} from "@tlon/indigo-react"; -import { useField } from "formik"; + Label +} from '@tlon/indigo-react'; type IconRadioProps = Parameters[0] & { id: string; @@ -40,45 +40,50 @@ type IconIndicatorProps = Parameters & { const indicator = { state: { on: { - //"*": { fill: "white" }, - backgroundColor: "blue", - borderColor: "blue", + // "*": { fill: "white" }, + backgroundColor: 'blue', + borderColor: 'blue' }, off: { - //"*": { fill: "transparent" }, - backgroundColor: "white", - borderColor: "lightGray", + // "*": { fill: "transparent" }, + backgroundColor: 'white', + borderColor: 'lightGray' }, onError: { - //"*": { fill: "white" }, - backgroundColor: "red", - borderColor: "red", + // "*": { fill: "white" }, + backgroundColor: 'red', + borderColor: 'red' }, offError: { // "*": { fill: "transparent" }, - backgroundColor: "washedRed", - borderColor: "red", + backgroundColor: 'washedRed', + borderColor: 'red' }, offDisabled: { - //"*": { fill: "transparent" }, - backgroundColor: "washedGray", - borderColor: "lightGray", + // "*": { fill: "transparent" }, + backgroundColor: 'washedGray', + borderColor: 'lightGray' }, onDisabled: { - //"*": { fill: "lightGray" }, - backgroundColor: "washedGray", - borderColor: "lightGray", - }, - }, + // "*": { 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; + 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]); @@ -95,7 +100,7 @@ export function IconRadio(props: IconRadioProps) { name, id, value: id, - type: "radio", + type: 'radio' }); const onChange = useCallback( @@ -122,12 +127,12 @@ export function IconRadio(props: IconRadioProps) { > - + {caption ? ( diff --git a/pkg/interface/src/views/components/Invite/Group.tsx b/pkg/interface/src/views/components/Invite/Group.tsx index ea5c37b989..9345d1c78c 100644 --- a/pkg/interface/src/views/components/Invite/Group.tsx +++ b/pkg/interface/src/views/components/Invite/Group.tsx @@ -1,11 +1,11 @@ -import React, { ReactNode } from "react"; -import { Text, Box, Button, Icon, Row, Rule, Col } from "@tlon/indigo-react"; +import React, { ReactElement, ReactNode } from 'react'; +import { Text, Box, Icon, Row } from '@tlon/indigo-react'; -import { cite } from "~/logic/lib/util"; -import { MetadataUpdatePreview, JoinProgress, Invite } from "~/types"; -import { GroupSummary } from "~/views/landscape/components/GroupSummary"; -import { InviteSkeleton } from "./InviteSkeleton"; -import { JoinSkeleton } from "./JoinSkeleton"; +import { cite } from '~/logic/lib/util'; +import { MetadataUpdatePreview, JoinProgress, Invite } from '@urbit/api'; +import { GroupSummary } from '~/views/landscape/components/GroupSummary'; +import { InviteSkeleton } from './InviteSkeleton'; +import { JoinSkeleton } from './JoinSkeleton'; interface GroupInviteProps { preview: MetadataUpdatePreview; @@ -15,12 +15,12 @@ interface GroupInviteProps { onDecline: () => Promise; } -export function GroupInvite(props: GroupInviteProps) { +export function GroupInvite(props: GroupInviteProps): ReactElement { const { preview, invite, status, onAccept, onDecline } = props; const { metadata, members } = props.preview; let inner: ReactNode = null; - let Outer: (p: { children: ReactNode }) => JSX.Element = (p) => ( + let Outer: (p: { children: ReactNode }) => JSX.Element = p => ( <>{p.children} ); @@ -68,7 +68,7 @@ export function GroupInvite(props: GroupInviteProps) { gray metadata={metadata} memberCount={members} - channelCount={preview?.["channel-count"]} + channelCount={preview?.['channel-count']} /> diff --git a/pkg/interface/src/views/components/Invite/InviteSkeleton.tsx b/pkg/interface/src/views/components/Invite/InviteSkeleton.tsx index c1b4d3899b..bcf773f68e 100644 --- a/pkg/interface/src/views/components/Invite/InviteSkeleton.tsx +++ b/pkg/interface/src/views/components/Invite/InviteSkeleton.tsx @@ -1,8 +1,8 @@ -import React, { ReactNode } from "react"; -import { Text, Box, Button, Icon, Row, Rule, Col } from "@tlon/indigo-react"; +import React, { ReactElement, ReactNode } from 'react'; +import { Row, Rule, Col } from '@tlon/indigo-react'; -import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction"; -import { PropFunc } from "~/types"; +import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction'; +import { PropFunc } from '~/types'; export interface InviteSkeletonProps { onAccept: () => Promise; @@ -14,7 +14,7 @@ export interface InviteSkeletonProps { export function InviteSkeleton( props: InviteSkeletonProps & PropFunc -) { +): ReactElement { const { children, acceptDesc, diff --git a/pkg/interface/src/views/components/Invite/JoinSkeleton.tsx b/pkg/interface/src/views/components/Invite/JoinSkeleton.tsx index c3e39bd63f..c3e6874f3b 100644 --- a/pkg/interface/src/views/components/Invite/JoinSkeleton.tsx +++ b/pkg/interface/src/views/components/Invite/JoinSkeleton.tsx @@ -1,14 +1,15 @@ -import React, { ReactNode } from "react"; -import { Col, Row, SegmentedProgressBar, Text, Rule } from "@tlon/indigo-react"; -import { JoiningStatus } from "~/views/apps/notifications/joining"; -import { JoinProgress, PropFunc } from "~/types"; +import React, { ReactElement, ReactNode } from 'react'; +import { Col, Rule } from '@tlon/indigo-react'; +import { JoiningStatus } from '~/views/apps/notifications/joining'; +import { JoinProgress } from '@urbit/api'; +import { PropFunc } from '~/types/util'; type JoinSkeletonProps = { children: ReactNode; status: JoinProgress; } & PropFunc; -export function JoinSkeleton(props: JoinSkeletonProps) { +export function JoinSkeleton(props: JoinSkeletonProps): ReactElement { const { children, status, ...rest } = props; return ( <> diff --git a/pkg/interface/src/views/components/Invite/index.tsx b/pkg/interface/src/views/components/Invite/index.tsx index 4bdab74b5a..d57cdc1b85 100644 --- a/pkg/interface/src/views/components/Invite/index.tsx +++ b/pkg/interface/src/views/components/Invite/index.tsx @@ -1,25 +1,23 @@ -import React, { Component, useState, useEffect, useCallback, useMemo } from "react"; -import { Invite } from "~/types/invite-update"; -import { Text, Box, Button, Icon, Row, Rule, Col } from "@tlon/indigo-react"; -import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction"; -import { cite } from "~/logic/lib/util"; +import React, { useState, useEffect, useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; + import { MetadataUpdatePreview, Contacts, JoinRequests, - JoinProgress, Groups, - Associations, -} from "~/types"; -import GlobalApi from "~/logic/api/global"; -import { GroupSummary } from "~/views/landscape/components/GroupSummary"; -import { JoiningStatus } from "~/views/apps/notifications/joining"; -import { resourceFromPath } from "~/logic/lib/group"; -import { GroupInvite } from "./Group"; -import { InviteSkeleton } from "./InviteSkeleton"; -import { JoinSkeleton } from "./JoinSkeleton"; -import { useWaitForProps } from "~/logic/lib/useWaitForProps"; -import { useHistory } from "react-router-dom"; + Associations +} from '@urbit/api'; +import { Invite } from '@urbit/api/invite'; +import { Text, Icon, Row } from '@tlon/indigo-react'; + +import { cite } from '~/logic/lib/util'; +import GlobalApi from '~/logic/api/global'; +import { resourceFromPath } from '~/logic/lib/group'; +import { GroupInvite } from './Group'; +import { InviteSkeleton } from './InviteSkeleton'; +import { JoinSkeleton } from './JoinSkeleton'; +import { useWaitForProps } from '~/logic/lib/useWaitForProps'; interface InviteItemProps { invite?: Invite; @@ -46,6 +44,10 @@ export function InviteItem(props: InviteItemProps) { if (!(app && invite && uid)) { return; } + if(resource in props.groups) { + await api.invite.decline(app, uid); + return; + } api.groups.join(ship, name); await waiter(p => resource in p.pendingJoin); @@ -78,10 +80,10 @@ export function InviteItem(props: InviteItemProps) { await api.invite.decline(app, uid); }, [app, uid]); - const handlers = { onAccept: inviteAccept, onDecline: inviteDecline } + const handlers = { onAccept: inviteAccept, onDecline: inviteDecline }; useEffect(() => { - if (!app || app === "groups") { + if (!app || app === 'groups') { (async () => { setPreview(await api.metadata.preview(resource)); })(); @@ -102,7 +104,7 @@ export function InviteItem(props: InviteItemProps) { {...handlers} /> ); - } else if (invite && name.startsWith("dm--")) { + } else if (invite && name.startsWith('dm--')) { return ( ); - } else if (status && name.startsWith("dm--")) { + } else if (status && name.startsWith('dm--')) { return ( @@ -148,7 +150,7 @@ export function InviteItem(props: InviteItemProps) { ); } else if (status) { - const [, , ship, name] = resource.split("/"); + const [, , ship, name] = resource.split('/'); return ( diff --git a/pkg/interface/src/views/components/Loading.tsx b/pkg/interface/src/views/components/Loading.tsx index a11f5e09c8..85bbcc8bb1 100644 --- a/pkg/interface/src/views/components/Loading.tsx +++ b/pkg/interface/src/views/components/Loading.tsx @@ -1,17 +1,17 @@ -import React from "react"; -import { Text, Center, LoadingSpinner } from "@tlon/indigo-react"; +import React from 'react'; +import { Text, Center, LoadingSpinner } from '@tlon/indigo-react'; -import { Body } from "./Body"; +import { Body } from './Body'; interface LoadingProps { text?: string; } export function Loading({ text }: LoadingProps) { return ( - +
- {!!text && {text}} + {Boolean(text) && {text}}
); diff --git a/pkg/interface/src/views/components/MentionText.tsx b/pkg/interface/src/views/components/MentionText.tsx index 02bdf0c220..d6a0adb86e 100644 --- a/pkg/interface/src/views/components/MentionText.tsx +++ b/pkg/interface/src/views/components/MentionText.tsx @@ -1,7 +1,7 @@ import React, { useState, useCallback } from 'react'; import _ from 'lodash'; import { Text, Box } from '@tlon/indigo-react'; -import { Contact, Contacts, Content, Group } from '~/types'; +import { Contact, Contacts, Content, Group } from '@urbit/api'; import RichText from '~/views/components/RichText'; import { cite, useShowNickname, uxToHex } from '~/logic/lib/util'; import OverlaySigil from '~/views/components/OverlaySigil'; @@ -38,31 +38,31 @@ export function Mention(props: { group: Group; scrollWindow?: HTMLElement; ship: string; + first?: Boolean; }) { - const { contacts, ship, scrollWindow } = props; + const { contacts, ship, scrollWindow, first, ...rest } = props; let { contact } = props; - contact = contact?.color ? contact : contacts?.[ship]; + contact = contact?.color ? contact : contacts?.[`~${ship}`]; const history = useHistory(); const showNickname = useShowNickname(contact); const name = showNickname ? contact?.nickname : cite(ship); const group = props.group ?? { hidden: true }; const [showOverlay, setShowOverlay] = useState(false); - const toggleOverlay = useCallback( - () => { - setShowOverlay(value => !value); - }, - [showOverlay] - ); + const toggleOverlay = useCallback(() => { + setShowOverlay((value) => !value); + }, [showOverlay]); return ( - + toggleOverlay()} - mx='2px' - px='2px' + marginLeft={first? 0 : 1} + marginRight={1} + px={1} bg='washedBlue' color='blue' + fontSize={showNickname ? 1 : 0} mono={!showNickname} > {name} diff --git a/pkg/interface/src/views/components/ModalOverlay.tsx b/pkg/interface/src/views/components/ModalOverlay.tsx index fee8c578e8..9838fe1ad1 100644 --- a/pkg/interface/src/views/components/ModalOverlay.tsx +++ b/pkg/interface/src/views/components/ModalOverlay.tsx @@ -1,9 +1,9 @@ -import React, { useCallback, UIEvent, MouseEvent, useRef } from "react"; -import { Box } from "@tlon/indigo-react"; -import { PropFunc } from "~/types/util"; +import React, { useCallback, UIEvent, MouseEvent, useRef } from 'react'; +import { Box } from '@tlon/indigo-react'; +import { PropFunc } from '~/types/util'; interface ModalOverlayProps { - spacing: PropFunc["m"]; + spacing: PropFunc['m']; dismiss: () => void; } type Props = ModalOverlayProps & PropFunc; @@ -22,7 +22,7 @@ export const ModalOverlay = (props: Props) => { const onKeyDown = useCallback( (e: any) => { - if (e.key === "Escape") { + if (e.key === 'Escape') { props.dismiss(); e.stopPropagation(); } @@ -49,4 +49,4 @@ export const ModalOverlay = (props: Props) => { ); -} +}; diff --git a/pkg/interface/src/views/components/OverlaySigil.tsx b/pkg/interface/src/views/components/OverlaySigil.tsx index f2d031feb8..066e1de7d6 100644 --- a/pkg/interface/src/views/components/OverlaySigil.tsx +++ b/pkg/interface/src/views/components/OverlaySigil.tsx @@ -1,5 +1,5 @@ -import React, { useState, useRef, useEffect, PureComponent } from 'react'; -import { Contact, Group } from '~/types'; +import React, { useState, useRef, useEffect } from 'react'; +import { Contact, Group } from '@urbit/api'; import ProfileOverlay, { OVERLAY_HEIGHT } from './ProfileOverlay'; import { Box, ColProps } from '@tlon/indigo-react'; diff --git a/pkg/interface/src/views/components/Portal.tsx b/pkg/interface/src/views/components/Portal.tsx index 7bc5d51b6e..297168e901 100644 --- a/pkg/interface/src/views/components/Portal.tsx +++ b/pkg/interface/src/views/components/Portal.tsx @@ -1,10 +1,10 @@ -import { useEffect, ReactNode, useMemo } from "react"; -import { createPortal } from "react-dom"; +import { useEffect, ReactNode, useMemo } from 'react'; +import { createPortal } from 'react-dom'; export function Portal(props: { children: ReactNode }) { - const root = document.getElementById("portal-root"); + const root = document.getElementById('portal-root'); - const el = useMemo(() => document.createElement("div"), []); + const el = useMemo(() => document.createElement('div'), []); useEffect(() => { root?.appendChild(el); diff --git a/pkg/interface/src/views/components/ProfileOverlay.tsx b/pkg/interface/src/views/components/ProfileOverlay.tsx index a77f3737d0..283dad6247 100644 --- a/pkg/interface/src/views/components/ProfileOverlay.tsx +++ b/pkg/interface/src/views/components/ProfileOverlay.tsx @@ -1,6 +1,5 @@ import React, { PureComponent } from 'react'; - -import { Contact, Group } from '~/types'; +import { Contact, Group } from '@urbit/api'; import { cite, useShowNickname } from '~/logic/lib/util'; import { Sigil } from '~/logic/lib/sigil'; @@ -34,7 +33,10 @@ type ProfileOverlayProps = ColProps & { api: any; }; -class ProfileOverlay extends PureComponent { +class ProfileOverlay extends PureComponent< + ProfileOverlayProps, + Record +> { public popoverRef: React.Ref; constructor(props) { @@ -92,75 +94,102 @@ class ProfileOverlay extends PureComponent { const isOwn = window.ship === ship; - const img = contact?.avatar && !hideAvatars - ? - : ; + const img = + contact?.avatar && !hideAvatars ? ( + + ) : ( + + ); const showNickname = useShowNickname(contact, hideNicknames); return ( - - {(!isOwn) && ( - history.push(`/~landscape/dm/${ship}`)}/> + + {!isOwn && ( + history.push(`/~landscape/dm/${ship}`)} + /> )} history.push(`/~profile/~${ship}`)}> + alignSelf='center' + height='72px' + cursor='pointer' + onClick={() => history.push(`/~profile/~${ship}`)} + overflow='hidden' + borderRadius={2} + > {img} - - + + {showNickname ? contact?.nickname : cite(ship)} - { isOwn ? ( + {isOwn ? ( ) : ( - + {contact?.status ? contact.status : ''} - ) - } + )} ); diff --git a/pkg/interface/src/views/components/ProfileStatus.js b/pkg/interface/src/views/components/ProfileStatus.js index 007b2539b5..d0f617fecf 100644 --- a/pkg/interface/src/views/components/ProfileStatus.js +++ b/pkg/interface/src/views/components/ProfileStatus.js @@ -1,16 +1,11 @@ -import React, { - useState, - useCallback, - useEffect -} from 'react'; +import React, { useState, useCallback, useEffect } from 'react'; import { Row, Button, StatelessTextInput as Input, Text -} from "@tlon/indigo-react"; - +} from '@tlon/indigo-react'; export const ProfileStatus = (props) => { const { contact, ship, api, callback } = props; @@ -29,7 +24,7 @@ export const ProfileStatus = (props) => { }, [contact]); const editStatus = () => { - api.contacts.edit(ship, {status: _status}); + api.contacts.edit(ship, { status: _status }); setNotice('Success!'); setTimeout(() => { @@ -43,13 +38,13 @@ export const ProfileStatus = (props) => { return ( <> - + { if (evt.key === 'Enter') { editStatus(); @@ -60,7 +55,15 @@ export const ProfileStatus = (props) => { }} /> - {notice} + + {notice} + ); }; diff --git a/pkg/interface/src/views/components/RemoteContent.tsx b/pkg/interface/src/views/components/RemoteContent.tsx index c6f16c658b..f128a099d0 100644 --- a/pkg/interface/src/views/components/RemoteContent.tsx +++ b/pkg/interface/src/views/components/RemoteContent.tsx @@ -1,11 +1,13 @@ -import React, { PureComponent, Fragment } from 'react'; +import React, { Component, Fragment } from 'react'; import { BaseAnchor, BaseImage, Box, Button, Text } from '@tlon/indigo-react'; import { hasProvider } from 'oembed-parser'; import EmbedContainer from 'react-oembed-container'; import { withLocalState } from '~/logic/state/local'; import { RemoteContentPolicy } from '~/types/local-update'; +import { VirtualContextProps, withVirtual } from "~/logic/lib/virtualContext"; +import { IS_IOS } from '~/logic/lib/platform'; -interface RemoteContentProps { +type RemoteContentProps = VirtualContextProps & { url: string; text?: string; unfold?: boolean; @@ -17,33 +19,55 @@ interface RemoteContentProps { oembedProps?: any; textProps?: any; style?: any; - onLoad?(): void; } interface RemoteContentState { unfold: boolean; embed: any | undefined; + noCors: boolean; } const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i); const AUDIO_REGEX = new RegExp(/(mp3|wav|ogg)$/i); const VIDEO_REGEX = new RegExp(/(mov|mp4|ogv)$/i); -class RemoteContent extends PureComponent { + +class RemoteContent extends Component { private fetchController: AbortController | undefined; containerRef: HTMLDivElement | null = null; + private saving = false; constructor(props) { super(props); this.state = { unfold: props.unfold || false, - embed: undefined + embed: undefined, + noCors: false }; this.unfoldEmbed = this.unfoldEmbed.bind(this); this.loadOembed = this.loadOembed.bind(this); this.wrapInLink = this.wrapInLink.bind(this); + this.onError = this.onError.bind(this); + } + + save = () => { + console.log(`saving for: ${this.props.url}`); + if(this.saving) { + return; + } + this.saving = true; + this.props.save(); + }; + + restore = () => { + console.log(`restoring for: ${this.props.url}`); + this.saving = false; + this.props.restore(); } componentWillUnmount() { + if(this.saving) { + this.restore(); + } if (this.fetchController) { this.fetchController.abort(); } @@ -53,8 +77,35 @@ class RemoteContent extends PureComponent { + this.restore(); + }); + } + + + componentDidUpdate(prevProps, prevState) { + if(prevState.embed !== this.state.embed) { + //console.log('remotecontent: restoring'); + //prevProps.shiftLayout.restore(); + } + const { url } = this.props; + if(url !== prevProps.url && (IMAGE_REGEX.test(url) || AUDIO_REGEX.test(url) || VIDEO_REGEX.test(url))) { + this.save(); + }; + + } + + componentDidMount() { + } + + onLoad = () => { + window.requestAnimationFrame(() => { + const { restore } = this; + restore(); + }); + } loadOembed() { @@ -66,7 +117,8 @@ class RemoteContent extends PureComponent { this.setState({ embed: result }); }).catch((error) => { - if (error.name === 'AbortError') return; + if (error.name === 'AbortError') +return; this.setState({ embed: 'error' }); }); } @@ -81,11 +133,16 @@ class RemoteContent extends PureComponent + > {contents} ); } + onError(e: Event) { + this.restore(); + this.setState({ noCors: true }); + } + render() { const { remoteContentPolicy, @@ -99,9 +156,10 @@ class RemoteContent extends PureComponent {}, ...props } = this.props; + const { onLoad } = this; + const { noCors } = this.state; const isImage = IMAGE_REGEX.test(url); const isAudio = AUDIO_REGEX.test(url); const isVideo = VIDEO_REGEX.test(url); @@ -110,10 +168,12 @@ class RemoteContent extends PureComponent @@ -129,6 +189,7 @@ class RemoteContent extends PureComponent @@ -171,7 +232,7 @@ class RemoteContent extends PureComponent + > {this.state.unfold ? 'collapse' : 'expand'} : null} {this.state.embed && this.state.embed.html && this.state.unfold ? -
{ this.containerRef = el; }} - dangerouslySetInnerHTML={{__html: this.state.embed.html}}>
+
{ + this.onLoad(); + this.containerRef = el; +}} + dangerouslySetInnerHTML={{ __html: this.state.embed.html }} + >
: null}
@@ -203,4 +268,4 @@ class RemoteContent extends PureComponent ( return ; } - return {linkProps.children}; + return {linkProps.children}; }, linkReference: (linkProps) => { const linkText = String(linkProps.children[0].props.children); @@ -46,6 +46,18 @@ const RichText = React.memo(({ disableRemoteContent, ...props }) => ( } return linkText; }, + blockquote: (blockquoteProps) => { + return ( + + {blockquoteProps.children} + + ) + }, paragraph: (paraProps) => { return {paraProps.children}; } diff --git a/pkg/interface/src/views/components/SetStatusBarModal.js b/pkg/interface/src/views/components/SetStatusBarModal.js index 85f9769e71..8ef0f17f30 100644 --- a/pkg/interface/src/views/components/SetStatusBarModal.js +++ b/pkg/interface/src/views/components/SetStatusBarModal.js @@ -1,31 +1,18 @@ -import React, { - useState, - useEffect -} from 'react'; +import React, { useState, useEffect } from 'react'; -import { - Row, - Box, - Text -} from '@tlon/indigo-react'; +import { Row, Box, Text } from '@tlon/indigo-react'; import { SetStatus } from '~/views/apps/profile/components/SetStatus'; - export const SetStatusBarModal = (props) => { - const { - ship, - contact, - api, - ...rest - } = props; + const { ship, contact, api, isControl, ...rest } = props; const [modalShown, setModalShown] = useState(false); const handleKeyDown = (event) => { if (event.key === 'Escape') { setModalShown(false); } - } + }; useEffect(() => { window.addEventListener('keydown', handleKeyDown); @@ -40,28 +27,28 @@ export const SetStatusBarModal = (props) => { {modalShown && ( setModalShown(false)} > e.stopPropagation()} - display="flex" - alignItems="stretch" - flexDirection="column" + borderColor={['washedGray', 'washedGray']} + onClick={(e) => e.stopPropagation()} + display='flex' + alignItems='stretch' + flexDirection='column' > { api={api} callback={() => { setModalShown(false); - }} /> + }} + /> )} - setModalShown(true)}> - setModalShown(true)}> + + fontSize={1} + > Set Status ); -} - +}; diff --git a/pkg/interface/src/views/components/ShipSearch.tsx b/pkg/interface/src/views/components/ShipSearch.tsx index e4e6c4566d..d80c4ed09f 100644 --- a/pkg/interface/src/views/components/ShipSearch.tsx +++ b/pkg/interface/src/views/components/ShipSearch.tsx @@ -2,33 +2,28 @@ import React, { useMemo, useCallback, ChangeEvent, - useState, - SyntheticEvent, - useEffect, useRef, -} from "react"; + ReactElement +} from 'react'; +import _ from 'lodash'; +import ob from 'urbit-ob'; +import * as Yup from 'yup'; +import { FieldArray, useFormikContext } from 'formik'; + import { - Box, Label, Icon, Text, Row, Col, - ErrorLabel, -} from "@tlon/indigo-react"; -import _ from "lodash"; -import ob from "urbit-ob"; -import * as Yup from "yup"; -import { useField, FieldArray, useFormikContext } from "formik"; -import styled from "styled-components"; + ErrorLabel +} from '@tlon/indigo-react'; +import { Rolodex, Groups } from '@urbit/api'; -import { DropdownSearch } from "./DropdownSearch"; -import { Associations, Association } from "~/types/metadata-update"; -import { cite, deSig } from "~/logic/lib/util"; -import { Rolodex, Groups } from "~/types"; -import { HoverBox } from "./HoverBox"; -const INVALID_SHIP_ERR = "Invalid ship"; +import { DropdownSearch } from './DropdownSearch'; +import { cite, deSig } from '~/logic/lib/util'; +import { HoverBox } from './HoverBox'; interface InviteSearchProps { autoFocus?: boolean; @@ -42,27 +37,29 @@ interface InviteSearchProps { maxLength?: number; } -const getNicknameForShips = (groups: Groups, contacts: Rolodex) => { +const getNicknameForShips = (groups: Groups, contacts: Rolodex, selected: string[]): readonly [string[], Map] => { 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); + if(!selected.includes(member)) { + peerSet.add(member); + } } } - const groupContacts = contacts[path]; + const groupContacts = contacts; if (groupContacts) { const groupEntries = group.members.values(); for (const member of groupEntries) { - if (groupContacts[member]) { + if (groupContacts[`~${member}`]) { if (nicknames.has(member)) { - nicknames.get(member)?.push(groupContacts[member].nickname); + nicknames.get(member)?.push(groupContacts[`~${member}`].nickname); } else { - nicknames.set(member, [groupContacts[member].nickname]); + nicknames.set(member, [groupContacts[`~${member}`].nickname]); } } } @@ -71,7 +68,7 @@ const getNicknameForShips = (groups: Groups, contacts: Rolodex) => { return [Array.from(peerSet), nicknames] as const; }; -const Candidate = ({ title, detail, selected, onClick }) => ( +const Candidate = ({ title, detail, selected, onClick }): ReactElement => ( ( bg="white" color="black" fontSize={0} + cursor="pointer" p={1} width="100%" > @@ -96,44 +94,44 @@ type Value = { }; const shipItemSchema = Yup.string().test( - "is-patp", - "${value} is not a valid @p", + 'is-patp', + '${value} is not a valid @p', x => ob.isValidPatp(`~${x}`) ); 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(); + Yup.array(shipItemSchema.oneOf(members, '${value} not a member of this group')).compact(); export function ShipSearch>( props: InviteSearchProps -) { +): ReactElement { const { id, label, caption } = props; const { values, touched, errors, initialValues, - setFieldValue, + setFieldValue } = useFormikContext(); const inputIdx = useRef(initialValues[id].length); - const selected: string[] = values[id] ?? []; + const selected: string[] = useMemo(() => values[id] ?? [], [values, id]); const name = () => `${props.id}[${inputIdx.current}]`; const pills = selected.slice(0, inputIdx.current); const [peers, nicknames] = useMemo( - () => getNicknameForShips(props.groups, props.contacts), - [props.contacts, props.groups] + () => getNicknameForShips(props.groups, props.contacts, selected), + [props.contacts, props.groups, selected] ); const renderCandidate = useCallback( (s: string, selected: boolean, onSelect: (s: string) => void) => { - const detail = _.uniq(nicknames.get(s)).join(", "); + const detail = _.uniq(nicknames.get(s)).join(', '); const onClick = () => { onSelect(s); }; @@ -152,7 +150,7 @@ export function ShipSearch>( const onChange = (e: ChangeEvent) => { const newValue = - e.target.value?.length > 0 ? `~${deSig(e.target.value)}` : ""; + e.target.value?.length > 0 ? `~${deSig(e.target.value)}` : ''; setFieldValue(name(), newValue); }; @@ -165,7 +163,7 @@ export function ShipSearch>( const onAdd = (ship: string) => { setFieldValue(name(), ship); inputIdx.current += 1; - arrayHelpers.push(""); + arrayHelpers.push(''); }; const onRemove = (idx: number) => { @@ -196,7 +194,7 @@ export function ShipSearch>( props.maxLength ? selected.length >= props.maxLength : false } search={(s: string, t: string) => - (t || "").toLowerCase().startsWith(s.toLowerCase()) + (t || '').toLowerCase().startsWith(s.toLowerCase()) } getKey={(s: string) => s} onChange={onChange} @@ -227,7 +225,7 @@ export function ShipSearch>( ))}
0}> - {error.join(", ")} + {error.join(', ')} ); diff --git a/pkg/interface/src/views/components/ShuffleFields.tsx b/pkg/interface/src/views/components/ShuffleFields.tsx new file mode 100644 index 0000000000..2f991a17b2 --- /dev/null +++ b/pkg/interface/src/views/components/ShuffleFields.tsx @@ -0,0 +1,56 @@ +import React, { ReactNode, useMemo, useCallback } from "react"; + +import { + FieldArray, + FieldArrayRenderProps, + Field, + useFormikContext, +} from "formik"; +import { Icon, Col, Row, Box } from "@tlon/indigo-react"; + +interface ShuffleFieldsProps { + name: N; + children: (index: number, props: FieldArrayRenderProps) => ReactNode; +} + +type Value = { + [k in I]: T[]; +}; + +export function ShuffleFields>( + props: ShuffleFieldsProps +) { + const { name, children } = props; + const { values } = useFormikContext(); + const fields: T[] = useMemo(() => values[name], [values, name]); + + return ( + { + const goUp = (i: number) => () => { + if(i > 0) { + arrayHelpers.swap(i - 1, i); + } + }; + const goDown = (i: number) => () => { + if(i < fields.length - 1) { + arrayHelpers.swap(i + 1, i); + + } + }; + return ( + + {fields.map((field, i) => ( + + + + {children(i, arrayHelpers)} + + ))} + + ); + }} + /> + ); +} diff --git a/pkg/interface/src/views/components/StarIcon.tsx b/pkg/interface/src/views/components/StarIcon.tsx new file mode 100644 index 0000000000..6bbd07bffa --- /dev/null +++ b/pkg/interface/src/views/components/StarIcon.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import css, { SystemStyleObject } from "@styled-system/css"; +import styled from "styled-components"; +import { BaseSVG } from "@tlon/indigo-react"; +import { PropFunc } from "~/types"; + +type StarIconProps = PropFunc & SvgProps; + +interface SvgProps { + color?: string; +} +const Svg = styled(BaseSVG)(({ color }: SvgProps) => + css({ + "& > *": { + fill: typeof color === "undefined" ? "inherit" : color || "black", + }, + flexShrink: 0, + } as SystemStyleObject) +); + +export function StarIcon(props: StarIconProps) { + return ( + + + + ); +} diff --git a/pkg/interface/src/views/components/StatelessAsyncAction.tsx b/pkg/interface/src/views/components/StatelessAsyncAction.tsx index be46d4de13..68ab25f63a 100644 --- a/pkg/interface/src/views/components/StatelessAsyncAction.tsx +++ b/pkg/interface/src/views/components/StatelessAsyncAction.tsx @@ -1,7 +1,7 @@ -import React, { ReactNode } from "react"; +import React, { ReactNode } from 'react'; import { useStatelessAsyncClickable } from '~/logic/lib/useStatelessAsyncClickable'; -import { LoadingSpinner, Action } from "@tlon/indigo-react"; +import { LoadingSpinner, Action } from '@tlon/indigo-react'; interface AsyncActionProps { children: ReactNode; @@ -19,7 +19,7 @@ export function StatelessAsyncAction({ }: AsyncActionProps & Parameters[0]) { const { onClick: handleClick, - buttonState: state, + buttonState: state } = useStatelessAsyncClickable(onClick, name); return ( @@ -27,16 +27,17 @@ export function StatelessAsyncAction({ height="18px" hideDisabled={!disabled} disabled={disabled || state === 'loading'} - onClick={handleClick} {...rest}> - {state === "error" ? ( - "Error" - ) : state === "loading" ? ( + onClick={handleClick} {...rest} + > + {state === 'error' ? ( + 'Error' + ) : state === 'loading' ? ( - ) : state === "success" ? ( - "Done" + ) : state === 'success' ? ( + 'Done' ) : ( children )} diff --git a/pkg/interface/src/views/components/StatelessAsyncButton.tsx b/pkg/interface/src/views/components/StatelessAsyncButton.tsx index faf933ed34..68c27d2263 100644 --- a/pkg/interface/src/views/components/StatelessAsyncButton.tsx +++ b/pkg/interface/src/views/components/StatelessAsyncButton.tsx @@ -1,9 +1,8 @@ -import React, { ReactNode, useState, useEffect, useCallback } from "react"; +import React, { ReactElement, ReactNode } from 'react'; -import { Button, LoadingSpinner } from "@tlon/indigo-react"; -import { useFormikContext } from "formik"; +import { Button, LoadingSpinner } from '@tlon/indigo-react'; -import { useStatelessAsyncClickable } from "~/logic/lib/useStatelessAsyncClickable"; +import { useStatelessAsyncClickable } from '~/logic/lib/useStatelessAsyncClickable'; interface AsyncButtonProps { children: ReactNode; @@ -14,33 +13,33 @@ interface AsyncButtonProps { export function StatelessAsyncButton({ children, onClick, - name = "", + name = '', disabled = false, ...rest -}: AsyncButtonProps & Parameters[0]) { +}: AsyncButtonProps & Parameters[0]): ReactElement { const { onClick: handleClick, - buttonState: state, + buttonState: state } = useStatelessAsyncClickable(onClick, name); return ( - + { isAdmin && ( diff --git a/pkg/interface/src/views/landscape/components/GroupSettings/Personal.tsx b/pkg/interface/src/views/landscape/components/GroupSettings/Personal.tsx index 5d262d6b28..cf32e156a3 100644 --- a/pkg/interface/src/views/landscape/components/GroupSettings/Personal.tsx +++ b/pkg/interface/src/views/landscape/components/GroupSettings/Personal.tsx @@ -1,41 +1,22 @@ -import React, { useCallback } from "react"; +import React from 'react'; -import { AsyncButton } from "~/views/components/AsyncButton"; -import * as Yup from "yup"; import { - Box, - ManagedTextInputField as Input, - ManagedToggleSwitchField as Toggle, Col, Label, - Button, - LoadingSpinner, BaseLabel, - Anchor, - BaseAnchor -} from "@tlon/indigo-react"; -import { Group, GroupPolicy } from "~/types/group-update"; -import { Enc } from "~/types/noun"; -import { Association } from "~/types/metadata-update"; -import GlobalApi from "~/logic/api/global"; -import { resourceFromPath, roleForShip } from "~/logic/lib/group"; -import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton"; -import { ColorInput } from "~/views/components/ColorInput"; -import { useHistory } from "react-router-dom"; - -import { uxToHex } from "~/logic/lib/util"; -import { FormikOnBlur } from "~/views/components/FormikOnBlur"; -import {GroupNotificationsConfig} from "~/types"; -import {StatelessAsyncToggle} from "~/views/components/StatelessAsyncToggle"; - + Text +} from '@tlon/indigo-react'; +import { GroupNotificationsConfig } from '@urbit/api'; +import { Association } from '@urbit/api/metadata'; +import GlobalApi from '~/logic/api/global'; +import { StatelessAsyncToggle } from '~/views/components/StatelessAsyncToggle'; export function GroupPersonalSettings(props: { api: GlobalApi; association: Association; notificationsGroupConfig: GroupNotificationsConfig; }) { - const groupPath = props.association.group; const watching = props.notificationsGroupConfig.findIndex(g => g === groupPath) !== -1; @@ -47,8 +28,8 @@ export function GroupPersonalSettings(props: { return ( - Group Notifications - Group Notifications
+ ) { +export function GroupSummary(props: GroupSummaryProps & PropFunc): ReactElement { const { channelCount, memberCount, metadata, resource, children, ...rest } = props; const anchorRef = useRef(null); useTutorialModal( - "group-desc", + 'group-desc', resource === `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`, - anchorRef.current + anchorRef ); return ( ) { fontSize="1" textOverflow="ellipsis" whiteSpace="nowrap" - overflow="hidden">{metadata.title} + overflow="hidden" + >{metadata.title} {memberCount} participants @@ -57,7 +55,8 @@ export function GroupSummary(props: GroupSummaryProps & PropFunc) { width="100%" fontSize="1" textOverflow="ellipsis" - overflow="hidden"> + overflow="hidden" + > {metadata.description} } diff --git a/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx b/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx index 769963f984..245d639708 100644 --- a/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx +++ b/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx @@ -9,11 +9,11 @@ import { import { uxToHex } from '~/logic/lib/util'; import { Link } from 'react-router-dom'; -import { Associations } from '~/types/metadata-update'; +import { Associations } from '@urbit/api/metadata'; import { Dropdown } from '~/views/components/Dropdown'; -import { Workspace } from '~/types'; import { getTitleFromWorkspace } from '~/logic/lib/workspace'; -import {MetadataIcon} from './MetadataIcon'; +import { MetadataIcon } from './MetadataIcon'; +import { Workspace } from '~/types/workspace'; const GroupSwitcherItem = ({ to, children, bottom = false, ...rest }) => ( @@ -93,7 +93,8 @@ export function GroupSwitcher(props: { top="0px" pl='3' borderBottom='1px solid' - borderColor='washedGray'> + borderColor='washedGray' + > - { metadata && } + { metadata && } {title} diff --git a/pkg/interface/src/views/landscape/components/GroupifyForm.tsx b/pkg/interface/src/views/landscape/components/GroupifyForm.tsx index 6e8424a2b4..7fa3bc5a0b 100644 --- a/pkg/interface/src/views/landscape/components/GroupifyForm.tsx +++ b/pkg/interface/src/views/landscape/components/GroupifyForm.tsx @@ -1,16 +1,17 @@ -import React, { useEffect } from "react"; -import { Box, Col, Text } from "@tlon/indigo-react"; -import * as Yup from "yup"; -import GlobalApi from "~/logic/api/global"; +import React from 'react'; +import { Box, Col, Text } from '@tlon/indigo-react'; +import * as Yup from 'yup'; +import { Formik, FormikHelpers, Form } from 'formik'; +import { useHistory } from 'react-router-dom'; -import { Groups, Associations, Association } from "~/types"; -import { Formik, FormikHelpers, Form } from "formik"; -import GroupSearch from "~/views/components/GroupSearch"; -import { AsyncButton } from "~/views/components/AsyncButton"; -import {useHistory} from "react-router-dom"; +import { Groups, Associations, Association } from '@urbit/api'; + +import GlobalApi from '~/logic/api/global'; +import GroupSearch from '~/views/components/GroupSearch'; +import { AsyncButton } from '~/views/components/AsyncButton'; const formSchema = Yup.object({ - group: Yup.string().nullable(), + group: Yup.string().nullable() }); interface FormSchema { @@ -58,7 +59,7 @@ export function GroupifyForm(props: GroupifyFormProps) { } const initialValues: FormSchema = { - group: null, + group: null }; return ( diff --git a/pkg/interface/src/views/landscape/components/GroupsPane.tsx b/pkg/interface/src/views/landscape/components/GroupsPane.tsx index 6520fb4ac3..bdd43dc522 100644 --- a/pkg/interface/src/views/landscape/components/GroupsPane.tsx +++ b/pkg/interface/src/views/landscape/components/GroupsPane.tsx @@ -1,33 +1,32 @@ -import React, { useState, useEffect, ReactNode } from "react"; +import React, { useEffect, ReactNode } from 'react'; import { Switch, Route, - useLocation, - RouteComponentProps, -} from "react-router-dom"; -import { Col, Box, Text } from "@tlon/indigo-react"; -import _ from "lodash"; + RouteComponentProps +} from 'react-router-dom'; +import { Col, Box, Text } from '@tlon/indigo-react'; +import _ from 'lodash'; import Helmet from 'react-helmet'; -import { Resource } from "./Resource"; -import { PopoverRoutes } from "./PopoverRoutes"; -import { Skeleton } from "./Skeleton"; -import { InvitePopover } from "./InvitePopover"; -import { NewChannel } from "./NewChannel"; +import { AppName } from '@urbit/api'; -import { appIsGraph } from "~/logic/lib/util"; -import { AppName } from "~/types/noun"; -import GlobalApi from "~/logic/api/global"; -import { StoreState } from "~/logic/store/type"; -import { UnjoinedResource } from "~/views/components/UnjoinedResource"; -import { useLocalStorageState } from "~/logic/lib/useLocalStorageState"; +import { Resource } from './Resource'; +import { PopoverRoutes } from './PopoverRoutes'; +import { Skeleton } from './Skeleton'; +import { InvitePopover } from './InvitePopover'; +import { NewChannel } from './NewChannel'; + +import GlobalApi from '~/logic/api/global'; +import { StoreState } from '~/logic/store/type'; +import { UnjoinedResource } from '~/views/components/UnjoinedResource'; +import { useLocalStorageState } from '~/logic/lib/useLocalStorageState'; import { Loading } from '~/views/components/Loading'; -import "~/views/apps/links/css/custom.css"; -import "~/views/apps/publish/css/custom.css"; -import { Workspace } from "~/types"; -import { getGroupFromWorkspace } from "~/logic/lib/workspace"; -import {GroupSummary} from "./GroupSummary"; +import '~/views/apps/links/css/custom.css'; +import '~/views/apps/publish/css/custom.css'; +import { getGroupFromWorkspace } from '~/logic/lib/workspace'; +import { GroupSummary } from './GroupSummary'; +import { Workspace } from '~/types/workspace'; type GroupsPaneProps = StoreState & { baseUrl: string; @@ -48,15 +47,15 @@ export function GroupsPane(props: GroupsPaneProps) { (groupPath && associations.groups[groupPath]) || undefined; const group = (groupPath && groups[groupPath]) || undefined; const [recentGroups, setRecentGroups] = useLocalStorageState( - "recent-groups", + 'recent-groups', [] ); useEffect(() => { - if (workspace.type !== "group") { + if (workspace.type !== 'group') { return; } - setRecentGroups((gs) => _.uniq([workspace.group, ...gs])); + setRecentGroups(gs => _.uniq([workspace.group, ...gs])); }, [workspace]); if (!(associations && (groupPath ? groupPath in groups : true))) { @@ -71,13 +70,13 @@ export function GroupsPane(props: GroupsPaneProps) { association={groupAssociation!} group={group!} api={api} - s3={props.s3} + storage={props.storage} notificationsGroupConfig={props.notificationsGroupConfig} associations={associations} {...routeProps} baseUrl={baseUrl} - />)} + />)} - ) + ); return ( { const { app, host, name } = routeProps.match.params as Record< string, @@ -102,7 +101,7 @@ export function GroupsPane(props: GroupsPaneProps) { const appName = app as AppName; const resource = `/ship/${host}/${name}`; - const association = associations.graph[resource] + const association = associations.graph[resource]; const resourceUrl = `${baseUrl}/resource/${app}${resource}`; if (!association) { @@ -130,7 +129,7 @@ export function GroupsPane(props: GroupsPaneProps) { }} /> { const { app, host, name } = routeProps.match.params; const appPath = `/ship/${host}/${name}`; @@ -170,7 +169,7 @@ export function GroupsPane(props: GroupsPaneProps) { }} /> { const newUrl = `${baseUrl}/new`; return ( @@ -191,10 +190,10 @@ export function GroupsPane(props: GroupsPaneProps) { }} /> { const hasDescription = groupAssociation?.metadata?.description; - const channelCount = Object.keys(props?.associations?.graph ?? {}).filter(e => { + const channelCount = Object.keys(props?.associations?.graph ?? {}).filter((e) => { return props?.associations?.graph?.[e]?.['group'] === groupPath; }).length; let summary: ReactNode; @@ -205,13 +204,11 @@ export function GroupsPane(props: GroupsPaneProps) { channelCount={channelCount} metadata={groupAssociation.metadata} resource={groupAssociation.group} - /> + />; } else { - summary = ( + summary = ( Create or select a channel to get started ); - - } const title = groupAssociation?.metadata?.title ?? 'Landscape'; return ( @@ -223,7 +220,7 @@ export function GroupsPane(props: GroupsPaneProps) { {summary} diff --git a/pkg/interface/src/views/landscape/components/InvitePopover.tsx b/pkg/interface/src/views/landscape/components/InvitePopover.tsx index 72dc13d93b..de02b7588e 100644 --- a/pkg/interface/src/views/landscape/components/InvitePopover.tsx +++ b/pkg/interface/src/views/landscape/components/InvitePopover.tsx @@ -1,26 +1,26 @@ -import React, { useCallback, useRef, useMemo } from "react"; +import React, { useCallback, useRef } from 'react'; import _ from 'lodash'; -import { Switch, Route, useHistory } from "react-router-dom"; -import { Formik, Form } from "formik"; +import { Switch, Route, useHistory } from 'react-router-dom'; +import { Formik, Form } from 'formik'; import * as Yup from 'yup'; import { ManagedTextInputField as Input, Box, Text, Col, - Button, Row -} from "@tlon/indigo-react"; +} from '@tlon/indigo-react'; -import { ShipSearch } from "~/views/components/ShipSearch"; -import { Association } from "~/types/metadata-update"; -import { AsyncButton } from "~/views/components/AsyncButton"; -import { useOutsideClick } from "~/logic/lib/useOutsideClick"; -import { FormError } from "~/views/components/FormError"; -import { resourceFromPath } from "~/logic/lib/group"; -import GlobalApi from "~/logic/api/global"; -import { Groups, Rolodex, Workspace } from "~/types"; -import { deSig } from "~/logic/lib/util"; +import { ShipSearch } from '~/views/components/ShipSearch'; +import { Association } from '@urbit/api/metadata'; +import { AsyncButton } from '~/views/components/AsyncButton'; +import { useOutsideClick } from '~/logic/lib/useOutsideClick'; +import { FormError } from '~/views/components/FormError'; +import { resourceFromPath } from '~/logic/lib/group'; +import GlobalApi from '~/logic/api/global'; +import { Groups, Rolodex } from '@urbit/api'; +import { deSig } from '~/logic/lib/util'; +import { Workspace } from '~/types/workspace'; interface InvitePopoverProps { baseUrl: string; @@ -38,15 +38,15 @@ interface FormSchema { } const formSchema = Yup.object({ - emails: Yup.array(Yup.string().email("Invalid email")), - ships: Yup.array(Yup.string()).min(1, "Must invite at least one ship") + emails: Yup.array(Yup.string().email('Invalid email')), + ships: Yup.array(Yup.string()).min(1, 'Must invite at least one ship') }); export function InvitePopover(props: InvitePopoverProps) { const { baseUrl, api, association } = props; const relativePath = (p: string) => baseUrl + p; - const { title } = association?.metadata || ""; + const { title } = association?.metadata || ''; const innerRef = useRef(null); const history = useHistory(); @@ -75,10 +75,9 @@ export function InvitePopover(props: InvitePopoverProps) { const initialValues: FormSchema = { ships: [], emails: [], description: '' }; - return ( - + { + .required('Must provide group to join') + .test('is-valid', 'Invalid group', (group: string | null | undefined) => { if (!group) { return false; } - const [patp, name] = group.split("/"); + const [patp, name] = group.split('/'); return urbitOb.isValidPatp(patp) && name.length > 0; - }), + }) }); interface FormSchema { @@ -60,27 +59,29 @@ function Autojoin(props: { autojoin: string | null }) { return null; } -export function JoinGroup(props: JoinGroupProps) { +export function JoinGroup(props: JoinGroupProps): ReactElement { const { api, autojoin, associations, groups } = props; const history = useHistory(); const initialValues: FormSchema = { - group: autojoin || "", + group: autojoin || '' }; const [preview, setPreview] = useState< MetadataUpdatePreview | string | null >(null); - const waiter = useWaitForProps(props, _.isString(preview) ? 1 : 5000); const onConfirm = useCallback(async (group: string) => { const [,,ship,name] = group.split('/'); + if(group === TUTORIAL_GROUP_RESOURCE) { + await api.settings.putEntry('tutorial', 'joined', Date.now()); + } await api.groups.join(ship, name); try { await waiter((p: JoinGroupProps) => { - return group in p.groups && + return group in p.groups && (group in (p.associations?.graph ?? {}) - || group in (p.associations?.groups ?? {})) + || group in (p.associations?.groups ?? {})); }); if(props.groups?.[group]?.hidden) { @@ -98,7 +99,7 @@ export function JoinGroup(props: JoinGroupProps) { const onSubmit = useCallback( async (values: FormSchema, actions: FormikHelpers) => { - const [ship, name] = values.group.split("/"); + const [ship, name] = values.group.split('/'); const path = `/ship/${ship}/${name}`; // skip if it's unmanaged try { @@ -107,13 +108,13 @@ export function JoinGroup(props: JoinGroupProps) { setPreview(prev); } catch (e) { if (!(e instanceof Error)) { - actions.setStatus({ error: "Unknown error" }); - } else if (e.message === "no-permissions") { + actions.setStatus({ error: 'Unknown error' }); + } else if (e.message === 'no-permissions') { actions.setStatus({ error: - "Unable to join group, you do not have the correct permissions", + 'Unable to join group, you do not have the correct permissions' }); - } else if (e.message === "offline") { + } else if (e.message === 'offline') { setPreview(path); } } @@ -131,8 +132,8 @@ export function JoinGroup(props: JoinGroupProps) { {_.isString(preview) ? ( The host appears to be offline. Join anyway? - onConfirm(preview)} > @@ -173,7 +174,7 @@ export function JoinGroup(props: JoinGroupProps) { )} - onConfirm(preview.group)} @@ -188,7 +189,7 @@ export function JoinGroup(props: JoinGroupProps) { initialValues={initialValues} onSubmit={onSubmit} > - + & { metadata: Metadata; @@ -15,7 +15,7 @@ export function MetadataIcon(props: MetadataIconProps) { const bgColor = metadata.picture ? {} : { bg: `#${uxToHex(metadata.color)}` }; return ( - + {metadata.picture && } ); diff --git a/pkg/interface/src/views/landscape/components/NewChannel.tsx b/pkg/interface/src/views/landscape/components/NewChannel.tsx index 900210ac99..cf82256d9c 100644 --- a/pkg/interface/src/views/landscape/components/NewChannel.tsx +++ b/pkg/interface/src/views/landscape/components/NewChannel.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { ReactElement } from 'react'; import { Box, ManagedTextInputField as Input, @@ -14,13 +14,14 @@ import { FormError } from '~/views/components/FormError'; import { RouteComponentProps } from 'react-router-dom'; import { stringToSymbol, parentPath, deSig } from '~/logic/lib/util'; import { resourceFromPath } from '~/logic/lib/group'; -import { Associations } from '~/types/metadata-update'; +import { Associations } from '@urbit/api/metadata'; import { useWaitForProps } from '~/logic/lib/useWaitForProps'; -import { Groups } from '~/types/group-update'; +import { Groups } from '@urbit/api/groups'; import { ShipSearch, shipSearchSchemaInGroup, shipSearchSchema } from '~/views/components/ShipSearch'; -import { Rolodex, Workspace } from '~/types'; +import { Rolodex } from '@urbit/api'; import { IconRadio } from '~/views/components/IconRadio'; import { ChannelWriteFieldSchema, ChannelWritePerms } from './ChannelWritePerms'; +import { Workspace } from '~/types/workspace'; type FormSchema = { name: string; @@ -47,7 +48,7 @@ interface NewChannelProps { workspace: Workspace; } -export function NewChannel(props: NewChannelProps & RouteComponentProps) { +export function NewChannel(props: NewChannelProps & RouteComponentProps): ReactElement { const { history, api, group, workspace, groups } = props; const waiter = useWaitForProps(props, 5000); @@ -59,7 +60,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) { : ''); try { let { description, moduleType, ships, writers } = values; - ships = ships.filter(e => e !== ""); + ships = ships.filter(e => e !== ''); if (workspace?.type === 'messages' && ships.length === 1) { return history.push(`/~landscape/dm/${deSig(ships[0])}`); } @@ -114,10 +115,14 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) { return ( - history.push(props.baseUrl)}> - {'<- Back'} + history.push(props.baseUrl)} + > + {'<- Back'} - + {workspace?.type === 'messages' ? 'Direct Message' : 'New Channel'} - + Channel Type = isPrivate ? { invite: { - pending: [], - }, + pending: [] + } } : { open: { banRanks: [], - banned: [], - }, + banned: [] + } }; await api.groups.create(name, policy, title, description); const path = `/ship/~${window.ship}/${name}`; diff --git a/pkg/interface/src/views/landscape/components/Participants.tsx b/pkg/interface/src/views/landscape/components/Participants.tsx index 6ae9275fbb..41ca3050d4 100644 --- a/pkg/interface/src/views/landscape/components/Participants.tsx +++ b/pkg/interface/src/views/landscape/components/Participants.tsx @@ -2,7 +2,6 @@ import React, { useState, useMemo, useCallback, - SyntheticEvent, ChangeEvent } from 'react'; import { @@ -11,27 +10,27 @@ import { Row, Text, Icon, - Center, - Button, + Image, Action, StatelessTextInput as Input } from '@tlon/indigo-react'; import _ from 'lodash'; import f from 'lodash/fp'; import VisibilitySensor from 'react-visibility-sensor'; +import styled from 'styled-components'; +import { Link } from 'react-router-dom'; + +import { Contact, Contacts } from '@urbit/api/contacts'; +import { Group, RoleTags } from '@urbit/api/groups'; +import { Association } from '@urbit/api/metadata'; -import { Contact, Contacts } from '~/types/contact-update'; import { Sigil } from '~/logic/lib/sigil'; import { cite, uxToHex } from '~/logic/lib/util'; -import { Group, RoleTags } from '~/types/group-update'; import { roleForShip, resourceFromPath } from '~/logic/lib/group'; -import { Association } from '~/types/metadata-update'; -import { useHistory, Link } from 'react-router-dom'; import { Dropdown } from '~/views/components/Dropdown'; import GlobalApi from '~/logic/api/global'; import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction'; -import styled from 'styled-components'; -import useLocalState from '~/logic/state/local'; +import useSettingsState, { selectCalmState } from '~/logic/state/settings'; const TruncText = styled(Text)` white-space: nowrap; @@ -81,13 +80,14 @@ function getParticipants(cs: Contacts, group: Group) { const emptyContact = (patp: string, pending: boolean): Participant => ({ nickname: '', - email: '', - phone: '', + bio: '', + status: '', color: '', avatar: null, - notes: '', - website: '', + cover: null, + groups: [], patp, + 'last-updated': 0, pending }); @@ -109,7 +109,7 @@ export function Participants(props: { group: Group; association: Association; api: GlobalApi; -}) { +}): ReactElement { const { api } = props; const tabFilters: Record< ParticipantsTabId, @@ -211,13 +211,7 @@ export function Participants(props: { onChange={onSearchChange} /> - + {filtered.map((cs, idx) => ( ))} - + ); @@ -258,9 +252,7 @@ function Participant(props: { }) { const { contact, association, group, api } = props; const { title } = association.metadata; - const { hideAvatars, hideNicknames } = useLocalState( - ({ hideAvatars, hideNicknames }) => ({ hideAvatars, hideNicknames }) - ); + const { hideAvatars, hideNicknames } = useSettingsState(selectCalmState); const color = uxToHex(contact.color); const isInvite = 'invite' in group.policy; @@ -294,12 +286,25 @@ function Participant(props: { const onKick = useCallback(async () => { const resource = resourceFromPath(association.group); - await api.groups.remove(resource, [`~${contact.patp}`]); - }, [api, association]); + if(contact.pending) { + await api.groups.changePolicy( + resource, + { invite: { removeInvites: [`~${contact.patp}`] } } + ); + } else { + await api.groups.remove(resource, [`~${contact.patp}`]); + } + }, [api, contact, association]); const avatar = contact?.avatar !== null && !hideAvatars ? ( - + ) : ( ); @@ -308,10 +313,12 @@ function Participant(props: { return ( <> + + {avatar} - + {hasNickname && ( - + {contact.nickname} @@ -321,10 +328,12 @@ function Participant(props: { {cite(contact.patp)} + @@ -383,21 +392,19 @@ function Participant(props: { + - + ); } function BlankParticipant({ length }) { + const height = [`${length * 97}px`, `${length * 61}px`]; return ( - + ); } diff --git a/pkg/interface/src/views/landscape/components/PopoverRoutes.tsx b/pkg/interface/src/views/landscape/components/PopoverRoutes.tsx index 4c18d93c0d..03cec5da20 100644 --- a/pkg/interface/src/views/landscape/components/PopoverRoutes.tsx +++ b/pkg/interface/src/views/landscape/components/PopoverRoutes.tsx @@ -1,20 +1,21 @@ -import React, { useRef, useCallback } from "react"; -import { Route, Switch, RouteComponentProps, Link } from "react-router-dom"; -import { Box, Row, Col, Icon, Text } from "@tlon/indigo-react"; -import { HoverBoxLink } from "~/views/components/HoverBox"; -import { Contacts, Contact } from "~/types/contact-update"; -import { Group } from "~/types/group-update"; -import { Association } from "~/types/metadata-update"; -import GlobalApi from "~/logic/api/global"; -import { GroupNotificationsConfig, S3State, Associations } from "~/types"; +import React, { useRef, useCallback, ReactElement } from 'react'; +import { Route, Switch, RouteComponentProps, Link } from 'react-router-dom'; +import { Box, Col, Text } from '@tlon/indigo-react'; -import { GroupSettings } from "./GroupSettings/GroupSettings"; -import { Participants } from "./Participants"; -import {useHashLink} from "~/logic/lib/useHashLink"; -import {DeleteGroup} from "./DeleteGroup"; -import {resourceFromPath} from "~/logic/lib/group"; -import {ModalOverlay} from "~/views/components/ModalOverlay"; -import { SidebarItem } from "~/views/landscape/components/SidebarItem"; +import { GroupNotificationsConfig, Associations } from '@urbit/api'; +import { Contacts, Contact } from '@urbit/api/contacts'; +import { Group } from '@urbit/api/groups'; +import { Association } from '@urbit/api/metadata'; + +import GlobalApi from '~/logic/api/global'; +import { GroupSettings } from './GroupSettings/GroupSettings'; +import { Participants } from './Participants'; +import { useHashLink } from '~/logic/lib/useHashLink'; +import { DeleteGroup } from './DeleteGroup'; +import { resourceFromPath } from '~/logic/lib/group'; +import { ModalOverlay } from '~/views/components/ModalOverlay'; +import { SidebarItem } from '~/views/landscape/components/SidebarItem'; +import { StorageState } from '~/types'; export function PopoverRoutes( props: { @@ -23,12 +24,12 @@ export function PopoverRoutes( group: Group; association: Association; associations: Associations; - s3: S3State; + storage: StorageState; api: GlobalApi; notificationsGroupConfig: GroupNotificationsConfig; rootIdentity: Contact; } & RouteComponentProps -) { +): ReactElement { const relativeUrl = (url: string) => `${props.baseUrl}/popover${url}`; const innerRef = useRef(null); @@ -47,7 +48,7 @@ export function PopoverRoutes( return ( { const { view } = routeProps.match.params; return ( @@ -64,13 +65,13 @@ export function PopoverRoutes( > @@ -79,14 +80,14 @@ export function PopoverRoutes( Group {groupSize} { admin && ( @@ -96,12 +97,12 @@ export function PopoverRoutes( @@ -110,16 +111,16 @@ export function PopoverRoutes( - - {"<- Back"} + + {'<- Back'} - {view === "settings" && ( + {view === 'settings' && ( )} - {view === "participants" && ( + {view === 'participants' && ( @@ -48,11 +39,11 @@ export function Resource(props: ResourceProps) { - {app === "chat" ? ( + {app === 'chat' ? ( - ) : app === "publish" ? ( + ) : app === 'publish' ? ( ) : ( @@ -60,7 +51,7 @@ export function Resource(props: ResourceProps) { { return ( diff --git a/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx b/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx index 63bc35f86a..fb95b50410 100644 --- a/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx +++ b/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx @@ -1,24 +1,18 @@ -import React, { ReactNode } from "react"; -import { Row, Icon, Box, Col, Text } from "@tlon/indigo-react"; -import styled from "styled-components"; -import { Link } from "react-router-dom"; - -import { ChatResource } from "~/views/apps/chat/ChatResource"; -import { PublishResource } from "~/views/apps/publish/PublishResource"; - -import RichText from "~/views/components/RichText"; - -import { Association } from "~/types/metadata-update"; -import GlobalApi from "~/logic/api/global"; -import { RouteComponentProps, Route, Switch } from "react-router-dom"; -import { ChannelSettings } from "./ChannelSettings"; -import { ChannelMenu } from "./ChannelMenu"; -import { NotificationGraphConfig, Groups } from "~/types"; -import {isWriter} from "~/logic/lib/group"; +import React, { ReactElement, ReactNode } from 'react'; +import { Icon, Box, Col, Row, Text } from '@tlon/indigo-react'; +import styled from 'styled-components'; +import { Link } from 'react-router-dom'; import urbitOb from 'urbit-ob'; + +import { Association } from '@urbit/api/metadata'; +import { Groups, Rolodex } from '@urbit/api'; + +import RichText from '~/views/components/RichText'; +import GlobalApi from '~/logic/api/global'; +import { isWriter } from '~/logic/lib/group'; import { getItemTitle } from '~/logic/lib/util'; -const TruncatedBox = styled(Box)` +const TruncatedText = styled(RichText)` white-space: pre; text-overflow: ellipsis; overflow: hidden; @@ -26,7 +20,7 @@ const TruncatedBox = styled(Box)` type ResourceSkeletonProps = { groups: Groups; - contacts: any; + contacts: Rolodex; association: Association; api: GlobalApi; baseUrl: string; @@ -35,31 +29,33 @@ type ResourceSkeletonProps = { groupTags?: any; }; -export function ResourceSkeleton(props: ResourceSkeletonProps) { - const { association, api, baseUrl, children, atRoot, groups } = props; - const app = association?.metadata?.module || association["app-name"]; +export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement { + const { association, baseUrl, children, groups } = props; + const app = association?.metadata?.module || association['app-name']; const rid = association.resource; const group = groups[association.group]; let workspace = association.group; - if (group?.hidden && app === "chat") { - workspace = "/messages"; + if (group?.hidden && app === 'chat') { + workspace = '/messages'; } else if (group?.hidden) { - workspace = "/home"; + workspace = '/home'; } - let title = (workspace === "/messages") + let title = (workspace === '/messages') ? getItemTitle(association) : association?.metadata?.title; - let recipient = false; + let recipient = ""; if (urbitOb.isValidPatp(title)) { recipient = title; title = (props.contacts?.[title]?.nickname) ? props.contacts[title].nickname : title; + } else { + recipient = Array.from(group.members).map(e => `~${e}`).join(", ") } - const [, , ship, resource] = rid.split("/"); + const [, , ship, resource] = rid.split('/'); const resourcePath = (p: string) => baseUrl + p; @@ -89,12 +85,12 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) { fontSize='1' mr={3} my="1" - display={["block", "none"]} + display={['block', 'none']} flexShrink={0} > - {"<- Back"} + {'<- Back'} - + + minWidth={0} + flexShrink={1} + > {title} - - - {(workspace === "/messages") ? recipient : association?.metadata?.description} - - - + {(workspace === '/messages') ? recipient : association?.metadata?.description} + + + {canWrite && ( + New Post diff --git a/pkg/interface/src/views/landscape/components/Sidebar/Apps.tsx b/pkg/interface/src/views/landscape/components/Sidebar/Apps.tsx index d56236ad00..fd32f998ad 100644 --- a/pkg/interface/src/views/landscape/components/Sidebar/Apps.tsx +++ b/pkg/interface/src/views/landscape/components/Sidebar/Apps.tsx @@ -1,8 +1,8 @@ -import { useEffect, useCallback } from "react"; -import { Graphs, UnreadStats } from "~/types"; -import { SidebarItemStatus, SidebarAppConfig } from "./types"; +import { useCallback } from 'react'; +import { Graphs, UnreadStats } from '@urbit/api'; +import { SidebarAppConfig } from './types'; export function useGraphModule( graphKeys: Set, @@ -11,10 +11,10 @@ export function useGraphModule( ): SidebarAppConfig { const getStatus = useCallback( (s: string) => { - const [, , host, name] = s.split("/"); + const [, , host, name] = s.split('/'); const graphKey = `${host.slice(1)}/${name}`; if (!graphKeys.has(graphKey)) { - return "unsubscribed"; + return 'unsubscribed'; } const unreads = graphUnreads?.[s]?.['/']?.unreads; @@ -38,7 +38,6 @@ export function useGraphModule( return 0; } return 1; - }, [getStatus, graphUnreads]); return { getStatus, lastUpdated }; diff --git a/pkg/interface/src/views/landscape/components/Sidebar/Sidebar.tsx b/pkg/interface/src/views/landscape/components/Sidebar/Sidebar.tsx index 7cd0e2dcb2..5049243220 100644 --- a/pkg/interface/src/views/landscape/components/Sidebar/Sidebar.tsx +++ b/pkg/interface/src/views/landscape/components/Sidebar/Sidebar.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useRef } from 'react'; +import React, { ReactElement, ReactNode, useRef } from 'react'; import styled from 'styled-components'; import { Col @@ -12,14 +12,14 @@ import { Groups, Invites, Rolodex -} from '~/types'; +} from '@urbit/api'; import { SidebarListHeader } from './SidebarListHeader'; import { useLocalStorageState } from '~/logic/lib/useLocalStorageState'; import { getGroupFromWorkspace } from '~/logic/lib/workspace'; import { SidebarAppConfigs } from './types'; import { SidebarList } from './SidebarList'; import { roleForShip } from '~/logic/lib/group'; -import {useTutorialModal} from '~/views/components/useTutorialModal'; +import { useTutorialModal } from '~/views/components/useTutorialModal'; const ScrollbarLessCol = styled(Col)` scrollbar-width: none !important; @@ -46,7 +46,7 @@ interface SidebarProps { workspace: Workspace; } -export function Sidebar(props: SidebarProps) { +export function Sidebar(props: SidebarProps): ReactElement { const { associations, selected, workspace } = props; const groupPath = getGroupFromWorkspace(workspace); const display = props.mobileHide ? ['none', 'flex'] : 'flex'; @@ -66,7 +66,7 @@ export function Sidebar(props: SidebarProps) { const isAdmin = (role === 'admin') || (workspace?.type === 'home'); const anchorRef = useRef(null); - useTutorialModal('channels', true, anchorRef.current); + useTutorialModal('channels', true, anchorRef); return ( ; - case "unsubscribed": + case 'unsubscribed': return ; - case "mention": + case 'mention': return ; - case "loading": + case 'loading': return ; default: return null; @@ -30,35 +32,37 @@ function SidebarItemIndicator(props: { status?: SidebarItemStatus }) { export function SidebarItem(props: { hideUnjoined: boolean; association: Association; - contacts: any; + contacts: Rolodex; groups: Groups; path: string; selected: boolean; apps: SidebarAppConfigs; workspace: Workspace; -}) { +}): ReactElement { const { association, path, selected, apps, groups } = props; let title = getItemTitle(association); - const appName = association?.["app-name"]; + const appName = association?.['app-name']; const mod = association?.metadata?.module || appName; - const rid = association?.resource + const rid = association?.resource; const groupPath = association?.group; - const anchorRef = useRef(null) + const anchorRef = useRef(null); useTutorialModal( mod as any, groupPath === `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`, - anchorRef.current + anchorRef ); const app = apps[appName]; const isUnmanaged = groups?.[groupPath]?.hidden || false; if (!app) { return null; } - const DM = (isUnmanaged && props.workspace?.type === "messages"); - const itemStatus = app.getStatus(path); - const hasUnread = itemStatus === "unread" || itemStatus === "mention"; + const DM = (isUnmanaged && props.workspace?.type === 'messages'); + const { hideAvatars, hideNicknames } = useSettingsState(selectCalmState); - const isSynced = itemStatus !== "unsubscribed"; + const itemStatus = app.getStatus(path); + const hasUnread = itemStatus === 'unread' || itemStatus === 'mention'; + + const isSynced = itemStatus !== 'unsubscribed'; let baseUrl = `/~landscape${groupPath}`; @@ -72,7 +76,7 @@ export function SidebarItem(props: { ? `${baseUrl}/resource/${mod}${rid}` : `${baseUrl}/join/${mod}${rid}`; - const color = selected ? "black" : isSynced ? "gray" : "lightGray"; + const color = selected ? 'black' : isSynced ? 'gray' : 'lightGray'; if (props.hideUnjoined && !isSynced) { return null; @@ -81,16 +85,16 @@ export function SidebarItem(props: { let img = null; if (urbitOb.isValidPatp(title)) { - if (props.contacts?.[title] && props.contacts[title].avatar) { - img = ; + if (props.contacts?.[title]?.avatar && !hideAvatars) { + img = ; } else { - img = + img = ; } - if (props.contacts?.[title] && props.contacts[title].nickname) { + if (props.contacts?.[title]?.nickname && !hideNicknames) { title = props.contacts[title].nickname; } } else { - img = + img = ; } return ( @@ -125,9 +129,9 @@ export function SidebarItem(props: { overflow='hidden' width='100%' mono={urbitOb.isValidPatp(title)} - fontWeight={hasUnread ? "bold" : "regular"} - color={selected || isSynced ? "black" : "lightGray"} - style={{ textOverflow: 'ellipsis', whiteSpace: 'pre'}} + fontWeight={hasUnread ? 'bold' : 'regular'} + color={selected || isSynced ? 'black' : 'lightGray'} + style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }} > {title} diff --git a/pkg/interface/src/views/landscape/components/Sidebar/SidebarList.tsx b/pkg/interface/src/views/landscape/components/Sidebar/SidebarList.tsx index 25af63bff7..8264b5d303 100644 --- a/pkg/interface/src/views/landscape/components/Sidebar/SidebarList.tsx +++ b/pkg/interface/src/views/landscape/components/Sidebar/SidebarList.tsx @@ -1,8 +1,10 @@ -import React, { useMemo } from "react"; -import { alphabeticalOrder } from "~/logic/lib/util"; -import { Associations, AppAssociations, Workspace, Groups } from "~/types"; -import { SidebarAppConfigs, SidebarListConfig, SidebarSort } from "./types"; -import { SidebarItem } from "./SidebarItem"; +import React, { ReactElement } from 'react'; +import { Associations, AppAssociations, Groups, Rolodex } from '@urbit/api'; + +import { alphabeticalOrder } from '~/logic/lib/util'; +import { SidebarAppConfigs, SidebarListConfig, SidebarSort } from './types'; +import { SidebarItem } from './SidebarItem'; +import { Workspace } from '~/types/workspace'; function sidebarSort( associations: AppAssociations, @@ -20,8 +22,8 @@ function sidebarSort( const lastUpdated = (a: string, b: string) => { const aAssoc = associations[a]; const bAssoc = associations[b]; - const aAppName = aAssoc?.["app-name"]; - const bAppName = bAssoc?.["app-name"]; + const aAppName = aAssoc?.['app-name']; + const bAppName = bAssoc?.['app-name']; const aUpdated = apps[aAppName]?.lastUpdated(a) || 0; const bUpdated = apps[bAppName]?.lastUpdated(b) || 0; @@ -37,7 +39,7 @@ function sidebarSort( export function SidebarList(props: { apps: SidebarAppConfigs; - contacts: any; + contacts: Rolodex; config: SidebarListConfig; associations: Associations; groups: Groups; @@ -45,7 +47,7 @@ export function SidebarList(props: { group?: string; selected?: string; workspace: Workspace; -}) { +}): ReactElement { const { selected, group, config, workspace } = props; const associations = { ...props.associations.graph }; @@ -53,11 +55,11 @@ export function SidebarList(props: { .filter((a) => { const assoc = associations[a]; if (workspace?.type === 'messages') { - return (!(assoc.group in props.associations.groups) && assoc.metadata.module === "chat"); + 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"); + : (!(assoc.group in props.associations.groups) && assoc.metadata.module !== 'chat'); } }) .sort(sidebarSort(associations, props.apps)[config.sortBy]); diff --git a/pkg/interface/src/views/landscape/components/Sidebar/SidebarListHeader.tsx b/pkg/interface/src/views/landscape/components/Sidebar/SidebarListHeader.tsx index 7d713d66eb..829e21e9c7 100644 --- a/pkg/interface/src/views/landscape/components/Sidebar/SidebarListHeader.tsx +++ b/pkg/interface/src/views/landscape/components/Sidebar/SidebarListHeader.tsx @@ -1,5 +1,7 @@ -import React, { useCallback } from "react"; -import * as Yup from "yup"; +import React, { ReactElement, useCallback } from 'react'; +import { FormikHelpers } from 'formik'; +import { Link } from 'react-router-dom'; + import { Row, Box, @@ -7,18 +9,18 @@ import { ManagedRadioButtonField as Radio, ManagedCheckboxField as Checkbox, Col, - Text, -} from "@tlon/indigo-react"; -import { FormikOnBlur } from "~/views/components/FormikOnBlur"; -import { Dropdown } from "~/views/components/Dropdown"; -import { FormikHelpers } from "formik"; -import { SidebarListConfig, Workspace } from "./types"; -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"; + Text +} from '@tlon/indigo-react'; +import { Groups, Rolodex, Associations } from '@urbit/api'; + +import { FormikOnBlur } from '~/views/components/FormikOnBlur'; +import { Dropdown } from '~/views/components/Dropdown'; +import { SidebarListConfig } from './types'; +import { getGroupFromWorkspace } from '~/logic/lib/workspace'; +import { roleForShip } from '~/logic/lib/group'; +import { NewChannel } from '~/views/landscape/components/NewChannel'; +import GlobalApi from '~/logic/api/global'; +import { Workspace } from '~/types/workspace'; export function SidebarListHeader(props: { api: GlobalApi; @@ -30,9 +32,7 @@ export function SidebarListHeader(props: { selected: string; workspace: Workspace; handleSubmit: (c: SidebarListConfig) => void; -}) { - - const history = useHistory(); +}): ReactElement { const onSubmit = useCallback( (values: SidebarListConfig, actions: FormikHelpers) => { props.handleSubmit(values); @@ -46,9 +46,9 @@ export function SidebarListHeader(props: { const memberMetadata = groupPath ? props.associations.groups?.[groupPath].metadata.vip === 'member-metadata' : false; - const isAdmin = memberMetadata || (role === "admin") || (props.workspace?.type === 'home') || (props.workspace?.type === "messages"); + const isAdmin = memberMetadata || (role === 'admin') || (props.workspace?.type === 'home') || (props.workspace?.type === 'messages'); - const noun = (props.workspace?.type === "messages") ? "Messages" : "Channels"; + const noun = (props.workspace?.type === 'messages') ? 'Messages' : 'Channels'; return ( - {props.workspace?.type === "messages" + {props.workspace?.type === 'messages' ? ( } > - + ) : ( - + : `/~landscape/${props.workspace?.type}/new`} + > + ) } @@ -111,7 +112,7 @@ export function SidebarListHeader(props: { flexShrink='0' width="auto" alignY="top" - alignX={["right", "left"]} + alignX={['right', 'left']} options={ diff --git a/pkg/interface/src/views/landscape/components/Sidebar/types.ts b/pkg/interface/src/views/landscape/components/Sidebar/types.ts index 7cdc970e08..56ecda5512 100644 --- a/pkg/interface/src/views/landscape/components/Sidebar/types.ts +++ b/pkg/interface/src/views/landscape/components/Sidebar/types.ts @@ -1,11 +1,11 @@ export type SidebarItemStatus = - | "unread" - | "mention" - | "unsubscribed" - | "disconnected" - | "loading"; + | 'unread' + | 'mention' + | 'unsubscribed' + | 'disconnected' + | 'loading'; -export type SidebarSort = "asc" | "lastUpdated"; +export type SidebarSort = 'asc' | 'lastUpdated'; export interface SidebarListConfig { sortBy: SidebarSort; @@ -18,5 +18,5 @@ export interface SidebarAppConfig { } export type SidebarAppConfigs = { - [a in "chat" | "link" | "publish"]: SidebarAppConfig; + [a in 'chat' | 'link' | 'publish']: SidebarAppConfig; }; diff --git a/pkg/interface/src/views/landscape/components/SidebarItem.tsx b/pkg/interface/src/views/landscape/components/SidebarItem.tsx index 798d31e3eb..667ed2fed3 100644 --- a/pkg/interface/src/views/landscape/components/SidebarItem.tsx +++ b/pkg/interface/src/views/landscape/components/SidebarItem.tsx @@ -1,8 +1,8 @@ -import React from "react"; -import { Row, Icon, Text } from "@tlon/indigo-react"; +import React from 'react'; +import { Row, Icon, Text } from '@tlon/indigo-react'; -import { IconRef, PropFunc } from "~/types/util"; -import { HoverBoxLink } from "~/views/components/HoverBox"; +import { IconRef, PropFunc } from '~/types/util'; +import { HoverBoxLink } from '~/views/components/HoverBox'; interface SidebarItemProps { selected?: boolean; @@ -11,17 +11,17 @@ interface SidebarItemProps { to: string; color?: string; children?: JSX.Element; -} +} export const SidebarItem = ({ icon, text, to, selected = false, - color = "black", + color = 'black', children, ...rest -}: SidebarItemProps & PropFunc) => { +}: SidebarItemProps & PropFunc): ReactElement => { return ( - + {text} diff --git a/pkg/interface/src/views/landscape/components/Skeleton.tsx b/pkg/interface/src/views/landscape/components/Skeleton.tsx index 9b494a2d0c..ddfb5b0880 100644 --- a/pkg/interface/src/views/landscape/components/Skeleton.tsx +++ b/pkg/interface/src/views/landscape/components/Skeleton.tsx @@ -1,17 +1,14 @@ -import React, { ReactNode, useEffect, useMemo } from 'react'; -import { Box, Text } from '@tlon/indigo-react'; -import { Link } from 'react-router-dom'; +import React, { ReactElement, ReactNode, useMemo } from 'react'; + +import { Groups, Graphs, Invites, Rolodex, Path, AppName } from '@urbit/api'; +import { Associations } from '@urbit/api/metadata'; 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'; +import { Workspace } from '~/types/workspace'; interface SkeletonProps { contacts: Rolodex; @@ -22,8 +19,6 @@ interface SkeletonProps { graphKeys: Set; graphs: Graphs; linkListening: Set; - links: LinkCollections; - notebooks: Notebooks; invites: Invites; selected?: string; selectedApp?: AppName; @@ -33,10 +28,10 @@ interface SkeletonProps { subscription: GlobalSubscription; includeUnmanaged: boolean; workspace: Workspace; - unreads: any; + unreads: unknown; } -export function Skeleton(props: SkeletonProps) { +export function Skeleton(props: SkeletonProps): ReactElement { const graphConfig = useGraphModule(props.graphKeys, props.graphs, props.unreads.graph); const config = useMemo( () => ({ diff --git a/pkg/interface/src/views/landscape/components/TutorialModal.tsx b/pkg/interface/src/views/landscape/components/TutorialModal.tsx index 350287fc80..03d4b7facf 100644 --- a/pkg/interface/src/views/landscape/components/TutorialModal.tsx +++ b/pkg/interface/src/views/landscape/components/TutorialModal.tsx @@ -1,11 +1,11 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback } from 'react'; import _ from 'lodash'; -import { Box, Col, Row, Button, Text, Icon, Action } from "@tlon/indigo-react"; -import { useHistory } from "react-router-dom"; -import { TutorialProgress, tutorialProgress as progress } from "~/types"; +import { Box, Col, Row, Button, Text, Icon } from '@tlon/indigo-react'; +import { useHistory } from 'react-router-dom'; +import { TutorialProgress, tutorialProgress as progress } from '~/types'; -import { Portal } from "~/views/components/Portal"; -import useLocalState, { selectLocalState } from "~/logic/state/local"; +import { Portal } from '~/views/components/Portal'; +import useLocalState, { selectLocalState } from '~/logic/state/local'; import { progressDetails, MODAL_HEIGHT_PX, @@ -14,21 +14,21 @@ import { MODAL_HEIGHT, TUTORIAL_HOST, TUTORIAL_GROUP, - getTrianglePosition, -} from "~/logic/lib/tutorialModal"; -import { getRelativePosition } from "~/logic/lib/relativePosition"; -import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton"; -import GlobalApi from "~/logic/api/global"; -import {Triangle} from "~/views/components/Triangle"; -import {ModalOverlay} from "~/views/components/ModalOverlay"; + getTrianglePosition +} from '~/logic/lib/tutorialModal'; +import { getRelativePosition } from '~/logic/lib/relativePosition'; +import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton'; +import GlobalApi from '~/logic/api/global'; +import { Triangle } from '~/views/components/Triangle'; +import { ModalOverlay } from '~/views/components/ModalOverlay'; const localSelector = selectLocalState([ - "tutorialProgress", - "nextTutStep", - "prevTutStep", - "tutorialRef", - "hideTutorial", - "set" + 'tutorialProgress', + 'nextTutStep', + 'prevTutStep', + 'tutorialRef', + 'hideTutorial', + 'set' ]); export function TutorialModal(props: { api: GlobalApi }) { @@ -47,7 +47,7 @@ export function TutorialModal(props: { api: GlobalApi }) { alignX, alignY, offsetX, - offsetY, + offsetY } = progressDetails[tutorialProgress]; const [coords, setCoords] = useState({}); @@ -56,7 +56,7 @@ export function TutorialModal(props: { api: GlobalApi }) { const history = useHistory(); const next = useCallback( () => { - const idx = progress.findIndex((p) => p === tutorialProgress); + const idx = progress.findIndex(p => p === tutorialProgress); const { url } = progressDetails[progress[idx + 1]]; nextTutStep(); history.push(url); @@ -64,7 +64,7 @@ export function TutorialModal(props: { api: GlobalApi }) { [nextTutStep, history, tutorialProgress, setCoords] ); const prev = useCallback(() => { - const idx = progress.findIndex((p) => p === tutorialProgress); + const idx = progress.findIndex(p => p === tutorialProgress); prevTutStep(); history.push(progressDetails[progress[idx - 1]].url); }, [prevTutStep, history, tutorialProgress]); @@ -94,11 +94,11 @@ export function TutorialModal(props: { api: GlobalApi }) { setCoords(withMobile); } else { setCoords({}); - } }, [tutorialRef]); const dismiss = useCallback(async () => { + setPaused(false); hideTutorial(); await props.api.settings.putEntry('tutorial', 'seen', true); }, [hideTutorial, props.api]); @@ -113,14 +113,15 @@ export function TutorialModal(props: { api: GlobalApi }) { const leaveGroup = useCallback(async () => { await props.api.groups.leaveGroup(TUTORIAL_HOST, TUTORIAL_GROUP); - }, [props.api]); + await dismiss(); + }, [props.api, dismiss]); - const progressIdx = progress.findIndex((p) => p === tutorialProgress); + const progressIdx = progress.findIndex(p => p === tutorialProgress); useEffect(() => { if ( - tutorialProgress !== "hidden" && - tutorialProgress !== "done" && + tutorialProgress !== 'hidden' && + tutorialProgress !== 'done' && tutorialRef ) { const interval = setInterval(updatePos, 100); @@ -138,19 +139,19 @@ export function TutorialModal(props: { api: GlobalApi }) { return ( - - + + Tutorial Finished - {progressIdx} of {progress.length - 1} + {progressIdx} of {progress.length - 2} This tutorial is finished. Would you like to leave Beginner Island? - + @@ -164,15 +165,15 @@ export function TutorialModal(props: { api: GlobalApi }) { ); } - if (tutorialProgress === "hidden") { + if (tutorialProgress === 'hidden') { return null; } if(paused) { return ( - - + + End Tutorial Now? @@ -180,7 +181,7 @@ export function TutorialModal(props: { api: GlobalApi }) { You can always restart the tutorial by typing "tutorial" in Leap. - + @@ -191,11 +192,9 @@ export function TutorialModal(props: { api: GlobalApi }) { - ) - + ); } - if(Object.keys(coords).length === 0) { return null; } @@ -207,7 +206,8 @@ export function TutorialModal(props: { api: GlobalApi }) { {...coords} bg="white" zIndex={50} - height={MODAL_HEIGHT_PX} + display="flex" + flexDirection="column" width={["100%", MODAL_WIDTH_PX]} borderRadius="2" > @@ -217,11 +217,11 @@ export function TutorialModal(props: { api: GlobalApi }) { height="100%" width="100%" borderRadius="2" - p="2" + p="3" bg="lightBlue" - + > - - + - + {title} @@ -248,9 +249,9 @@ export function TutorialModal(props: { api: GlobalApi }) { {progressIdx} of {progress.length - 2} - + {description} - + { progressIdx > 1 && ( +
+
+    
+ + + \ No newline at end of file diff --git a/pkg/npm/http-api/package.json b/pkg/npm/http-api/package.json new file mode 100644 index 0000000000..ca756dc1bc --- /dev/null +++ b/pkg/npm/http-api/package.json @@ -0,0 +1,61 @@ +{ + "name": "@urbit/http-api", + "version": "1.1.0", + "license": "MIT", + "description": "Library to interact with an Urbit ship over HTTP", + "repository": { + "type": "git", + "url": "ssh://git@github.com/urbit/urbit.git", + "directory": "pkg/npm/http-api" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist", + "src" + ], + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "npm run clean && tsc -p tsconfig.json", + "clean": "rm -rf dist/*" + }, + "peerDependencies": {}, + "prettier": { + "printWidth": 80, + "semi": true, + "singleQuote": true, + "trailingComma": "es5" + }, + "author": "", + "devDependencies": { + "@babel/core": "^7.12.3", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/plugin-proposal-object-rest-spread": "^7.12.1", + "@babel/plugin-proposal-optional-chaining": "^7.12.1", + "@babel/preset-typescript": "^7.12.1", + "@types/browser-or-node": "^1.2.0", + "@types/eventsource": "^1.1.5", + "@types/react": "^16.9.56", + "@typescript-eslint/eslint-plugin": "^4.7.0", + "@typescript-eslint/parser": "^4.7.0", + "babel-loader": "^8.2.1", + "clean-webpack-plugin": "^3.0.0", + "tslib": "^2.0.3", + "typescript": "^3.9.7", + "util": "^0.12.3", + "webpack": "^5.4.0", + "webpack-cli": "^3.3.12", + "webpack-dev-server": "^3.11.0" + }, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@microsoft/fetch-event-source": "^2.0.0", + "@urbit/api": "file:../api", + "browser-or-node": "^1.3.0", + "browserify-zlib": "^0.2.0", + "buffer": "^6.0.3", + "node-fetch": "^2.6.1", + "stream-browserify": "^3.0.0", + "stream-http": "^3.1.1" + } +} diff --git a/pkg/npm/http-api/src/Urbit.ts b/pkg/npm/http-api/src/Urbit.ts new file mode 100644 index 0000000000..4f01361683 --- /dev/null +++ b/pkg/npm/http-api/src/Urbit.ts @@ -0,0 +1,372 @@ +import { isBrowser, isNode } from 'browser-or-node'; +import { Action, Scry, Thread } from '@urbit/api'; +import { fetchEventSource, EventSourceMessage } from '@microsoft/fetch-event-source'; + +import { AuthenticationInterface, SubscriptionInterface, CustomEventHandler, PokeInterface, SubscriptionRequestInterface, headers, UrbitInterface, SSEOptions, PokeHandlers } from './types'; +import { uncamelize, hexString } from './utils'; + +/** + * A class for interacting with an urbit ship, given its URL and code + */ +export class Urbit implements UrbitInterface { + /** + * UID will be used for the channel: The current unix time plus a random hex string + */ + uid: string = `${Math.floor(Date.now() / 1000)}-${hexString(6)}`; + + /** + * Last Event ID is an auto-updated index of which events have been sent over this channel + */ + lastEventId: number = 0; + + lastAcknowledgedEventId: number = 0; + + /** + * SSE Client is null for now; we don't want to start polling until it the channel exists + */ + sseClientInitialized: boolean = false; + + /** + * Cookie gets set when we log in. + */ + cookie?: string | undefined; + + /** + * A registry of requestId to successFunc/failureFunc + * + * These functions are registered during a +poke and are executed + * in the onServerEvent()/onServerError() callbacks. Only one of + * the functions will be called, and the outstanding poke will be + * removed after calling the success or failure function. + */ + + outstandingPokes: Map = new Map(); + + /** + * A registry of requestId to subscription functions. + * + * These functions are registered during a +subscribe and are + * executed in the onServerEvent()/onServerError() callbacks. The + * event function will be called whenever a new piece of data on this + * subscription is available, which may be 0, 1, or many times. The + * disconnect function may be called exactly once. + */ + + outstandingSubscriptions: Map = new Map(); + + /** + * Ship can be set, in which case we can do some magic stuff like send chats + */ + ship?: string | null; + + /** + * If verbose, logs output eagerly. + */ + verbose?: boolean; + + /** This is basic interpolation to get the channel URL of an instantiated Urbit connection. */ + get channelUrl(): string { + return `${this.url}/~/channel/${this.uid}`; + } + + get fetchOptions(): any { + const headers: headers = { + 'Content-Type': 'application/json', + }; + if (!isBrowser) { + headers.Cookie = this.cookie; + } + return { + credentials: 'include', + headers + }; + } + + /** + * Constructs a new Urbit connection. + * + * @param url The URL (with protocol and port) of the ship to be accessed + * @param code The access code for the ship at that address + */ + constructor( + public url: string, + public code: string + ) { + return this; + } + + /** + * All-in-one hook-me-up. + * + * Given a ship, url, and code, this returns an airlock connection + * that is ready to go. It `|hi`s itself to create the channel, + * then opens the channel via EventSource. + * + * @param AuthenticationInterface + */ + static async authenticate({ ship, url, code, verbose = false }: AuthenticationInterface) { + const airlock = new Urbit(`http://${url}`, code); + airlock.verbose = verbose; + airlock.ship = ship; + await airlock.connect(); + await airlock.poke({ app: 'hood', mark: 'helm-hi', json: 'opening airlock' }); + await airlock.eventSource(); + return airlock; + } + + /** + * Connects to the Urbit ship. Nothing can be done until this is called. + * That's why we roll it into this.authenticate + */ + async connect(): Promise { + if (this.verbose) { + console.log(`password=${this.code} `, isBrowser ? "Connecting in browser context at " + `${this.url}/~/login` : "Connecting from node context"); + } + return fetch(`${this.url}/~/login`, { + method: 'post', + body: `password=${this.code}`, + credentials: 'include', + }).then(response => { + if (this.verbose) { + console.log('Received authentication response', response); + } + const cookie = response.headers.get('set-cookie'); + if (!this.ship) { + this.ship = new RegExp(/urbauth-~([\w-]+)/).exec(cookie)[1]; + } + if (!isBrowser) { + this.cookie = cookie; + } + }); + } + + + /** + * Initializes the SSE pipe for the appropriate channel. + */ + eventSource(): void{ + if (!this.sseClientInitialized) { + const sseOptions: SSEOptions = { + headers: {} + }; + if (isBrowser) { + sseOptions.withCredentials = true; + } else if (isNode) { + sseOptions.headers.Cookie = this.cookie; + } + fetchEventSource(this.channelUrl, { + // withCredentials: true, + onmessage: (event: EventSourceMessage) => { + if (this.verbose) { + console.log('Received SSE: ', event); + } + this.ack(Number(event.id)); + if (event.data && JSON.parse(event.data)) { + const data: any = JSON.parse(event.data); + if (data.response === 'poke' && this.outstandingPokes.has(data.id)) { + const funcs = this.outstandingPokes.get(data.id); + if (data.hasOwnProperty('ok')) { + funcs.onSuccess(); + } else if (data.hasOwnProperty('err')) { + funcs.onError(data.err); + } else { + console.error('Invalid poke response', data); + } + this.outstandingPokes.delete(data.id); + } else if (data.response === 'subscribe' || + (data.response === 'poke' && this.outstandingSubscriptions.has(data.id))) { + const funcs = this.outstandingSubscriptions.get(data.id); + if (data.hasOwnProperty('err')) { + funcs.err(data.err); + this.outstandingSubscriptions.delete(data.id); + } + } else if (data.response === 'diff' && this.outstandingSubscriptions.has(data.id)) { + const funcs = this.outstandingSubscriptions.get(data.id); + funcs.event(data.json); + } else if (data.response === 'quit' && this.outstandingSubscriptions.has(data.id)) { + const funcs = this.outstandingSubscriptions.get(data.id); + funcs.quit(data); + this.outstandingSubscriptions.delete(data.id); + } else { + console.log('Unrecognized response', data); + } + } + }, + onerror: (error) => { + console.error('pipe error', error); + } + }); + this.sseClientInitialized = true; + } + return; + } + + /** + * Autoincrements the next event ID for the appropriate channel. + */ + getEventId(): number { + this.lastEventId = Number(this.lastEventId) + 1; + return this.lastEventId; + } + + /** + * Acknowledges an event. + * + * @param eventId The event to acknowledge. + */ + ack(eventId: number): Promise { + return this.sendMessage('ack', { 'event-id': eventId }); + } + + /** + * This is a wrapper method that can be used to send any action with data. + * + * Every message sent has some common parameters, like method, headers, and data + * structure, so this method exists to prevent duplication. + * + * @param action The action to send + * @param data The data to send with the action + * + * @returns void | number If successful, returns the number of the message that was sent + */ + async sendMessage(action: Action, data?: object): Promise { + + const id = this.getEventId(); + if (this.verbose) { + console.log(`Sending message ${id}:`, action, data,); + } + let response: Response | undefined; + try { + response = await fetch(this.channelUrl, { + ...this.fetchOptions, + method: 'put', + body: JSON.stringify([{ + id, + action, + ...data, + }]), + }); + } catch (error) { + console.error('message error', error); + response = undefined; + } + if (this.verbose) { + console.log(`Received from message ${id}: `, response); + } + return id; + } + + /** + * Pokes a ship with data. + * + * @param app The app to poke + * @param mark The mark of the data being sent + * @param json The data to send + */ + poke(params: PokeInterface): Promise { + const { app, mark, json, onSuccess, onError } = { onSuccess: () => {}, onError: () => {}, ...params }; + return new Promise((resolve, reject) => { + this + .sendMessage('poke', { ship: this.ship, app, mark, json }) + .then(pokeId => { + if (!pokeId) { + return reject('Poke failed'); + } + if (!this.sseClientInitialized) resolve(pokeId); // A poke may occur before a listener has been opened + this.outstandingPokes.set(pokeId, { + onSuccess: () => { + onSuccess(); + resolve(pokeId); + }, + onError: (event) => { + onError(event); + reject(event.err); + } + }); + }).catch(error => { + console.error(error); + }); + }); + } + + /** + * Subscribes to a path on an app on a ship. + * + * @param app The app to subsribe to + * @param path The path to which to subscribe + * @param handlers Handlers to deal with various events of the subscription + */ + async subscribe(params: SubscriptionRequestInterface): Promise { + const { app, path, err, event, quit } = { err: () => {}, event: () => {}, quit: () => {}, ...params }; + + const subscriptionId = await this.sendMessage('subscribe', { ship: this.ship, app, path }); + + if (!subscriptionId) return; + + this.outstandingSubscriptions.set(subscriptionId, { + err, event, quit + }); + + return subscriptionId; + } + + /** + * Unsubscribes to a given subscription. + * + * @param subscription + */ + unsubscribe(subscription: string): Promise { + return this.sendMessage('unsubscribe', { subscription }); + } + + /** + * Deletes the connection to a channel. + */ + delete(): Promise { + return this.sendMessage('delete'); + } + + /** + * + * @param app The app into which to scry + * @param path The path at which to scry + */ + async scry(params: Scry): Promise { + const { app, path } = params; + const response = await fetch(`/~/scry/${app}${path}.json`, this.fetchOptions); + return await response.json(); + } + + /** + * + * @param inputMark The mark of the data being sent + * @param outputMark The mark of the data being returned + * @param threadName The thread to run + * @param body The data to send to the thread + */ + async thread(params: Thread): Promise { + const { inputMark, outputMark, threadName, body } = params; + const res = await fetch(`/spider/${inputMark}/${threadName}/${outputMark}.json`, { + ...this.fetchOptions, + method: 'POST', + body: JSON.stringify(body) + }); + + return res.json(); + } + + /** + * Utility function to connect to a ship that has its *.arvo.network domain configured. + * + * @param name Name of the ship e.g. zod + * @param code Code to log in + */ + static async onArvoNetwork(ship: string, code: string): Promise { + const url = `https://${ship}.arvo.network`; + return await Urbit.authenticate({ ship, url, code }); + } +} + + + +export default Urbit; diff --git a/pkg/npm/http-api/src/example/browser.js b/pkg/npm/http-api/src/example/browser.js new file mode 100644 index 0000000000..2dfc71410a --- /dev/null +++ b/pkg/npm/http-api/src/example/browser.js @@ -0,0 +1,3 @@ +// import Urbit from '../../dist/browser'; + +// window.Urbit = Urbit; \ No newline at end of file diff --git a/pkg/npm/http-api/src/example/node.js b/pkg/npm/http-api/src/example/node.js new file mode 100644 index 0000000000..bbceb0c7b5 --- /dev/null +++ b/pkg/npm/http-api/src/example/node.js @@ -0,0 +1,14 @@ +// import Urbit from '../../dist/index'; + +// async function blastOff() { +// const airlock = await Urbit.authenticate({ +// ship: 'zod', +// url: 'localhost:8080', +// code: 'lidlut-tabwed-pillex-ridrup', +// verbose: true +// }); + +// airlock.subscribe('chat-view', '/primary'); +// } + +// blastOff(); diff --git a/pkg/npm/http-api/src/index.ts b/pkg/npm/http-api/src/index.ts new file mode 100644 index 0000000000..8e242ef6b0 --- /dev/null +++ b/pkg/npm/http-api/src/index.ts @@ -0,0 +1,3 @@ +export * from './types'; +import Urbit from './Urbit'; +export { Urbit as default }; \ No newline at end of file diff --git a/pkg/npm/http-api/src/types.ts b/pkg/npm/http-api/src/types.ts new file mode 100644 index 0000000000..04cc62da5d --- /dev/null +++ b/pkg/npm/http-api/src/types.ts @@ -0,0 +1,66 @@ +import { Action, Poke, Scry, Thread } from '@urbit/api'; + +export interface PokeHandlers { + onSuccess?: () => void; + onError?: (e: any) => void; +} + +export type PokeInterface = PokeHandlers & Poke; + +export interface AuthenticationInterface { + ship: string; + url: string; + code: string; + verbose?: boolean; +} + +export interface SubscriptionInterface { + err?(error: any): void; + event?(data: any): void; + quit?(data: any): void; +} + +export type SubscriptionRequestInterface = SubscriptionInterface & { + app: string; + path: string; +} + +export interface headers { + 'Content-Type': string; + Cookie?: string; +} + +export interface UrbitInterface { + uid: string; + lastEventId: number; + lastAcknowledgedEventId: number; + sseClientInitialized: boolean; + cookie?: string | undefined; + outstandingPokes: Map; + outstandingSubscriptions: Map; + verbose?: boolean; + ship?: string | null; + connect(): void; + connect(): Promise; + eventSource(): void; + getEventId(): number; + ack(eventId: number): Promise; + sendMessage(action: Action, data?: object): Promise; + poke(params: PokeInterface): Promise; + subscribe(params: SubscriptionRequestInterface): Promise; + unsubscribe(subscription: string): Promise; + delete(): Promise; + scry(params: Scry): Promise; + thread(params: Thread): Promise; +} + +export interface CustomEventHandler { + (data: any, response: string): void; +} + +export interface SSEOptions { + headers?: { + Cookie?: string + }; + withCredentials?: boolean; +} diff --git a/pkg/npm/http-api/src/utils.ts b/pkg/npm/http-api/src/utils.ts new file mode 100644 index 0000000000..94c0998cb0 --- /dev/null +++ b/pkg/npm/http-api/src/utils.ts @@ -0,0 +1,82 @@ +import * as http from 'http'; + +interface HttpResponse { + req: http.ClientRequest; + res: http.IncomingMessage; + data: string; +} + +export function request( + url: string, + options: http.ClientRequestArgs, + body?: string +): Promise { + return new Promise((resolve, reject) => { + const req = http.request(url, options, res => { + let data = ""; + res.on("data", chunk => { + data += chunk; + }); + res.on("end", () => { + resolve({ req, res, data }); + }); + res.on("error", e => { + reject(e); + }); + }); + if (body) { + req.write(body); + } + req.end(); + }); +} + +export function camelize(str: string) { + return str + .replace(/\s(.)/g, function($1: string) { return $1.toUpperCase(); }) + .replace(/\s/g, '') + .replace(/^(.)/, function($1: string) { return $1.toLowerCase(); }); +} + +export function uncamelize(str: string, separator = '-') { + // Replace all capital letters by separator followed by lowercase one + var str = str.replace(/[A-Z]/g, function (letter: string) { + return separator + letter.toLowerCase(); + }); + return str.replace(new RegExp('^' + separator), ''); +} + +/** + * Returns a hex string of given length. + * + * Poached from StackOverflow. + * + * @param len Length of hex string to return. + */ +export function hexString(len: number): string { + const maxlen = 8; + const min = Math.pow(16, Math.min(len, maxlen) - 1); + const max = Math.pow(16, Math.min(len, maxlen)) - 1; + const n = Math.floor(Math.random() * (max - min + 1)) + min; + let r = n.toString(16); + while (r.length < len) { + r = r + hexString(len - maxlen); + } + return r; +} + +/** + * Generates a random UID. + * + * Copied from https://github.com/urbit/urbit/blob/137e4428f617c13f28ed31e520eff98d251ed3e9/pkg/interface/src/lib/util.js#L3 + */ +export function uid(): string { + let str = '0v'; + str += Math.ceil(Math.random() * 8) + '.'; + for (let i = 0; i < 5; i++) { + let _str = Math.ceil(Math.random() * 10000000).toString(32); + _str = ('00000' + _str).substr(-5, 5); + str += _str + '.'; + } + return str.slice(0, -1); +} \ No newline at end of file diff --git a/pkg/npm/http-api/test/default.test.ts b/pkg/npm/http-api/test/default.test.ts new file mode 100644 index 0000000000..719f10efbe --- /dev/null +++ b/pkg/npm/http-api/test/default.test.ts @@ -0,0 +1,8 @@ +import Urbit from '../src'; + +describe('blah', () => { + it('works', () => { + const connection = new Urbit('~sampel-palnet', '+code'); + expect(connection).toEqual(2); + }); +}); diff --git a/pkg/npm/http-api/tsconfig.json b/pkg/npm/http-api/tsconfig.json new file mode 100644 index 0000000000..2245cf9dc3 --- /dev/null +++ b/pkg/npm/http-api/tsconfig.json @@ -0,0 +1,19 @@ +{ + "include": ["src/*.ts"], + "exclude": ["node_modules", "dist", "@types"], + "compilerOptions": { + "outDir": "./dist", + "module": "ESNext", + "noImplicitAny": true, + "target": "ESNext", + "pretty": true, + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "declaration": true, + "sourceMap": true, + "strict": false, + "noErrorTruncation": true, + "allowJs": true, + } +} \ No newline at end of file diff --git a/sh/build-interface b/sh/build-interface index cebf791014..e90e04a14b 100755 --- a/sh/build-interface +++ b/sh/build-interface @@ -2,7 +2,10 @@ set -ex -cd pkg/interface +cd pkg/npm/api +npm install & + +cd ../../interface npm install npm run build:prod & diff --git a/sh/poke-gcp-account-json b/sh/poke-gcp-account-json new file mode 100755 index 0000000000..3c5c24fa05 --- /dev/null +++ b/sh/poke-gcp-account-json @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +import json +import re +import subprocess +import sys + + +def herb_poke_gcp_setting(pier, key, val): + """ + Poke a value into settings-store under the %gcp-store bucket. + + This does not sanitize or check its inputs. Please make sure they are + correct before calling this function. + + :pier: Pier of the ship to poke. + :key: Key to poke. Must be a @tas (i.e. include the '%'). + :val: Value to poke. Must be a @t. (will be passed through crude_t.) + """ + print('herb_poke ' + key) + # XXX use +same because herb's cell parser is cursed. + poke_arg = "(same %put-entry %gcp-store {} %s {})".format( + key, crude_t(val)) + return subprocess.run(['herb', pier, '-p', 'settings-store', '-d', + poke_arg, '-m', 'settings-event'], + check=True) + +def crude_t(pin): + """ + Very crude, bad, dangerous, and evil @t transform. + + Puts single quotes around the string. Escapes instances of single quote and + backslash within the string, and turns newlines into \0a. + """ + replaces = [(r'\\', r'\\\\'), ("'", r"\\'"), ("\n", r'\\0a')] + for pattern, replace in replaces: + pin = re.sub(pattern, replace, pin, flags=re.MULTILINE) + return "'{}'".format(pin) + +def read_gcp_json(keyfile): + with open(keyfile, 'r') as f: + return json.loads(f.read()) + +def main(): + pier, keyfile = sys.argv[1:] + obj = read_gcp_json(keyfile) + herb_poke_gcp_setting(pier, '%token-uri', obj['token_uri']) + herb_poke_gcp_setting(pier, '%client-email', obj['client_email']) + herb_poke_gcp_setting(pier, '%private-key-id', obj['private_key_id']) + herb_poke_gcp_setting(pier, '%private-key', obj['private_key']) + +if __name__ == '__main__': + main()