diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..63b3ea5fe0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Landscape feature request + url: https://github.com/urbit/landscape/issues/new?assignees=&labels=feature+request&template=feature_request.md&title= + about: Landscape is comprised of Tlon's user applications and client for Urbit. Submit Landscape feature requests here. + - name: urbit-dev mailing list + url: https://groups.google.com/a/urbit.org/g/dev + about: Developer questions and discussions also take place on the urbit-dev mailing list. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/os1-bug-report.md b/.github/ISSUE_TEMPLATE/os1-bug-report.md index 974acf0453..b6800a7a7f 100644 --- a/.github/ISSUE_TEMPLATE/os1-bug-report.md +++ b/.github/ISSUE_TEMPLATE/os1-bug-report.md @@ -1,6 +1,6 @@ --- -name: OS1 Bug report -about: 'Use this template to file a bug for any OS1 app: Chat, Publish, Links, Groups, +name: Landscape bug report +about: 'Use this template to file a bug for any Landscape app: Chat, Publish, Links, Groups, Weather or Clock' title: '' labels: landscape diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1a1b08a2d7..b64ef9c03f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,12 +2,14 @@ Thank you for your interest in contributing to Urbit. -See [urbit.org/docs/getting-started][start] for basic orientation and usage +See [urbit.org/using/install][start] for basic orientation and usage instructions. You may also want to subscribe to [urbit-dev][list], the Urbit development mailing list. For specific information on contributing to the Urbit interface, see its [contribution guidelines][interface]. -[start]: https://urbit.org/docs/getting-started/#arvo +For information on Arvo's maintainers, see [pkg/arvo][main]. + +[start]: https://urbit.org/using/install [interface]: /pkg/interface/CONTRIBUTING.md ## Fake ships @@ -45,11 +47,10 @@ The canonical source tree is the `master` branch of `master` when commencing new work; similarly, when we pull in your contribution, we'll do so by merging it to `master`. -Since we use GitHub, it's helpful (though not required) to contribute via a -GitHub pull request. You can also post patches to the [mailing list][list], -email them to maintainers, or request a maintainer pull from your tree directly --- but note that some maintainers will be more receptive to these methods than -others. +Since we use GitHub, we request you contribute via a GitHub pull request. Tag +the [maintainer][main] for the component. If you have a question for the +maintainer, you can direct message them from your Urbit ship using that +information. When contributing changes, via whatever means, make sure you describe them appropriately. You should attach a reasonably high-level summary of what the @@ -58,8 +59,8 @@ exist, e.g. a GitHub issue, a mailing list discussion, a UP, etc. [Here][jbpr] is a good example of a pull request with a useful, concise description. If your changes replace significant extant functionality, be sure to compare -them with the thing you're replacing. You may also want to cc maintainers, -reviewers, or other parties who might have a particular interest in what you're +them with the thing you're replacing. You may also want to cc reviewers, +or other parties who might have a particular interest in what you're contributing. [jbpr]: https://github.com/urbit/urbit/pull/1782 @@ -283,3 +284,4 @@ Questions or other communications about contributing to Urbit can go to [reba]: https://git-rebase.io/ [issu]: https://github.com/urbit/urbit/issues [hoon]: https://urbit.org/docs/learn/hoon/style/ +[main]: https://github.com/urbit/urbit/tree/master/pkg/arvo#maintainers \ No newline at end of file diff --git a/README.md b/README.md index c5018a871e..914815947c 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,38 @@ # Urbit -A personal server operating function. +[Urbit](https://urbit.org) is a personal server stack built from scratch. It +has an identity layer (Azimuth), virtual machine (Vere), and operating system +(Arvo). -> The Urbit address space, Azimuth, is now live on the Ethereum blockchain. You -> can find it at [`0x223c067f8cf28ae173ee5cafea60ca44c335fecb`][azim] or -> [`azimuth.eth`][aens]. Owners of Azimuth points (galaxies, stars, or planets) -> can view or manage them using [Bridge][brid], and can also use them to boot -> [Arvo][arvo], the Urbit OS. +A running Urbit "ship" is designed to operate with other ships peer-to-peer. +Urbit is a general-purpose, peer-to-peer computer and network. + +This repository contains: + +- The [Arvo OS][arvo] +- [herb][herb], a tool for Unix control of an Urbit ship +- Source code for [Landscape's web interface][land] +- Source code for the [vere][vere] virtual machine. + +For more on the identity layer, see [Azimuth][azim]. To manage your Urbit +identity, use [Bridge][brid]. -[azim]: https://etherscan.io/address/0x223c067f8cf28ae173ee5cafea60ca44c335fecb -[aens]: https://etherscan.io/address/azimuth.eth -[brid]: https://github.com/urbit/bridge [arvo]: https://github.com/urbit/urbit/tree/master/pkg/arvo +[azim]: https://github.com/urbit/azimuth +[brid]: https://github.com/urbit/bridge +[herb]: https://github.com/urbit/urbit/tree/master/pkg/herb +[land]: https://github.com/urbit/urbit/tree/master/pkg/interface +[vere]: https://github.com/urbit/urbit/tree/master/pkg/urbit ## Install To install and run Urbit, please follow the instructions at -[urbit.org/docs/getting-started/][start]. You'll be on the live network in a +[urbit.org/using/install][start]. You'll be on the live network in a few minutes. If you're interested in Urbit development, keep reading. -[start]: https://urbit.org/docs/getting-started/ +[start]: https://urbit.org/using/install/ ## Development @@ -38,7 +49,7 @@ The Makefile in the project's root directory contains useful phony targets for building, installing, testing, and so on. You can use it to avoid dealing with Nix explicitly. -To build Urbit, for example, use: +To build the Urbit virtual machine binary, for example, use: ``` make build @@ -68,12 +79,10 @@ Contributions of any form are more than welcome! Please take a look at our [contributing guidelines][cont] for details on our git practices, coding styles, how we manage issues, and so on. -You might also be interested in: +For instructions on contributing to Landscape, see [its][lcont] guidelines. -- joining the [urbit-dev][list] mailing list. -- [applying to Hoon School][mail], a course we run to teach the Hoon - programming language and Urbit application development. +You might also be interested in joining the [urbit-dev][list] mailing list. [list]: https://groups.google.com/a/urbit.org/forum/#!forum/dev -[mail]: mailto:support@urbit.org [cont]: https://github.com/urbit/urbit/blob/master/CONTRIBUTING.md +[lcont]: https://github.com/urbit/urbit/blob/master/pkg/interface/CONTRIBUTING.md \ No newline at end of file diff --git a/bin/solid.pill b/bin/solid.pill index 26952f2819..437b4aac8a 100644 --- a/bin/solid.pill +++ b/bin/solid.pill @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfb556a9e6b473f6cf6c75b30a3b12cb986e57df1600dad4383b9d3380cffdb6 -size 6263010 +oid sha256:3f5741b71f11a562d443fc619eb1b6bb1ccf419375aa2f1eebbd1c06dce20cd0 +size 6268477 diff --git a/pkg/arvo/README.md b/pkg/arvo/README.md index 8f1d068700..4331571774 100644 --- a/pkg/arvo/README.md +++ b/pkg/arvo/README.md @@ -41,20 +41,20 @@ Most parts of Arvo have dedicated maintainers. * `/sys/hoon`: @pilfer-pandex (~pilfer-pandex) * `/sys/zuse`: @pilfer-pandex (~pilfer-pandex) -* `/sys/arvo`: @jtobin (~nidsut-tomdun) -* `/sys/vane/ames`: @belisarius222 (~rovnys-ricfer) & @joemfb (~master-morzod) +* `/sys/arvo`: @joemfb (~master-morzod) +* `/sys/vane/ames`: @belisarius222 (~rovnys-ricfer) & @philipcmonk (~wicdev-wisryt) * `/sys/vane/behn`: @belisarius222 (~rovnys-ricfer) -* `/sys/vane/clay`: @philipcmonk (~wicdev-wisryt) -* `/sys/vane/dill`: @bernardodelaplaz (~rigdyn-sondur) +* `/sys/vane/clay`: @philipcmonk (~wicdev-wisryt) & @belisarius222 (~rovnys-ricfer) +* `/sys/vane/dill`: @joemfb (~master-morzod) * `/sys/vane/eyre`: @eglaysher (~littel-ponnys) -* `/sys/vane/ford`: @belisarius222 (~rovnys-ricfer) & @eglaysher (~littel-ponnys) -* `/sys/vane/gall`: @jtobin (~nidsut-tomdun) -* `/sys/vane/jael`: @fang- (~palfun-foslup) & @joemfb (~master-morzod) +* `/sys/vane/gall`: @philipcmonk (~wicdev-wisryt) +* `/sys/vane/jael`: @fang- (~palfun-foslup) & @philipcmonk (~wicdev-wisryt) * `/app/acme`: @joemfb (~master-morzod) * `/app/dns`: @joemfb (~master-morzod) -* `/app/hall`: @fang- (~palfun-foslup) -* `/app/talk`: @fang- (~palfun-foslup) * `/app/aqua`: @philipcmonk (~wicdev-wisryt) +* `/app/hood`: @belisarius222 (~rovnys-ricfer) +* `/lib/hood/drum`: @philipcmonk (~wicdev-wisryt) +* `/lib/hood/kiln`: @philipcmonk (~wicdev-wisryt) * `/lib/test`: @eglaysher (~littel-ponnys) ## Contributing diff --git a/pkg/arvo/app/chat-hook.hoon b/pkg/arvo/app/chat-hook.hoon index fc8d546ec8..e6aa7e2e5e 100644 --- a/pkg/arvo/app/chat-hook.hoon +++ b/pkg/arvo/app/chat-hook.hoon @@ -1,4 +1,4 @@ -:: chat-hook: +:: chat-hook [landscape]: :: mirror chat data from foreign to local based on read permissions :: allow sending chat messages to foreign paths based on write perms :: @@ -114,7 +114,7 @@ i.syncs ?> ?=(^ pax) ?. =('~' i.pax) - $(syncs t.syncs) + $(syncs t.syncs) =/ new-path=path t.pax =. synced.old diff --git a/pkg/arvo/app/chat-store.hoon b/pkg/arvo/app/chat-store.hoon index 3004a1c5ff..5118babbcc 100644 --- a/pkg/arvo/app/chat-store.hoon +++ b/pkg/arvo/app/chat-store.hoon @@ -1,4 +1,6 @@ -:: chat-store: data store that holds linear sequences of chat messages +:: chat-store [landscape]: +:: +:: data store that holds linear sequences of chat messages :: /+ store=chat-store, default-agent, verb, dbug, group-store ~% %chat-store-top ..is ~ diff --git a/pkg/arvo/app/chat-view.hoon b/pkg/arvo/app/chat-view.hoon index 5f0bfb18a3..f12a64edbc 100644 --- a/pkg/arvo/app/chat-view.hoon +++ b/pkg/arvo/app/chat-view.hoon @@ -1,4 +1,6 @@ -:: chat-view: sets up chat JS client, paginates data, and combines commands +:: chat-view [landscape]: +:: +:: sets up chat JS client, paginates data, and combines commands :: into semantic actions for the UI :: /- *permission-store, @@ -157,7 +159,7 @@ (on-arvo:def wire sign-arvo) :: ++ on-save !>(state) - ++ on-load + ++ on-load |= old-vase=vase ^- (quip card _this) =/ old ((soft state-0) q.old-vase) @@ -211,8 +213,8 @@ ?- -.act %create ?> ?=(^ app-path.act) - ?> ?| =(+:group-path.act app-path.act) - =(~(tap in members.act) ~) + ?> ?| =(+:group-path.act app-path.act) + =(~(tap in members.act) ~) == ?^ (chat-scry app-path.act) ~& %chat-already-exists @@ -295,6 +297,7 @@ ~[(chat-hook-poke %add-synced ship.act app-path.act ask-history.act)] =/ rid=resource (de-path:resource ship+app-path.act) + ?: =(our.bol entity.rid) ~ =/ =cage :- %group-update !> ^- action:group-store diff --git a/pkg/arvo/app/clock.hoon b/pkg/arvo/app/clock.hoon index 74af060986..c4db5f62de 100644 --- a/pkg/arvo/app/clock.hoon +++ b/pkg/arvo/app/clock.hoon @@ -1,4 +1,6 @@ -:: clock: deprecated, should be removed +:: clock [landscape]: +:: +:: deprecated, should be removed :: /+ *server, default-agent, verb, dbug =, format diff --git a/pkg/arvo/app/contact-hook.hoon b/pkg/arvo/app/contact-hook.hoon index acb3b95724..4bc66de485 100644 --- a/pkg/arvo/app/contact-hook.hoon +++ b/pkg/arvo/app/contact-hook.hoon @@ -1,4 +1,5 @@ -:: contact-hook: +:: contact-hook [landscape] +:: :: /- group-hook, *contact-hook, @@ -54,7 +55,7 @@ =/ old !<(versioned-state old-vase) =| cards=(list card) |^ - |- ^- (quip card _this) + |- ^- (quip card _this) ?: ?=(%3 -.old) [cards this(state old)] ?: ?=(%2 -.old) @@ -80,7 +81,7 @@ %_ $ -.old %2 :: - synced.old + synced.old %- malt %+ turn ~(tap by synced.old) @@ -126,7 +127,7 @@ %json (poke-json:cc !<(json vase)) :: - %contact-action + %contact-action (poke-contact-action:cc !<(contact-action vase)) :: %contact-hook-action @@ -149,7 +150,7 @@ %kick [(kick:cc wire) this] %watch-ack =^ cards state - (watch-ack:cc wire p.sign) + (watch-ack:cc wire p.sign) [cards this] :: %fact @@ -301,8 +302,8 @@ [%pass /group %agent [our.bol %group-store] %watch /groups]~ :: [%contacts @ *] - =/ wir - ?: =(%ship i.t.wir) + =/ wir + ?: =(%ship i.t.wir) wir (migrate wir) ?> ?=([%contacts @ @ *] wir) diff --git a/pkg/arvo/app/contact-store.hoon b/pkg/arvo/app/contact-store.hoon index 50b6f9cfb7..6089cae80c 100644 --- a/pkg/arvo/app/contact-store.hoon +++ b/pkg/arvo/app/contact-store.hoon @@ -1,4 +1,6 @@ -:: contact-store: data store that holds group-based contact data +:: contact-store [landscape]: +:: +:: data store that holds group-based contact data :: /+ *contact-json, default-agent, dbug |% @@ -253,7 +255,7 @@ ++ send-diff |= [pax=path upd=contact-update] ^- (list card) - :~ :* + :~ :* %give %fact ~[/all /updates [%contacts pax]] %contact-update !>(upd) diff --git a/pkg/arvo/app/contact-view.hoon b/pkg/arvo/app/contact-view.hoon index 417b754524..9fab4648c8 100644 --- a/pkg/arvo/app/contact-view.hoon +++ b/pkg/arvo/app/contact-view.hoon @@ -1,4 +1,6 @@ -:: contact-view: sets up contact JS client and combines commands +:: contact-view [landscape]: +:: +:: sets up contact JS client and combines commands :: into semantic actions for the UI :: /- diff --git a/pkg/arvo/app/file-server.hoon b/pkg/arvo/app/file-server.hoon index 7969a37296..ddeb7b3d90 100644 --- a/pkg/arvo/app/file-server.hoon +++ b/pkg/arvo/app/file-server.hoon @@ -1,3 +1,7 @@ +:: file-server [landscape]: +:: +:: mounts HTTP endpoints for Landscape (and third-party) user applications +:: /- srv=file-server, glob /+ *server, default-agent, verb, dbug |% @@ -218,8 +222,8 @@ :: [~ %html] %. file - %* . html-response:gen - cache + %* . html-response:gen + cache !=(/app/landscape/index/html (slag 3 scry-path)) == == diff --git a/pkg/arvo/app/glob.hoon b/pkg/arvo/app/glob.hoon index cdaff45f24..951c95e616 100644 --- a/pkg/arvo/app/glob.hoon +++ b/pkg/arvo/app/glob.hoon @@ -1,7 +1,11 @@ +:: glob [landscape]: +:: +:: prompts content delivery and Gall state storage for Landscape JS blob +:: /- glob /+ default-agent, verb, dbug |% -++ hash 0v6.8fpt6.7mcjg.nb019.df3fo.haav6 +++ hash 0v4.kdc52.27is2.c7mnh.7vsrb.ij4jo +$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))] +$ all-states $% state-0 diff --git a/pkg/arvo/app/graph-pull-hook.hoon b/pkg/arvo/app/graph-pull-hook.hoon new file mode 100644 index 0000000000..5971ef9482 --- /dev/null +++ b/pkg/arvo/app/graph-pull-hook.hoon @@ -0,0 +1,48 @@ +/- *resource +/+ store=graph-store, graph, default-agent, verb, dbug, pull-hook +~% %graph-pull-hook-top ..is ~ +|% ++$ card card:agent:gall +++ config + ^- config:pull-hook + :* %graph-store + update:store + %graph-update + %graph-push-hook + == +-- +:: +%- agent:dbug +^- agent:gall +%- (agent:pull-hook config) +^- (pull-hook:pull-hook config) +|_ =bowl:gall ++* this . + def ~(. (default-agent this %|) bowl) + dep ~(. (default:pull-hook this config) bowl) +:: +++ on-init on-init:def +++ on-save !>(~) +++ on-load on-load:def +++ on-poke on-poke:def +++ on-peek on-peek:def +++ on-arvo on-arvo:def +++ on-fail on-fail:def +++ on-agent on-agent:def +++ on-watch on-watch:def +++ on-leave on-leave:def +++ on-pull-nack + |= [=resource =tang] + ^- (quip card _this) + :_ this + =- [%pass /pull-nack %agent [our.bowl %graph-store] %poke %graph-update -]~ + !> ^- update:store + [%0 now.bowl [%archive-graph resource]] +:: +++ on-pull-kick + |= =resource + ^- (unit path) + =/ maybe-time (peek-update-log:graph resource) + ?~ maybe-time `/ + `/(scot %da u.maybe-time) +-- diff --git a/pkg/arvo/app/graph-push-hook.hoon b/pkg/arvo/app/graph-push-hook.hoon new file mode 100644 index 0000000000..12cd8cd46b --- /dev/null +++ b/pkg/arvo/app/graph-push-hook.hoon @@ -0,0 +1,123 @@ +/+ store=graph-store +/+ met=metadata +/+ res=resource +/+ graph +/+ group +/+ default-agent +/+ dbug +/+ push-hook +~% %graph-push-hook-top ..is ~ +|% ++$ card card:agent:gall +++ config + ^- config:push-hook + :* %graph-store + /updates + update:store + %graph-update + %graph-pull-hook + == +:: ++$ agent (push-hook:push-hook config) +:: +++ is-member + |= [=resource:res =bowl:gall] + ^- ? + =/ grp ~(. group bowl) + =/ group-paths (groups-from-resource:met [%graph (en-path:res resource)]) + ?~ group-paths %.n + (is-member:grp src.bowl i.group-paths) +-- +:: +%- agent:dbug +^- agent:gall +%- (agent:push-hook config) +^- agent +|_ =bowl:gall ++* this . + def ~(. (default-agent this %|) bowl) + grp ~(. group bowl) + gra ~(. graph bowl) +:: +++ on-init on-init:def +++ on-save !>(~) +++ on-load on-load:def +++ on-poke on-poke:def +++ on-agent on-agent:def +++ on-watch on-watch:def +++ on-leave on-leave:def +++ on-peek on-peek:def +++ on-arvo on-arvo:def +++ on-fail on-fail:def +:: +++ should-proxy-update + |= =vase + ^- ? + =/ =update:store !<(update:store vase) + ?- -.q.update + %add-graph (is-member resource.q.update bowl) + %remove-graph (is-member resource.q.update bowl) + %add-nodes (is-member resource.q.update bowl) + %remove-nodes (is-member resource.q.update bowl) + %add-signatures (is-member resource.uid.q.update bowl) + %remove-signatures (is-member resource.uid.q.update bowl) + %archive-graph (is-member resource.q.update bowl) + %unarchive-graph %.n + %add-tag %.n + %remove-tag %.n + %keys %.n + %tags %.n + %tag-queries %.n + %run-updates (is-member resource.q.update bowl) + == +:: +++ resource-for-update + |= =vase + ^- (unit resource:res) + =/ =update:store !<(update:store vase) + ?- -.q.update + %add-graph `resource.q.update + %remove-graph `resource.q.update + %add-nodes `resource.q.update + %remove-nodes `resource.q.update + %add-signatures `resource.uid.q.update + %remove-signatures `resource.uid.q.update + %archive-graph `resource.q.update + %unarchive-graph ~ + %add-tag ~ + %remove-tag ~ + %keys ~ + %tags ~ + %tag-queries ~ + %run-updates `resource.q.update + == +:: +++ initial-watch + |= [=path =resource:res] + ^- vase + ?> (is-member resource bowl) + !> ^- update:store + ?~ path + :: new subscribe + :: + (get-graph:gra resource) + :: resubscribe + :: + =/ =time (slav %da i.path) + =/ =update-log:store (get-update-log-subset:gra resource time) + [%0 now.bowl [%run-updates resource update-log]] +:: +++ take-update + |= =vase + ^- [(list card) agent] + =/ =update:store !<(update:store vase) + ?+ -.q.update [~ this] + %remove-graph + :_ this + [%give %kick ~[resource+(en-path:res resource.q.update)] ~]~ + :: + %archive-graph + :_ this + [%give %kick ~[resource+(en-path:res resource.q.update)] ~]~ + == +-- diff --git a/pkg/arvo/app/graph-store.hoon b/pkg/arvo/app/graph-store.hoon index 16b18bb632..bdfabe7449 100644 --- a/pkg/arvo/app/graph-store.hoon +++ b/pkg/arvo/app/graph-store.hoon @@ -1,3 +1,6 @@ +:: graph-store [landscape] +:: +:: /+ store=graph-store, sigs=signatures, res=resource, default-agent, dbug ~% %graph-store-top ..is ~ |% @@ -282,7 +285,7 @@ ?~ index graph =* atom i.index =/ =node:store - ~| "node does not exist to add signatures to!" + ~| "node does not exist to add signatures to!" (need (get:orm graph atom)) :: last index in list :: @@ -327,7 +330,7 @@ ?~ index graph =* atom i.index =/ =node:store - ~| "node does not exist to add signatures to!" + ~| "node does not exist to add signatures to!" (need (get:orm graph atom)) :: last index in list :: @@ -529,6 +532,15 @@ ^- [index:store node:store] [(snoc index atom) node] == + :: + [%x %update-log-subset @ @ @ @ ~] + =/ =ship (slav %p i.t.t.path) + =/ =term i.t.t.t.path + =/ start=(unit time) (slaw %da i.t.t.t.t.path) + =/ end=(unit time) (slaw %da i.t.t.t.t.t.path) + =/ update-log=(unit update-log:store) (~(get by update-logs) [ship term]) + ?~ update-log [~ ~] + ``noun+!>((subset:orm-log u.update-log start end)) :: [%x %update-log @ @ ~] =/ =ship (slav %p i.t.t.path) @@ -543,7 +555,7 @@ =/ update-log=(unit update-log:store) (~(get by update-logs) [ship term]) ?~ update-log [~ ~] =/ result=(unit [time update:store]) - (peek:orm-log:store u.update-log) + (peek:orm-log:store u.update-log) ?~ result [~ ~] ``noun+!>([~ -.u.result]) == diff --git a/pkg/arvo/app/group-hook.hoon b/pkg/arvo/app/group-hook.hoon index 4b2d7a2204..5a29752e35 100644 --- a/pkg/arvo/app/group-hook.hoon +++ b/pkg/arvo/app/group-hook.hoon @@ -1,4 +1,6 @@ -:: group-hook: allow syncing group data from foreign paths to local paths +:: group-hook [landscape]: +:: +:: allow syncing group data from foreign paths to local paths :: /- *group, hook=group-hook, *invite-store /+ default-agent, verb, dbug, store=group-store, grpl=group, pull-hook, push-hook, resource @@ -58,7 +60,7 @@ :: ignore duplicate publish groups ?: =(4 (lent path)) ~& "ignoring: {}" - ~ + ~ =/ pax=^path ?: =('~' i.path) t.path diff --git a/pkg/arvo/app/group-pull-hook.hoon b/pkg/arvo/app/group-pull-hook.hoon index 5a8c639c13..4d6da63891 100644 --- a/pkg/arvo/app/group-pull-hook.hoon +++ b/pkg/arvo/app/group-pull-hook.hoon @@ -1,5 +1,6 @@ -:: group-hook: allow syncing group data from foreign paths to local paths +:: group-hook [landscape]: :: +:: allow syncing group data from foreign paths to local paths :: /- *group, hook=group-hook, *invite-store, *resource /+ default-agent, verb, dbug, store=group-store, grpl=group, pull-hook diff --git a/pkg/arvo/app/group-push-hook.hoon b/pkg/arvo/app/group-push-hook.hoon index 443889e883..7e5215fa02 100644 --- a/pkg/arvo/app/group-push-hook.hoon +++ b/pkg/arvo/app/group-push-hook.hoon @@ -1,5 +1,6 @@ -:: group-hook: allow syncing group data from foreign paths to local paths +:: group-hook [landscape]: :: +:: allow syncing group data from foreign paths to local paths :: /- *group, hook=group-hook, *invite-store /+ default-agent, verb, dbug, store=group-store, grpl=group, push-hook, diff --git a/pkg/arvo/app/group-store.hoon b/pkg/arvo/app/group-store.hoon index bec2254017..57552866af 100644 --- a/pkg/arvo/app/group-store.hoon +++ b/pkg/arvo/app/group-store.hoon @@ -1,4 +1,6 @@ -:: group-store: Store groups of ships +:: group-store [landscape]: +:: +:: Store groups of ships :: :: group-store stores groups of ships, so that resources in other apps can be :: associated with a group. The current model of group-store rolls @@ -128,7 +130,7 @@ ^- [resource group] =/ members=(set ship) (~(got by groups.old) pax) - =| =invite:policy + =| =invite:policy ?> ?=(^ pax) =/ rid=resource (resource-from-old-path t.pax) @@ -149,7 +151,7 @@ |= pax=path =/ members (~(got by groups.old) pax) - =| =invite:policy + =| =invite:policy =/ rid=resource (resource-from-old-path pax) =/ =tags @@ -239,7 +241,7 @@ (~(has in members.group) ship) == %open - ?! ?| + ?! ?| (~(has in banned.policy) ship) (~(has in ban-ranks.policy) (clan:title ship)) == @@ -285,7 +287,7 @@ ^- resource ?> ?=([@ @ *] path) :- (slav %p i.path) - i.t.path + i.t.path :: ++ add-new |= =permission:permission-store @@ -293,7 +295,7 @@ ?: ?=(%black kind.permission) [~ ~ [%open ~ who.permission] %.y] [who.permission ~ [%invite ~] %.y] - :: + :: ++ update-existing |= =permission:permission-store |= =group diff --git a/pkg/arvo/app/hood.hoon b/pkg/arvo/app/hood.hoon index 1bc70ed80d..a816843c0e 100644 --- a/pkg/arvo/app/hood.hoon +++ b/pkg/arvo/app/hood.hoon @@ -2,7 +2,7 @@ /+ drum=hood-drum, helm=hood-helm, kiln=hood-kiln |% +$ state - $: %9 + $: %10 drum=state:drum helm=state:helm kiln=state:kiln @@ -12,6 +12,7 @@ [ver=?(%1 %2 %3 %4 %5 %6) lac=(map @tas fin-any-state)] [%7 drum=state:drum helm=state:helm kiln=state:kiln] [%8 drum=state:drum helm=state:helm kiln=state:kiln] + [%9 drum=state:drum helm=state:helm kiln=state:kiln] == +$ any-state-tuple $: drum=any-state:drum diff --git a/pkg/arvo/app/invite-hook.hoon b/pkg/arvo/app/invite-hook.hoon index 2e4cef97b9..0e7b93d97a 100644 --- a/pkg/arvo/app/invite-hook.hoon +++ b/pkg/arvo/app/invite-hook.hoon @@ -1,4 +1,6 @@ -:: invite-hook: receive invites from any source +:: invite-hook [landscape]: +:: +:: receive invites from any source :: :: only handles %invite actions. accepts json, but only from the host team. :: can be poked by the host team to send an invite out to someone. diff --git a/pkg/arvo/app/invite-store.hoon b/pkg/arvo/app/invite-store.hoon index f08bb6cbf4..c09bd4cbae 100644 --- a/pkg/arvo/app/invite-store.hoon +++ b/pkg/arvo/app/invite-store.hoon @@ -1,3 +1,4 @@ +:: invite-store [landscape] /+ *invite-json, default-agent, dbug |% +$ card card:agent:gall diff --git a/pkg/arvo/app/invite-view.hoon b/pkg/arvo/app/invite-view.hoon index 666ed54730..72f3a0cc5a 100644 --- a/pkg/arvo/app/invite-view.hoon +++ b/pkg/arvo/app/invite-view.hoon @@ -1,3 +1,7 @@ +:: invite-view [landscape]: +:: +:: deprecated +:: /+ default-agent ^- agent:gall |_ =bowl:gall diff --git a/pkg/arvo/app/landscape/index.html b/pkg/arvo/app/landscape/index.html index 8ba549f11b..17031a9338 100644 --- a/pkg/arvo/app/landscape/index.html +++ b/pkg/arvo/app/landscape/index.html @@ -23,7 +23,7 @@
- + diff --git a/pkg/arvo/app/launch.hoon b/pkg/arvo/app/launch.hoon index 2eb3b3e675..ee0b23964b 100644 --- a/pkg/arvo/app/launch.hoon +++ b/pkg/arvo/app/launch.hoon @@ -1,3 +1,7 @@ +:: launch [landscape]: +:: +:: registers Landscape (and third party) applications, tiles +:: /+ store=launch-store, default-agent, dbug |% +$ card card:agent:gall diff --git a/pkg/arvo/app/link-listen-hook.hoon b/pkg/arvo/app/link-listen-hook.hoon index 9fae8d98b7..b9cd262669 100644 --- a/pkg/arvo/app/link-listen-hook.hoon +++ b/pkg/arvo/app/link-listen-hook.hoon @@ -1,644 +1,46 @@ -:: link-listen-hook: get your friends' bookmarks +:: link-listen-hook: no longer in use :: -:: keeps track of a listening=(set app-path). users can manually add to and -:: remove from this set. -:: -:: for all ships in groups associated with those resources, we subscribe to -:: their link's local-pages and annotations at the resource path (through -:: link-proxy-hook), and forward all entries into our link-store as -:: submissions and comments. -:: -:: if a subscription to a target fails, we assume it's because their -:: metadata+groups definition hasn't been updated to include us yet. -:: we retry with exponential backoff, maxing out at one hour timeouts. -:: to expede this process, we prod other potential listeners when we add -:: them to our metadata+groups definition. -:: -:: -/- listen-hook=link-listen-hook, *metadata-store, *group, *link -/+ mdl=metadata, default-agent, verb, dbug, group-store, grpl=group, resource, store=link-store +/+ default-agent, verb, dbug :: ~% %link-listen-hook-top ..is ~ |% +$ versioned-state - $% [%0 state-0] - [%1 state-1] - [%2 state-2] - [%3 state-3] + $% [%0 *] + [%1 *] + [%2 *] + [%3 *] + [%4 ~] == -+$ state-3 state-1 -+$ state-2 state-1 -+$ state-1 - $: listening=(set app-path) - state-0 - == -+$ state-0 - $: retry-timers=(map target @dr) - :: reasoning: the resources we're subscribed to, - :: and the groups that cause that. - :: - :: we don't strictly need to track this in state, but doing so heavily - :: simplifies logic and reduces the amount of big scries we do. - :: this also gives us the option to check & restore subscriptions, - :: should we ever need that. - :: - reasoning=(jug [ship app-path] group-path) - == -:: -+$ what-target ?(%local-pages %annotations) -+$ target - $: what=what-target - who=ship - where=path - == -++ wire-to-target - |= =wire - ^- target - ?> ?=([what-target @ ^] wire) - [i.wire (slav %p i.t.wire) t.t.wire] -++ target-to-wire - |= target - ^- wire - [what (scot %p who) where] -:: +$ card card:agent:gall -- :: -=| [%3 state-3] +=| [%4 ~] =* state - :: %- agent:dbug %+ verb | ^- agent:gall -=< - |_ =bowl:gall - +* this . - do ~(. +> bowl) - def ~(. (default-agent this %|) bowl) - :: - ++ on-init - ^- (quip card _this) - :_ this - ~[watch-metadata:do watch-groups:do] - :: - ++ on-save !>(state) - ++ on-load - |= =vase - ^- (quip card _this) - =/ old=versioned-state - !<(versioned-state vase) - =| cards=(list card) - |- - =* upgrade-loop $ - ?- -.old - %3 [cards this(state old)] - :: - %2 - :_ this(state [%3 +.old]) - %+ welp cards - :~ [%pass /groups %agent [our.bowl %group-store] %leave ~] - watch-groups:do - == - :: - %1 - :: the upgrade from 0 left out local-only collections. - :: here, we pull those back in. - :: - =. listening.old - (~(run in ~(key by reasoning.old)) tail) - =/ resources=(list [=group-path =app-path]) - %~ tap in - %. %link - %~ get ju - .^ (jug app-name [group-path app-path]) - %gy - (scot %p our.bowl) - %metadata-store - (scot %da now.bowl) - /app-indices - == - |- - ?~ resources - upgrade-loop(old [%2 +.old]) - =, i.resources - =/ members=(set ship) - (members-from-path:grp:do group-path) - :: if we're the only group member, this got incorrectly ignored - :: during 0's upgrade logic. watch it now. - :: - ?. &(=(1 ~(wyt in members)) (~(has in members) our.bowl)) - $(resources t.resources) - =^ more-cards state - (handle-listen-action:do %watch app-path) - $(resources t.resources, cards (weld more-cards cards)) - :: - %0 - =/ listening=(set app-path) - (~(run in ~(key by reasoning.old)) tail) - $(old [%1 listening +.old]) - == - :: - ++ on-agent - |= [=wire =sign:agent:gall] - ^- (quip card _this) - =^ cards state - ?+ wire ~|([dap.bowl %weird-agent-wire wire] !!) - [%metadata ~] - (take-metadata-sign:do sign) - :: - [%groups ~] - (take-groups-sign:do sign) - :: - [%links ?(%local-pages %annotations) @ ^] - (take-link-sign:do (wire-to-target t.wire) sign) - :: - [%forward ^] - (take-forward-sign:do t.wire sign) - :: - [%prod *] - ?> ?=(%poke-ack -.sign) - ?~ p.sign [~ state] - %- (slog leaf+"prod failed" u.p.sign) - [~ state] - == - [cards this] - :: - ++ on-poke - |= [=mark =vase] - ?+ mark (on-poke:def mark vase) - %link-listen-poke - =/ =path !<(path vase) - :_ this - %+ weld - (take-retry:do %local-pages src.bowl path) - (take-retry:do %annotations src.bowl path) - :: - %link-listen-action - ?> (team:title [our src]:bowl) - =^ cards state - ~| p.vase - (handle-listen-action:do !<(action:listen-hook vase)) - [cards this] - == - :: - ++ on-arvo - |= [=wire =sign-arvo] - ^- (quip card _this) - ?+ sign-arvo (on-arvo:def wire sign-arvo) - [%g %done *] - ?~ error.sign-arvo [~ this] - =/ =tank leaf+"{(trip dap.bowl)}'s message went wrong!" - %- (slog tank tang.u.error.sign-arvo) - [~ this] - :: - [%b %wake *] - ?> ?=([%retry @ @ ^] wire) - ?^ error.sign-arvo - =/ =tank leaf+"wake on {(spud wire)} went wrong!" - %- (slog tank u.error.sign-arvo) - [~ this] - :_ this - (take-retry:do (wire-to-target t.wire)) - == - :: - ++ on-peek - |= =path - ^- (unit (unit cage)) - ?+ path ~ - [%x %listening ~] ``noun+!>(listening) - [%x %listening ^] ``noun+!>((~(has in listening) t.t.path)) - == - :: - ++ on-watch - |= =path - ^- (quip card _this) - ?. ?=([%listening ~] path) (on-watch:def path) - ?> (team:title [our src]:bowl) - :_ this - [%give %fact ~ %link-listen-update !>([%listening listening])]~ - :: - ++ on-leave on-leave:def - ++ on-fail on-fail:def - -- -:: -:: |_ =bowl:gall -+* md ~(. mdl bowl) -++ grp ~(. grpl bowl) ++* this . + def ~(. (default-agent this %|) bowl) :: -:: user actions & updates -:: -++ handle-listen-action - |= =action:listen-hook - ^- (quip card _state) - ::NOTE no-opping where appropriate happens further down the call stack. - :: we *could* no-op here, as %watch when we're already listening should - :: result in no-ops all the way down, but walking through everything - :: makes this a nice "resurrect if broken unexpectedly" option. - :: - =* app-path path.action - =^ cards listening - ^- (quip card _listening) - =/ had=? (~(has in listening) app-path) - ?- -.action - %watch - :_ (~(put in listening) app-path) - ?:(had ~ [(send-update action)]~) - :: - %leave - :_ (~(del in listening) app-path) - ?.(had ~ [(send-update action)]~) - == - =/ groups=(list group-path) - (groups-from-resource:md %link app-path) - |- - ?~ groups [cards state] - =^ more-cards state - ?- -.action - %watch (listen-to-group app-path i.groups) - %leave (leave-from-group app-path i.groups) - == - $(cards (weld cards more-cards), groups t.groups) -:: -++ send-update - |= =update:listen-hook +++ on-init [~ this] +++ on-save !>(state) +++ on-load + |= =vase + ^- (quip card _this) + :_ this + :- [%pass /groups %agent [our.bowl %group-store] %leave ~] + %+ turn ~(tap in ~(key by wex.bowl)) + |= [=wire =ship =term] ^- card - [%give %fact ~[/listening] %link-listen-update !>(update)] + [%pass wire %agent [ship term] %leave ~] :: -:: metadata subscription -:: -++ watch-metadata - ^- card - [%pass /metadata %agent [our.bowl %metadata-store] %watch /app-name/link] -:: -++ take-metadata-sign - |= =sign:agent:gall - ^- (quip card _state) - ?- -.sign - %poke-ack ~|([dap.bowl %unexpected-poke-ack /metadata] !!) - %kick [[watch-metadata]~ state] - :: - %watch-ack - ?~ p.sign [~ state] - =/ =tank - :- %leaf - "{(trip dap.bowl)} failed subscribe to metadata store. very wrong!" - %- (slog tank u.p.sign) - [~ state] - :: - %fact - =* mark p.cage.sign - =* vase q.cage.sign - ?. ?=(%metadata-update mark) - ~| [dap.bowl %unexpected-mark mark] - !! - %- handle-metadata-update - !<(metadata-update vase) - == -:: -++ handle-metadata-update - |= upd=metadata-update - ^- (quip card _state) - ?+ -.upd [~ state] - %add - ?> =(%link app-name.resource.upd) - :: auto-listen to collections in unmanaged groups only - :: - =/ rid=resource - (de-path:resource group-path.upd) - =/ =group - (need (scry-group:grp rid)) - ?. hidden.group - [~ state] - =, resource.upd - =^ update listening - ^- (quip card _listening) - ?: (~(has in listening) app-path) - [~ listening] - :- [(send-update %watch app-path)]~ - (~(put in listening) app-path) - =^ cards state - (listen-to-group app-path group-path.upd) - [(weld update cards) state] - :: - %remove - ?> =(%link app-name.resource.upd) - =? listening - ?=(~ (groups-from-resource:md %link app-path.resource.upd)) - (~(del in listening) app-path.resource.upd) - (leave-from-group app-path.resource.upd group-path.upd) - == -:: -:: groups subscriptions -:: -++ watch-groups - ^- card - [%pass /groups %agent [our.bowl %group-store] %watch /groups] -:: -++ take-groups-sign - |= =sign:agent:gall - ^- (quip card _state) - ?- -.sign - %poke-ack ~|([dap.bowl %unexpected-poke-ack /groups] !!) - %kick [[watch-groups]~ state] - :: - %watch-ack - ?~ p.sign [~ state] - =/ =tank - :- %leaf - "{(trip dap.bowl)} failed subscribe to groups. very wrong!" - %- (slog tank u.p.sign) - [~ state] - :: - %fact - =* mark p.cage.sign - =* vase q.cage.sign - ?+ mark ~|([dap.bowl %unexpected-mark mark] !!) - %group-initial [~ state] ::NOTE initial handled using metadata - %group-update (handle-group-update !<(update:group-store vase)) - == - == -:: -++ handle-group-update - |= upd=update:group-store - ^- (quip card _state) - ?. ?=(?(%add-members %initial-group %remove-members) -.upd) - [~ state] - =/ =path - (en-path:resource resource.upd) - =/ socs=(list app-path) - (app-paths-from-group:md %link path) - =/ whos=(list ship) - ?- -.upd - %add-members ~(tap in ships.upd) - %remove-members ~(tap in ships.upd) - %initial-group ~(tap in members.group.upd) - == - =| cards=(list card) - |- - =* loop-socs $ - ?~ socs [cards state] - ?. (~(has in listening) i.socs) - loop-socs(socs t.socs) - |- - =* loop-whos $ - ?~ whos loop-socs(socs t.socs) - =^ caz state - ?. ?=(%remove-members -.upd) - (listen-to-peer i.socs path i.whos) - ?: =(our.bowl i.whos) - (handle-listen-action %leave i.socs) - (leave-from-peer i.socs path i.whos) - loop-whos(whos t.whos, cards (weld cards caz)) -:: -:: link subscriptions -:: -++ listen-to-group - |= [=app-path =group-path] - ^- (quip card _state) - =/ peers=(list ship) - ~| group-path - %~ tap in - (members-from-path:grp group-path) - =| cards=(list card) - |- - ?~ peers [cards state] - =^ caz state - (listen-to-peer app-path group-path i.peers) - $(peers t.peers, cards (weld cards caz)) -:: -++ leave-from-group - |= [=app-path =group-path] - ^- (quip card _state) - =/ peers=(list ship) - %~ tap in - (members-from-path:grp group-path) - =| cards=(list card) - |- - ?~ peers [cards state] - =^ caz state - (leave-from-peer app-path group-path i.peers) - $(peers t.peers, cards (weld cards caz)) -:: -++ listen-to-peer - |= [=app-path =group-path who=ship] - ^- (quip card _state) - ?: =(our.bowl who) - [~ state] - :_ =- state(reasoning -) - (~(put ju reasoning) [who app-path] group-path) - :- (prod-other-listener who app-path) - ?^ (~(get ju reasoning) [who app-path]) - ~ - (start-link-subscriptions who app-path) -:: -++ leave-from-peer - |= [=app-path =group-path who=ship] - ^- (quip card _state) - ?: =(our.bowl who) - [~ state] - =. reasoning (~(del ju reasoning) [who app-path] group-path) - ::NOTE leaving is always safe, so we just do it unconditionally - (end-link-subscriptions who app-path) -:: -++ start-link-subscriptions - |= [=ship =app-path] - ^- (list card) - :~ (start-link-subscription %local-pages ship app-path) - (start-link-subscription %annotations ship app-path) - == -:: -++ start-link-subscription - |= =target - ^- card - :* %pass - [%links (target-to-wire target)] - %agent - [who.target %link-proxy-hook] - %watch - ?- what.target - %local-pages [what where]:target - %annotations [what %$ where]:target - == - == -:: -++ end-link-subscriptions - |= [who=ship where=path] - ^- (quip card _state) - =. retry-timers (~(del by retry-timers) [%local-pages who where]) - =. retry-timers (~(del by retry-timers) [%annotations who where]) - :_ state - |^ ~[(end %local-pages) (end %annotations)] - :: - ++ end - |= what=what-target - :* %pass - [%links (target-to-wire what who where)] - %agent - [who %link-proxy-hook] - %leave - ~ - == - -- -:: -++ prod-other-listener - |= [who=ship where=path] - ^- card - :* %pass - [%prod (scot %p who) where] - %agent - [who %link-listen-hook] - %poke - %link-listen-poke - !>(where) - == -:: -++ take-link-sign - |= [=target =sign:agent:gall] - ^- (quip card _state) - ?- -.sign - %poke-ack ~|([dap.bowl %unexpected-poke-ack /links target] !!) - %kick [[(start-link-subscription target)]~ state] - :: - %watch-ack - ?~ p.sign - =. retry-timers (~(del by retry-timers) target) - [~ state] - :: our subscription request got rejected, - :: most likely because our group definition is out of sync with theirs. - :: set timer for retry. - :: - (start-retry target) - :: - %fact - =* mark p.cage.sign - =* vase q.cage.sign - ?+ mark ~|([dap.bowl %unexpected-mark mark] !!) - %link-initial - %- handle-link-initial - [who.target where.target !<(initial:store vase)] - :: - %link-update - %- handle-link-update - [who.target where.target !<(update:store vase)] - == - == -:: -++ start-retry - |= =target - ^- (quip card _state) - =/ timer=@dr - %+ min ~h1 - %+ mul 2 - (~(gut by retry-timers) target ~s15) - =. retry-timers - (~(put by retry-timers) target timer) - :_ state - :_ ~ - :* %pass - [%retry (target-to-wire target)] - [%arvo %b %wait (add now.bowl timer)] - == -:: -++ take-retry - |= =target - ^- (list card) - :: relevant: whether :who is still associated with resource :where - :: - =; relevant=? - ?. relevant ~ - [(start-link-subscription target)]~ - ?. (~(has in listening) where.target) - | - ?: %- ~(has by wex.bowl) - [[%links (target-to-wire target)] who.target %link-proxy-hook] - | - %+ lien (groups-from-resource:md %link where.target) - |= =group-path - ^- ? - %. who.target - ~(has in (members-from-path:grp group-path)) - -:: -++ do-link-action - |= [=wire =action:store] - ^- card - :* %pass - wire - %agent - [our.bowl %link-store] - %poke - %link-action - !>(action) - == -:: -++ handle-link-initial - |= [who=ship where=path =initial:store] - ^- (quip card _state) - ?> =(src.bowl who) - ?+ -.initial ~|([dap.bowl %unexpected-initial -.initial] !!) - %local-pages - =/ =pages (~(got by pages.initial) where) - (handle-link-update who where [%local-pages where pages]) - :: - %annotations - =/ urls=(list [=url =notes]) - ~(tap by (~(got by notes.initial) where)) - =| cards=(list card) - |- ^- (quip card _state) - ?~ urls [cards state] - =^ caz state - ^- (quip card _state) - =, i.urls - (handle-link-update who where [%annotations where url notes]) - $(urls t.urls, cards (weld cards caz)) - == -:: -++ handle-link-update - |= [who=ship where=path =update:store] - ^- (quip card _state) - ?> =(src.bowl who) - :_ state - ?+ -.update ~|([dap.bowl %unexpected-update -.update] !!) - %local-pages - %+ turn pages.update - |= =page - %+ do-link-action - [%forward %local-page (scot %p who) where] - [%hear where who page] - :: - %annotations - %+ turn notes.update - |= =note - ^- card - %+ do-link-action - `wire`[%forward %annotation (scot %p who) where] - `action:store`[%read where url.update `comment`[who note]] - == -:: -++ take-forward-sign - |= [=wire =sign:agent:gall] - ^- (quip card _state) - ~| [%unexpected-sign on=[%forward wire] -.sign] - ?> ?=(%poke-ack -.sign) - ?~ p.sign [~ state] - =/ =tank - :- %leaf - ;: weld - (trip dap.bowl) - " failed to save submission from " - (spud wire) - == - %- (slog tank u.p.sign) - [~ state] -:: -++ scry-for - |* [=mold =app-name =path] - .^ mold - %gx - (scot %p our.bowl) - app-name - (scot %da now.bowl) - (snoc `^path`path %noun) - == +++ on-agent on-agent:def +++ on-poke on-poke:def +++ on-arvo on-arvo:def +++ on-peek on-peek:def +++ on-watch on-watch:def +++ on-leave on-leave:def +++ on-fail on-fail:def -- diff --git a/pkg/arvo/app/link-proxy-hook.hoon b/pkg/arvo/app/link-proxy-hook.hoon index 98f61f6f8c..9557d0f185 100644 --- a/pkg/arvo/app/link-proxy-hook.hoon +++ b/pkg/arvo/app/link-proxy-hook.hoon @@ -1,337 +1,46 @@ -:: link-proxy-hook: make local pages available to foreign ships +:: link-proxy-hook: no longer in use :: -:: this is a "proxy" style hook, relaying foreign subscriptions into local -:: stores if permission conditions are met. -:: the patterns herein should one day be generalized into a proxy-hook lib. -:: -:: this uses metadata-store to discover resources and their associated -:: groups. it sets the permission condition to be that a ship must be in a -:: group associated with the resource it's subscribing to. -:: we check this on-watch, but also subscribe to metadata & groups so that -:: we can kick subscriptions if needed (eg ship removed from group). -:: -:: we deduplicate incoming subscriptions on the same path, ensuring we have -:: exactly one local subscription per unique incoming subscription path. -:: this comes at the cost of assuming that the store's initial response is -:: whatever's returned by the scry at that path, but perhaps that should -:: become part of the stores standard anyway. -:: -:: when adding support for new paths, the only things you'll likely want -:: to touch are +permitted, +initial-response, & +kick-proxies. -:: -/- *link, *metadata-store, *group -/+ metadata, default-agent, verb, dbug, group-store, grpl=group, - resource, store=link-store +/+ default-agent, verb, dbug ~% %link-proxy-hook-top ..is ~ |% -+$ state-0 - $: %0 - ::TODO we use this to detect "first sub started" and "last sub left", - :: but can't we use [wex sup]:bowl for that? - active=(map path (set ship)) - == -+$ state-1 - $: %1 - active=(map path (set ship)) - == -:: +$ versioned-state - $% state-0 - state-1 + $% [%0 *] + [%1 *] + [%2 ~] == :: +$ card card:agent:gall -- :: -=| state-1 +=| [%2 ~] =* state - :: %- agent:dbug %+ verb | ^- agent:gall -=< - |_ =bowl:gall - +* this . - do ~(. +> bowl) - def ~(. (default-agent this %&) bowl) - :: - ++ on-init - ^- (quip card _this) - :_ this - ~[watch-groups:do watch-metadata:do] - :: - ++ on-save !>(state) - ++ on-load - |= old-vase=vase - ^- (quip card _this) - =/ old - !<(versioned-state old-vase) - ?- -.old - %1 [~ this(state old)] - :: - %0 - :_ this(state [%1 +.old]) - :~ [%pass /groups %agent [our.bowl %group-store] %leave ~] - watch-groups:do - == - == - - :: - ++ on-watch - |= =path - ^- (quip card _this) - :: the local ship should just use link-store directly - ::TODO do we want to allow this anyway, to avoid client-side target checks? - :: - ?< (team:title [our src]:bowl) - ?> (permitted:do src.bowl path) - =^ cards state - (start-proxy:do src.bowl path) - [cards this] - :: - ++ on-leave - |= =path - ^- (quip card _this) - =^ cards state - (stop-proxy:do src.bowl path) - [cards this] - :: - ++ on-agent - |= [=wire =sign:agent:gall] - ^- (quip card _this) - ?: ?=([%groups ~] wire) - =^ cards state - (take-groups-sign:do sign) - [cards this] - ?: ?=([%proxy ^] wire) - =^ cards state - (handle-proxy-sign t.wire sign) - [cards this] - ~| [dap.bowl %weird-wire wire] - !! - :: - ++ on-poke on-poke:def - ++ on-peek on-peek:def - ++ on-arvo on-arvo:def - ++ on-fail on-fail:def - -- -:: |_ =bowl:gall -+* md ~(. metadata bowl) - grp ~(. grpl bowl) ++* this . + def ~(. (default-agent this %&) bowl) :: -:: permissions +++ on-init on-init:def +++ on-save !>(state) +++ on-load + |= old-vase=vase + ^- (quip card _this) + =/ paths + %+ turn ~(val by sup.bowl) + |=([=ship =path] path) + :_ this + :- [%pass /groups %agent [our.bowl %group-store] %leave ~] + ?~ paths ~ + [%give %kick paths ~]~ :: -++ permitted - |= [who=ship =path] - ^- ? - :: we only expose /local-pages and /annotations, - :: to ships in the groups associated with the resource. - :: (no url-specific annotations subscriptions, either.) - :: - =/ target=(unit ^path) - ?: ?=([%local-pages ^] path) - `t.path - ?: ?=([%annotations ~ ^] path) - `t.t.path - ~ - ?~ target | - %+ lien (groups-from-resource:md %link u.target) - |= =group-path - ^- ? - (~(has in (members-from-path:grp group-path)) who) -:: -++ kick-revoked-permissions - |= [=path who=(list ship)] - ^- (list card) - %+ murn who - |= =ship - ^- (unit card) - :: no need to remove to ourselves - :: - ?: =(our.bowl ship) ~ - ?: (permitted ship path) ~ - `(kick-proxies ship path) -:: -:: metadata subscription -:: -++ watch-metadata - ^- card - [%pass /metadata %agent [our.bowl %metadata-store] %watch /app-name/link] -:: -++ take-metadata-sign - |= =sign:agent:gall - ^- (quip card _state) - ?- -.sign - %poke-ack ~|([dap.bowl %unexpected-poke-ack /metadata] !!) - %kick [[watch-metadata]~ state] - :: - %watch-ack - ?~ p.sign [~ state] - =/ =tank - :- %leaf - "{(trip dap.bowl)} failed subscribe to metadata store. very wrong!" - %- (slog tank u.p.sign) - [~ state] - :: - %fact - =* mark p.cage.sign - =* vase q.cage.sign - ?. ?=(%metadata-update mark) - ~| [dap.bowl %unexpected-mark mark] - !! - %- handle-metadata-update - !<(metadata-update vase) - == -:: -++ handle-metadata-update - |= upd=metadata-update - ^- (quip card _state) - :_ state - ?. ?=(%remove -.upd) ~ - ?> =(%link app-name.resource.upd) - :: if a group is no longer associated with a resource, - :: we need to re-check permissions for everyone in that group. - :: - %+ kick-revoked-permissions - app-path.resource.upd - %~ tap in - (members-from-path:grp group-path.upd) -:: -:: groups subscription -::TODO largely copied from link-listen-hook. maybe make a store-listener lib? -:: -++ watch-groups - ^- card - [%pass /groups %agent [our.bowl %group-store] %watch /groups] -:: -++ take-groups-sign - |= =sign:agent:gall - ^- (quip card _state) - ?- -.sign - %poke-ack ~|([dap.bowl %unexpected-poke-ack /groups] !!) - %kick [[watch-groups]~ state] - :: - %watch-ack - ?~ p.sign [~ state] - =/ =tank - :- %leaf - "{(trip dap.bowl)} failed subscribe to group store. very wrong!" - %- (slog tank u.p.sign) - [~ state] - :: - %fact - =* mark p.cage.sign - =* vase q.cage.sign - ?+ mark ~|([dap.bowl %unexpected-mark mark] !!) - %group-initial [~ state] - %group-update (handle-group-update !<(update:group-store vase)) - == - == -:: -++ handle-group-update - |= upd=update:group-store - ^- (quip card _state) - :_ state - ?. ?=(%remove-members -.upd) ~ - :: if someone was removed from a group, find all link resources associated - :: with that group, then kick their subscriptions if they're no longer - :: - %- zing - %+ turn (app-paths-from-group:md %link (en-path:resource resource.upd)) - |= =app-path - ^- (list card) - %+ kick-revoked-permissions - app-path - ~(tap in ships.upd) -:: -:: proxy subscriptions -:: -++ kick-proxies - |= [who=ship =path] - ^- card - =- [%give %kick - `who] - :~ [%local-pages path] - [%annotations %$ path] - == -:: -++ handle-proxy-sign - |= [=wire =sign:agent:gall] - ^- (quip card _state) - ?- -.sign - %poke-ack ~|([dap.bowl %unexpected-poke-ack wire] !!) - %fact [[%give %fact ~[wire] cage.sign]~ state] - %kick [[(proxy-pass-link-store wire %watch wire)]~ state] - :: - %watch-ack - ?~ p.sign [~ state] - =/ =tank - :- %leaf - "{(trip dap.bowl)} failed subscribe to link-store. very wrong!" - %- (slog tank u.p.sign) - [~ state] - == -:: -++ proxy-pass-link-store - |= [=path =task:agent:gall] - ^- card - :* %pass - [%proxy path] - %agent - [our.bowl %link-store] - task - == -:: -++ initial-response - |= =path - ^- card - =; =initial:store - [%give %fact ~ %link-initial !>(initial)] - ?+ path !! - [%local-pages ^] - [%local-pages (scry-for (map ^path pages) %link-store path)] - :: - [%annotations %$ ^] - [%annotations (scry-for (per-path-url notes) %link-store path)] - == -:: -++ start-proxy - |= [who=ship =path] - ^- (quip card _state) - :_ state(active (~(put ju active) path who)) - :_ ~ - :: if we already have a local subscription open, - :: - ?. =(~ (~(get ju active) path)) - :: gather the initial response ourselves, and send that. - :: - (initial-response path) - :: else, open a local subscription, - :: sending outward its initial response when we hear it. - :: - (proxy-pass-link-store path %watch path) -:: -++ stop-proxy - |= [who=ship =path] - ^- (quip card _state) - =. active (~(del ju active) path who) - :_ state - :: if there are still subscriptions remaining, do nothing. - :: - ?. =(~ (~(get ju active) path)) ~ - :: else, close the local subscription. - :: - [(proxy-pass-link-store path %leave ~)]~ -:: -:: helpers -:: -++ scry-for - |* [=mold =app-name =path] - .^ mold - %gx - (scot %p our.bowl) - app-name - (scot %da now.bowl) - (snoc `^path`path %noun) - == +++ on-watch on-watch:def +++ on-leave on-leave:def +++ on-agent on-agent:def +++ on-poke on-poke:def +++ on-peek on-peek:def +++ on-arvo on-arvo:def +++ on-fail on-fail:def -- + diff --git a/pkg/arvo/app/link-store.hoon b/pkg/arvo/app/link-store.hoon index fed3d1206a..6dfcc4ab8a 100644 --- a/pkg/arvo/app/link-store.hoon +++ b/pkg/arvo/app/link-store.hoon @@ -1,4 +1,6 @@ -:: link: social bookmarking +:: link [landscape]: +:: +:: social bookmarking :: :: the paths under which links are submitted are generally expected to :: correspond to existing group paths. for strictly-local collections of @@ -50,10 +52,12 @@ :: ? :: /seen/wood-url/some-path have we seen this here :: -/- *link -/+ store=link-store, default-agent, verb, dbug +/- *link, gra=graph-store, *resource +/+ store=link-store, graph-store, default-agent, verb, dbug :: |% ++$ state-any $%(state-1 state-0) ++$ state-1 [%1 ~] +$ state-0 $: %0 by-group=(map path links) @@ -76,414 +80,107 @@ +$ card card:agent:gall -- :: -=| state-0 +=| state-1 =* state - :: %- agent:dbug %+ verb | ^- agent:gall -=< - |_ =bowl:gall - +* this . - do ~(. +> bowl) - def ~(. (default-agent this %|) bowl) - :: - ++ on-init on-init:def - ++ on-save !>(state) - ++ on-load - |= old=vase - ^- (quip card _this) - [~ this(state !<(state-0 old))] - :: - ++ on-poke - |= [=mark =vase] - ^- (quip card _this) - ?> (team:title [our src]:bowl) ::TODO /lib/store - =^ cards state - ?+ mark (on-poke:def mark vase) - ::TODO move json conversion into mark once mark performance improves - %json (do-action:do (action:dejs:store !<(json vase))) - %link-action (do-action:do !<(action:store vase)) - == - [cards this] - :: - ++ on-peek - |= =path - ^- (unit (unit cage)) - ?+ path (on-peek:def path) - [%y ?(%local-pages %submissions) ~] - ``noun+!>(~(key by by-group)) - :: - [%x %local-pages *] - ``noun+!>((get-local-pages:do t.t.path)) - :: - [%x %submissions *] - ``noun+!>((get-submissions:do t.t.path)) - :: - [%y ?(%annotations %discussions) *] - =/ [spath=^path surl=url] - (break-discussion-path:store t.t.path) - =- ``noun+!>(-) - :: - ?: =(~ surl) - :: no url, provide urls that have comments - :: - ^- (set url) - ?~ spath - :: no path, find urls accross all paths - :: - %- ~(rep by discussions) - |= [[* discussions=(map url discussion)] urls=(set url)] - %- ~(uni in urls) - ~(key by discussions) - :: specified path, find urls for that specific path - :: - %~ key by - (~(gut by discussions) spath *(map url *)) - :: specified url and path, nothing to list here - :: - ?^ spath !! - :: no path, find paths with comments for this url - :: - ^- (set ^path) - %- ~(rep by discussions) - |= [[=^path urls=(map url discussion)] paths=(set ^path)] - ?. (~(has by urls) surl) paths - (~(put in paths) path) - :: - [%x %annotations *] - ``noun+!>((get-annotations:do t.t.path)) - :: - [%x %discussions *] - ``noun+!>((get-discussions:do t.t.path)) - :: - [%x %seen @ ^] - ``noun+!>((is-seen:do t.t.path)) - :: - [%x %unseen ~] - ``noun+!>(get-all-unseen:do) - :: - [%x %unseen ^] - ``noun+!>((get-unseen:do t.t.path)) - == - :: - ++ on-watch - |= =path - ^- (quip card _this) - ?> (team:title [our src]:bowl) ::TODO /lib/store - :_ this - |^ ?+ path (on-watch:def path) - [%local-pages *] - %+ give %link-initial - ^- initial:store - [%local-pages (get-local-pages:do t.path)] - :: - [%submissions *] - %+ give %link-initial - ^- initial:store - [%submissions (get-submissions:do t.path)] - :: - [%annotations *] - %+ give %link-initial - ^- initial:store - [%annotations (get-annotations:do t.path)] - :: - [%discussions *] - %+ give %link-initial - ^- initial:store - [%discussions (get-discussions:do t.path)] - :: - [%seen ~] - ~ - == - :: - ++ give - |* [=mark =noun] - ^- (list card) - [%give %fact ~ mark !>(noun)]~ - :: - ++ give-single - |* [=mark =noun] - ^- card - [%give %fact ~ mark !>(noun)] - -- - :: - ++ on-leave on-leave:def - ++ on-agent on-agent:def - ++ on-arvo on-arvo:def - ++ on-fail on-fail:def - -- -:: |_ =bowl:gall ++* this . + do ~(. +> bowl) + def ~(. (default-agent this %|) bowl) :: -:: writing -:: -++ do-action - |= =action:store - ^- (quip card _state) - ?- -.action - %save (save-page +.action) - %note (note-note +.action) - %seen (seen-submission +.action) +++ on-init on-init:def +++ on-save !>(state) +++ on-load + |= old=vase + ^- (quip card _this) + =/ s !<(state-any old) + ?: ?=(%1 -.s) + [~ this(state s)] :: - %hear (hear-submission +.action) - %read (read-comment +.action) - == -:: +save-page: save a page ourselves -:: -++ save-page - |= [=path title=@t =url] - ^- (quip card _state) - ?< |(=(~ path) =(~ title) =(~ url)) - :: add page to group ours - :: - =/ =links (~(gut by by-group) path *links) - =/ =page [title url now.bowl] - =. ours.links [page ours.links] - =. by-group (~(put by by-group) path links) - :: do generic submission logic - :: - =^ submission-cards state - (hear-submission path [our.bowl page]) - :: mark page as seen (because we submitted it ourselves) - :: - =^ seen-cards state - (seen-submission path `url) - :: send updates to subscribers - :: - :_ state - :_ (weld submission-cards seen-cards) - :+ %give %fact - :+ :~ /local-pages - [%local-pages path] - == - %link-update - !>([%local-pages path [page]~]) -:: +note-note: save a note for a url -:: -++ note-note - |= [=path =url udon=@t] - ^- (quip card _state) - ?< |(=(~ path) =(~ url) =(~ udon)) - :: add note to discussion ours - :: - =/ urls (~(gut by discussions) path *(map ^url discussion)) - =/ =discussion (~(gut by urls) url *discussion) - =/ =note [now.bowl udon] - =. ours.discussion [note ours.discussion] - =. urls (~(put by urls) url discussion) - =. discussions (~(put by discussions) path urls) - :: do generic comment logic - :: - =^ cards state - (read-comment path url [our.bowl note]) - :: send updates to subscribers - :: - :_ state + :_ this(state *state-1) + =/ orm orm:graph-store + |^ ^- (list card) + %- zing + %+ turn ~(tap by by-group.s) + |= [=path =links] ^- (list card) - :_ cards - :+ %give %fact - :+ :~ /annotations - [%annotations %$ path] - [%annotations (build-discussion-path:store url)] - [%annotations (build-discussion-path:store path url)] + ?. ?=([@ @ *] path) + (on-bad-path path links) + =/ =resource [(slav %p i.path) i.t.path] + :_ [(archive-graph resource)]~ + %+ add-graph resource + ^- graph:gra + %+ gas:orm ~ + =/ comments (~(gut by discussions.s) path *(map url discussion)) + %+ turn submissions.links + |= sub=submission + ^- [atom node:gra] + :- time.sub + =/ contents ~[text+title.sub url+url.sub] + =/ parent-hash `@ux`(sham ~ ship.sub time.sub contents) + :- ^- post:gra + :* author=ship.sub + index=~[time.sub] + time-sent=time.sub + contents + hash=`parent-hash + signatures=~ == - %link-update - !>([%annotations path url [note]~]) -:: +seen-submission: mark url as seen/read -:: -:: if no url specified, all under path are marked as read -:: -++ seen-submission - |= [=path murl=(unit url)] - ^- (quip card _state) - =/ =links (~(gut by by-group) path *links) - :: new: urls we want to, but haven't yet, marked as seen + ^- internal-graph:gra + =/ dis (~(get by comments) url.sub) + ?~ dis + [%empty ~] + :- %graph + ^- graph:gra + %+ gas:orm ~ + %+ turn comments.u.dis + |= [=ship =time udon=@t] + ^- [atom node:gra] + :- time + :_ `internal-graph:gra`[%empty ~] + =/ contents ~[text+udon] + :* author=ship + index=~[time.sub time] + time-sent=time + contents + hash=``@ux`(sham `parent-hash ship time contents) + signatures=~ + == :: - =/ new=(set url) - %. seen.links - %~ dif in - ^- (set url) - ?^ murl (sy ~[u.murl]) - %- ~(gas in *(set url)) - %+ turn submissions.links - |=(submission url) - ?: =(~ new) [~ state] - =. seen.links (~(uni in seen.links) new) - :_ state(by-group (~(put by by-group) path links)) - [%give %fact ~[/seen] %link-update !>([%observation path new])]~ -:: +hear-submission: record page someone else saved -:: -++ hear-submission - |= [=path =submission] - ^- (quip card _state) - ?< =(~ path) - :: add link to group submissions + ++ on-bad-path + |= [=path =links] + ^- (list card) + ~| discarding-malformed-links+[path links] + ~ :: - =/ =links (~(gut by by-group) path *links) - =^ added submissions.links - ?: ?=(^ (find ~[submission] submissions.links)) - [| submissions.links] - :- & - (submissions:merge:store submissions.links ~[submission]) - =. by-group (~(put by by-group) path links) - :: add submission to global sites + ++ add-graph + |= [=resource =graph:gra] + ^- card + %- poke-graph-store + [%0 now.bowl %add-graph resource graph `%graph-validator-link] :: - =/ =site (site-from-url:store url.submission) - =. by-site (~(add ja by-site) site [path submission]) - :: send updates to subscribers + ++ archive-graph + |= =resource + ^- card + %- poke-graph-store + [%0 now.bowl %archive-graph resource] :: - :_ state - ?. added ~ - :_ ~ - :+ %give %fact - :+ :~ /submissions - [%submissions path] - == - %link-update - !>([%submissions path [submission]~]) -:: +read-comment: record a comment someone else made -:: -++ read-comment - |= [=path =url =comment] - ^- (quip card _state) - :: add comment to url's discussion - :: - =/ urls (~(gut by discussions) path *(map ^url discussion)) - =/ =discussion (~(gut by urls) url *discussion) - =^ added comments.discussion - ?: ?=(^ (find ~[comment] comments.discussion)) - [| comments.discussion] - :- & - (comments:merge:store comments.discussion ~[comment]) - =. urls (~(put by urls) url discussion) - =. discussions (~(put by discussions) path urls) - :: send updates to subscribers - :: - :_ state - ?. added ~ - :_ ~ - :+ %give %fact - :+ :~ /discussions - [%discussions '' path] - [%discussions (build-discussion-path:store url)] - [%discussions (build-discussion-path:store path url)] - == - %link-update - !>([%discussions path url [comment]~]) -:: -:: reading -:: -++ get-local-pages - |= =path - ^- (map ^path pages) - ?~ path - :: all paths - :: - %- ~(run by by-group) - |=(links ours) - :: specific path - :: - %+ ~(put by *(map ^path pages)) path - ours:(~(gut by by-group) path *links) -:: -++ get-submissions - |= =path - ^- (map ^path submissions) - ?~ path - :: all paths - :: - %- ~(run by by-group) - |=(links submissions) - :: specific path - :: - %+ ~(put by *(map ^path submissions)) path - submissions:(~(gut by by-group) path *links) -:: -++ get-all-unseen - ^- (jug path url) - %- ~(rut by by-group) - |= [=path *] - (get-unseen path) -:: -++ get-unseen - |= =path - ^- (set url) - =/ =links - (~(gut by by-group) path *links) - %- ~(gas in *(set url)) - %+ murn submissions.links - |= submission - ?: (~(has in seen.links) url) ~ - (some url) -:: -++ is-seen - |= =path - ^- ? - =/ [=^path =url] - (break-discussion-path:store path) - %. url - %~ has in - seen:(~(gut by by-group) path *links) -:: -:: -++ get-annotations - |= =path - ^- (per-path-url notes) - =/ args=[=^path =url] - (break-discussion-path:store path) - |^ ?~ path - :: all paths - :: - (~(run by discussions) get-ours) - :: specific path - :: - %+ ~(put by *(per-path-url notes)) path.args - %- get-ours - %+ ~(gut by discussions) path.args - *(map url discussion) - :: - ++ get-ours - |= m=(map url discussion) - ^- (map url notes) - ?: =(~ url.args) - :: all urls - :: - %- ~(run by m) - |=(discussion ours) - :: specific url - :: - %+ ~(put by *(map url notes)) url.args - ours:(~(gut by m) url.args *discussion) + ++ poke-graph-store + |= =update:gra + ^- card + :* %pass /migrate-link %agent [our.bowl %graph-store] + %poke %graph-update !>(update) + == -- :: -++ get-discussions - |= =path - ^- (per-path-url comments) - =/ args=[=^path =url] - (break-discussion-path:store path) - |^ ?~ path - :: all paths - :: - (~(run by discussions) get-comments) - :: specific path - :: - %+ ~(put by *(per-path-url comments)) path.args - %- get-comments - %+ ~(gut by discussions) path.args - *(map url discussion) - :: - ++ get-comments - |= m=(map url discussion) - ^- (map url comments) - ?: =(~ url.args) - :: all urls - :: - %- ~(run by m) - |=(discussion comments) - :: specific url - :: - %+ ~(put by *(map url comments)) url.args - comments:(~(gut by m) url.args *discussion) - -- +++ on-poke on-poke:def +++ on-peek on-peek:def +++ on-watch on-watch:def +++ on-leave on-leave:def +++ on-agent on-agent:def +++ on-arvo on-arvo:def +++ on-fail on-fail:def -- diff --git a/pkg/arvo/app/link-view.hoon b/pkg/arvo/app/link-view.hoon index eaeefae07c..0850862124 100644 --- a/pkg/arvo/app/link-view.hoon +++ b/pkg/arvo/app/link-view.hoon @@ -1,626 +1,39 @@ -:: link-view: frontend endpoints -:: -:: endpoints, mapping onto link-store's paths. p is for page as in pagination. -:: only the /0/submissions endpoint provides updates. -:: as with link-store, urls are expected to use +wood encoding. -:: -:: /json/0/submissions initial + updates for all -:: /json/[p]/submissions/[collection] page for one collection -:: /json/[p]/discussions/[wood-url]/[collection] page for url in collection -:: /json/[n]/submission/[wood-url]/[collection] nth matching submission -:: /json/seen mark-as-read updates -:: -/- *link, view=link-view -/- *invite-store, group-store -/- listen-hook=link-listen-hook -/- group-hook, permission-hook, permission-group-hook -/- metadata-hook, contact-view -/- pull-hook, *group -/+ store=link-store, metadata, *server, default-agent, verb, dbug, grpl=group -/+ group-store, resource +:: link-view: no longer in use +/+ default-agent, verb, dbug ~% %link-view-top ..is ~ -:: -:: |% +$ versioned-state - $% state-0 - state-1 - == -+$ state-0 - $: %0 - ~ - == -:: -+$ state-1 - $: %1 - ~ + $% [%0 ~] + [%1 ~] + [%2 ~] == :: +$ card card:agent:gall -- :: -=| state-1 +=| [%2 ~] =* state - :: %+ verb | %- agent:dbug ^- agent:gall -=< - |_ =bowl:gall - +* this . - do ~(. +> bowl) - def ~(. (default-agent this %|) bowl) - :: - ++ on-init - ^- (quip card _this) - :_ this - :~ [%pass /submissions %agent [our.bowl %link-store] %watch /submissions] - [%pass /discussions %agent [our.bowl %link-store] %watch /discussions] - [%pass /seen %agent [our.bowl %link-store] %watch /seen] - :: - =+ [%invite-action !>([%create /link])] - [%pass /invitatory/create %agent [our.bowl %invite-store] %poke -] - :: - =+ /invitatory/link - [%pass - %agent [our.bowl %invite-store] %watch -] - :* %pass /srv %agent [our.bowl %file-server] - %poke %file-server-action - !>([%serve-dir /'~link' /app/landscape %.n %.y]) - == - == - :: - ++ on-save !>(state) - ++ on-load - |= old-vase=vase - ^- (quip card _this) - =/ old !<(versioned-state old-vase) - ?- -.old - %1 [~ this] - %0 - :_ this(state [%1 ~]) - :- [%pass /connect %arvo %e %disconnect [~ /'~link']] - :~ :* %pass /srv %agent [our.bowl %file-server] - %poke %file-server-action - !>([%serve-dir /'~link' /app/landscape %.n %.y]) - == == - == - :: - ++ on-poke - |= [=mark =vase] - ^- (quip card _this) - ?> (team:title our.bowl src.bowl) - :_ this - ?+ mark (on-poke:def mark vase) - %link-action - [(handle-action:do !<(action:store vase)) ~] - :: - %link-view-action - (handle-view-action:do !<(action:view vase)) - == - :: - ++ on-watch - |= =path - ^- (quip card _this) - ?: ?=([%json %seen ~] path) - [~ this] - ?: ?=([%tile ~] path) - :_ this - ~[give-tile-data:do] - ?. ?=([%json @ @ *] path) - (on-watch:def path) - =/ p=@ud (slav %ud i.t.path) - ?+ t.t.path (on-watch:def path) - [%submissions ~] - :_ this - (give-initial-submissions:do p ~) - :: - [%submissions ^] - :_ this - (give-initial-submissions:do p t.t.t.path) - :: - [%submission @ ^] - :_ this - (give-specific-submission:do p (break-discussion-path:store t.t.t.path)) - :: - [%discussions @ ^] - :_ this - (give-initial-discussions:do p (break-discussion-path:store t.t.t.path)) - == - :: - ++ on-agent - |= [=wire =sign:agent:gall] - ^- (quip card _this) - ?+ -.sign (on-agent:def wire sign) - %poke-ack - ?. ?=([%join-group @ @ @ @ ~] wire) - (on-agent:def wire sign) - ?^ p.sign - (on-agent:def wire sign) - =/ rid=resource - (de-path:resource t.t.wire) - =/ host=ship - (slav %p i.t.wire) - :_ this - (joined-group:do host rid) - :: - %kick - :_ this - =/ app=term - ?: ?=([%invites *] wire) - %invite-store - %link-store - [%pass wire %agent [our.bowl app] %watch wire]~ - :: - %fact - =* mark p.cage.sign - =* vase q.cage.sign - ?+ mark (on-agent:def wire sign) - %invite-update [(handle-invite-update:do !<(invite-update vase)) this] - %link-initial [~ this] - :: - %link-update - :_ this - :- (send-update:do !<(update:store vase)) - ?: =(/discussions wire) ~ - ~[give-tile-data:do] - == - == - :: - ++ on-arvo - |= [=wire =sign-arvo] - ^- (quip card _this) - ?. ?=([%e %bound *] sign-arvo) - (on-arvo:def wire sign-arvo) - ~? !accepted.sign-arvo - [dap.bowl "bind rejected!" binding.sign-arvo] - [~ this] - :: - ++ on-peek on-peek:def - ++ on-leave on-leave:def - ++ on-fail on-fail:def - -- -:: -~% %link-view-logic ..card ~ |_ =bowl:gall -+* md ~(. metadata bowl) - grp ~(. grpl bowl) ++* this . + def ~(. (default-agent this %|) bowl) :: -++ page-size 25 -++ get-paginated - |* [page=(unit @ud) list=(list)] - ^- [total=@ud pages=@ud page=_list] - =/ l=@ud (lent list) - :+ l - %+ add (div l page-size) - (min 1 (mod l page-size)) - ?~ page list - %+ swag - [(mul u.page page-size) page-size] - list +++ on-init [~ this] +++ on-save !>(state) +++ on-load + |= old-vase=vase + ^- (quip card _this) + :_ this(state [%2 ~]) + [%pass /connect %arvo %e %disconnect [~ /'~link']]~ :: -++ page-to-json - =, enjs:format - |* $: page-number=@ud - [total-items=@ud total-pages=@ud page=(list)] - item-to-json=$-(* json) - == - ^- json - %- pairs - :~ 'totalItems'^(numb total-items) - 'totalPages'^(numb total-pages) - 'pageNumber'^(numb page-number) - 'page'^a+(turn page item-to-json) - == -++ do-poke - |= [app=term =mark =vase] - ^- card - [%pass /create/[app]/[mark] %agent [our.bowl app] %poke mark vase] -:: -++ joined-group - |= [host=ship rid=resource] - ^- (list card) - =/ =path - (en-path:resource rid) - :~ - :: sync the group - :: - %^ do-poke %group-pull-hook - %pull-hook-action - !> ^- action:pull-hook - [%add host rid] - :: - :: sync the metadata - :: - %^ do-poke %metadata-hook - %metadata-hook-action - !> ^- metadata-hook-action:metadata-hook - [%add-synced host path] - :: - :: sync the collection - :: - %^ do-poke %link-listen-hook - %link-listen-action - !> ^- action:listen-hook - [%watch ~[name.rid]] - == -:: -++ handle-invite-update - |= upd=invite-update - ^- (list card) - ?. ?=(%accepted -.upd) ~ - ?. =(/link path.upd) ~ - =/ rid=resource - (de-path:resource path.invite.upd) - :~ :: add self - :* %pass - [%join-group (scot %p ship.invite.upd) path.invite.upd] - %agent [entity.rid %group-push-hook] - %poke %group-update - !> ^- action:group-store - [%add-members rid (sy our.bowl ~)] - == == -:: -++ handle-action - |= =action:store - ^- card - [%pass /action %agent [our.bowl %link-store] %poke %link-action !>(action)] -:: -++ handle-view-action - |= act=action:view - ^- (list card) - ?- -.act - %create (handle-create +.act) - %delete (handle-delete +.act) - %invite (handle-invite +.act) - == -:: -++ handle-create - |= [=path title=@t description=@t members=create-members:view real-group=?] - ^- (list card) - =/ group-path=^path - ?- -.members - %group path.members - :: - %ships - [%ship (scot %p our.bowl) path] - == - =; group-setup=(list card) - %+ weld group-setup - :~ :: add collection to metadata-store - :: - %^ do-poke %metadata-hook - %metadata-action - !> ^- metadata-action:md - :^ %add group-path - [%link path] - %* . *metadata:md - title title - description description - date-created now.bowl - creator our.bowl - == - :: - :: expose the metadata - :: - %^ do-poke %metadata-hook - %metadata-hook-action - !> ^- metadata-hook-action:metadata-hook - [%add-owned group-path] - :: - :: watch the collection ourselves - :: - %^ do-poke %link-listen-hook - %link-listen-action - !> ^- action:listen-hook - [%watch path] - == - ?: ?=(%group -.members) ~ - :: if the group is "real", make contact-view do the heavy lifting - =/ rid=resource - (de-path:resource group-path) - ?: real-group - :- %^ do-poke %contact-view - %contact-view-action - !> ^- contact-view-action:contact-view - [%groupify rid title description] - %+ turn ~(tap in ships.members) - |= =ship - ^- card - %^ do-poke %invite-hook - %invite-action - !> ^- invite-action - :^ %invite /link - (sham group-path eny.bowl) - :* our.bowl - %group-hook - group-path - ship - title - == - :: for "unmanaged" groups, do it ourselves - :: - =/ =policy - [%invite ships.members] - :* :: create the new group - :: - %^ do-poke %group-store - %group-action - !> ^- action:group-store - [%add-group rid policy %.y] - :: - :: send invites - :: - %+ turn ~(tap in ships.members) - |= =ship - ^- card - %^ do-poke %invite-hook - %invite-action - !> ^- invite-action - :^ %invite /link - (sham group-path eny.bowl) - :* our.bowl - %group-hook - group-path - ship - title - == - == -:: -++ handle-delete - |= =path - ^- (list card) - =/ groups=(list ^path) - (groups-from-resource:md [%link path]) - %- zing - %+ turn groups - |= =group=^path - =/ rid=resource - (de-path:resource group-path) - %+ snoc - ^- (list card) - :: if it's a real group, we can't/shouldn't unsync it. this leaves us with - :: no way to stop propagation of collection deletion. - :: - ?. ?=([%'~' ^] group-path) ~ - :: if it's an unmanaged group, we just stop syncing the group & metadata, - :: and clean up the group (after un-hooking it, to not push deletion). - :: - :~ %^ do-poke %group-hook - %group-hook-action - !> ^- action:group-hook - [%remove rid] - :: - %^ do-poke %metadata-hook - %metadata-hook-action - !> ^- metadata-hook-action:metadata-hook - [%remove group-path] - :: - %^ do-poke %group-store - %group-action - !> ^- action:group-store - [%remove-group rid ~] - == - :: remove collection from metadata-store - :: - %^ do-poke %metadata-store - %metadata-action - !> ^- metadata-action:md - [%remove group-path [%link path]] -:: -++ handle-invite - |= [=path ships=(set ship)] - ^- (list card) - %- zing - %+ turn (groups-from-resource:md %link path) - |= =group=^path - ^- (list card) - =/ rid=resource - (de-path:resource group-path) - =/ =group - (need (scry-group:grp rid)) - %- zing - :~ - ?. ?=(%invite -.policy.group) - ~ - :~ %^ do-poke %group-store - %group-action - !> ^- action:group-store - [%change-policy rid %invite %add-invites ships] - == - :: - %+ turn ~(tap in ships) - |= =ship - ^- card - %^ do-poke %invite-hook - %invite-action - !> ^- invite-action - :^ %invite /link - (sham group-path eny.bowl) - :* our.bowl - %group-pull-hook - group-path - ship - (rsh 3 1 (spat path)) - == - == -:: +give-tile-data: total unread count as json object -:: -::NOTE the full recalc of totals here probably isn't the end of the world. -:: but in case it is, well, here it is. -:: -++ give-tile-data - ^- card - =; =json - [%give %fact ~[/tile] %json !>(json)] - %+ frond:enjs:format 'unseen' - %- numb:enjs:format - %- %~ rep in - (scry-for (jug path url) /unseen) - |= [[=path unseen=(set url)] total=@ud] - %+ add total - ~(wyt in unseen) -:: -:: +give-initial-submissions: page of submissions on path -:: -:: for the / path, give page for every path -:: -:: result is in the shape of: { -:: "/some/path": { -:: totalItems: 1, -:: totalPages: 1, -:: pageNumber: 0, -:: page: [ -:: { commentCount: 1, ...restOfTheSubmission } -:: ] -:: }, -:: "/maybe/more": { etc } -:: } -:: -++ give-initial-submissions - ~/ %link-view-initial-submissions - |= [p=@ud =requested=path] - ^- (list card) - :_ :: only keep the base case alive (for updates), kick all others - :: - ?: &(=(0 p) ?=(~ requested-path)) ~ - [%give %kick ~ ~]~ - =; =json - [%give %fact ~ %json !>(json)] - %+ frond:enjs:format 'link-update' - %+ frond:enjs:format 'initial-submissions' - %- pairs:enjs:format - %+ turn - %~ tap by - %+ scry-for (map path submissions) - [%submissions requested-path] - |= [=path =submissions] - ^- [@t json] - :- (spat path) - =; =json - :: add unseen count - :: - ?> ?=(%o -.json) - :- %o - %+ ~(put by p.json) 'unseenCount' - %- numb:enjs:format - %~ wyt in - %+ scry-for (set url) - [%unseen path] - ?: &(=(0 p) ?=(~ requested-path)) - :: for a broad-scope initial result, only give total counts - :: - =, enjs:format - %- pairs - =+ l=(lent submissions) - :~ 'totalItems'^(numb l) - 'totalPages'^(numb (div l page-size)) - == - %^ page-to-json p - %+ get-paginated `p - submissions - |= =submission - ^- json - =/ =json (submission:enjs:store submission) - ?> ?=([%o *] json) - :: add in seen status - :: - =. p.json - %+ ~(put by p.json) 'seen' - :- %b - %+ scry-for ? - [%seen (build-discussion-path:store path url.submission)] - :: add in comment count - :: - =; comment-count=@ud - :- %o - %+ ~(put by p.json) 'commentCount' - (numb:enjs:format comment-count) - %- lent - ~| [path url.submission] - ^- comments - =- (~(got by (~(got by -) path)) url.submission) - %+ scry-for (per-path-url comments) - :- %discussions - (build-discussion-path:store path url.submission) -:: -++ give-specific-submission - |= [n=@ud =path =url] - :_ [%give %kick ~ ~]~ - =; =json - [%give %fact ~ %json !>(json)] - %+ frond:enjs:format 'link-update' - %+ frond:enjs:format 'submission' - ^- json - =; sub=(unit submission) - ?~ sub ~ - (submission:enjs:store u.sub) - =/ =submissions - =- (~(got by -) path) - %+ scry-for (map ^path submissions) - [%submissions path] - |- - ?~ submissions ~ - =* sub i.submissions - ?. =(url.sub url) - $(submissions t.submissions) - ?: =(0 n) `sub - $(n (dec n), submissions t.submissions) -:: -++ give-initial-discussions - |= [p=@ud =path =url] - ^- (list card) - :_ ?: =(0 p) ~ - [%give %kick ~ ~]~ - =; =json - [%give %fact ~ %json !>(json)] - %+ frond:enjs:format 'link-update' - %+ frond:enjs:format 'initial-discussions' - %^ page-to-json p - %+ get-paginated `p - =- (~(got by (~(got by -) path)) url) - %+ scry-for (per-path-url comments) - [%discussions (build-discussion-path:store path url)] - comment:enjs:store -:: -++ send-update - |= =update:store - ^- card - ?+ -.update ~|([dap.bowl %unexpected-update -.update] !!) - %submissions - %+ give-json - %+ frond:enjs:format 'link-update' - (update:enjs:store update) - :~ /json/0/submissions - (weld /json/0/submissions path.update) - == - :: - %discussions - %+ give-json - %+ frond:enjs:format 'link-update' - (update:enjs:store update) - :_ ~ - %+ weld /json/0/discussions - (build-discussion-path:store [path url]:update) - :: - %observation - %+ give-json - %+ frond:enjs:format 'link-update' - (update:enjs:store update) - ~[/json/seen] - == -:: -++ give-json - |= [=json paths=(list path)] - ^- card - [%give %fact paths %json !>(json)] -:: -++ scry-for - |* [=mold =path] - .^ mold - %gx - (scot %p our.bowl) - %link-store - (scot %da now.bowl) - (snoc `^path`path %noun) - == +++ on-poke on-poke:def +++ on-watch on-watch:def +++ on-agent on-agent:def +++ on-arvo on-arvo:def +++ on-peek on-peek:def +++ on-leave on-leave:def +++ on-fail on-fail:def -- diff --git a/pkg/arvo/app/metadata-hook.hoon b/pkg/arvo/app/metadata-hook.hoon index e3acbd0129..5cad893191 100644 --- a/pkg/arvo/app/metadata-hook.hoon +++ b/pkg/arvo/app/metadata-hook.hoon @@ -1,4 +1,6 @@ -:: metadata-hook: allow syncing foreign metadata +:: metadata-hook [landscape]: +:: +:: allow syncing foreign metadata :: :: watch paths: :: /group/%group-path all updates related to this group @@ -37,7 +39,7 @@ [[%pass /updates %agent [our.bowl %metadata-store] %watch /updates]~ this] :: ++ on-save !>(state) - ++ on-load + ++ on-load |= =vase =/ old !<(versioned-state vase) diff --git a/pkg/arvo/app/metadata-store.hoon b/pkg/arvo/app/metadata-store.hoon index a0e88e2e6d..0a6c52db6e 100644 --- a/pkg/arvo/app/metadata-store.hoon +++ b/pkg/arvo/app/metadata-store.hoon @@ -1,4 +1,6 @@ -:: metadata-store: data store for application metadata and mappings +:: metadata-store [landscape]: +:: +:: data store for application metadata and mappings :: between groups and resources within applications :: :: group-paths are expected to be an existing group path diff --git a/pkg/arvo/app/permission-group-hook.hoon b/pkg/arvo/app/permission-group-hook.hoon index 266666a8c0..535b54caf7 100644 --- a/pkg/arvo/app/permission-group-hook.hoon +++ b/pkg/arvo/app/permission-group-hook.hoon @@ -1,4 +1,6 @@ -:: permission-group-hook: groups into permissions +:: permission-group-hook [landscape]: +:: +:: groups into permissions :: :: mirror the ships in specified groups to specified permission paths :: diff --git a/pkg/arvo/app/permission-hook.hoon b/pkg/arvo/app/permission-hook.hoon index 7c55bdc682..ba715b64c5 100644 --- a/pkg/arvo/app/permission-hook.hoon +++ b/pkg/arvo/app/permission-hook.hoon @@ -1,4 +1,6 @@ -:: permission-hook: mirror remote permissions +:: permission-hook [landscape]: +:: +:: mirror remote permissions :: :: allows mirroring permissions between local and foreign ships. :: local permission path are exposed according to the permssion paths diff --git a/pkg/arvo/app/permission-store.hoon b/pkg/arvo/app/permission-store.hoon index be9b2923e6..0d051aac75 100644 --- a/pkg/arvo/app/permission-store.hoon +++ b/pkg/arvo/app/permission-store.hoon @@ -1,4 +1,6 @@ -:: permission-store: track black- and whitelists of ships +:: permission-store [landscape]: +:: +:: track black- and whitelists of ships :: /- *permission-store /+ default-agent, verb, dbug diff --git a/pkg/arvo/app/pool-group-hook.hoon b/pkg/arvo/app/pool-group-hook.hoon index 74e9385d32..e858112c55 100644 --- a/pkg/arvo/app/pool-group-hook.hoon +++ b/pkg/arvo/app/pool-group-hook.hoon @@ -1,4 +1,6 @@ -:: pool-group-hook: maintain groups based on invite pool +:: pool-group-hook [landscape]: +:: +:: maintain groups based on invite pool :: :: looks at our invite tree, adds our siblings to group at +group-path :: diff --git a/pkg/arvo/app/publish.hoon b/pkg/arvo/app/publish.hoon index 9761db7837..cf0654df80 100644 --- a/pkg/arvo/app/publish.hoon +++ b/pkg/arvo/app/publish.hoon @@ -1,3 +1,7 @@ +:: publish [landscape] +:: +:: stores notebooks in clay, subscribes and allow subscriptions to notebooks +:: /- *publish /- *group /- group-hook diff --git a/pkg/arvo/app/s3-store.hoon b/pkg/arvo/app/s3-store.hoon index 390ae8e7ff..5b0bf0c70c 100644 --- a/pkg/arvo/app/s3-store.hoon +++ b/pkg/arvo/app/s3-store.hoon @@ -1,3 +1,7 @@ +:: s3-store [landscape]: +:: +:: stores s3 keys for uploading and sharing images and objects +:: /- *s3 /+ s3-json, default-agent, verb, dbug ~% %s3-top ..is ~ diff --git a/pkg/arvo/app/soto.hoon b/pkg/arvo/app/soto.hoon index 98645dafde..2ca0c26202 100644 --- a/pkg/arvo/app/soto.hoon +++ b/pkg/arvo/app/soto.hoon @@ -1,5 +1,6 @@ :: -:: Soto: A Dojo relay for Urbit's Landscape interface +:: soto [landscape]: A Dojo relay for Urbit's Landscape interface +:: :: Relays sole-effects to subscribers and forwards sole-action pokes :: /- sole diff --git a/pkg/arvo/app/spider.hoon b/pkg/arvo/app/spider.hoon index a6286ba8cb..6a974f5006 100644 --- a/pkg/arvo/app/spider.hoon +++ b/pkg/arvo/app/spider.hoon @@ -1,5 +1,5 @@ /- spider -/+ libstrand=strand, default-agent, verb +/+ libstrand=strand, default-agent, verb, server =, strand=strand:libstrand |% +$ card card:agent:gall @@ -17,15 +17,25 @@ $: starting=(map yarn [=trying =vase]) running=trie tid=(map tid yarn) + serving=(map tid [@ta =mark]) == :: +$ clean-slate-any $^ clean-slate-ket $% clean-slate-sig + clean-slate-1 clean-slate == :: +$ clean-slate + $: %2 + starting=(map yarn [=trying =vase]) + running=(list yarn) + tid=(map tid yarn) + serving=(map tid [@ta =mark]) + == +:: ++$ clean-slate-1 $: %1 starting=(map yarn [=trying =vase]) running=(list yarn) @@ -133,7 +143,10 @@ sc ~(. spider-core bowl) def ~(. (default-agent this %|) bowl) :: - ++ on-init on-init:def + ++ on-init + ^- (quip card _this) + :_ this + ~[bind-eyre:sc] ++ on-save clean-state:sc ++ on-load |^ @@ -141,7 +154,9 @@ =+ !<(any=clean-slate-any old-state) =? any ?=(^ -.any) (old-to-1 any) =? any ?=(~ -.any) (old-to-1 any) - ?> ?=(%1 -.any) + =^ upgrade-cards any + (old-to-2 any) + ?> ?=(%2 -.any) :: =. tid.state tid.any =/ yarns=(list yarn) @@ -154,12 +169,26 @@ (handle-stop-thread:sc (yarn-to-tid i.yarns) |) =^ cards-2 this $(yarns t.yarns) - [(weld cards-1 cards-2) this] + [:(weld upgrade-cards cards-1 cards-2) this] :: ++ old-to-1 |= old=clean-slate-ket - ^- clean-slate + ^- clean-slate-1 1+old(starting (~(run by starting.old) |=([* v=vase] none+v))) + :: + ++ old-to-2 + |= old=clean-slate-any + ^- (quip card clean-slate) + ?> ?=(?(%1 %2) -.old) + ?: ?=(%2 -.old) + `old + :- ~[bind-eyre:sc] + :* %2 + starting.old + running.old + tid.old + ~ + == -- :: ++ on-poke @@ -172,6 +201,9 @@ %spider-input (on-poke-input:sc !<(input vase)) %spider-start (handle-start-thread:sc !<(start-args vase)) %spider-stop (handle-stop-thread:sc !<([tid ?] vase)) + :: + %handle-http-request + (handle-http-request:sc !<([@ta =inbound-request:eyre] vase)) == [cards this] :: @@ -182,6 +214,7 @@ ?+ path (on-watch:def path) [%thread @ *] (on-watch:sc t.path) [%thread-result @ ~] (on-watch-result:sc i.t.path) + [%http-response *] `state == [cards this] :: @@ -216,6 +249,7 @@ ?+ wire (on-arvo:def wire sign-arvo) [%thread @ *] (handle-sign:sc i.t.wire t.t.wire sign-arvo) [%build @ ~] (handle-build:sc i.t.wire sign-arvo) + [%bind ~] `state == [cards this] :: On unexpected failure, kill all outstanding strands @@ -228,6 +262,41 @@ -- :: |_ =bowl:gall +:: +++ bind-eyre + ^- card + [%pass /bind %arvo %e %connect [~ /spider] %spider] +:: +++ handle-http-request + |= [eyre-id=@ta =inbound-request:eyre] + ^- (quip card _state) + ?> authenticated.inbound-request + =/ url + (parse-request-line:server url.request.inbound-request) + ?> ?=([%spider @t @t @t ~] site.url) + =* input-mark i.t.site.url + =* thread i.t.t.site.url + =* output-mark i.t.t.t.site.url + =/ =tid + (scot %uv (sham eny.bowl)) + =. serving.state + (~(put by serving.state) tid [eyre-id output-mark]) + =+ .^ + =tube:clay + %cc + /(scot %p our.bowl)/[q.byk.bowl]/(scot %da now.bowl)/json/[input-mark] + == + ?> ?=(^ body.request.inbound-request) + =/ body=json + (need (de-json:html q.u.body.request.inbound-request)) + =/ input=vase + (tube !>(body)) + =/ =start-args + [~ `tid thread input] + =^ cards state + (handle-start-thread start-args) + [cards state] +:: ++ on-poke-input |= input =/ yarn (~(got by tid.state) tid) @@ -394,6 +463,25 @@ :~ [%give %fact ~[/thread-result/[tid]] %thread-fail !>([term tang])] [%give %kick ~[/thread-result/[tid]] ~] == +++ thread-http-fail + |= [=tid =term =tang] + ^- (quip card ^state) + =- (fall - `state) + %+ bind + (~(get by serving.state) tid) + |= [eyre-id=@ta output=mark] + :_ state(serving (~(del by serving.state) tid)) + %+ give-simple-payload:app:server eyre-id + ^- simple-payload:http + :_ ~ :_ ~ + ?. ?=(http-error:spider term) + ((slog tang) 500) + ?- term + %bad-request 400 + %forbidden 403 + %nonexistent 404 + %offline 504 + == :: ++ thread-fail |= [=yarn =term =tang] @@ -402,7 +490,24 @@ =/ =tid (yarn-to-tid yarn) =/ fail-cards (thread-say-fail tid term tang) =^ cards state (thread-clean yarn) - [(weld fail-cards cards) state] + =^ http-cards state (thread-http-fail tid term tang) + [:(weld fail-cards cards http-cards) state] +:: +++ thread-http-response + |= [=tid =vase] + ^- (quip card ^state) + =- (fall - `state) + %+ bind + (~(get by serving.state) tid) + |= [eyre-id=@ta output=mark] + =+ .^ + =tube:clay + %cc + /(scot %p our.bowl)/[q.byk.bowl]/(scot %da now.bowl)/[output]/json + == + :_ state(serving (~(del by serving.state) tid)) + %+ give-simple-payload:app:server eyre-id + (json-response:gen:server !<(json (tube vase))) :: ++ thread-done |= [=yarn =vase] @@ -413,8 +518,10 @@ :~ [%give %fact ~[/thread-result/[tid]] %thread-done vase] [%give %kick ~[/thread-result/[tid]] ~] == + =^ http-cards state + (thread-http-response tid vase) =^ cards state (thread-clean yarn) - [(weld done-cards cards) state] + [:(weld done-cards cards http-cards) state] :: ++ thread-clean |= =yarn @@ -474,5 +581,5 @@ :: ++ clean-state !> ^- clean-slate - 1+state(running (turn (tap-yarn running.state) head)) + 2+state(running (turn (tap-yarn running.state) head)) -- diff --git a/pkg/arvo/app/weather.hoon b/pkg/arvo/app/weather.hoon index 9c07b0faec..684956b151 100644 --- a/pkg/arvo/app/weather.hoon +++ b/pkg/arvo/app/weather.hoon @@ -1,3 +1,7 @@ +:: weather [landscape]: +:: +:: holds latlong, gets weather data from API, passes it on to subscribers +:: /+ *server, default-agent, verb, dbug =, format :: diff --git a/pkg/arvo/lib/graph-view.hoon b/pkg/arvo/lib/graph-view.hoon new file mode 100644 index 0000000000..2d0b473bbc --- /dev/null +++ b/pkg/arvo/lib/graph-view.hoon @@ -0,0 +1,61 @@ +/- sur=graph-view +/+ resource, group-store +^? +=< [sur .] +=, sur +|% +++ dejs + =, dejs:format + |% + ++ action + |^ + ^- $-(json ^action) + %- of + :~ create+create + delete+delete + join+join + leave+leave + groupify+groupify + ::invite+invite + == + :: + ++ create + %- ou + :~ resource+(un dejs:resource) + title+(un so) + description+(un so) + mark+(uf ~ (mu so)) + associated+(un associated) + == + :: + ++ leave + %- ot + :~ resource+dejs:resource + == + :: + ++ delete + %- ot + :~ resource+dejs:resource + == + :: + ++ join + %- ot + :~ resource+dejs:resource + ship+(su ;~(pfix sig fed:ag)) + == + :: + ++ groupify + %- ou + :~ resource+(un dejs:resource) + to+(uf ~ (mu dejs:resource)) + == + ++ invite !! + :: + ++ associated + %- of + :~ group+dejs:resource + policy+policy:dejs:group-store + == + -- + -- +-- diff --git a/pkg/arvo/lib/graph.hoon b/pkg/arvo/lib/graph.hoon index 395c0554fa..2e2bcdf1e4 100644 --- a/pkg/arvo/lib/graph.hoon +++ b/pkg/arvo/lib/graph.hoon @@ -13,12 +13,24 @@ :: ++ get-graph |= res=resource - ^- marked-graph:store - %+ scry-for marked-graph:store + ^- update:store + %+ scry-for update:store /graph/(scot %p entity.res)/[name.res] :: -++ peek-log +++ get-update-log + |= rid=resource + ^- update-log:store + %+ scry-for update-log:store + /update-log/(scot %p entity.rid)/[name.rid] +:: +++ peek-update-log |= res=resource ^- (unit time) (scry-for (unit time) /peek-update-log/(scot %p entity.res)/[name.res]) +:: +++ get-update-log-subset + |= [res=resource start=@da] + ^- update-log:store + %+ scry-for update-log:store + /update-log-subset/(scot %p entity.res)/[name.res]/(scot %da start)/'~' -- diff --git a/pkg/arvo/lib/hood/drum.hoon b/pkg/arvo/lib/hood/drum.hoon index e7b91cfbe2..a41c917a7c 100644 --- a/pkg/arvo/lib/hood/drum.hoon +++ b/pkg/arvo/lib/hood/drum.hoon @@ -105,6 +105,8 @@ %file-server %glob %graph-store + %graph-pull-hook + %graph-push-hook == :: ++ deft-fish :: default connects @@ -207,7 +209,7 @@ == :: ++ on-load - |= [hood-version=?(%1 %2 %3 %4 %5 %6 %7 %8 %9) old=any-state] + |= [hood-version=?(%1 %2 %3 %4 %5 %6 %7 %8 %9 %10) old=any-state] =< se-abet =< se-view =. sat old =. dev (~(gut by bin) ost *source) @@ -236,6 +238,9 @@ (se-born | %home %group-pull-hook) =? ..on-load (lte hood-version %9) (se-born | %home %graph-store) + =? ..on-load (lte hood-version %10) + => (se-born | %home %graph-push-hook) + (se-born | %home %graph-pull-hook) ..on-load :: ++ reap-phat :: ack connect diff --git a/pkg/arvo/lib/pull-hook.hoon b/pkg/arvo/lib/pull-hook.hoon index 23903fda6e..ce4954bb6c 100644 --- a/pkg/arvo/lib/pull-hook.hoon +++ b/pkg/arvo/lib/pull-hook.hoon @@ -1,3 +1,23 @@ +:: lib/pull-hook: helper for creating a push hook +:: +:: lib/pull-hook is a helper for automatically pulling data from a +:: corresponding push-hook to a store. +:: +:: ## Interfacing notes: +:: +:: The inner door may interact with the library by producing cards. +:: Do not pass any cards on a wire beginning with /helper as these +:: wires are reserved by this library. Any watches/pokes/peeks not +:: listed below will be routed to the inner door. +:: +:: ## Subscription paths +:: +:: /tracking: The set of resources we are pulling +:: +:: ## Pokes +:: +:: %pull-hook-action: Add/remove a resource from pulling. +:: /- *pull-hook /+ default-agent, resource :: @@ -5,12 +25,24 @@ |% +$ card card:agent:gall :: +:: $config: configuration for the pull hook +:: +:: .store-name: name of the store to send subscription updates to. +:: .update-mark: mark that updates will be tagged with +:: .push-hook-name: name of the corresponding push-hook +:: +$ config $: store-name=term update=mold update-mark=term push-hook-name=term == +:: +:: $state-0: state for the pull hook +:: +:: .tracking: a map of resources we are pulling, and the ships that +:: we are pulling them from. +:: .inner-state: state given to internal door :: +$ state-0 $: %0 @@ -37,7 +69,29 @@ |* config $_ ^| |_ bowl:gall + :: +on-pull-nack: handle failed pull subscription :: + :: This arm is called when a pull subscription fails. lib/pull-hook + :: will automatically delete the resource from .tracking by the + :: time this arm is called. + :: + ++ on-pull-nack + |~ [resource tang] + *[(list card) _^|(..on-init)] + :: +on-pull-kick: produce any additional resubscribe path + :: + :: If non-null, the produced path is appended to the original + :: subscription path. This should be used to encode extra + :: information onto the path in order to reduce the payload of a + :: kick and resubscribe. + :: + :: If null, a resubscribe is not attempted + :: + ++ on-pull-kick + |~ resource + *(unit path) + :: + :: from agent:gall ++ on-init *[(list card) _^|(..on-init)] :: @@ -75,26 +129,6 @@ ++ on-fail |~ [term tang] *[(list card) _^|(..on-init)] - :: +on-pull-nack: handle failed pull subscription - :: - :: This arm is called when a pull subscription fails. - :: - ++ on-pull-nack - |~ [resource tang] - *[(list card) _^|(..on-init)] - :: +on-pull-kick: produce any additional resubscribe path - :: - :: If non-null, the produced path is appended to the original - :: subscription path. This should be used to encode extra - :: information onto the path in order to reduce the payload of a - :: kick and resubscribe. - :: - :: If null, a resubscribe is not attempted - :: - ++ on-pull-kick - |~ resource - *(unit path) - :: :: -- ++ agent |* =config @@ -209,7 +243,10 @@ =^ cards pull-hook (on-fail:og term tang) [cards this] - ++ on-peek on-peek:def + ++ on-peek + |= =path + ^- (unit (unit cage)) + (on-peek:og path) -- |_ =bowl:gall +* og ~(. pull-hook bowl) @@ -225,6 +262,7 @@ ++ add |= [=ship =resource] ~| resource + ?< |(=(our.bowl ship) =(our.bowl entity.resource)) ?: (~(has by tracking) resource) [~ state] =. tracking diff --git a/pkg/arvo/lib/push-hook.hoon b/pkg/arvo/lib/push-hook.hoon index 776aa0f597..bbe6f73c66 100644 --- a/pkg/arvo/lib/push-hook.hoon +++ b/pkg/arvo/lib/push-hook.hoon @@ -1,8 +1,41 @@ +:: lib/push-hook: helper for creating a push hook +:: +:: lib/push-hook is a helper for automatically pushing data from a +:: local store to the corresponding pull-hook on remote ships. It also +:: proxies remote pokes to the store. +:: +:: ## Interfacing notes: +:: +:: The inner door may interact with the library by producing cards. +:: Do not pass any cards on a wire beginning with /helper as these +:: wires are reserved by this library. Any watches/pokes/peeks not +:: listed below will be routed to the inner door. +:: +:: ## Subscription paths +:: +:: /resource/[resource]: Receive initial state and updates to +:: .resource. .resource should be encoded with en-path:resource from +:: /lib/resource. Facts on this path will be of mark +:: update-mark.config +:: +:: ## Pokes +:: +:: %push-hook-action: Add/remove a resource from pushing. +:: [update-mark.config]: A poke to proxy to the local store +:: /- *push-hook /+ default-agent, resource |% +$ card card:agent:gall :: +:: $config: configuration for the push hook +:: +:: .store-name: name of the store to proxy pokes and +:: subscriptions to +:: .store-path: subscription path to receive updates on +:: .update-mark: mark that updates will be tagged with +:: .pull-hook-name: name of the corresponding pull-hook +:: +$ config $: store-name=term store-path=path @@ -10,6 +43,12 @@ update-mark=term pull-hook-name=term == +:: +:: $state-0: state for the push hook +:: +:: .sharing: resources that the push hook is proxying +:: .inner-state: state given to internal door +:: +$ state-0 $: %0 sharing=(set resource) @@ -21,6 +60,48 @@ $_ ^| |_ bowl:gall :: + :: +resource-for-update: get affected resource from an update + :: + :: Given a vase of the update, the mark of which is + :: update-mark.config, produce the affected resource, if any. + :: + ++ resource-for-update + |~ vase + *(unit resource) + :: + :: +take-update: handle update from store + :: + :: Given an update from the store, do other things after proxying + :: the update + :: + ++ take-update + |~ vase + *[(list card) _^|(..on-init)] + :: +should-proxy-update: should forward update to store + :: + :: 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. + :: + ++ should-proxy-update + |~ vase + *? + :: +initial-watch: produce initial state for a subscription + :: + :: .resource is the resource being subscribed to. + :: .path is any additional information in the subscription wire. + :: This would typically be used to encode state that the subscriber + :: already has. For example, a chat client might encode + :: the number of messages that it already has, or the date it last + :: received an update. + :: + :: If +initial-watch crashes, the subscription fails. + :: + ++ initial-watch + |~ [path resource] + *vase + :: from agent:gall + :: ++ on-init *[(list card) _^|(..on-init)] :: @@ -58,35 +139,6 @@ ++ on-fail |~ [term tang] *[(list card) _^|(..on-init)] - :: +resource-for-update: get affected resource from an update - ++ resource-for-update - |~ vase - *(unit resource) - :: - :: +on-update: handle update from store - :: - :: Do extra stuff on store update - ++ take-update - |~ vase - *[(list card) _^|(..on-init)] - :: +should-proxy-update: should forward update to store - :: - :: 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. - :: - ++ should-proxy-update - |~ vase - *? - :: +initial-watch: produce initial state for a subscription - :: - :: .resource is the resource being subscribed to. - :: .path is any additional information in the subscription wire - :: - ++ initial-watch - |~ [path resource] - *vase - :: -- ++ agent |* =config diff --git a/pkg/arvo/lib/strandio.hoon b/pkg/arvo/lib/strandio.hoon index 71299cb8bc..80c4178a1e 100644 --- a/pkg/arvo/lib/strandio.hoon +++ b/pkg/arvo/lib/strandio.hoon @@ -241,6 +241,16 @@ ;< our=@p bind:m get-our (watch wire [our term] path) :: +++ scry + |* [=mold =path] + =/ m (strand ,mold) + ^- form:m + ?> ?=(^ path) + ?> ?=(^ t.path) + ;< =bowl:spider bind:m get-bowl + %- pure:m + .^(mold i.path (scot %p our.bowl) i.t.path (scot %da now.bowl) t.t.path) +:: ++ leave |= [=wire =dock] =/ m (strand ,~) @@ -285,6 +295,20 @@ [%pass /wait/(scot %da until) %arvo %b %wait until] (send-raw-card card) :: +++ map-err + |* computation-result=mold + =/ m (strand ,computation-result) + |= [f=$-([term tang] [term tang]) computation=form:m] + ^- form:m + |= tin=strand-input:strand + =* loop $ + =/ c-res (computation tin) + ?: ?=(%cont -.next.c-res) + c-res(self.next ..loop(computation self.next.c-res)) + ?. ?=(%fail -.next.c-res) + c-res + c-res(err.next (f err.next.c-res)) +:: ++ set-timeout |* computation-result=mold =/ m (strand ,computation-result) @@ -478,6 +502,17 @@ `[%skip ~] `[%done +>.sign-arvo.u.in.tin] == +:: +check-online: require that peer respond before timeout +:: +++ check-online + |= [who=ship lag=@dr] + =/ m (strand ,~) + ^- form:m + %+ (map-err ,~) |=(* [%offline *tang]) + %+ (set-timeout ,~) lag + ;< ~ bind:m + (poke [who %hood] %helm-hi !>(~)) + (pure:m ~) :: :: Queue on skip, try next on fail %ignore :: diff --git a/pkg/arvo/mar/graph/update.hoon b/pkg/arvo/mar/graph/update.hoon index d5f0f4abec..1cd2a20686 100644 --- a/pkg/arvo/mar/graph/update.hoon +++ b/pkg/arvo/mar/graph/update.hoon @@ -1,7 +1,9 @@ /+ *graph-store |_ upd=update +++ grad %noun ++ grow |% + ++ noun upd ++ json (update:enjs upd) -- :: diff --git a/pkg/arvo/mar/graph/validator/link.hoon b/pkg/arvo/mar/graph/validator/link.hoon new file mode 100644 index 0000000000..02de528df2 --- /dev/null +++ b/pkg/arvo/mar/graph/validator/link.hoon @@ -0,0 +1,27 @@ +/- *post +|_ i=indexed-post +++ grow + |% + ++ noun i + -- +++ grab + |% + ++ noun + |= p=* + =/ ip ;;(indexed-post p) + ?+ index.p.ip ~|(index+index.p.ip !!) + :: top-level link post; title and url + :: + [@ ~] + ?> ?=([[%text @] [%url @] ~] contents.p.ip) + ip + :: + :: comment on link post; comment text + :: + [@ @ ~] + ?> ?=([[%text @] ~] contents.p.ip) + ip + == + -- +++ grad %noun +-- diff --git a/pkg/arvo/mar/graph/view-action.hoon b/pkg/arvo/mar/graph/view-action.hoon new file mode 100644 index 0000000000..ab8b0ef12e --- /dev/null +++ b/pkg/arvo/mar/graph/view-action.hoon @@ -0,0 +1,13 @@ +/+ *graph-view +|_ act=action +++ grad %noun +++ grow + |% + ++ noun act + -- +++ grab + |% + ++ noun action + ++ json action:dejs + -- +-- diff --git a/pkg/arvo/sur/graph-view.hoon b/pkg/arvo/sur/graph-view.hoon new file mode 100644 index 0000000000..6514f2a49d --- /dev/null +++ b/pkg/arvo/sur/graph-view.hoon @@ -0,0 +1,45 @@ +/- *group, store=graph-store +/+ resource +^? +|% +:: $associated: A group to associate, or a policy if it is unmanaged +:: ++$ associated + $% [%group rid=resource] + [%policy =policy] + == +:: +:: $error: An error from a graph-view poke +:: +:: %offline: Ship is offline +:: %bad-perms: Not permitted +:: %unknown: Anything not described above +:: ++$ error + ?(%offline %bad-perms %unknown) +:: $action: A semantic action on graphs +:: +:: %create: Create a graph and associated metadata +:: %delete: Delete a graph +:: %join: Join a graph +:: %invite: Invite users to a graph +:: %groupify: Make graph into managed group +:: ++$ action + $% + $: %create + rid=resource + title=@t + description=@t + mark=(unit mark) + =associated + == + [%delete rid=resource] + [%leave rid=resource] + [%join rid=resource =ship] + ::[%invite rid=resource ships=(set ship)] + [%groupify rid=resource to=(unit resource)] + [%forward rid=resource =update:store] + == +-- + diff --git a/pkg/arvo/sur/spider.hoon b/pkg/arvo/sur/spider.hoon index f9035a91e5..5fe7a76268 100644 --- a/pkg/arvo/sur/spider.hoon +++ b/pkg/arvo/sur/spider.hoon @@ -5,4 +5,10 @@ +$ input [=tid =cage] +$ tid tid:strand +$ bowl bowl:strand ++$ http-error + $? %bad-request :: 400 + %forbidden :: 403 + %nonexistent :: 404 + %offline :: 504 + == -- diff --git a/pkg/arvo/ted/graph/create.hoon b/pkg/arvo/ted/graph/create.hoon new file mode 100644 index 0000000000..580d26019b --- /dev/null +++ b/pkg/arvo/ted/graph/create.hoon @@ -0,0 +1,60 @@ +/- spider, graph=graph-store, *metadata-store, *group, group-store +/+ strandio, resource, graph-view +=> +|% +++ strand strand:spider +++ poke poke:strandio +++ poke-our poke-our:strandio +:: +++ handle-group + |= [rid=resource =associated:graph-view] + =/ m (strand ,resource) + ?: ?=(%group -.associated) + (pure:m rid.associated) + =/ =action:group-store + [%add-group rid policy.associated %&] + ;< ~ bind:m (poke-our %group-store %group-action !>(action)) + ;< ~ bind:m + (poke-our %group-push-hook %push-hook-action !>([%add rid])) + (pure:m rid) +-- +:: +=, strand=strand:spider +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<(=action:graph-view arg) +?> ?=(%create -.action) +;< =bowl:spider bind:m get-bowl:strandio +:: Add graph to graph-store +:: +?. =(our.bowl entity.rid.action) + (strand-fail:strandio %bad-request ~) +=/ =update:graph + [%0 now.bowl %add-graph rid.action *graph:graph mark.action] +;< ~ bind:m + (poke-our %graph-store graph-update+!>(update)) +;< ~ bind:m + (poke-our %graph-push-hook %push-hook-action !>([%add rid.action])) +:: Add group, if graph is unmanaged +:: +;< group=resource bind:m + (handle-group rid.action associated.action) +=/ group-path=path + (en-path:resource group) +:: Setup metadata +:: +=/ =metadata + %* . *metadata + title title.action + description description.action + date-created now.bowl + creator our.bowl + == +=/ act=metadata-action + [%add group-path graph+(en-path:resource rid.action) metadata] +;< ~ bind:m (poke-our %metadata-hook %metadata-action !>(act)) +;< ~ bind:m + (poke-our %metadata-hook %metadata-hook-action !>([%add-owned group-path])) +(pure:m !>(~)) diff --git a/pkg/arvo/ted/graph/delete.hoon b/pkg/arvo/ted/graph/delete.hoon new file mode 100644 index 0000000000..70c7911949 --- /dev/null +++ b/pkg/arvo/ted/graph/delete.hoon @@ -0,0 +1,70 @@ +/- spider, graph-view, graph=graph-store, *metadata-store, *group +/+ strandio, resource +=> +|% +++ strand strand:spider +++ poke poke:strandio +++ poke-our poke-our:strandio +:: +++ scry-metadata + |= rid=resource + =/ m (strand ,(unit resource)) + ;< paxs=(unit (set path)) bind:m + %+ scry:strandio ,(unit (set path)) + ;: weld + /gx/metadata-store/resource/publish + (en-path:resource rid) + /noun + == + ?~ paxs (pure:m ~) + ?~ u.paxs (pure:m ~) + (pure:m `(de-path:resource n.u.paxs)) +:: +++ scry-group + |= rid=resource + =/ m (strand ,group) + ;< ugroup=(unit group) bind:m + %+ scry:strandio ,(unit group) + ;: weld + /gx/group-store/groups + (en-path:resource rid) + /noun + == + (pure:m (need ugroup)) +:: +++ delete-graph + |= rid=resource + =/ m (strand ,~) + ^- form:m + ;< ~ bind:m + (poke-our %graph-push-hook %push-hook-action !>([%remove rid])) + ;< =bowl:spider bind:m get-bowl:strandio + ;< ~ bind:m + (poke-our %graph-store %graph-update !>([%0 now.bowl %archive-graph rid])) + (pure:m ~) +-- +:: +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<(=action:graph-view arg) +?> ?=(%delete -.action) +;< =bowl:spider bind:m get-bowl:strandio +?. =(our.bowl entity.rid.action) + (strand-fail:strandio %bad-request ~) +;< ugroup-rid=(unit resource) bind:m + (scry-metadata rid.action) +?~ ugroup-rid !! +;< =group bind:m + (scry-group u.ugroup-rid) +?. hidden.group + ;< ~ bind:m + (delete-graph rid.action) + (pure:m !>(~)) +;< ~ bind:m + (poke-our %group-push-hook %push-hook-action !>([%remove rid.action])) +;< ~ bind:m + (poke-our %group-store %group-action !>([%remove-group rid.action])) +;< ~ bind:m (delete-graph rid.action) +(pure:m !>(~)) diff --git a/pkg/arvo/ted/graph/forward.hoon b/pkg/arvo/ted/graph/forward.hoon new file mode 100644 index 0000000000..8f4f2c4e42 --- /dev/null +++ b/pkg/arvo/ted/graph/forward.hoon @@ -0,0 +1,20 @@ +/- spider, graph-view, graph=graph-store, *metadata-store, *group +/+ strandio, resource +=> +|% +++ strand strand:spider +++ poke poke:strandio +-- +:: +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<(=action:graph-view arg) +?> ?=(%forward -.action) +;< ~ bind:m + %+ (map-err:strandio ,~) |=(* [%forbidden ~]) + %+ poke + [entity.rid.action %graph-push-hook] + [%graph-update !>(update.action)] +(pure:m !>(~)) diff --git a/pkg/arvo/ted/graph/groupify.hoon b/pkg/arvo/ted/graph/groupify.hoon new file mode 100644 index 0000000000..ca9c26edfe --- /dev/null +++ b/pkg/arvo/ted/graph/groupify.hoon @@ -0,0 +1,74 @@ +/- spider, graph-view, graph=graph-store, *metadata-store, *group, *metadata-store +/+ strandio, resource +=> +|% +++ strand strand:spider +++ poke poke:strandio +++ poke-our poke-our:strandio +:: +++ check-live + |= who=ship + =/ m (strand ,~) + ^- form:m + %+ (set-timeout:strandio ,~) ~s20 + ;< ~ bind:m + (poke [who %hood] %helm-hi !>(~)) + (pure:m ~) +:: +++ scry-group + |= rid=resource + =/ m (strand ,group) + ^- form:m + ;< ugroup=(unit group) bind:m + %+ scry:strandio (unit group) + %+ weld /gx/group-store/groups + (snoc (en-path:resource rid) %noun) + ?> ?=(^ ugroup) + (pure:m u.ugroup) +:: +++ scry-metadatum + |= rid=resource + =/ m (strand ,metadata) + ^- form:m + =/ enc-path=@t + (scot %t (spat (en-path:resource rid))) + ;< umeta=(unit metadata) bind:m + %+ scry:strandio (unit metadata) + %+ weld /gx/metadata-store/metadata + /[enc-path]/graph/[enc-path]/noun + ?> ?=(^ umeta) + (pure:m u.umeta) +-- +:: +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<(=action:graph-view arg) +?> ?=(%groupify -.action) +;< =group bind:m (scry-group rid.action) +?. hidden.group + (strand-fail:strandio %bad-request ~) +;< =metadata bind:m + (scry-metadatum rid.action) +?~ to.action + ;< ~ bind:m + %+ poke-our %contact-view + contact-view-action+!>([%groupify rid.action title.metadata description.metadata]) + (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]) +;< ~ bind:m + (poke-our %group-store %group-update !>([%remove-group rid.action])) +(pure:m !>(~)) diff --git a/pkg/arvo/ted/graph/join.hoon b/pkg/arvo/ted/graph/join.hoon new file mode 100644 index 0000000000..59b48ec927 --- /dev/null +++ b/pkg/arvo/ted/graph/join.hoon @@ -0,0 +1,61 @@ +/- spider, graph-view, graph=graph-store, *metadata-store, *group +/+ strandio, resource +=> +|% +++ strand strand:spider +++ fail strand-fail:strand +++ poke poke:strandio +++ poke-our poke-our:strandio +:: +++ scry-metadata + |= rid=resource + =/ m (strand ,(unit resource)) + ^- form:m + ;< pax=(unit (set path)) bind:m + %+ scry:strandio ,(unit (set path)) + ;: weld + /gx/metadata-store/resource/graph + (en-path:resource rid) + /noun + == + %- pure:m + ?~ pax ~ + ?~ u.pax ~ + `(de-path:resource n.u.pax) +-- +:: +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<(=action:graph-view arg) +?> ?=(%join -.action) +;< =bowl:spider bind:m get-bowl:strandio +?: =(our.bowl entity.rid.action) + (fail %bad-request ~) +;< group=(unit resource) bind:m (scry-metadata rid.action) +?^ group + :: We have group, graph is managed + ;< ~ bind:m + %+ poke-our %graph-pull-hook + pull-hook-action+!>([%add ship.action rid.action]) + (pure:m !>(~)) +:: Else, add group then join +;< ~ bind:m + %+ (map-err:strandio ,~) |=(* [%forbidden ~]) + %+ poke + [ship.action %group-push-hook] + group-update+!>([%add-members rid.action (sy our.bowl ~)]) +:: +;< ~ bind:m + %+ poke-our %group-pull-hook + pull-hook-action+!>([%add ship.action rid.action]) +:: +;< ~ bind:m + %+ poke-our %metadata-hook + metadata-hook-action+!>([%add-synced ship.action rid.action]) +:: +;< ~ bind:m + %+ poke-our %graph-pull-hook + pull-hook-action+!>([%add ship.action rid.action]) +(pure:m !>(~)) diff --git a/pkg/arvo/ted/graph/leave.hoon b/pkg/arvo/ted/graph/leave.hoon new file mode 100644 index 0000000000..edc9af6636 --- /dev/null +++ b/pkg/arvo/ted/graph/leave.hoon @@ -0,0 +1,67 @@ +/- spider, graph-view, graph=graph-store, *metadata-store, *group +/+ strandio, resource +=> +|% +++ strand strand:spider +++ poke poke:strandio +++ poke-our poke-our:strandio +:: +++ scry-metadata + |= rid=resource + =/ m (strand ,resource) + ^- form:m + ;< pax=(unit (set path)) bind:m + %+ scry:strandio ,(unit (set path)) + ;: weld + /gx/metadata-store/resource/graph + (en-path:resource rid) + /noun + == + ?> ?=(^ pax) + ?> ?=(^ u.pax) + (pure:m (de-path:resource n.u.pax)) +:: +++ scry-group + |= rid=resource + =/ m (strand ,group) + ^- form:m + ;< ugroup=(unit group) bind:m + %+ scry:strandio ,(unit group) + ;: weld + /gx/group-store/resource/graph + (en-path:resource rid) + /noun + == + (pure:m (need ugroup)) +:: +++ delete-graph + |= rid=resource + =/ m (strand ,~) + ^- form:m + ;< ~ bind:m + (poke-our %graph-pull-hook %pull-hook-action !>([%remove rid])) + ;< ~ bind:m + (poke-our %graph-store %graph-update !>([%archive-graph rid])) + (pure:m ~) +-- +:: +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<([=action:graph-view ~] arg) +?> ?=(%leave -.action) +;< =bowl:spider bind:m get-bowl:strandio +?: =(our.bowl entity.rid.action) + (strand-fail:strandio %bad-request ~) +;< group-rid=resource bind:m (scry-metadata rid.action) +;< g=group bind:m (scry-group group-rid) +?. hidden.g + ;< ~ bind:m (delete-graph rid.action) + (pure:m !>(~)) +;< ~ bind:m + (poke-our %group-push-hook %pull-hook-action !>([%remove rid.action])) +;< ~ bind:m + (poke-our %group-store %group-action !>([%remove-group rid.action])) +;< ~ bind:m (delete-graph rid.action) +(pure:m !>(~)) diff --git a/pkg/interface/README.md b/pkg/interface/README.md new file mode 100644 index 0000000000..1af93b2b4f --- /dev/null +++ b/pkg/interface/README.md @@ -0,0 +1,17 @@ +## interface + +Landscape is Tlon's suite of userspace applications (and web interface), +currently bundled as part of Arvo. + +This directory comprises the source code for the web interface. For code related +to the Gall agents that make up the Landscape suite in Arvo, see +[pkg/arvo][arvo]. + +### Contributions and feature requests + +For information on how to contribute, see [CONTRIBUTING][cont]. To submit +a feature request, submit to the product board at [urbit/landscape][land]. + +[arvo]: https://github.com/urbit/urbit/tree/master/pkg/arvo +[cont]: https://github.com/urbit/urbit/blob/master/pkg/interface/CONTRIBUTING.md +[land]: https://github.com/urbit/landscape/issues/new?assignees=&labels=feature+request&template=feature_request.md&title= \ No newline at end of file diff --git a/pkg/interface/package-lock.json b/pkg/interface/package-lock.json index 921fcbadf0..f379ea40f2 100644 --- a/pkg/interface/package-lock.json +++ b/pkg/interface/package-lock.json @@ -6383,11 +6383,6 @@ "p-is-promise": "^2.0.0" } }, - "memoize-one": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", - "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" - }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -6807,6 +6802,11 @@ "tslib": "^1.10.0" } }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + }, "node-forge": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", @@ -7058,6 +7058,14 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, + "oembed-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/oembed-parser/-/oembed-parser-1.4.1.tgz", + "integrity": "sha512-1KqnfrXF3TiAQhJ9+vv3dEtMhPSVSOT9D9XPqLjEtaQg5liPc3LQ65YjgKHo7Z/YY/kmZ1PDb5gMcOxxCPPdBA==", + "requires": { + "node-fetch": "^2.6.0" + } + }, "omit-deep": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/omit-deep/-/omit-deep-0.3.0.tgz", @@ -7937,6 +7945,14 @@ "xtend": "^4.0.1" } }, + "react-oembed-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/react-oembed-container/-/react-oembed-container-1.0.0.tgz", + "integrity": "sha512-YppvCDgxZkn6qgwAIpxRtmMtxaMpau8yQhm8nzmH7yHpDapmHxzakXvQke5qPfmdYyYW4CsKDfVfGoX14NvQkw==", + "requires": { + "prop-types": "^15.6.0" + } + }, "react-router": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz", @@ -7973,13 +7989,13 @@ "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.0.tgz", "integrity": "sha512-IgmcegOSi5SNX+2Snh1vqmF0Vg/CbkycU9XZbOHJlZ6kMzTmi3yc254oB1WCkgA7OQtIAoLmcSFuHTc/tlcqXg==" }, - "react-window": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.5.tgz", - "integrity": "sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q==", + "react-virtuoso": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-0.20.0.tgz", + "integrity": "sha512-h+U6t/+m91AzfUe6bBfaacdLLJl1y8v7CfcXwPgQ/Dic+vNlgQmi6cIKTq18zuF+kI8Q7QN0ojIeqPHWbU8TZA==", "requires": { - "@babel/runtime": "^7.0.0", - "memoize-one": ">=3.1.1 <6" + "resize-observer-polyfill": "^1.5.1", + "tslib": "^1.11.1" } }, "readable-stream": { @@ -8260,6 +8276,11 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, + "resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "resolve": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", diff --git a/pkg/interface/package.json b/pkg/interface/package.json index 65a6a0181c..45e644e8b4 100644 --- a/pkg/interface/package.json +++ b/pkg/interface/package.json @@ -20,6 +20,7 @@ "moment": "^2.20.1", "mousetrap": "^1.6.5", "mousetrap-global-bind": "^1.1.0", + "oembed-parser": "^1.4.1", "prop-types": "^15.7.2", "react": "^16.5.2", "react-codemirror2": "^6.0.1", @@ -29,8 +30,9 @@ "react-dom": "^16.8.6", "react-helmet": "^6.1.0", "react-markdown": "^4.3.1", + "react-oembed-container": "^1.0.0", "react-router-dom": "^5.0.0", - "react-window": "^1.8.5", + "react-virtuoso": "^0.20.0", "remark-disable-tokenizers": "^1.0.24", "style-loader": "^1.2.1", "styled-components": "^5.1.0", diff --git a/pkg/interface/src/logic/api/base.ts b/pkg/interface/src/logic/api/base.ts index b18a345162..42019ba41d 100644 --- a/pkg/interface/src/logic/api/base.ts +++ b/pkg/interface/src/logic/api/base.ts @@ -57,4 +57,16 @@ export default class BaseApi { scry(app: string, path: Path): Promise { return fetch(`/~/scry/${app}${path}.json`).then(r => r.json() as Promise); } + + + async spider(inputMark: string, outputMark: string, threadName: string, body: any): Promise { + + const res = await fetch(`/spider/${inputMark}/${threadName}/${outputMark}.json`, { + method: 'POST', + body: JSON.stringify(body) + }); + + return res.json(); + } + } diff --git a/pkg/interface/src/logic/api/chat.ts b/pkg/interface/src/logic/api/chat.ts index bd16529cfc..ad6e5029d0 100644 --- a/pkg/interface/src/logic/api/chat.ts +++ b/pkg/interface/src/logic/api/chat.ts @@ -12,7 +12,7 @@ export default class ChatApi extends BaseApi { * Fetch backlog */ fetchMessages(start: number, end: number, path: Path) { - fetch(`/chat-view/paginate/${start}/${end}${path}`) + return fetch(`/chat-view/paginate/${start}/${end}${path}`) .then(response => response.json()) .then((json) => { this.store.handleEvent({ diff --git a/pkg/interface/src/logic/api/graph.ts b/pkg/interface/src/logic/api/graph.ts index b48686d04d..78885bbaed 100644 --- a/pkg/interface/src/logic/api/graph.ts +++ b/pkg/interface/src/logic/api/graph.ts @@ -1,7 +1,9 @@ import BaseApi from './base'; import { StoreState } from '../store/type'; import { Patp, Path, PatpNoSig } from '~/types/noun'; - +import _ from 'lodash'; +import {makeResource, resourceFromPath} from '../lib/group'; +import {GroupPolicy, Enc, Post} from '~/types'; export const createPost = (contents: Object[], parentIndex: string = '') => { return { @@ -20,6 +22,68 @@ export default class GraphApi extends BaseApi { return this.action('graph-store', 'graph-update', action) } + private viewAction(threadName: string, action: any) { + return this.spider('graph-view-action', 'json', threadName, action); + } + + createManagedGraph(name: string, title: string, description: string, group: Path) { + const associated = { group: resourceFromPath(group) }; + + const resource = makeResource(`~${window.ship}`, name); + return this.viewAction('graph-create', { + "create": { + resource, + title, + description, + associated + } + }); + } + + createUnmanagedGraph(name: string, title: string, description: string, policy: Enc) { + + const resource = makeResource(`~${window.ship}`, name); + return this.viewAction('graph-create', { + "create": { + resource, + title, + description, + associated: { policy } + } + }); + } + + joinGraph(ship: Patp, name: string) { + const resource = makeResource(ship, name); + return this.viewAction('graph-join', { + join: { + resource, + ship, + } + }); + } + + deleteGraph(name: string) { + const resource = makeResource(`~${window.ship}`, name); + return this.viewAction('graph-delete', { + "delete": { + resource + } + }); + } + + groupifyGraph(ship: Patp, name: string, toPath?: string) { + const resource = makeResource(ship, name); + const to = toPath && resourceFromPath(toPath); + + return this.viewAction('graph-groupify', { + groupify: { + resource, + to + } + }); + } + addGraph(ship: Patp, name: string, graph: any, mark: any) { this.storeAction({ 'add-graph': { @@ -38,8 +102,9 @@ export default class GraphApi extends BaseApi { }); } - addPost(ship: Patp, name: string, post: Object) { + addPost(ship: Patp, name: string, post: Post) { let nodes = {}; + const resource = { ship, name }; nodes[post.index] = { post, children: { empty: null } @@ -47,7 +112,7 @@ export default class GraphApi extends BaseApi { return this.storeAction({ 'add-nodes': { - resource: { ship, name }, + resource, nodes } }); @@ -63,7 +128,7 @@ export default class GraphApi extends BaseApi { } removeNodes(ship: Patp, name: string, indices: string[]) { - this.storeAction({ + return this.storeAction({ 'remove-nodes': { resource: { ship, name }, indices @@ -107,7 +172,7 @@ export default class GraphApi extends BaseApi { }); } - getGraphSubset(ship: string, resource: string, start: string, end: start) { + getGraphSubset(ship: string, resource: string, start: string, end: string) { this.scry( 'graph-store', `/graph-subset/${ship}/${resource}/${end}/${start}` diff --git a/pkg/interface/src/logic/api/local.ts b/pkg/interface/src/logic/api/local.ts index 976f337c5a..3b874e060d 100644 --- a/pkg/interface/src/logic/api/local.ts +++ b/pkg/interface/src/logic/api/local.ts @@ -1,6 +1,6 @@ import BaseApi from "./base"; import { StoreState } from "../store/type"; -import { BackgroundConfig } from "../types/local-update"; +import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from "../types/local-update"; export default class LocalApi extends BaseApi { getBaseHash() { @@ -69,6 +69,16 @@ export default class LocalApi extends BaseApi { }); } + setRemoteContentPolicy(policy: LocalUpdateRemoteContentPolicy) { + this.store.handleEvent({ + data: { + local: { + remoteContentPolicy: policy + } + } + }); + } + dehydrate() { this.store.dehydrate(); } diff --git a/pkg/interface/src/logic/api/publish.ts b/pkg/interface/src/logic/api/publish.ts index 02648f651b..5b49aabfc9 100644 --- a/pkg/interface/src/logic/api/publish.ts +++ b/pkg/interface/src/logic/api/publish.ts @@ -82,6 +82,17 @@ export default class PublishApi extends BaseApi { return this.action('publish', 'publish-action', act); } + groupify(bookId: string, group: Path | null) { + return this.publishAction({ + groupify: { + book: bookId, + target: group, + inclusive: false + } + }); + } + + newBook(bookId: string, title: string, description: string, group?: Path) { const groupInfo = group ? { 'group-path': group, invitees: [], diff --git a/pkg/interface/src/logic/lib/group.ts b/pkg/interface/src/logic/lib/group.ts index 64b035b51d..bd4939c2f5 100644 --- a/pkg/interface/src/logic/lib/group.ts +++ b/pkg/interface/src/logic/lib/group.ts @@ -13,3 +13,8 @@ export function resourceFromPath(path: Path): Resource { const [, , ship, name] = path.split('/'); return { ship, name } } + +export function makeResource(ship: string, name:string) { + return { ship, name }; +} + diff --git a/pkg/interface/src/logic/lib/omnibox.js b/pkg/interface/src/logic/lib/omnibox.js index 60dc95c01f..ee616ddc93 100644 --- a/pkg/interface/src/logic/lib/omnibox.js +++ b/pkg/interface/src/logic/lib/omnibox.js @@ -1,10 +1,12 @@ import defaultApps from './default-apps'; +import { cite } from '~/logic/lib/util'; const indexes = new Map([ ['commands', []], ['subscriptions', []], ['groups', []], - ['apps', []] + ['apps', []], + ['other', []] ]); // result schematic @@ -41,8 +43,6 @@ const commandIndex = function () { } }); - commands.push(result('Profile', '/~profile', 'profile', null)); - return commands; }; @@ -54,6 +54,9 @@ const appIndex = function (apps) { .filter((e) => { return apps[e]?.type?.basic; }) + .sort((a,b) => { + return a.localeCompare(b); + }) .map((e) => { const obj = result( apps[e].type.basic.title, @@ -70,6 +73,14 @@ const appIndex = function (apps) { return applications; }; +const otherIndex = function() { + const other = []; + other.push(result('Profile and Settings', '/~profile/identity', 'profile', null)); + other.push(result('Log Out', '/~/logout', 'logout', null)); + + return other; +}; + export default function index(associations, apps) { // all metadata from all apps is indexed // into subscriptions and groups @@ -99,7 +110,7 @@ export default function index(associations, apps) { title, `/~${app}${each['app-path']}`, app.charAt(0).toUpperCase() + app.slice(1), - shipStart.slice(0, shipStart.indexOf('/')) + cite(shipStart.slice(0, shipStart.indexOf('/'))) ); groups.push(obj); } else { @@ -107,7 +118,7 @@ export default function index(associations, apps) { title, `/~${each['app-name']}/join${each['app-path']}`, app.charAt(0).toUpperCase() + app.slice(1), - shipStart.slice(0, shipStart.indexOf('/')) + (associations?.contacts?.[each['group-path']]?.metadata?.title || null) ); subscriptions.push(obj); } @@ -118,6 +129,7 @@ export default function index(associations, apps) { indexes.set('subscriptions', subscriptions); indexes.set('groups', groups); indexes.set('apps', appIndex(apps)); + indexes.set('other', otherIndex()); return indexes; }; diff --git a/pkg/interface/src/logic/lib/tokenizeMessage.js b/pkg/interface/src/logic/lib/tokenizeMessage.js new file mode 100644 index 0000000000..6f43c421c8 --- /dev/null +++ b/pkg/interface/src/logic/lib/tokenizeMessage.js @@ -0,0 +1,67 @@ +const URL_REGEX = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source)); + +const isUrl = (string) => { + try { + return URL_REGEX.test(string); + } catch (e) { + return false; + } +} + +const tokenizeMessage = (text) => { + let messages = []; + let message = []; + let isInCodeBlock = false; + let endOfCodeBlock = false; + text.split(/\r?\n/).forEach((line, index) => { + if (index !== 0) { + message.push('\n'); + } + // A line of backticks enters and exits a codeblock + if (line.startsWith('```')) { + // But we need to check if we've ended a codeblock + endOfCodeBlock = isInCodeBlock; + isInCodeBlock = (!isInCodeBlock); + } else { + endOfCodeBlock = false; + } + + if (isInCodeBlock || endOfCodeBlock) { + message.push(line); + } else { + line.split(/\s/).forEach((str) => { + if ( + (str.startsWith('`') && str !== '`') + || (str === '`' && !isInCodeBlock) + ) { + isInCodeBlock = true; + } else if ( + (str.endsWith('`') && str !== '`') + || (str === '`' && isInCodeBlock) + ) { + isInCodeBlock = false; + } + + if (isUrl(str) && !isInCodeBlock) { + if (message.length > 0) { + // If we're in the middle of a message, add it to the stack and reset + messages.push(message); + message = []; + } + messages.push([str]); + message = []; + } else { + message.push(str); + } + }); + } + }); + + if (message.length) { + // Add any remaining message + messages.push(message); + } + return messages; +}; + +export { tokenizeMessage as default, isUrl, URL_REGEX }; \ No newline at end of file diff --git a/pkg/interface/src/logic/lib/useWaitForProps.ts b/pkg/interface/src/logic/lib/useWaitForProps.ts index 5a2d752e6a..80e92e92a8 100644 --- a/pkg/interface/src/logic/lib/useWaitForProps.ts +++ b/pkg/interface/src/logic/lib/useWaitForProps.ts @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; -export function useWaitForProps

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

(props: P, timeout: number = 0) { const [resolve, setResolve] = useState<() => void>(() => () => {}); const [ready, setReady] = useState<(p: P) => boolean | undefined>(); @@ -24,9 +24,11 @@ export function useWaitForProps

(props: P, timeout: number) { setReady(() => r); return new Promise((resolve, reject) => { setResolve(() => resolve); - setTimeout(() => { - reject(new Error("Timed out")); - }, timeout); + if(timeout > 0) { + setTimeout(() => { + reject(new Error("Timed out")); + }, timeout); + } }); }, [setResolve, setReady, timeout] diff --git a/pkg/interface/src/logic/reducers/chat-update.ts b/pkg/interface/src/logic/reducers/chat-update.ts index 2921750381..531d97453a 100644 --- a/pkg/interface/src/logic/reducers/chat-update.ts +++ b/pkg/interface/src/logic/reducers/chat-update.ts @@ -1,8 +1,9 @@ import _ from 'lodash'; -import { StoreState } from '../../../store/type'; +import { StoreState } from '~/logic/store/type'; import { Cage } from '~/types/cage'; import { ChatUpdate } from '~/types/chat-update'; import { ChatHookUpdate } from '~/types/chat-hook-update'; +import { Envelope } from "~/types/chat-update"; type ChatState = Pick; @@ -49,8 +50,11 @@ export default class ChatReducer { messages(json: ChatUpdate, state: S) { const data = _.get(json, 'messages', false); if (data) { - state.inbox[data.path].envelopes = - state.inbox[data.path].envelopes.concat(data.envelopes); + state.inbox[data.path].envelopes = _.unionBy( + state.inbox[data.path].envelopes, + data.envelopes, + (envelope: Envelope) => envelope.uid + ); } } diff --git a/pkg/interface/src/logic/reducers/graph-update.js b/pkg/interface/src/logic/reducers/graph-update.js index c8253f229c..54047e72a5 100644 --- a/pkg/interface/src/logic/reducers/graph-update.js +++ b/pkg/interface/src/logic/reducers/graph-update.js @@ -140,10 +140,21 @@ const addNodes = (json, state) => { }; 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); + } + }; const data = _.get(json, 'remove-nodes', false); if (data) { console.log(data); - if (!(data.resource in state.graphs)) { return; } + const { ship, name } = data.resource; + const res = `${ship}/${name}`; + if (!(res in state.graphs)) { return; } data.indices.forEach((index) => { console.log(index); @@ -151,13 +162,7 @@ const removeNodes = (json, state) => { let indexArr = index.split('/').slice(1).map((ind) => { return parseInt(ind, 10); }); - - if (indexArr.length === 1) { - state.graphs[data.resource].delete(indexArr[0]); - } else { - // TODO: recursive - } - + _remove(state.graphs[res], indexArr); }); } }; diff --git a/pkg/interface/src/logic/reducers/group-update.ts b/pkg/interface/src/logic/reducers/group-update.ts index 09adb35b26..5862aaca1d 100644 --- a/pkg/interface/src/logic/reducers/group-update.ts +++ b/pkg/interface/src/logic/reducers/group-update.ts @@ -78,6 +78,7 @@ export default class GroupReducer { this.addGroup(data, state); this.removeGroup(data, state); this.changePolicy(data, state); + this.expose(data, state); } } @@ -187,6 +188,15 @@ export default class GroupReducer { } } + expose(json: GroupUpdate, state: S) { + if( 'expose' in json && state) { + const { resource } = json.expose; + const resourcePath = resourceAsPath(resource); + state.groups[resourcePath].hidden = false; + } + } + + private inviteChangePolicy(diff: InvitePolicyDiff, policy: InvitePolicy) { if ('addInvites' in diff) { const { addInvites } = diff; diff --git a/pkg/interface/src/logic/reducers/local.ts b/pkg/interface/src/logic/reducers/local.ts index 805e57e273..89a3fa8227 100644 --- a/pkg/interface/src/logic/reducers/local.ts +++ b/pkg/interface/src/logic/reducers/local.ts @@ -3,7 +3,7 @@ import { StoreState } from '~/store/type'; import { Cage } from '~/types/cage'; import { LocalUpdate, BackgroundConfig } from '~/types/local-update'; -type LocalState = Pick; +type LocalState = Pick; export default class LocalReducer { rehydrate(state: S) { @@ -18,7 +18,7 @@ export default class LocalReducer { } dehydrate(state: S) { - const json = _.pick(state, ['hideNicknames' , 'hideAvatars' , 'background']); + const json = _.pick(state, ['hideNicknames' , 'hideAvatars' , 'background', 'remoteContentPolicy']); localStorage.setItem('localReducer', JSON.stringify(json)); } reduce(json: Cage, state: S) { @@ -31,6 +31,7 @@ export default class LocalReducer { this.hideAvatars(data, state) this.hideNicknames(data, state) this.omniboxShown(data, state); + this.remoteContentPolicy(data, state); } } baseHash(obj: LocalUpdate, state: S) { @@ -70,6 +71,12 @@ export default class LocalReducer { } } + remoteContentPolicy(obj: LocalUpdate, state: S) { + if('remoteContentPolicy' in obj) { + state.remoteContentPolicy = obj.remoteContentPolicy; + } + } + hideAvatars(obj: LocalUpdate, state: S) { if('hideAvatars' in obj) { state.hideAvatars = obj.hideAvatars; diff --git a/pkg/interface/src/logic/store/links.js b/pkg/interface/src/logic/store/links.js index d8d7fd7dd4..eb395603fa 100644 --- a/pkg/interface/src/logic/store/links.js +++ b/pkg/interface/src/logic/store/links.js @@ -6,6 +6,7 @@ import InviteReducer from '../reducers/invite-update'; import LinkReducer from '../reducers/link-update'; import ListenReducer from '../reducers/listen-update'; import LocalReducer from '../reducers/local'; +import S3Reducer from '../reducers/s3-update'; import BaseStore from './base'; @@ -21,6 +22,7 @@ export default class LinksStore extends BaseStore { this.localReducer = new LocalReducer(); this.linkReducer = new LinkReducer(); this.listenReducer = new ListenReducer(); + this.s3Reducer = new S3Reducer(); } initialState() { @@ -37,6 +39,7 @@ export default class LinksStore extends BaseStore { comments: {}, seen: {}, permissions: {}, + s3: {}, sidebarShown: true }; } @@ -50,6 +53,7 @@ export default class LinksStore extends BaseStore { this.localReducer.reduce(data, this.state); this.linkReducer.reduce(data, this.state); this.listenReducer.reduce(data, this.state); + this.s3Reducer.reduce(data, this.state); } } diff --git a/pkg/interface/src/logic/store/store.ts b/pkg/interface/src/logic/store/store.ts index 8926231cc7..8bb5794d9b 100644 --- a/pkg/interface/src/logic/store/store.ts +++ b/pkg/interface/src/logic/store/store.ts @@ -54,6 +54,12 @@ export default class GlobalStore extends BaseStore { suspendedFocus: null, baseHash: null, background: undefined, + remoteContentPolicy: { + imageShown: true, + audioShown: true, + videoShown: true, + oembedShown: true, + }, hideAvatars: false, hideNicknames: false, invites: {}, diff --git a/pkg/interface/src/logic/store/type.ts b/pkg/interface/src/logic/store/type.ts index 525ec23a23..6e94da33d6 100644 --- a/pkg/interface/src/logic/store/type.ts +++ b/pkg/interface/src/logic/store/type.ts @@ -11,7 +11,8 @@ import { Permissions } from '~/types/permission-update'; import { LaunchState, WeatherState } from '~/types/launch-update'; import { LinkComments, LinkCollections, LinkSeen } from '~/types/link-update'; import { ConnectionStatus } from '~/types/connection'; -import { BackgroundConfig } from '~/types/local-update'; +import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from '~/types/local-update'; +import {Graphs} from '~/types/graph-update'; export interface StoreState { // local state @@ -22,6 +23,7 @@ export interface StoreState { connection: ConnectionStatus; baseHash: string | null; background: BackgroundConfig; + remoteContentPolicy: LocalUpdateRemoteContentPolicy; hideAvatars: boolean; hideNicknames: boolean; // invite state @@ -35,7 +37,7 @@ export interface StoreState { groupKeys: Set; permissions: Permissions; s3: S3State; - graphs: Object; + graphs: Graphs; graphKeys: Set; diff --git a/pkg/interface/src/types/chat-update.ts b/pkg/interface/src/types/chat-update.ts index 63b23eee11..6d03048f4e 100644 --- a/pkg/interface/src/types/chat-update.ts +++ b/pkg/interface/src/types/chat-update.ts @@ -73,6 +73,10 @@ export interface Envelope { letter: Letter; } +export type IMessage = Envelope & { + pending?: boolean +}; + interface LetterText { text: string; } diff --git a/pkg/interface/src/types/graph-update.ts b/pkg/interface/src/types/graph-update.ts new file mode 100644 index 0000000000..c9fb6a19c9 --- /dev/null +++ b/pkg/interface/src/types/graph-update.ts @@ -0,0 +1,30 @@ +import {Patp} from "./noun"; + + +export interface TextContent { text: string; }; +export interface UrlContent { url: string; } +export interface CodeContent { expresssion: string; output: string; }; +export interface ReferenceContent { uid: string; } +export type Content = TextContent | UrlContent | CodeContent | ReferenceContent; + +export interface Post { + author: Patp; + contents: Content[]; + hash?: string; + index: string; + pending?: boolean; + signatures: string[]; + 'time-sent': number; +} + + +export interface GraphNode { + children: Graph; + post: Post; +} + +export type Graph = Map; + +export type Graphs = { [rid: string]: Graph }; + + diff --git a/pkg/interface/src/types/index.ts b/pkg/interface/src/types/index.ts index 3f7cda54c5..44a608d9c2 100644 --- a/pkg/interface/src/types/index.ts +++ b/pkg/interface/src/types/index.ts @@ -5,6 +5,7 @@ export * from './connection'; export * from './contact-update'; export * from './global'; export * from './group-update'; +export * from './graph-update'; export * from './invite-update'; export * from './launch-update'; export * from './link-listen-update'; diff --git a/pkg/interface/src/types/local-update.ts b/pkg/interface/src/types/local-update.ts index 25d679d467..a5739fd218 100644 --- a/pkg/interface/src/types/local-update.ts +++ b/pkg/interface/src/types/local-update.ts @@ -1,12 +1,3 @@ -export type LocalUpdate = - LocalUpdateSidebarToggle -| LocalUpdateSetDark -| LocalUpdateBaseHash -| LocalUpdateBackgroundConfig -| LocalUpdateHideAvatars -| LocalUpdateHideNicknames -| LocalUpdateSetOmniboxShown; - interface LocalUpdateSidebarToggle { sidebarToggle: boolean; } @@ -31,7 +22,16 @@ interface LocalUpdateHideNicknames { hideNicknames: boolean; } -export type BackgroundConfig = BackgroundConfigUrl | BackgroundConfigColor | undefined; +interface LocalUpdateSetOmniboxShown { + omniboxShown: boolean; +} + +export interface LocalUpdateRemoteContentPolicy { + imageShown: boolean; + audioShown: boolean; + videoShown: boolean; + oembedShown: boolean; +} interface BackgroundConfigUrl { type: 'url'; @@ -43,6 +43,14 @@ interface BackgroundConfigColor { color: string; } -interface LocalUpdateSetOmniboxShown { - omniboxShown: boolean; -} +export type BackgroundConfig = BackgroundConfigUrl | BackgroundConfigColor | undefined; + +export type LocalUpdate = + LocalUpdateSidebarToggle +| LocalUpdateSetDark +| LocalUpdateBaseHash +| LocalUpdateBackgroundConfig +| LocalUpdateHideAvatars +| LocalUpdateHideNicknames +| LocalUpdateSetOmniboxShown +| LocalUpdateRemoteContentPolicy; \ No newline at end of file diff --git a/pkg/interface/src/views/App.js b/pkg/interface/src/views/App.js index 497c5677b9..e72d0b1d16 100644 --- a/pkg/interface/src/views/App.js +++ b/pkg/interface/src/views/App.js @@ -1,7 +1,7 @@ import { hot } from 'react-hot-loader/root'; import 'react-hot-loader'; import * as React from 'react'; -import { BrowserRouter as Router, Route, withRouter, Switch } from 'react-router-dom'; +import { BrowserRouter as Router, withRouter } from 'react-router-dom'; import styled, { ThemeProvider, createGlobalStyle } from 'styled-components'; import { sigil as sigiljs, stringRenderer } from 'urbit-sigil-js'; import Helmet from 'react-helmet'; @@ -16,8 +16,7 @@ import dark from './themes/old-dark'; import { Content } from './components/Content'; import StatusBar from './components/StatusBar'; -import Omnibox from './components/Omnibox'; -import ErrorComponent from './components/Error'; +import Omnibox from './components/leap/Omnibox'; import GlobalStore from '~/logic/store/store'; import GlobalSubscription from '~/logic/subscription/global'; @@ -36,7 +35,7 @@ const Root = styled.div` background-size: cover; ` : p.background?.type === 'color' ? ` background-color: ${p.background.color}; - ` : `` + ` : '' } display: flex; flex-flow: column nowrap; @@ -135,7 +134,8 @@ class App extends React.Component { ship={this.ship} api={this.api} subscription={this.subscription} - {...state} /> + {...state} + /> @@ -143,6 +143,5 @@ class App extends React.Component { } } - export default process.env.NODE_ENV === 'production' ? App : hot(App); diff --git a/pkg/interface/src/views/apps/chat/app.tsx b/pkg/interface/src/views/apps/chat/app.tsx index cadf3e265e..45790f2030 100644 --- a/pkg/interface/src/views/apps/chat/app.tsx +++ b/pkg/interface/src/views/apps/chat/app.tsx @@ -7,7 +7,6 @@ import './css/custom.css'; import { Skeleton } from './components/skeleton'; import { Sidebar } from './components/sidebar'; import { ChatScreen } from './components/chat'; -import { MemberScreen } from './components/member'; import { SettingsScreen } from './components/settings'; import { NewScreen } from './components/new'; import { JoinScreen } from './components/join'; @@ -89,7 +88,8 @@ export default class ChatApp extends React.Component { pendingMessages, groups, hideAvatars, - hideNicknames + hideNicknames, + remoteContentPolicy } = props; const renderChannelSidebar = (props, station?) => ( @@ -194,6 +194,11 @@ export default class ChatApp extends React.Component { render={(props) => { let station = `/${props.match.params.ship}/${props.match.params.station}`; + // ensure we know joined chats + if(!chatInitialized) { + return null; + } + return ( { envelopes: [] }; - let roomContacts = {}; - const associatedGroup = - station in associations['chat'] && - 'group-path' in associations.chat[station] - ? associations.chat[station]['group-path'] - : ''; + let roomContacts = {}; + const associatedGroup = + station in associations['chat'] && + 'group-path' in associations.chat[station] + ? associations.chat[station]['group-path'] + : ''; - if (associations.chat[station] && associatedGroup in contacts) { - roomContacts = contacts[associatedGroup]; - } + if (associations.chat[station] && associatedGroup in contacts) { + roomContacts = contacts[associatedGroup]; + } - const association = - station in associations['chat'] ? associations.chat[station] : {}; + const association = + station in associations['chat'] ? associations.chat[station] : {}; - const group = groups[association['group-path']] || groupBunts.group(); + const group = groups[association['group-path']] || groupBunts.group(); - const popout = props.match.url.includes('/popout/'); + const popout = props.match.url.includes('/popout/'); - return ( - + - - - ); - }} + chatInitialized={chatInitialized} + hideAvatars={hideAvatars} + hideNicknames={hideNicknames} + remoteContentPolicy={remoteContentPolicy} + {...props} + /> + + ); + }} /> ; + dragover: boolean; } export class ChatScreen extends Component { + private chatInput: React.RefObject; lastNumPending = 0; activityTimeout: NodeJS.Timeout | null = null; @@ -53,8 +58,11 @@ export class ChatScreen extends Component { this.state = { messages: new Map(), + dragover: false, }; + this.chatInput = React.createRef(); + moment.updateLocale("en", { calendar: { sameDay: "[Today]", @@ -67,6 +75,26 @@ export class ChatScreen extends Component { }); } + readyToUpload(): boolean { + return Boolean(this.chatInput.current?.s3Uploader.current?.inputRef.current); + } + + onDragEnter() { + if (!this.readyToUpload()) { + return; + } + this.setState({ dragover: true }); + } + + onDrop(event: DragEvent) { + this.setState({ dragover: false }); + if (!event.dataTransfer || !event.dataTransfer.files.length) { + return; + } + event.preventDefault(); + this.chatInput.current?.uploadFiles(event.dataTransfer.files); + } + render() { const { props, state } = this; @@ -97,42 +125,36 @@ export class ChatScreen extends Component { !(props.station in props.chatSynced) && props.envelopes.length > 0; - const unreadCount = props.length - props.read; + const unreadCount = props.mailboxSize - props.read; const unreadMsg = unreadCount > 0 && props.envelopes[unreadCount - 1]; - + return (

- + className="h-100 w-100 overflow-hidden flex flex-column relative" + onDragEnter={this.onDragEnter.bind(this)} + onDragOver={event => { + event.preventDefault(); + if (!this.state.dragover) { + this.setState({ dragover: true }); + } + }} + onDragLeave={() => this.setState({ dragover: false })} + onDrop={this.onDrop.bind(this)} + > + {this.state.dragover ? : null} + + {...props} /> { this.submit(); + }, + 'Esc': () => { + this.editor?.getInputField().blur(); } } }; @@ -124,11 +132,11 @@ export default class ChatEditor extends Component {
this.messageChange(e, d, v)} editorDidMount={(editor) => { @@ -137,6 +145,7 @@ export default class ChatEditor extends Component { editor.focus(); } }} + {...props} />
); diff --git a/pkg/interface/src/views/apps/chat/components/lib/chat-input.js b/pkg/interface/src/views/apps/chat/components/lib/chat-input.js deleted file mode 100644 index c4919cefce..0000000000 --- a/pkg/interface/src/views/apps/chat/components/lib/chat-input.js +++ /dev/null @@ -1,268 +0,0 @@ -import React, { Component } from 'react'; -import ChatEditor from './chat-editor'; -import { S3Upload } from './s3-upload' -; -import { uxToHex } from '~/logic/lib/util'; -import { Sigil } from '~/logic/lib/sigil'; - - -const URL_REGEX = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source)); - -export class ChatInput extends Component { - constructor(props) { - super(props); - - this.state = { - inCodeMode: false, - }; - - this.submit = this.submit.bind(this); - this.toggleCode = this.toggleCode.bind(this); - } - - uploadSuccess(url) { - const { props } = this; - props.api.chat.message( - props.station, - `~${window.ship}`, - Date.now(), - { url } - ); - } - - uploadError(error) { - // no-op for now - } - - toggleCode() { - this.setState({ - inCodeMode: !this.state.inCodeMode - }); - } - - getLetterType(letter) { - if (letter.startsWith('/me ')) { - letter = letter.slice(4); - // remove insignificant leading whitespace. - // aces might be relevant to style. - while (letter[0] === '\n') { - letter = letter.slice(1); - } - - return { - me: letter - }; - } else if (this.isUrl(letter)) { - return { - url: letter - }; - } else { - return { - text: letter - }; - } - } - - isUrl(string) { - try { - return URL_REGEX.test(string); - } catch (e) { - return false; - } - } - - submit(text) { - const { props, state } = this; - if (state.inCodeMode) { - this.setState({ - inCodeMode: false - }, () => { - props.api.chat.message( - props.station, - `~${window.ship}`, - Date.now(), { - code: { - expression: text, - output: undefined - } - } - ); - }); - return; - } - - let messages = []; - let message = []; - let isInCodeBlock = false; - let endOfCodeBlock = false; - text.split(/\r?\n/).forEach((line, index) => { - if (index !== 0) { - message.push('\n'); - } - // A line of backticks enters and exits a codeblock - if (line.startsWith('```')) { - // But we need to check if we've ended a codeblock - endOfCodeBlock = isInCodeBlock; - isInCodeBlock = (!isInCodeBlock); - } else { - endOfCodeBlock = false; - } - - if (isInCodeBlock || endOfCodeBlock) { - message.push(line); - } else { - line.split(/\s/).forEach((str) => { - if ( - (str.startsWith('`') && str !== '`') - || (str === '`' && !isInCodeBlock) - ) { - isInCodeBlock = true; - } else if ( - (str.endsWith('`') && str !== '`') - || (str === '`' && isInCodeBlock) - ) { - isInCodeBlock = false; - } - - if (this.isUrl(str) && !isInCodeBlock) { - if (message.length > 0) { - // If we're in the middle of a message, add it to the stack and reset - messages.push(message); - message = []; - } - messages.push([str]); - message = []; - } else { - message.push(str); - } - }); - } - }); - - if (message.length) { - // Add any remaining message - messages.push(message); - } - - props.deleteMessage(); - - messages.forEach((message) => { - if (message.length > 0) { - message = this.getLetterType(message.join(' ')); - props.api.chat.message( - props.station, - `~${window.ship}`, - Date.now(), - message - ); - } - }); - - // perf testing: - /*let closure = () => { - let x = 0; - for (var i = 0; i < 30; i++) { - x++; - props.api.chat.message( - props.station, - `~${window.ship}`, - Date.now(), - { - text: `${x}` - } - ); - } - setTimeout(closure, 1000); - }; - this.closure = closure.bind(this); - setTimeout(this.closure, 2000);*/ - } - - uploadSuccess(url) { - const { props } = this; - props.api.chat.message( - props.station, - `~${window.ship}`, - Date.now(), - { url } - ); - } - - uploadError(error) { - // no-op for now - } - - render() { - const { props, state } = this; - - const color = props.ownerContact - ? uxToHex(props.ownerContact.color) : '000000'; - - const sigilClass = props.ownerContact - ? '' : 'mix-blend-diff'; - - const avatar = ( - props.ownerContact && - ((props.ownerContact.avatar !== null) && !props.hideAvatars) - ) - ? - : ; - - return ( -
-
- {avatar} -
- -
- -
-
- -
-
- ); - } -} diff --git a/pkg/interface/src/views/apps/chat/components/lib/chat-input.tsx b/pkg/interface/src/views/apps/chat/components/lib/chat-input.tsx new file mode 100644 index 0000000000..ba76714638 --- /dev/null +++ b/pkg/interface/src/views/apps/chat/components/lib/chat-input.tsx @@ -0,0 +1,255 @@ +import React, { Component } from 'react'; +import ChatEditor from './chat-editor'; +import { S3Upload, SubmitDragger } from '~/views/components/s3-upload' +; +import { uxToHex } from '~/logic/lib/util'; +import { Sigil } from '~/logic/lib/sigil'; +import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage'; +import GlobalApi from '~/logic/api/global'; +import { Envelope } from '~/types/chat-update'; +import { Contacts, S3Configuration } from '~/types'; + +interface ChatInputProps { + api: GlobalApi; + numMsgs: number; + station: any; + owner: string; + ownerContact: any; + envelopes: Envelope[]; + contacts: Contacts; + onUnmount(msg: string): void; + s3: any; + placeholder: string; + message: string; + deleteMessage(): void; + hideAvatars: boolean; + onPaste?(): void; +} + +interface ChatInputState { + inCodeMode: boolean; + submitFocus: boolean; + uploadingPaste: boolean; +} + + +export class ChatInput extends Component { + public s3Uploader: React.RefObject; + private chatEditor: React.RefObject; + + constructor(props) { + super(props); + + this.state = { + inCodeMode: false, + submitFocus: false, + uploadingPaste: false, + }; + + this.s3Uploader = React.createRef(); + this.chatEditor = React.createRef(); + + this.submit = this.submit.bind(this); + this.toggleCode = this.toggleCode.bind(this); + + } + + toggleCode() { + this.setState({ + inCodeMode: !this.state.inCodeMode + }); + } + + getLetterType(letter) { + if (letter.startsWith('/me ')) { + letter = letter.slice(4); + // remove insignificant leading whitespace. + // aces might be relevant to style. + while (letter[0] === '\n') { + letter = letter.slice(1); + } + + return { + me: letter + }; + } else if (isUrl(letter)) { + return { + url: letter + }; + } else { + return { + text: letter + }; + } + } + + + + submit(text) { + const { props, state } = this; + if (state.inCodeMode) { + this.setState({ + inCodeMode: false + }, () => { + props.api.chat.message( + props.station, + `~${window.ship}`, + Date.now(), { + code: { + expression: text, + output: undefined + } + } + ); + }); + return; + } + + const messages = tokenizeMessage(text); + + props.deleteMessage(); + + messages.forEach((message) => { + if (message.length > 0) { + message = this.getLetterType(message.join(' ')); + props.api.chat.message( + props.station, + `~${window.ship}`, + Date.now(), + message + ); + } + }); + } + + uploadSuccess(url) { + const { props } = this; + if (this.state.uploadingPaste) { + this.chatEditor.current.editor.setValue(url); + this.setState({ uploadingPaste: false }); + } else { + props.api.chat.message( + props.station, + `~${window.ship}`, + Date.now(), + { url } + ); + } + + } + + uploadError(error) { + // no-op for now + } + + readyToUpload(): boolean { + return Boolean(this.s3Uploader.current?.inputRef.current); + } + + onPaste(codemirrorInstance, event: ClipboardEvent) { + if (!event.clipboardData || !event.clipboardData.files.length) { + return; + } + this.setState({ uploadingPaste: true }); + event.preventDefault(); + event.stopPropagation(); + this.uploadFiles(event.clipboardData.files); + } + + uploadFiles(files: FileList) { + if (!this.readyToUpload()) { + return; + } + this.s3Uploader.current.inputRef.current.files = files; + const fire = document.createEvent("HTMLEvents"); + fire.initEvent("change", true, true); + this.s3Uploader.current?.inputRef.current?.dispatchEvent(fire); + } + + render() { + const { props, state } = this; + + const color = props.ownerContact + ? uxToHex(props.ownerContact.color) : '000000'; + + const sigilClass = props.ownerContact + ? '' : 'mix-blend-diff'; + + const avatar = ( + props.ownerContact && + ((props.ownerContact.avatar !== null) && !props.hideAvatars) + ) + ? + : ; + + return ( +
+
+ {avatar} +
+ +
+ + + +
+
+ +
+
+ ); + } +} diff --git a/pkg/interface/src/views/apps/chat/components/lib/chat-message.tsx b/pkg/interface/src/views/apps/chat/components/lib/chat-message.tsx index 3d8573927b..e29e18802c 100644 --- a/pkg/interface/src/views/apps/chat/components/lib/chat-message.tsx +++ b/pkg/interface/src/views/apps/chat/components/lib/chat-message.tsx @@ -2,87 +2,91 @@ import React, { PureComponent, Fragment } from "react"; import moment from "moment"; import { Message } from "./message"; +import { Envelope } from "~/types/chat-update"; +import _ from "lodash"; -type IMessage = Envelope & { pending?: boolean }; - - -export const ChatMessage = (props) => { - const { - msg, - previousMsg, - nextMsg, - isLastUnread, - group, - association, - contacts, - unreadRef, - hideAvatars, - hideNicknames - } = props; - - // Render sigil if previous message is not by the same sender - const aut = ["author"]; - const renderSigil = - _.get(nextMsg, aut) !== _.get(msg, aut, msg.author); - const paddingTop = renderSigil; - const paddingBot = - _.get(previousMsg, aut) !== _.get(msg, aut, msg.author); - - const when = ["when"]; - const dayBreak = - moment(_.get(nextMsg, when)).format("YYYY.MM.DD") !== - moment(_.get(msg, when)).format("YYYY.MM.DD"); - - const messageElem = ( - - ); - - if (props.isLastUnread) { - return ( - - {messageElem} -
-
-

New messages below

-
- {dayBreak && ( -

- {moment(_.get(msg, when)).calendar()} -

- )} -
-
-
+export class ChatMessage extends PureComponent { + render() { + const { + msg, + previousMsg, + nextMsg, + isFirstUnread, + group, + association, + contacts, + unreadRef, + hideAvatars, + hideNicknames, + remoteContentPolicy, + className = '' + } = this.props; + + // Render sigil if previous message is not by the same sender + const aut = ["author"]; + const renderSigil = + _.get(nextMsg, aut) !== _.get(msg, aut, msg.author); + const paddingTop = renderSigil; + const paddingBot = + _.get(previousMsg, aut) !== _.get(msg, aut, msg.author); + + const when = ["when"]; + const dayBreak = + moment(_.get(nextMsg, when)).format("YYYY.MM.DD") !== + moment(_.get(msg, when)).format("YYYY.MM.DD"); + + const messageElem = ( + ); - } else if (dayBreak) { - return ( - - {messageElem} -
-

{moment(_.get(msg, when)).calendar()}

-
-
- ); - } else { - return messageElem; + + if (isFirstUnread) { + return ( + + {messageElem} +
+
+

New messages below

+
+ {dayBreak && ( +

+ {moment(_.get(msg, when)).calendar()} +

+ )} +
+
+
+ ); + } else if (dayBreak) { + return ( + +
+

{moment(_.get(msg, when)).calendar()}

+
+ {messageElem} +
+ ); + } else { + return messageElem; + } } -}; - +} \ No newline at end of file diff --git a/pkg/interface/src/views/apps/chat/components/lib/chat-scroll-container.js b/pkg/interface/src/views/apps/chat/components/lib/chat-scroll-container.js deleted file mode 100644 index 74580f9954..0000000000 --- a/pkg/interface/src/views/apps/chat/components/lib/chat-scroll-container.js +++ /dev/null @@ -1,143 +0,0 @@ -import React, { Component, Fragment } from "react"; - -import { scrollIsAtTop, scrollIsAtBottom } from "~/logic/lib/util"; - -// Restore chat position on FF when new messages come in -const recalculateScrollTop = (lastScrollHeight, scrollContainer) => { - if (!scrollContainer || !lastScrollHeight) { - return; - } - - const newScrollTop = scrollContainer.scrollHeight - lastScrollHeight; - if (scrollContainer.scrollTop !== 0 || - scrollContainer.scrollTop === newScrollTop) { - return; - } - - scrollContainer.scrollTop = scrollContainer.scrollHeight - lastScrollHeight; -}; - - -export class ChatScrollContainer extends Component { - constructor(props) { - super(props); - - // only for FF - this.state = { - lastScrollHeight: null - }; - - this.isTriggeredScroll = false; - - this.isAtBottom = true; - this.isAtTop = false; - - this.containerDidScroll = this.containerDidScroll.bind(this); - - this.containerRef = React.createRef(); - this.scrollRef = React.createRef(); - } - - containerDidScroll(e) { - const { props } = this; - if (scrollIsAtTop(e.target)) { - // Save scroll position for FF - if (navigator.userAgent.includes("Firefox")) { - this.setState({ - lastScrollHeight: e.target.scrollHeight, - }); - } - - if (!this.isAtTop) { - props.scrollIsAtTop(); - } - - this.isTriggeredScroll = false; - this.isAtBottom = false; - this.isAtTop = true; - } else if (scrollIsAtBottom(e.target) && !this.isTriggeredScroll) { - if (!this.isAtBottom) { - props.scrollIsAtBottom(); - } - - this.isTriggeredScroll = false; - this.isAtBottom = true; - this.isAtTop = false; - } else { - this.isAtBottom = false; - this.isAtTop = false; - this.isTriggeredScroll = false; - } - } - - render() { - // Replace with just the "not Firefox" implementation - // when Firefox #1042151 is patched. - - if (navigator.userAgent.includes("Firefox")) { - return this.firefoxScrollContainer(); - } else { - return this.normalScrollContainer(); - } - } - - firefoxScrollContainer() { - return ( -
-
-
- {this.props.children} -
-
- ); - } - - normalScrollContainer() { - return ( -
-
- {this.props.children} -
- ); - } - - scrollToBottom() { - this.isTriggeredScroll = true; - if (this.scrollRef.current) { - this.scrollRef.current.scrollIntoView(false); - } - - if (navigator.userAgent.includes("Firefox")) { - recalculateScrollTop( - this.state.lastScrollHeight, - this.scrollContainer - ); - } - } - - scrollToReference(ref) { - this.isTriggeredScroll = true; - if (this.scrollRef.current && ref.current) { - ref.current.scrollIntoView({ block: 'center' }); - } - - if (navigator.userAgent.includes("Firefox")) { - recalculateScrollTop( - this.state.lastScrollHeight, - this.scrollContainer - ); - } - } - -} diff --git a/pkg/interface/src/views/apps/chat/components/lib/chat-window.tsx b/pkg/interface/src/views/apps/chat/components/lib/chat-window.tsx index 725abaf0fe..9cfd6d2d85 100644 --- a/pkg/interface/src/views/apps/chat/components/lib/chat-window.tsx +++ b/pkg/interface/src/views/apps/chat/components/lib/chat-window.tsx @@ -1,54 +1,140 @@ import React, { Component, Fragment } from "react"; +import { Virtuoso as VirtualList, VirtuosoMethods } from 'react-virtuoso'; import { ChatMessage } from './chat-message'; -import { ChatScrollContainer } from "./chat-scroll-container"; import { UnreadNotice } from "./unread-notice"; import { ResubscribeElement } from "./resubscribe-element"; import { BacklogElement } from "./backlog-element"; +import { Envelope, IMessage } from "~/types/chat-update"; +import { RouteComponentProps } from "react-router-dom"; +import { Patp, Path } from "~/types/noun"; +import { Contacts } from "~/types/contact-update"; +import { Association } from "~/types/metadata-update"; +import { Group } from "~/types/group-update"; +import GlobalApi from "~/logic/api/global"; +import _ from "lodash"; +import { LocalUpdateRemoteContentPolicy } from "~/types"; +import { ListRange } from "react-virtuoso/dist/engines/scrollSeekEngine"; + -const MAX_BACKLOG_SIZE = 1000; -const DEFAULT_BACKLOG_SIZE = 200; -const PAGE_SIZE = 50; const INITIAL_LOAD = 20; +const DEFAULT_BACKLOG_SIZE = 200; +const IDLE_THRESHOLD = 3; + +const Placeholder = ({ height, index, className = '', style = {}, ...props }) => ( +
+
+ +
+
+
+

+ +

+

+

+
+ +
+
+); -export class ChatWindow extends Component { +type ChatWindowProps = RouteComponentProps<{ + ship: Patp; + station: string; +}> & { + unreadCount: number; + envelopes: Envelope[]; + isChatMissing: boolean; + isChatLoading: boolean; + isChatUnsynced: boolean; + unreadMsg: Envelope | false; + stationPendingMessages: IMessage[]; + mailboxSize: number; + contacts: Contacts; + association: Association; + group: Group; + ship: Patp; + station: any; + api: GlobalApi; + hideNicknames: boolean; + hideAvatars: boolean; + remoteContentPolicy: LocalUpdateRemoteContentPolicy; +} + +interface ChatWindowState { + fetchPending: boolean; + idle: boolean; + range: ListRange; + initialized: boolean; +} + +export class ChatWindow extends Component { + private unreadReference: React.RefObject; + private virtualList: React.RefObject; + constructor(props) { super(props); - this.state = { - numPages: 1, - }; - this.hasAskedForMessages = false; + this.state = { + fetchPending: false, + idle: (this.initialIndex() < props.mailboxSize - IDLE_THRESHOLD) ? true : false, + range: { startIndex: 0, endIndex: 0}, + initialized: false + }; this.dismissUnread = this.dismissUnread.bind(this); - this.scrollIsAtBottom = this.scrollIsAtBottom.bind(this); - this.scrollIsAtTop = this.scrollIsAtTop.bind(this); + this.initialIndex = this.initialIndex.bind(this); + this.scrollToUnread = this.scrollToUnread.bind(this); - this.scrollReference = React.createRef(); this.unreadReference = React.createRef(); + this.virtualList = React.createRef(); } componentDidMount() { this.initialFetch(); + } - if (this.state.numPages === 1 && this.props.unreadCount < INITIAL_LOAD) { - this.dismissUnread(); - this.scrollToBottom(); - } + initialIndex() { + const { mailboxSize, unreadCount } = this.props; + return Math.min(Math.max(mailboxSize - 1 < INITIAL_LOAD + ? 0 + : unreadCount // otherwise if there are unread messages + ? mailboxSize - unreadCount - 1 // put the one right before at the top + : mailboxSize - 1, + 0), mailboxSize); } initialFetch() { - const { props } = this; - if (props.messages.length > 0) { - const unreadUnloaded = props.unreadCount - props.messages.length; - - if (unreadUnloaded <= MAX_BACKLOG_SIZE && - unreadUnloaded + INITIAL_LOAD > DEFAULT_BACKLOG_SIZE) { - this.fetchBacklog(unreadUnloaded + INITIAL_LOAD); - } else { - this.fetchBacklog(DEFAULT_BACKLOG_SIZE); + const { envelopes, mailboxSize, unreadCount } = this.props; + if (envelopes.length > 0) { + const start = Math.min(mailboxSize - unreadCount, mailboxSize - DEFAULT_BACKLOG_SIZE); + this.fetchMessages(start, start + DEFAULT_BACKLOG_SIZE, true); + const initialIndex = this.initialIndex(); + if (initialIndex < mailboxSize - IDLE_THRESHOLD) { + this.setState({ idle: true }); } + if (unreadCount !== mailboxSize) { + this.virtualList.current?.scrollToIndex({ + index: initialIndex, + align: initialIndex <= 1 ? 'end' : 'start' + }); + setTimeout(() => { + this.setState({ initialized: true }); + }, 500); + } else { + this.setState({ initialized: true }); + } + } else { setTimeout(() => { this.initialFetch(); @@ -57,140 +143,164 @@ export class ChatWindow extends Component { } componentDidUpdate(prevProps, prevState) { - const { props, state } = this; + const { isChatMissing, history, envelopes, mailboxSize, unreadCount } = this.props; + let { idle } = this.state; - if (props.isChatMissing) { - props.history.push("/~chat"); - } else if (props.messages.length >= prevProps.messages.length + 10) { - this.hasAskedForMessages = false; - let numPages = props.unreadCount > 0 ? - Math.ceil(props.unreadCount / PAGE_SIZE) : this.state.numPages; + if (isChatMissing) { + history.push("/~chat"); + } else if (envelopes.length !== prevProps.envelopes.length && this.state.fetchPending) { + this.setState({ fetchPending: false }); + } - if (this.state.numPages === numPages) { - if (props.unreadCount > 20) { - this.scrollToUnread(); + if (this.state.range.endIndex !== prevState.range.endIndex) { + if (this.state.range.endIndex < mailboxSize - IDLE_THRESHOLD) { + if (!idle) { + idle = true; } - } else { - this.setState({ numPages }, () => { - if (props.unreadCount > 20) { - this.scrollToUnread(); - } - }); + } else if (idle && (unreadCount === 0 || this.state.range.endIndex === 0)) { + idle = false; } - } else if ( - state.numPages === 1 && - this.props.unreadCount < INITIAL_LOAD && - this.props.unreadCount > 0 - ) { - this.dismissUnread(); - this.scrollToBottom(); + this.setState({ idle }); } - } - scrollIsAtTop() { - const { props, state } = this; - this.setState({ numPages: state.numPages + 1 }, () => { - if (state.numPages * PAGE_SIZE < props.length) { - this.fetchBacklog(DEFAULT_BACKLOG_SIZE); - } - }); - } - - scrollIsAtBottom() { - if (this.state.numPages !== 1) { - this.setState({ numPages: 1 }); - this.dismissUnread(); + if (!idle && idle !== prevState.idle) { + setTimeout(() => { + this.virtualList.current?.scrollToIndex(mailboxSize); + }, 500) } - } - scrollToBottom() { - if (this.scrollReference.current) { - this.scrollReference.current.scrollToBottom(); + if (!idle && prevProps.unreadCount !== unreadCount) { + this.virtualList.current?.scrollToIndex(mailboxSize); } - if (this.state.numPages !== 1) { - this.setState({ numPages: 1 }); + + if (!idle && envelopes.length !== prevProps.envelopes.length) { + this.virtualList.current?.scrollToIndex(mailboxSize); } } scrollToUnread() { - if (this.scrollReference.current && this.unreadReference.current) { - this.scrollReference.current.scrollToReference(this.unreadReference); - } + const { mailboxSize, unreadCount } = this.props; + this.virtualList.current?.scrollToIndex({ + index: mailboxSize - unreadCount, + align: 'center' + }); } dismissUnread() { this.props.api.chat.read(this.props.station); } - fetchBacklog(size) { - const { props } = this; + fetchMessages(start, end, force = false) { + start = Math.max(start, 0); + end = Math.max(end, 0); + const { api, mailboxSize, station } = this.props; if ( - props.messages.length >= props.length || - this.hasAskedForMessages || - props.length <= 0 + (this.state.fetchPending || + mailboxSize <= 0) + && !force ) { return; } + + api.chat + .fetchMessages(Math.max(mailboxSize - end, 0), Math.min(mailboxSize - start, mailboxSize), station) + .finally(() => { + this.setState({ fetchPending: false }); + }); - const start = - props.length - props.messages[props.messages.length - 1].number; - if (start > 0) { - const end = start + size < props.length ? start + size : props.length; - props.api.chat.fetchMessages(start + 1, end, props.station); - this.hasAskedForMessages = true; - } + this.setState({ fetchPending: true }); } render() { - const { props, state } = this; - const sliceLength = Math.min( - state.numPages * PAGE_SIZE, - props.messages.length + props.pendingMessages.length - ); - const messages = - props.pendingMessages - .concat(props.messages) - .slice(0, sliceLength); + const { + envelopes, + stationPendingMessages, + unreadCount, + unreadMsg, + isChatLoading, + isChatUnsynced, + api, + ship, + station, + association, + group, + contacts, + mailboxSize, + hideAvatars, + hideNicknames, + remoteContentPolicy, + } = this.props; + + const messages: Envelope[] = []; + const debouncedFetch = _.debounce(this.fetchMessages, 500).bind(this); + envelopes + .forEach((message) => { + messages[message.number] = message; + }); + + stationPendingMessages.sort((a, b) => a.when - b.when).forEach((message, index) => { + messages[mailboxSize + index + 1] = message; + }); + return ( - - - - { messages.map((msg, i) => ( - 0 && - i === props.unreadCount - 1 && - state.numPages !== 1 - } - msg={msg} - previousMsg={messages[i - 1]} - nextMsg={messages[i + 1]} - association={props.association} - group={props.group} - contacts={props.contacts} - hideAvatars={props.hideAvatars} - hideNicknames={props.hideNicknames} - /> - )) - } - + unreadCount={unreadCount} + unreadMsg={this.state.idle ? unreadMsg : false} + dismissUnread={this.dismissUnread} + onClick={this.scrollToUnread} + /> + + + {messages.length ? Math.abs(velocity) > 2000, + exit: velocity => Math.abs(velocity) < 200, + change: (_velocity, _range) => {}, + placeholder: this.state.initialized ? Placeholder : () =>
+ }} + startReached={() => debouncedFetch(0, DEFAULT_BACKLOG_SIZE)} + overscan={DEFAULT_BACKLOG_SIZE} + rangeChanged={(range) => { + this.setState({ range }); + debouncedFetch(range.startIndex - (DEFAULT_BACKLOG_SIZE / 2), range.endIndex + (DEFAULT_BACKLOG_SIZE / 2)); + }} + item={(i) => { + const number = i + 1; + const msg = messages[number]; + + if (!msg) { + debouncedFetch(number - DEFAULT_BACKLOG_SIZE, number + DEFAULT_BACKLOG_SIZE); + return ; + } + return + }} + /> :
}
); } diff --git a/pkg/interface/src/views/apps/chat/components/lib/content/url.js b/pkg/interface/src/views/apps/chat/components/lib/content/url.js deleted file mode 100644 index a6f1f989a5..0000000000 --- a/pkg/interface/src/views/apps/chat/components/lib/content/url.js +++ /dev/null @@ -1,106 +0,0 @@ -import React, { Component } from 'react'; -import { Button } from '@tlon/indigo-react'; - -const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i); - -const YOUTUBE_REGEX = - new RegExp( - String(/(?:https?:\/\/(?:[a-z]+.)?)/.source) // protocol - + /(?:youtu\.?be(?:\.com)?\/)(?:embed\/)?/.source // short and long-links - + /(?:(?:(?:(?:watch\?)?(?:time_continue=(?:[0-9]+))?.+v=)?([a-zA-Z0-9_-]+))(?:\?t\=(?:[0-9a-zA-Z]+))?)/.source // id - ); - -export default class UrlContent extends Component { - constructor() { - super(); - this.state = { - unfold: false, - copied: false - }; - this.unfoldEmbed = this.unfoldEmbed.bind(this); - } - - unfoldEmbed(id) { - let unfoldState = this.state.unfold; - unfoldState = !unfoldState; - this.setState({ unfold: unfoldState }); - this.iframe.setAttribute('src', this.iframe.dataset.src); - } - - render() { - const { props } = this; - const content = props.content; - const imgMatch = IMAGE_REGEX.exec(props.content.url); - const ytMatch = YOUTUBE_REGEX.exec(props.content.url); - - let contents = content.url; - if (imgMatch) { - contents = ( - - ); - return ( - - {contents} - - ); - } else if (ytMatch) { - contents = ( -
- -
- ); - return ( -
- - {content.url} - - - {contents} -
- ); - } else { - return ( - - {contents} - - ); - } - } -} diff --git a/pkg/interface/src/views/apps/chat/components/lib/delete-button.js b/pkg/interface/src/views/apps/chat/components/lib/delete-button.js index cb7b75ae75..1abb19f7e9 100644 --- a/pkg/interface/src/views/apps/chat/components/lib/delete-button.js +++ b/pkg/interface/src/views/apps/chat/components/lib/delete-button.js @@ -1,51 +1,54 @@ -import React, { Component } from 'react'; +import React, { memo } from 'react'; +export const DeleteButton = memo(({ isOwner, station, changeLoading, association, contacts, api }) => { + const leaveButtonClasses = (!isOwner) ? 'pointer' : 'c-default'; + const deleteButtonClasses = (isOwner) ? + 'b--red2 red2 pointer bg-gray0-d' : + 'b--gray3 gray3 bg-gray0-d c-default'; -export const DeleteButton = (props) => { - const { isOwner, station, changeLoading, api } = props; - const leaveButtonClasses = (!isOwner) ? 'pointer' : 'c-default'; - const deleteButtonClasses = (isOwner) ? - 'b--red2 red2 pointer bg-gray0-d' : - 'b--gray3 gray3 bg-gray0-d c-default'; - - const deleteChat = () => { - changeLoading( - true, - true, - isOwner ? 'Deleting chat...' : 'Leaving chat...', - () => { - api.chat.delete(station); - } - ); - }; - - return ( -
-
-

Leave Chat

-

- Remove this chat from your chat list.{' '} - You will need to request for access again. -

- - Leave this chat - -
-
-

Delete Chat

-

- Permanently delete this chat.{' '} - All current members will no longer see this chat. -

- Delete this chat -
-
+ const deleteChat = () => { + changeLoading( + true, + true, + isOwner ? 'Deleting chat...' : 'Leaving chat...', + () => { + api.chat.delete(station); + } ); -}; + }; + const groupPath = association['group-path']; + const unmanagedVillage = !contacts[groupPath]; + + return ( +
+
+

Leave Chat

+

+ Remove this chat from your chat list.{' '} + {unmanagedVillage + ? 'You will need to request for access again' + : 'You will need to join again from the group page.' + } +

+ + Leave this chat + +
+
+

Delete Chat

+

+ Permanently delete this chat.{' '} + All current members will no longer see this chat. +

+ Delete this chat +
+
+ ); +}) \ No newline at end of file diff --git a/pkg/interface/src/views/apps/chat/components/lib/group-item.js b/pkg/interface/src/views/apps/chat/components/lib/group-item.js index 495e16812d..6f047232a3 100644 --- a/pkg/interface/src/views/apps/chat/components/lib/group-item.js +++ b/pkg/interface/src/views/apps/chat/components/lib/group-item.js @@ -1,13 +1,13 @@ import React, { Component } from 'react'; import { Link } from 'react-router-dom'; import { ChannelItem } from './channel-item'; -import { deSig } from "~/logic/lib/util"; +import { deSig, cite } from "~/logic/lib/util"; export class GroupItem extends Component { render() { const { props } = this; const association = props.association ? props.association : {}; - const DEFAULT_TITLE_REGEX = new RegExp(`(( <-> )?~${deSig(window.ship)}( <-> )?)`); + const DEFAULT_TITLE_REGEX = new RegExp(`(( <-> )?~(?:${window.ship}|${deSig(cite(window.ship))})( <-> )?)`); let title = association['app-path'] ? association['app-path'] : 'Direct Messages'; if (association.metadata && association.metadata.title) { diff --git a/pkg/interface/src/views/apps/chat/components/lib/message-content.js b/pkg/interface/src/views/apps/chat/components/lib/message-content.js index 865ab3af20..41321470a8 100644 --- a/pkg/interface/src/views/apps/chat/components/lib/message-content.js +++ b/pkg/interface/src/views/apps/chat/components/lib/message-content.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import TextContent from './content/text'; import CodeContent from './content/code'; -import UrlContent from './content/url'; +import RemoteContent from '~/views/components/RemoteContent'; export default class MessageContent extends Component { @@ -15,7 +15,18 @@ export default class MessageContent extends Component { if ('code' in content) { return ; } else if ('url' in content) { - return ; + return ( + + ); } else if ('me' in content) { return (

diff --git a/pkg/interface/src/views/apps/chat/components/lib/message.js b/pkg/interface/src/views/apps/chat/components/lib/message.js index 2220658d66..a093bf2378 100644 --- a/pkg/interface/src/views/apps/chat/components/lib/message.js +++ b/pkg/interface/src/views/apps/chat/components/lib/message.js @@ -1,37 +1,41 @@ -import React, { Component } from 'react'; +import React from 'react'; import { OverlaySigil } from './overlay-sigil'; import MessageContent from './message-content'; import { uxToHex, cite, writeText } from '~/logic/lib/util'; import moment from 'moment'; - export const Message = (props) => { - const pending = props.msg.pending ? ' o-40' : ''; + const { + msg, + renderSigil, + remoteContentPolicy, + className = '' + } = props; + const pending = msg.pending ? ' o-40' : ''; const containerClass = - props.renderSigil ? - `w-100 f7 pl3 pt4 pr3 cf flex lh-copy ` + pending : - 'w-100 pr3 cf hide-child flex' + pending; + renderSigil + ? `w-100 f7 pl3 pt4 pr3 cf flex lh-copy ${pending} ${className}` + : `w-100 pr3 cf hide-child flex ${pending} ${className}`; const timestamp = - moment.unix(props.msg.when / 1000).format( - props.renderSigil ? 'hh:mm a' : 'hh:mm' + moment.unix(msg.when / 1000).format( + renderSigil ? 'hh:mm a' : 'hh:mm' ); - return (

{ - props.renderSigil ? ( + renderSigil ? ( renderWithSigil(props, timestamp) ) : (

{timestamp}

- +
) @@ -41,66 +45,80 @@ export const Message = (props) => { }; const renderWithSigil = (props, timestamp) => { - const paddingTop = props.paddingTop ? { 'paddingTop': '6px' } : ''; - const datestamp = - '~' + moment.unix(props.msg.when / 1000).format('YYYY.M.D'); - const contact = props.msg.author in props.contacts - ? props.contacts[props.msg.author] : false; - const showNickname = !props.hideNicknames && contact?.nickname; - let name = `~${props.msg.author}`; - let color = '#000000'; - let sigilClass = 'mix-blend-diff'; - if (contact) { - name = showNickname - ? contact.nickname - : `~${props.msg.author}`; - color = `#${uxToHex(contact.color)}`; - sigilClass = ''; - } + const paddingTop = props.paddingTop ? { 'paddingTop': '6px' } : ''; + const datestamp = + '~' + moment.unix(props.msg.when / 1000).format('YYYY.M.D'); - if (`~${props.msg.author}` === name) { - name = cite(props.msg.author); - } - - return ( -
- -
-
-

- { - writeText(props.msg.author); - }} - title={`~${props.msg.author}`} - > - {name} - -

-

{timestamp}

-

- {datestamp} -

-
- -
-
- ); + const contact = props.msg.author in props.contacts + ? props.contacts[props.msg.author] : false; + const showNickname = !props.hideNicknames && contact?.nickname; + let name = `~${props.msg.author}`; + let color = '#000000'; + let sigilClass = 'mix-blend-diff'; + if (contact) { + name = showNickname + ? contact.nickname + : `~${props.msg.author}`; + color = `#${uxToHex(contact.color)}`; + sigilClass = ''; } + if (`~${props.msg.author}` === name) { + name = cite(props.msg.author); + } + + let nameSpan = null; + + const copyNotice = (saveName) => { + if (nameSpan !== null) { + nameSpan.innerText = 'Copied'; + setTimeout(() => { + nameSpan.innerText = saveName; + }, 800); + } + }; + + return ( +
+ +
+
+

+ nameSpan = e} + onClick={() => { + writeText(`~${props.msg.author}`); + copyNotice(name); + }} + title={`~${props.msg.author}`} + > + {name} + +

+

{timestamp}

+

+ {datestamp} +

+
+ +
+
+ ); +}; + diff --git a/pkg/interface/src/views/apps/chat/components/lib/s3-upload.js b/pkg/interface/src/views/apps/chat/components/lib/s3-upload.js deleted file mode 100644 index b497413cfd..0000000000 --- a/pkg/interface/src/views/apps/chat/components/lib/s3-upload.js +++ /dev/null @@ -1,105 +0,0 @@ -import React, { Component } from 'react'; -import S3Client from '~/logic/lib/s3'; - -export class S3Upload extends Component { - constructor(props) { - super(props); - this.s3 = new S3Client(); - this.setCredentials(props.credentials, props.configuration); - this.inputRef = React.createRef(); - } - - isReady(creds, config) { - return ( - Boolean(creds) && - 'endpoint' in creds && - 'accessKeyId' in creds && - 'secretAccessKey' in creds && - creds.endpoint !== '' && - creds.accessKeyId !== '' && - creds.secretAccessKey !== '' && - Boolean(config) && - 'currentBucket' in config && - config.currentBucket !== '' - ); - } - - componentDidUpdate(prevProps) { - const { props } = this; - this.setCredentials(props.credentials, props.configuration); - } - - setCredentials(credentials, configuration) { - if (!this.isReady(credentials, configuration)) { - return; -} - this.s3.setCredentials( - credentials.endpoint, - credentials.accessKeyId, - credentials.secretAccessKey - ); - } - - getFileUrl(endpoint, filename) { - return endpoint + '/' + filename; - } - - onChange() { - const { props } = this; - if (!this.inputRef.current) { - return; -} - const files = this.inputRef.current.files; - if (files.length <= 0) { - return; -} - - const file = files.item(0); - const bucket = props.configuration.currentBucket; - - this.s3.upload(bucket, file.name, file).then((data) => { - if (!data || !('Location' in data)) { - return; - } - this.props.uploadSuccess(data.Location); - }).catch((err) => { - console.error(err); - this.props.uploadError(err); - }); - } - - onClick() { - if (!this.inputRef.current) { - return; - } - this.inputRef.current.click(); - } - - render() { - const { props } = this; - if (!this.isReady(props.credentials, props.configuration)) { - return
; - } else { - const classes = props.className ? - 'pointer ' + props.className : 'pointer'; - return ( -
- - -
- ); - } - } -} - diff --git a/pkg/interface/src/views/apps/chat/components/lib/unread-notice.js b/pkg/interface/src/views/apps/chat/components/lib/unread-notice.js index 2724d23c9b..954ed33578 100644 --- a/pkg/interface/src/views/apps/chat/components/lib/unread-notice.js +++ b/pkg/interface/src/views/apps/chat/components/lib/unread-notice.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import moment from 'moment'; export const UnreadNotice = (props) => { - const { unreadCount, unreadMsg, dismissUnread } = props; + const { unreadCount, unreadMsg, dismissUnread, onClick } = props; if (!unreadMsg || (unreadCount === 0)) { return null; @@ -22,7 +22,7 @@ export const UnreadNotice = (props) => { "ba b--green2 green2 bg-white bg-gray0-d flex items-center " + "pa2 f9 justify-between br1" }> -

+

{unreadCount} new messages since{' '} {datestamp && ( <> diff --git a/pkg/interface/src/views/apps/chat/components/settings.js b/pkg/interface/src/views/apps/chat/components/settings.js index c450b9e794..a79011ded0 100644 --- a/pkg/interface/src/views/apps/chat/components/settings.js +++ b/pkg/interface/src/views/apps/chat/components/settings.js @@ -85,6 +85,8 @@ export class SettingsScreen extends Component { isOwner={isOwner} changeLoading={this.changeLoading} station={station} + association={association} + contacts={contacts} api={api} /> + accept="image/*" + > + + Channels {shareSheet}

Members

- - {({ index, style }) => (
{ - index <= (contactItems.length - 1) // If the index is within the length of contact items, + totalCount={contactItems.length + groupItems.length} + itemHeight={44} // We happen to know this + item={ + (index) => index <= (contactItems.length - 1) // If the index is within the length of contact items, ? contactItems[index] // show a contact item : groupItems[index - contactItems.length] // Otherwise show a group item - }
)} -
+ } + />
diff --git a/pkg/interface/src/views/apps/groups/components/lib/group-detail.js b/pkg/interface/src/views/apps/groups/components/lib/group-detail.js index 3dbe30dac0..4d7b6ff942 100644 --- a/pkg/interface/src/views/apps/groups/components/lib/group-detail.js +++ b/pkg/interface/src/views/apps/groups/components/lib/group-detail.js @@ -324,11 +324,13 @@ export class GroupDetail extends Component { { if (groupOwner) { - this.setState({ awaiting: true, type: 'Deleting' }, (() => { - props.api.contacts.delete(props.path).then(() => { - props.history.push('/~groups'); - }); - })); + if (prompt(`To confirm deleting this group, type ${props.path.substr(6)}`) === props.path.substr(6)) { + this.setState({ awaiting: true, type: 'Deleting' }, (() => { + props.api.contacts.delete(props.path).then(() => { + props.history.push('/~groups'); + }); + })); + } } }} >Delete this group diff --git a/pkg/interface/src/views/apps/groups/components/lib/s3-upload.js b/pkg/interface/src/views/apps/groups/components/lib/s3-upload.js deleted file mode 100644 index b94b14728a..0000000000 --- a/pkg/interface/src/views/apps/groups/components/lib/s3-upload.js +++ /dev/null @@ -1,96 +0,0 @@ -import React, { Component } from 'react' -import S3Client from '~/logic/lib/s3'; - -export class S3Upload extends Component { - - constructor(props) { - super(props); - this.s3 = new S3Client(); - this.setCredentials(props.credentials, props.configuration); - this.inputRef = React.createRef(); - } - - isReady(creds, config) { - return ( - !!creds && - 'endpoint' in creds && - 'accessKeyId' in creds && - 'secretAccessKey' in creds && - creds.endpoint !== '' && - creds.accessKeyId !== '' && - creds.secretAccessKey !== '' && - !!config && - 'currentBucket' in config && - config.currentBucket !== '' - ); - } - - componentDidUpdate(prevProps) { - const { props } = this; - this.setCredentials(props.credentials, props.configuration); - } - - setCredentials(credentials, configuration) { - if (!this.isReady(credentials, configuration)) { return; } - this.s3.setCredentials( - credentials.endpoint, - credentials.accessKeyId, - credentials.secretAccessKey - ); - } - - getFileUrl(endpoint, filename) { - return endpoint + '/' + filename; - } - - onChange() { - const { props } = this; - if (!this.inputRef.current) { return; } - let files = this.inputRef.current.files; - if (files.length <= 0) { return; } - - let file = files.item(0); - let bucket = props.configuration.currentBucket; - - this.s3.upload(bucket, file.name, file).then((data) => { - if (!data || !('Location' in data)) { - return; - } - this.props.uploadSuccess(data.Location); - }).catch((err) => { - console.error(err); - this.props.uploadError(err); - }); - } - - onClick() { - if (!this.inputRef.current) { return; } - this.inputRef.current.click(); - } - - render() { - const { props } = this; - if (!this.isReady(props.credentials, props.configuration)) { - return
; - } else { - let classes = !!props.className ? - "pointer " + props.className : "pointer"; - return ( -
- - -
- ); - } - } -} - diff --git a/pkg/interface/src/views/apps/links/app.js b/pkg/interface/src/views/apps/links/app.js index 4266a6e27c..21773d3033 100644 --- a/pkg/interface/src/views/apps/links/app.js +++ b/pkg/interface/src/views/apps/links/app.js @@ -19,6 +19,7 @@ import { base64urlDecode } from '~/logic/lib/util'; + export class LinksApp extends Component { componentDidMount() { // preload spinner asset @@ -45,7 +46,7 @@ export class LinksApp extends Component { const invites = props.invites ? props.invites : {}; - const { api, sidebarShown, hideAvatars, hideNicknames } = this.props; + const { api, sidebarShown, hideAvatars, hideNicknames, s3, remoteContentPolicy } = this.props; return ( <> @@ -240,7 +241,8 @@ export class LinksApp extends Component { sidebarShown={sidebarShown} api={api} hideAvatars={hideAvatars} - hideNicknames={hideNicknames} /> + hideNicknames={hideNicknames} + remoteContentPolicy={remoteContentPolicy} /> ); }} diff --git a/pkg/interface/src/views/apps/links/components/lib/comment-item.js b/pkg/interface/src/views/apps/links/components/lib/comment-item.js index a9b7e114f4..3608a0a2a3 100644 --- a/pkg/interface/src/views/apps/links/components/lib/comment-item.js +++ b/pkg/interface/src/views/apps/links/components/lib/comment-item.js @@ -3,6 +3,7 @@ import { Sigil } from '~/logic/lib/sigil'; import { cite } from '~/logic/lib/util'; import moment from 'moment'; import { Box, Text, Row } from '@tlon/indigo-react'; +import RichText from '~/views/components/RichText'; export const CommentItem = (props) => { const content = props.post.contents[0].text; @@ -30,9 +31,13 @@ export const CommentItem = (props) => { {timeSent} - - {content} - - ); + + + {content} + + + + ); + } } diff --git a/pkg/interface/src/views/apps/links/components/new.js b/pkg/interface/src/views/apps/links/components/new.js index aa88ef5959..bca0f45318 100644 --- a/pkg/interface/src/views/apps/links/components/new.js +++ b/pkg/interface/src/views/apps/links/components/new.js @@ -1,9 +1,10 @@ import React, { Component } from 'react'; +import urbitOb from 'urbit-ob'; +import { Link } from 'react-router-dom'; + import { InviteSearch } from '~/views/components/InviteSearch'; import { Spinner } from '~/views/components/Spinner'; -import { Link } from 'react-router-dom'; import { makeRoutePath, deSig } from '~/logic/lib/util'; -import urbitOb from 'urbit-ob'; export const NewScreen = (props) => { const onClickCreate = () => { diff --git a/pkg/interface/src/views/apps/profile/components/lib/RemoteContent.tsx b/pkg/interface/src/views/apps/profile/components/lib/RemoteContent.tsx new file mode 100644 index 0000000000..557a2132d6 --- /dev/null +++ b/pkg/interface/src/views/apps/profile/components/lib/RemoteContent.tsx @@ -0,0 +1,94 @@ +import React from "react"; +import { Box, Button, Checkbox } from '@tlon/indigo-react'; +import { Formik, Form } from "formik"; +import * as Yup from "yup"; + +import GlobalApi from "~/logic/api/global"; +import { LocalUpdateRemoteContentPolicy } from "~/types/local-update"; + +const formSchema = Yup.object().shape({ + imageShown: Yup.boolean(), + audioShown: Yup.boolean(), + videoShown: Yup.boolean(), + oembedShown: Yup.boolean() +}); + +interface FormSchema { + imageShown: boolean; + audioShown: boolean; + videoShown: boolean; + oembedShown: boolean; +} + +interface RemoteContentFormProps { + api: GlobalApi; + remoteContentPolicy: LocalUpdateRemoteContentPolicy; +} + +export default function RemoteContentForm(props: RemoteContentFormProps) { + const { api, remoteContentPolicy } = props; + const imageShown = remoteContentPolicy.imageShown; + const audioShown = remoteContentPolicy.audioShown; + const videoShown = remoteContentPolicy.videoShown; + const oembedShown = remoteContentPolicy.oembedShown; + return ( + { + api.local.setRemoteContentPolicy({ + imageShown: values.imageShown, + audioShown: values.audioShown, + videoShown: values.videoShown, + oembedShown: values.oembedShown + }); + api.local.dehydrate(); + actions.setSubmitting(false); + }} + > + {(props) => ( +
+ + + Remote Content + + + + + + + + + +
+ )} +
+ ); +} \ No newline at end of file diff --git a/pkg/interface/src/views/apps/profile/components/settings.tsx b/pkg/interface/src/views/apps/profile/components/settings.tsx index d59f04f36c..72c6794cdf 100644 --- a/pkg/interface/src/views/apps/profile/components/settings.tsx +++ b/pkg/interface/src/views/apps/profile/components/settings.tsx @@ -19,6 +19,7 @@ import { StoreState } from "../../../store/type"; import DisplayForm from "./lib/DisplayForm"; import S3Form from "./lib/S3Form"; import SecuritySettings from "./lib/Security"; +import RemoteContentForm from "./lib/RemoteContent"; type ProfileProps = StoreState & { api: GlobalApi; ship: string }; @@ -30,6 +31,7 @@ export default function Settings({ hideAvatars, hideNicknames, background, + remoteContentPolicy }: ProfileProps) { return ( + diff --git a/pkg/interface/src/views/apps/profile/profile.tsx b/pkg/interface/src/views/apps/profile/profile.tsx index a3f5219d07..735a5f7690 100644 --- a/pkg/interface/src/views/apps/profile/profile.tsx +++ b/pkg/interface/src/views/apps/profile/profile.tsx @@ -1,12 +1,13 @@ import React from "react"; +import { Route, Link, Switch } from "react-router-dom"; +import Helmet from 'react-helmet'; -import { Box, Text, Row, Col, Center, Icon } from "@tlon/indigo-react"; +import { Box, Text, Row, Col, Icon } from "@tlon/indigo-react"; import { Sigil } from "~/logic/lib/sigil"; import { uxToHex, MOBILE_BROWSER_REGEX } from "~/logic/lib/util"; import Settings from "./components/settings"; -import { Route, Link } from "react-router-dom"; import { ContactCard } from "../groups/components/lib/ContactCard"; const SidebarItem = ({ children, view, current }) => { @@ -15,7 +16,6 @@ const SidebarItem = ({ children, view, current }) => { return ( { export default function ProfileScreen(props: any) { const { ship, dark } = props; return ( + <> + + OS1 - Profile + + { @@ -85,7 +90,7 @@ export default function ProfileScreen(props: any) { - + Your Identity @@ -120,5 +125,7 @@ export default function ProfileScreen(props: any) { ); }} > + + ); } diff --git a/pkg/interface/src/views/apps/publish/app.tsx b/pkg/interface/src/views/apps/publish/app.tsx index 59aafff85e..cc161f1719 100644 --- a/pkg/interface/src/views/apps/publish/app.tsx +++ b/pkg/interface/src/views/apps/publish/app.tsx @@ -63,6 +63,7 @@ export default function PublishApp(props: PublishAppProps) { associations, hideNicknames, hideAvatars, + remoteContentPolicy } = props; const active = location.pathname.endsWith("/~publish") @@ -161,6 +162,8 @@ export default function PublishApp(props: PublishAppProps) { api={api} hideNicknames={hideNicknames} hideAvatars={hideAvatars} + remoteContentPolicy={remoteContentPolicy} + associations={associations} {...props} /> ); diff --git a/pkg/interface/src/views/apps/publish/components/lib/CommentItem.tsx b/pkg/interface/src/views/apps/publish/components/lib/CommentItem.tsx index a8e9f68083..ea0e4cf53f 100644 --- a/pkg/interface/src/views/apps/publish/components/lib/CommentItem.tsx +++ b/pkg/interface/src/views/apps/publish/components/lib/CommentItem.tsx @@ -1,14 +1,13 @@ import React, { useState } from "react"; -import moment from "moment"; -import { Sigil } from "~/logic/lib/sigil"; import CommentInput from "./CommentInput"; -import { uxToHex, cite } from "~/logic/lib/util"; import { Comment, NoteId } from "~/types/publish-update"; import { Contacts } from "~/types/contact-update"; import GlobalApi from "~/logic/api/global"; -import { Button, Box, Row, Text } from "@tlon/indigo-react"; +import { Box, Row } from "@tlon/indigo-react"; import styled from "styled-components"; import { Author } from "./Author"; +import tokenizeMessage from '~/logic/lib/tokenizeMessage'; +import RichText from '~/views/components/RichText'; const ClickBox = styled(Box)` cursor: pointer; @@ -28,17 +27,11 @@ interface CommentItemProps { } export function CommentItem(props: CommentItemProps) { - const { ship, contacts, book, note, api } = props; + const { ship, contacts, book, note, api, remoteContentPolicy } = props; const [editing, setEditing] = useState(false); const commentPath = Object.keys(props.comment)[0]; const commentData = props.comment[commentPath]; - const content = commentData.content.split("\n").map((line, i) => { - return ( - - {line} - - ); - }); + const content = tokenizeMessage(commentData.content).flat().join(' '); const disabled = props.pending || window.ship !== commentData.author.slice(1); @@ -86,14 +79,13 @@ export function CommentItem(props: CommentItemProps) { - {!editing && content} - {editing && ( - - )} + : {content}} ); diff --git a/pkg/interface/src/views/apps/publish/components/lib/Comments.tsx b/pkg/interface/src/views/apps/publish/components/lib/Comments.tsx index e3f17749a9..19bcfaa679 100644 --- a/pkg/interface/src/views/apps/publish/components/lib/Comments.tsx +++ b/pkg/interface/src/views/apps/publish/components/lib/Comments.tsx @@ -8,6 +8,7 @@ import { Contacts } from "~/types/contact-update"; import _ from "lodash"; import GlobalApi from "~/logic/api/global"; import { FormikHelpers } from "formik"; +import { LocalUpdateRemoteContentPolicy } from "~/types"; interface CommentsProps { comments: Comment[]; @@ -21,6 +22,7 @@ interface CommentsProps { enabled: boolean; hideAvatars: boolean; hideNicknames: boolean; + remoteContentPolicy: LocalUpdateRemoteContentPolicy; } export function Comments(props: CommentsProps) { @@ -78,6 +80,7 @@ export function Comments(props: CommentsProps) { pending={true} hideNicknames={props.hideNicknames} hideAvatars={props.hideAvatars} + remoteContentPolicy={props.remoteContentPolicy} /> ); })} @@ -92,6 +95,7 @@ export function Comments(props: CommentsProps) { note={note["note-id"]} hideNicknames={props.hideNicknames} hideAvatars={props.hideAvatars} + remoteContentPolicy={props.remoteContentPolicy} /> ))} diff --git a/pkg/interface/src/views/apps/publish/components/lib/GroupifyForm.tsx b/pkg/interface/src/views/apps/publish/components/lib/GroupifyForm.tsx new file mode 100644 index 0000000000..3ceb350895 --- /dev/null +++ b/pkg/interface/src/views/apps/publish/components/lib/GroupifyForm.tsx @@ -0,0 +1,77 @@ +import React, { useEffect } from "react"; +import { Box, Col, Button, InputLabel, InputCaption } from "@tlon/indigo-react"; +import * as Yup from "yup"; +import GlobalApi from "~/logic/api/global"; +import { Notebook } from "~/types/publish-update"; +import { Contacts } from "~/types/contact-update"; + +import { MetadataForm } from "./MetadataForm"; +import { Groups, Associations } from "~/types"; +import { Formik, FormikHelpers, Form } from "formik"; +import GroupSearch from "~/views/components/GroupSearch"; +import { AsyncButton } from "~/views/components/AsyncButton"; + +const formSchema = Yup.object({ + group: Yup.string().nullable(), +}); + +interface FormSchema { + group: string | null; +} + +interface GroupifyFormProps { + host: string; + book: string; + notebook: Notebook; + groups: Groups; + api: GlobalApi; + associations: Associations; +} + +export function GroupifyForm(props: GroupifyFormProps) { + const onGroupify = async ( + values: FormSchema, + actions: FormikHelpers + ) => { + try { + await props.api.publish.groupify(props.book, values.group); + actions.setStatus({ success: null }); + } catch (e) { + actions.setStatus({ error: e.message }); + } + }; + + const groupPath = props.notebook?.["writers-group-path"]; + + const isUnmanaged = props.groups?.[groupPath]?.hidden || false; + + if (!isUnmanaged) { + return null; + } + + const initialValues: FormSchema = { + group: null + }; + + return ( + +
+ + + Groupify + + +
+ ); +} + +export default GroupifyForm; diff --git a/pkg/interface/src/views/apps/publish/components/lib/Join.tsx b/pkg/interface/src/views/apps/publish/components/lib/Join.tsx index 3c57d3f08a..ff7a91c14b 100644 --- a/pkg/interface/src/views/apps/publish/components/lib/Join.tsx +++ b/pkg/interface/src/views/apps/publish/components/lib/Join.tsx @@ -17,7 +17,7 @@ export function JoinScreen(props: JoinScreenProps & RouteComponentProps) { const [error, setError] = useState(false); const joining = useRef(false); - const waiter = useWaitForProps(props, 10000); + const waiter = useWaitForProps(props); const onJoin = useCallback(async () => { joining.current = true; diff --git a/pkg/interface/src/views/apps/publish/components/lib/MarkdownField.tsx b/pkg/interface/src/views/apps/publish/components/lib/MarkdownField.tsx index efd4f0e475..cec0f6d5ef 100644 --- a/pkg/interface/src/views/apps/publish/components/lib/MarkdownField.tsx +++ b/pkg/interface/src/views/apps/publish/components/lib/MarkdownField.tsx @@ -12,7 +12,7 @@ export const MarkdownField = ({ id, ...rest }: { id: string; } & Parameters + setTouched(true)} onBlur={() => setTouched(false)} diff --git a/pkg/interface/src/views/apps/publish/components/lib/MetadataForm.tsx b/pkg/interface/src/views/apps/publish/components/lib/MetadataForm.tsx new file mode 100644 index 0000000000..4209817ef4 --- /dev/null +++ b/pkg/interface/src/views/apps/publish/components/lib/MetadataForm.tsx @@ -0,0 +1,104 @@ +import React, { useEffect } from "react"; +import { AsyncButton } from "../../../../components/AsyncButton"; +import * as Yup from "yup"; +import { + Box, + Input, + Checkbox, + Col, + InputLabel, + InputCaption, + Button, + Center, +} from "@tlon/indigo-react"; +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 { FormError } from "~/views/components/FormError"; +import { RouteComponentProps, useHistory } from "react-router-dom"; + +interface MetadataFormProps { + host: string; + book: string; + notebook: Notebook; + contacts: Contacts; + api: GlobalApi; +} + +interface FormSchema { + name: string; + description: string; + comments: boolean; +} + +const formSchema = Yup.object({ + name: Yup.string().required("Notebook must have a name"), + description: Yup.string(), + comments: Yup.boolean(), +}); + +const ResetOnPropsChange = (props: { init: FormSchema; book: string }) => { + const { resetForm } = useFormikContext(); + useEffect(() => { + resetForm({ values: props.init }); + }, [props.book]); + + return null; +}; + + +export function MetadataForm(props: MetadataFormProps) { + const { host, notebook, api, book } = props; + const initialValues: FormSchema = { + name: notebook?.title, + description: notebook?.about, + comments: notebook?.comments, + }; + + const onSubmit = async ( + values: FormSchema, + actions: FormikHelpers + ) => { + try { + const { name, description, comments } = values; + await api.publish.editBook(book, name, description, comments); + api.publish.fetchNotebook(host, book); + actions.setStatus({ success: null }); + } catch (e) { + console.log(e); + actions.setStatus({ error: e.message }); + } + }; + + return ( + +
+ + + + + + Save + + + +
+ ); +} diff --git a/pkg/interface/src/views/apps/publish/components/lib/Note.tsx b/pkg/interface/src/views/apps/publish/components/lib/Note.tsx index d3cf8d9285..d460a08111 100644 --- a/pkg/interface/src/views/apps/publish/components/lib/Note.tsx +++ b/pkg/interface/src/views/apps/publish/components/lib/Note.tsx @@ -13,6 +13,7 @@ import { import { Contacts } from "~/types/contact-update"; import GlobalApi from "~/logic/api/global"; import { Author } from "./Author"; +import { LocalUpdateRemoteContentPolicy } from "~/types"; interface NoteProps { ship: string; @@ -24,6 +25,7 @@ interface NoteProps { api: GlobalApi; hideAvatars: boolean; hideNicknames: boolean; + remoteContentPolicy: LocalUpdateRemoteContentPolicy; } export function Note(props: NoteProps & RouteComponentProps) { @@ -115,6 +117,7 @@ export function Note(props: NoteProps & RouteComponentProps) { api={props.api} hideNicknames={props.hideNicknames} hideAvatars={props.hideAvatars} + remoteContentPolicy={props.remoteContentPolicy} /> )} p.theme.space[4]}px; @@ -38,6 +39,7 @@ interface NotebookProps { contacts: Rolodex; groups: Groups; hideNicknames: boolean; + associations: Associations; } export function Notebook(props: NotebookProps & RouteComponentProps) { @@ -130,6 +132,8 @@ export function Notebook(props: NotebookProps & RouteComponentProps) { api={api} notebook={notebook} contacts={notebookContacts} + associations={props.associations} + groups={groups} /> diff --git a/pkg/interface/src/views/apps/publish/components/lib/NotebookRoutes.tsx b/pkg/interface/src/views/apps/publish/components/lib/NotebookRoutes.tsx index ff0dc905b7..efe2f4449e 100644 --- a/pkg/interface/src/views/apps/publish/components/lib/NotebookRoutes.tsx +++ b/pkg/interface/src/views/apps/publish/components/lib/NotebookRoutes.tsx @@ -9,6 +9,7 @@ import { Contacts, Rolodex } from "../../../../types/contact-update"; import Notebook from "./Notebook"; import NewPost from "./new-post"; import { NoteRoutes } from './NoteRoutes'; +import { LocalUpdateRemoteContentPolicy, Associations } from "~/types"; interface NotebookRoutesProps { api: GlobalApi; @@ -21,6 +22,8 @@ interface NotebookRoutesProps { groups: Groups; hideAvatars: boolean; hideNicknames: boolean; + remoteContentPolicy: LocalUpdateRemoteContentPolicy; + associations: Associations; } export function NotebookRoutes( @@ -74,6 +77,7 @@ export function NotebookRoutes( contacts={notebookContacts} hideAvatars={props.hideAvatars} hideNicknames={props.hideNicknames} + remoteContentPolicy={props.remoteContentPolicy} {...routeProps} /> ); diff --git a/pkg/interface/src/views/apps/publish/components/lib/Settings.tsx b/pkg/interface/src/views/apps/publish/components/lib/Settings.tsx index aba15abf14..3bb3693c8d 100644 --- a/pkg/interface/src/views/apps/publish/components/lib/Settings.tsx +++ b/pkg/interface/src/views/apps/publish/components/lib/Settings.tsx @@ -1,128 +1,65 @@ import React, { useEffect } from "react"; -import { AsyncButton } from "../../../../components/AsyncButton"; -import * as Yup from "yup"; -import { - Box, - Input, - Checkbox, - Col, - InputLabel, - InputCaption, - Button, - Center, -} from "@tlon/indigo-react"; -import { Formik, Form, useFormikContext, FormikHelpers } from "formik"; +import { Box, Col, Button, InputLabel, InputCaption } from "@tlon/indigo-react"; import GlobalApi from "~/logic/api/global"; import { Notebook } from "~/types/publish-update"; import { Contacts } from "~/types/contact-update"; -import { FormError } from "~/views/components/FormError"; -import { RouteComponentProps, useHistory } from "react-router-dom"; + +import { MetadataForm } from "./MetadataForm"; +import { Groups, Associations } from "~/types"; +import GroupifyForm from "./GroupifyForm"; +import { useHistory } from "react-router-dom"; interface SettingsProps { host: string; book: string; notebook: Notebook; contacts: Contacts; + groups: Groups; api: GlobalApi; + associations: Associations; } -interface FormSchema { - name: string; - description: string; - comments: boolean; -} - -const formSchema = Yup.object({ - name: Yup.string().required("Notebook must have a name"), - description: Yup.string(), - comments: Yup.boolean(), -}); - -const ResetOnPropsChange = (props: { init: FormSchema; book: string }) => { - const { resetForm } = useFormikContext(); - useEffect(() => { - resetForm({ values: props.init }); - }, [props.book]); - - return null; -}; - +const Divider = (props) => ( + +); export function Settings(props: SettingsProps) { - const { host, notebook, api, book } = props; const history = useHistory(); - const initialValues: FormSchema = { - name: notebook?.title, - description: notebook?.about, - comments: notebook?.comments, - }; - - const onSubmit = async ( - values: FormSchema, - actions: FormikHelpers - ) => { - try { - const { name, description, comments } = values; - await api.publish.editBook(book, name, description, comments); - api.publish.fetchNotebook(host, book); - actions.setStatus({ success: null }); - } catch (e) { - console.log(e); - actions.setStatus({ error: e.message }); - } - }; - const onDelete = async () => { - await api.publish.delBook(book); + await props.api.publish.delBook(props.book); history.push("/~publish"); }; + const groupPath = props.notebook?.["writers-group-path"]; + + const isUnmanaged = props.groups?.[groupPath]?.hidden || false; return ( - -
- - - Delete Notebook - - Permanently delete this notebook. (All current members will no - longer see this notebook.) - - - - - - - - - Save - - - -
-
+ {isUnmanaged && ( + <> + + + + )} + + + + Delete Notebook + + Permanently delete this notebook. (All current members will no longer + see this notebook.) + + + +
); } diff --git a/pkg/interface/src/views/apps/publish/components/lib/Subscribers.tsx b/pkg/interface/src/views/apps/publish/components/lib/Subscribers.tsx index 4ff86c82bf..069e851f33 100644 --- a/pkg/interface/src/views/apps/publish/components/lib/Subscribers.tsx +++ b/pkg/interface/src/views/apps/publish/components/lib/Subscribers.tsx @@ -1,11 +1,12 @@ import React, { Component } from 'react'; import { GroupView } from '~/views/components/Group'; -import { resourceFromPath } from '~/logic/lib/group'; +import { resourceFromPath, roleForShip } from '~/logic/lib/group'; import {Notebook} from '~/types/publish-update'; import GlobalApi from '~/logic/api/global'; import {Groups} from '~/types/group-update'; import {Associations} from '~/types/metadata-update'; import {Rolodex} from '~/types/contact-update'; +import {Box, Button} from '@tlon/indigo-react'; interface SubscribersProps { notebook: Notebook; @@ -71,16 +72,20 @@ export class Subscribers extends Component { addDesc: 'Allow user to write to this notebook' }, ]; - + + if(!group) { + return null; + } + + const role = roleForShip(group, window.ship) return ( -
- + + { role === 'admin' && ( + + )} { associations={this.props.associations} api={this.props.api} /> -
+
); } } diff --git a/pkg/interface/src/views/components/Error.tsx b/pkg/interface/src/views/components/Error.tsx index 95f89d99ed..de502d72d6 100644 --- a/pkg/interface/src/views/components/Error.tsx +++ b/pkg/interface/src/views/components/Error.tsx @@ -12,7 +12,7 @@ class ErrorComponent extends Component { render () { const { code, error, history, description } = this.props; return ( - + {code ? code : 'Error'} diff --git a/pkg/interface/src/views/components/Group.tsx b/pkg/interface/src/views/components/Group.tsx index e364c0aeb4..78baf2bcdf 100644 --- a/pkg/interface/src/views/components/Group.tsx +++ b/pkg/interface/src/views/components/Group.tsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import _, { capitalize } from 'lodash'; -import { FixedSizeList as List } from 'react-window'; +import { Virtuoso as VirtualList } from 'react-virtuoso'; import { cite, deSig } from '~/logic/lib/util'; import { roleForShip, resourceFromPath } from '~/logic/lib/group'; @@ -143,10 +143,8 @@ export class GroupView extends Component< } isAdmin(): boolean { - const us = `~${window.ship}`; const role = roleForShip(this.props.group, window.ship); - const resource = resourceFromPath(this.props.resourcePath); - return resource.ship == us || role === 'admin'; + return role === 'admin'; } optionsForShip(ship: Patp, missing: GroupViewAppTag[]) { @@ -334,14 +332,11 @@ export class GroupView extends Component< {'open' in group.policy && this.renderBanned(group.policy)}
Members
- - {({ index, style }) =>
{memberElements[index]}
} -
+
{memberElements[index]}
} + />
a.metadata.title.toLowerCase().startsWith(s.toLowerCase()) } diff --git a/pkg/interface/src/views/components/OmniboxResult.js b/pkg/interface/src/views/components/OmniboxResult.js deleted file mode 100644 index 589ab12b75..0000000000 --- a/pkg/interface/src/views/components/OmniboxResult.js +++ /dev/null @@ -1,96 +0,0 @@ -import React, { Component } from 'react'; -import { Row, Icon, Text } from '@tlon/indigo-react'; -import defaultApps from '~/logic/lib/default-apps'; - -export class OmniboxResult extends Component { - constructor(props) { - super(props); - this.state = { - isSelected: false, - hovered: false - }; - this.setHover = this.setHover.bind(this); - this.result = React.createRef(); - } - - componentDidUpdate(prevProps) { - const { props, state } = this; - if (prevProps && - !state.hovered && - prevProps.selected !== props.selected && - props.selected === props.link - ) { - this.result.current.scrollIntoView({ block: 'nearest' }); - } - } - - setHover(boolean) { - this.setState({ hovered: boolean }); - } - render() { - const { icon, text, subtext, link, navigate, selected, dark } = this.props; - - let invertGraphic = {}; - - if (icon.toLowerCase() !== 'dojo') { - invertGraphic = (!dark && this.state.hovered) || - selected === link || - (dark && !(this.state.hovered || selected === link)) - ? { filter: 'invert(1)', paddingTop: 2 } - : { filter: 'invert(0)', paddingTop: 2 }; - } else { - invertGraphic = - (!dark && this.state.hovered) || - selected === link || - (dark && !(this.state.hovered || selected === link)) - ? { filter: 'invert(0)', paddingTop: 2 } - : { filter: 'invert(1)', paddingTop: 2 }; - } - - let graphic =
; - if (defaultApps.includes(icon.toLowerCase()) || icon.toLowerCase() === 'links') { - graphic = ; - } else { - graphic = ; - } - return ( - this.setHover(true)} - onMouseLeave={() => this.setHover(false)} - backgroundColor={ - this.state.hovered || selected === link ? 'blue' : 'white' - } - onClick={navigate} - width="100%" - ref={this.result} - > - {this.state.hovered || selected === link ? ( - <> - {graphic} - - {text} - - - {subtext} - - - ) : ( - <> - {graphic} - {text} - - {subtext} - - - )} - - ); - } -} - -export default OmniboxResult; diff --git a/pkg/interface/src/views/components/RemoteContent.tsx b/pkg/interface/src/views/components/RemoteContent.tsx new file mode 100644 index 0000000000..892f1168ec --- /dev/null +++ b/pkg/interface/src/views/components/RemoteContent.tsx @@ -0,0 +1,151 @@ +import React, { Component, Fragment } from 'react'; +import { LocalUpdateRemoteContentPolicy } from "~/types/local-update"; +import { Button } from '@tlon/indigo-react'; +import { hasProvider } from 'oembed-parser'; +import EmbedContainer from 'react-oembed-container'; + +interface RemoteContentProps { + url: string; + remoteContentPolicy: LocalUpdateRemoteContentPolicy; + unfold: boolean; + renderUrl: boolean; + imageProps: any; + audioProps: any; + videoProps: any; + oembedProps: any; +} + +interface RemoteContentState { + unfold: boolean; + embed: any | undefined; +} + +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); + +export default class RemoteContent extends Component { + constructor(props) { + super(props); + this.state = { + unfold: props.unfold || false, + embed: undefined + }; + this.unfoldEmbed = this.unfoldEmbed.bind(this); + this.loadOembed = this.loadOembed.bind(this); + this.wrapInLink = this.wrapInLink.bind(this); + } + + unfoldEmbed() { + let unfoldState = this.state.unfold; + unfoldState = !unfoldState; + this.setState({ unfold: unfoldState }); + } + + loadOembed() { + fetch(`https://noembed.com/embed?url=${this.props.url}`) + .then(response => response.json()) + .then((result) => { + this.setState({ embed: result }); + }).catch((error) => { + this.setState({ embed: 'error' }); + console.log('error fetching oembed', error); + }); + } + + wrapInLink(contents) { + return ( + {contents} + ); + } + + render() { + const { + remoteContentPolicy, + url, + unfold = false, + renderUrl = true, + imageProps = {}, + audioProps = {}, + videoProps = {}, + oembedProps = {}, + ...props + } = this.props; + const isImage = IMAGE_REGEX.test(url); + const isAudio = AUDIO_REGEX.test(url); + const isVideo = VIDEO_REGEX.test(url); + const isOembed = hasProvider(url); + + if (isImage && remoteContentPolicy.imageShown) { + return this.wrapInLink( + + ); + } else if (isAudio && remoteContentPolicy.audioShown) { + return ( + <> + {renderUrl ? this.wrapInLink(url) : null} +