diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..63b3ea5fe --- /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 974acf045..b6800a7a7 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 1a1b08a2d..b64ef9c03 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 c5018a871..914815947 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 02e11e82a..437b4aac8 100644 --- a/bin/solid.pill +++ b/bin/solid.pill @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cd7246753c12c7acb757e1a6ee54c177806c20a137ad8fb4300c000ac146a0f -size 6260139 +oid sha256:3f5741b71f11a562d443fc619eb1b6bb1ccf419375aa2f1eebbd1c06dce20cd0 +size 6268477 diff --git a/pkg/arvo/README.md b/pkg/arvo/README.md index 8f1d06870..433157177 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 fc8d546ec..e6aa7e2e5 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 3004a1c5f..5118babbc 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 b06250fc6..f12a64edb 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, @@ -61,7 +63,7 @@ :_ this :~ :* %pass /srv %agent [our.bol %file-server] %poke %file-server-action - !>([%serve-dir /'~chat' /app/landscape %.n]) + !>([%serve-dir /'~chat' /app/landscape %.n %.y]) == [%pass / %arvo %e %connect [~ /'chat-view'] %chat-view] [%pass /updates %agent [our.bol %chat-store] %watch /updates] @@ -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) @@ -167,7 +169,7 @@ [%pass / %arvo %e %connect [~ /'chat-view'] %chat-view] :* %pass /srv %agent [our.bol %file-server] %poke %file-server-action - !>([%serve-dir /'~chat' /app/landscape %.n]) + !>([%serve-dir /'~chat' /app/landscape %.n %.y]) == == :: @@ -193,7 +195,6 @@ =/ pax t.t.t.t.site.url =/ envelopes (envelope-scry [(scot %ud start) (scot %ud end) pax]) %- json-response:gen - %- json-to-octs %- update:enjs:store [%messages pax start end envelopes] == @@ -212,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 @@ -296,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 74af06098..c4db5f62d 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 afdcee974..4bc66de48 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 @@ -164,10 +165,7 @@ (fact-group-update:cc wire !<(update:group-store q.cage.sign)) [cards this] :: - %invite-update - =^ cards state - (fact-invite-update:cc wire !<(invite-update q.cage.sign)) - [cards this] + %invite-update [~ this] == == :: @@ -304,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) @@ -481,17 +479,6 @@ [%pass / %agent [our.bol %invite-hook] %poke %invite-action !>(act)] -- :: -++ fact-invite-update - |= [wir=wire fact=invite-update] - ^- (quip card _state) - ?+ -.fact [~ state] - %accepted - =/ rid=resource - (de-path:resource path.invite.fact) - :_ state - ~[(contact-view-poke %join rid)] - == -:: ++ group-hook-poke |= =action:group-hook ^- card diff --git a/pkg/arvo/app/contact-store.hoon b/pkg/arvo/app/contact-store.hoon index 50b6f9cfb..6089cae80 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 80bd55a45..9fab4648c 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 :: /- @@ -48,7 +50,7 @@ (contact-poke:cc [%add /~/default our.bowl *contact]) :* %pass /srv %agent [our.bol %file-server] %poke %file-server-action - !>([%serve-dir /'~groups' /app/landscape %.n]) + !>([%serve-dir /'~groups' /app/landscape %.n %.y]) == == :: @@ -63,7 +65,7 @@ [%pass / %arvo %e %connect [~ /'contact-view'] %contact-view] :* %pass /srv %agent [our.bol %file-server] %poke %file-server-action - !>([%serve-dir /'~groups' /app/landscape %.n]) + !>([%serve-dir /'~groups' /app/landscape %.n %.y]) == == :: diff --git a/pkg/arvo/app/dbug.hoon b/pkg/arvo/app/dbug.hoon index 5b2d517b2..80c5fc140 100644 --- a/pkg/arvo/app/dbug.hoon +++ b/pkg/arvo/app/dbug.hoon @@ -148,9 +148,7 @@ :: =; json=(unit json) ?~ json not-found:gen - %- json-response:gen - =, html - (as-octt:mimes (en-json u.json)) + (json-response:gen u.json) =, enjs:format ?+ site ~ :: /apps.json: {appname: running?} diff --git a/pkg/arvo/app/dojo.hoon b/pkg/arvo/app/dojo.hoon index ffd205c3e..da42334f2 100644 --- a/pkg/arvo/app/dojo.hoon +++ b/pkg/arvo/app/dojo.hoon @@ -502,8 +502,8 @@ ^+ +> :: XX needs filter :: - :: ?: ?=({$show $3} -.mad) - :: (dy-rash %tan (dy-show-source q.mad) ~) :: XX separate command + ?: ?=({$show $3} -.mad) + (dy-rash %tan (dy-show-source q.mad) ~) ?: ?=($brev -.mad) =. var (~(del by var) p.mad) =< dy-amok @@ -589,10 +589,8 @@ ?- p.p.mad %0 ~ %1 [[%rose [~ " " ~] (skol p.q.cay) ~] maar] - :: XX actually print something meaningful here - :: - %2 [[%rose [~ " " ~] *tank ~] maar] - %3 ~ + %2 [[%rose [~ " " ~] (dy-show-type-noun p.q.cay) ~] maar] + ::%3 handled above %4 ~ %5 [[%rose [~ " " ~] (xskol p.q.cay) ~] maar] == @@ -638,6 +636,70 @@ :- i="" t=(turn `wain`?~(r.hit ~ (to-wain:format q.u.r.hit)) trip) == + ++ dy-show-type-noun + |= a/type ^- tank + =- >[-]< + |- ^- $? $% {$atom @tas (unit @)} + {$cell _$ _$} + {$face $@(term tune) _$} + {$fork (set _$)} + {$hold _$ hoon} + == + wain :: "<|core|>" + $?($noun $void) + == + ?+ a a + {$face ^} a(q $(a q.a)) + {$cell ^} a(p $(a p.a), q $(a q.a)) + {$fork *} a(p (silt (turn ~(tap in p.a) |=(b/type ^$(a b))))) + {$hint *} !! + {$core ^} `wain`/core + {$hold *} a(p $(a p.a)) + == + :: + :: XX needs filter + :: + ++ dy-shown + =/ jank-bucwut :: FIXME just $? fishes when defined for some reason + |* [a=mold b=mold] + |=(c=_`*`*a ?:(& (a c) (b c))) + :: + ::$? hoon + ;: jank-bucwut + hoon + $^ {dy-shown dy-shown} + $% {$ur cord} + {$sa mark} + {$as mark dy-shown} + {$do hoon dy-shown} + {$te term (list dy-shown)} + {$ge path (list dy-shown) (map term (unit dy-shown))} + {$dv path} + == + == + :: + ++ dy-show-source + |= a/dojo-source ^- tank + =- >[-]< + =+ `{@ bil/dojo-build}`a + |- ^- dy-shown + ?- -.bil + $?($ur $dv $sa) bil + $ex ?. ?=({$cltr *} p.bil) p.bil + |- ^- hoon + ?~ p.p.bil !! + ?~ t.p.p.bil i.p.p.bil + [i.p.p.bil $(p.p.bil t.p.p.bil)] + $tu ?~ p.bil !! + |- + ?~ t.p.bil ^$(bil q.i.p.bil) + [^$(bil q.i.p.bil) $(p.bil t.p.bil)] + $as bil(q $(bil q.q.bil)) + $do bil(q $(bil q.q.bil)) + $te bil(q (turn q.bil ..$)) + $ge :+ %ge q.p.p.bil + [(turn p.q.p.bil ..$) (~(run by q.q.p.bil) (lift ..$))] + == :: ++ dy-edit :: handle edit |= cal/sole-change @@ -875,6 +937,8 @@ ?> ?=(~ cud) ?: =(nex num) dy-over + ?: =([%show %3] -.mad) :: just show source + dy-over dy-make(cud `[nex (~(got by job) nex)]) -- :: diff --git a/pkg/arvo/app/file-server.hoon b/pkg/arvo/app/file-server.hoon index 091d5815a..ddeb7b3d9 100644 --- a/pkg/arvo/app/file-server.hoon +++ b/pkg/arvo/app/file-server.hoon @@ -1,26 +1,28 @@ +:: file-server [landscape]: +:: +:: mounts HTTP endpoints for Landscape (and third-party) user applications +:: /- srv=file-server, glob /+ *server, default-agent, verb, dbug |% +$ card card:agent:gall -+$ serving (map url-base=path [=content public=?]) ++$ serving (map url-base=path [=content public=? single-page=?]) +$ content $% [%clay =path] [%glob =glob:glob] == -+$ state-base - $: =configuration:srv +:: ++$ state-3 + $: %3 + =configuration:srv =serving == -+$ state-2 - $: %2 - state-base - == -- :: %+ verb | %- agent:dbug :: -=| state-2 +=| state-3 =* state - ^- agent:gall |_ =bowl:gall @@ -36,7 +38,7 @@ %+ turn ^- (list path) [/ /'~landscape' ~] - |=(pax=path [pax [clay+/app/landscape %.n]]) + |=(pax=path [pax [clay+/app/landscape %.n %.y]]) == :~ (connect /) (connect /'~landscape') @@ -68,24 +70,35 @@ - %2 serving (~(del by serving.old-state) /'~landscape'/js/index) == - ?> ?=(%2 -.old-state) + =? old-state ?=(%2 -.old-state) + %= old-state + - %3 + serving + %- ~(run by serving.old-state) + |= [=content public=?] + ^- [^content ? ?] + [content public %.y] + == + ?> ?=(%3 -.old-state) [~ this(state old-state)] :: + +$ serving-0 (map url-base=path [=clay=path public=?]) + +$ serving-1 (map url-base=path [=content public=?]) +$ versioned-state $% state-0 - state-1 - state-2 + [%1 state-1] + [%2 state-1] + state-3 == :: - +$ serving-0 (map url-base=path [=clay=path public=?]) +$ state-0 $: %0 =configuration:srv =serving-0 == +$ state-1 - $: %1 - state-base + $: =configuration:srv + serving=serving-1 == -- :: @@ -113,14 +126,17 @@ ?: (~(has by serving) url-base) ~|("url already bound to {<(~(got by serving) url-base.act)>}" !!) :- [%pass url-base %arvo %e %connect [~ url-base] %file-server]~ - this(serving (~(put by serving) url-base clay+clay-base.act public.act)) + %_ this + serving + (~(put by serving) url-base clay+clay-base.act public.act spa.act) + == :: %serve-glob =* url-base url-base.act ?: (~(has by serving) url-base) ~|("url already bound to {<(~(got by serving) url-base.act)>}" !!) :- [%pass url-base %arvo %e %connect [~ url-base] %file-server]~ - this(serving (~(put by serving) url-base glob+glob.act public.act)) + this(serving (~(put by serving) url-base glob+glob.act public.act %.y)) :: %unserve-dir :- [%pass url-base.act %arvo %e %disconnect [~ url-base.act]]~ @@ -129,9 +145,9 @@ %toggle-permission ?. (~(has by serving) url-base.act) ~|("url is not bound" !!) - =/ [=content public=?] (~(got by serving) url-base.act) + =/ [=content public=? spa=?] (~(got by serving) url-base.act) :- ~ - this(serving (~(put by serving) url-base.act [content !public])) + this(serving (~(put by serving) url-base.act [content !public spa])) :: %set-landscape-homepage-prefix =. landscape-homepage-prefix.configuration prefix.act @@ -158,6 +174,7 @@ |= =cord ^- (unit ^cord) ?:(=(cord '') ~ `cord) + =/ is-file ?=(^ ext.req-line) =? req-line ?=(~ ext.req-line) [[[~ %html] (snoc site.req-line 'index')] args.req-line] ?~ site.req-line @@ -174,17 +191,18 @@ %- js-response:gen (as-octt:mimes:html "window.ship = '{+:(scow %p our.bowl)}';") :: - =/ [payload=simple-payload:http public=?] (get-file req-line) + =/ [payload=simple-payload:http public=?] (get-file req-line is-file) ?: public payload (require-authorization-simple:app inbound-request payload) :: ++ get-file - |= req-line=request-line + |= [req-line=request-line is-file=?] ^- [simple-payload:http ?] =/ pax=path ?~ ext.req-line site.req-line (snoc site.req-line u.ext.req-line) - =/ content=(unit [=content suffix=path public=?]) (get-content pax) + =/ content=(unit [=content suffix=path public=?]) + (get-content pax is-file) ?~ content [not-found:gen %.n] ?- -.content.u.content %clay @@ -204,8 +222,8 @@ :: [~ %html] %. file - %* . html-response:gen - cache + %* . html-response:gen + cache !=(/app/landscape/index/html (slag 3 scry-path)) == == @@ -234,23 +252,28 @@ (add char ^~((sub 'a' 'A'))) :: ++ get-content - |= pax=path + |= [pax=path is-file=?] ^- (unit [content path ?]) - =/ first-try (match-content-path pax (~(del by serving) /)) + =/ first-try (match-content-path pax (~(del by serving) /) is-file) ?^ first-try first-try =/ root (~(get by serving) /) ?~ root ~ - (match-content-path pax (~(gas by *^serving) [[/ u.root] ~])) + (match-content-path pax (~(gas by *^serving) [[/ u.root] ~]) is-file) :: ++ match-content-path - |= [pax=path =^serving] + |= [pax=path =^serving is-file=?] ^- (unit [content path ?]) %- ~(rep by serving) - |= [[url-base=path =content public=?] out=(unit [content path ?])] + |= $: [url-base=path =content public=? spa=?] + out=(unit [content path ?]) + == ?^ out out =/ suf (get-suffix url-base pax) ?~ suf ~ - `[content u.suf public] + =- `[content - public] + ?: ?&(spa !is-file) + /index/html + u.suf :: ++ get-suffix |= [a=path b=path] diff --git a/pkg/arvo/app/glob.hoon b/pkg/arvo/app/glob.hoon index f19d51515..951c95e61 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 0v2.pbthv.gd1q2.h2ura.5esrn.d361c +++ 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-store.hoon b/pkg/arvo/app/graph-store.hoon index ab9a6b971..b30c6f7b4 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 :: @@ -447,16 +450,29 @@ |^ ?> (team:title our.bowl src.bowl) ?+ path (on-peek:def path) - [%x %keys ~] ``noun+!>(~(key by graphs)) - [%x %tags ~] ``noun+!>(~(key by tag-queries)) - [%x %tag-queries ~] ``noun+!>(tag-queries) + [%x %keys ~] + :- ~ :- ~ :- %graph-update + !>(`update:store`[%0 now.bowl [%keys ~(key by graphs)]]) + :: + [%x %tags ~] + :- ~ :- ~ :- %graph-update + !>(`update:store`[%0 now.bowl [%tags ~(key by tag-queries)]]) + :: + [%x %tag-queries ~] + :- ~ :- ~ :- %graph-update + !>(`update:store`[%0 now.bowl [%tag-queries tag-queries]]) + :: [%x %graph @ @ ~] =/ =ship (slav %p i.t.t.path) =/ =term i.t.t.t.path =/ result=(unit marked-graph:store) (~(get by graphs) [ship term]) ?~ result [~ ~] - ``noun+!>(u.result) + :- ~ :- ~ :- %graph-update + !> ^- update:store + :+ %0 + now.bowl + [%add-graph [ship term] `graph:store`p.u.result q.u.result] :: [%x %graph-subset @ @ @ @ ~] =/ =ship (slav %p i.t.t.path) @@ -466,7 +482,16 @@ =/ graph=(unit marked-graph:store) (~(get by graphs) [ship term]) ?~ graph [~ ~] - ``noun+!>(`graph:store`(subset:orm p.u.graph start end)) + :- ~ :- ~ :- %graph-update + !> ^- update:store + :+ %0 now.bowl + :+ %add-nodes + [ship term] + %- ~(gas by *(map index:store node:store)) + %+ turn (tap:orm `graph:store`(subset:orm p.u.graph start end)) + |= [=atom =node:store] + ^- [index:store node:store] + [~[atom] node] :: [%x %node @ @ @ *] =/ =ship (slav %p i.t.t.path) @@ -475,28 +500,13 @@ (turn t.t.t.t.path |=(=cord (slav %ud cord))) =/ node=(unit node:store) (get-node ship term index) ?~ node [~ ~] - ``noun+!>(u.node) - :: - [%x %post @ @ @ *] - =/ =ship (slav %p i.t.t.path) - =/ =term i.t.t.t.path - =/ =index:store - (turn t.t.t.t.path |=(=cord (slav %ud cord))) - =/ node=(unit node:store) (get-node ship term index) - ?~ node [~ ~] - ``noun+!>(post.u.node) - :: - [%x %node-children @ @ @ *] - =/ =ship (slav %p i.t.t.path) - =/ =term i.t.t.t.path - =/ =index:store - (turn t.t.t.t.path |=(=cord (slav %ud cord))) - =/ node=(unit node:store) (get-node ship term index) - ?~ node [~ ~] - ?- -.children.u.node - %empty [~ ~] - %graph ``noun+!>(p.children.u.node) - == + :- ~ :- ~ :- %graph-update + !> ^- update:store + :+ %0 + now.bowl + :+ %add-nodes + [ship term] + (~(gas by *(map index:store node:store)) [index u.node] ~) :: [%x %node-children-subset @ @ @ @ @ *] =/ =ship (slav %p i.t.t.path) @@ -509,7 +519,18 @@ ?~ node [~ ~] ?- -.children.u.node %empty [~ ~] - %graph ``noun+!>(`graph:store`(subset:orm p.children.u.node start end)) + %graph + :- ~ :- ~ :- %graph-update + !> ^- update:store + :+ %0 + now.bowl + :+ %add-nodes + [ship term] + %- ~(gas by *(map index:store node:store)) + %+ turn (tap:orm `graph:store`(subset:orm p.children.u.node start end)) + |= [=atom =node:store] + ^- [index:store node:store] + [(snoc index atom) node] == :: [%x %update-log @ @ ~] @@ -525,7 +546,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 4b2d7a220..5a29752e3 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 5a8c639c1..4d6da6389 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 443889e88..7e5215fa0 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 0aa3634eb..57552866a 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 @@ -227,8 +229,11 @@ ++ peek-group-join |= [rid=resource =ship] - =/ =group - (~(gut by groups) rid *group) + =/ ugroup + (~(get by groups) rid) + ?~ ugroup + %.n + =* group u.ugroup =* policy policy.group ?- -.policy %invite @@ -236,7 +241,7 @@ (~(has in members.group) ship) == %open - ?! ?| + ?! ?| (~(has in banned.policy) ship) (~(has in ban-ranks.policy) (clan:title ship)) == @@ -282,7 +287,7 @@ ^- resource ?> ?=([@ @ *] path) :- (slav %p i.path) - i.t.path + i.t.path :: ++ add-new |= =permission:permission-store @@ -290,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/invite-hook.hoon b/pkg/arvo/app/invite-hook.hoon index 2e4cef97b..0e7b93d97 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 f08bb6cbf..c09bd4cba 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 666ed5473..72f3a0cc5 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 1a10125af..17031a933 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/language-server.hoon b/pkg/arvo/app/language-server.hoon index 3ad37f516..0b2e5c31e 100644 --- a/pkg/arvo/app/language-server.hoon +++ b/pkg/arvo/app/language-server.hoon @@ -113,7 +113,7 @@ ++ json-response |= [eyre-id=@ta jon=json] ^- (list card) - (give-simple-payload:app eyre-id (json-response:gen (json-to-octs jon))) + (give-simple-payload:app eyre-id (json-response:gen jon)) :: ++ give-rpc-notification |= res=out:notification:lsp-sur diff --git a/pkg/arvo/app/launch.hoon b/pkg/arvo/app/launch.hoon index 5ceb6296c..ee0b23964 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 @@ -77,7 +81,7 @@ :~ [%pass / %arvo %e %disconnect [~ /]] :* %pass /srv %agent [our.bowl %file-server] %poke %file-server-action - !>([%serve-dir / /app/landscape %.n]) + !>([%serve-dir / /app/landscape %.n %.y]) == == %+ turn ~(tap by wex.bowl) @@ -161,8 +165,11 @@ ++ on-peek |= =path ^- (unit (unit cage)) - ?+ path (on-peek:def path) - [%x %keys ~] ``noun+!>(~(key by tiles)) + ?. (team:title our.bowl src.bowl) ~ + ?+ path [~ ~] + [%x %tiles ~] ``noun+!>([tiles tile-ordering]) + [%x %first-time ~] ``noun+!>(first-time) + [%x %keys ~] ``noun+!>(~(key by tiles)) == :: ++ on-arvo diff --git a/pkg/arvo/app/lens.hoon b/pkg/arvo/app/lens.hoon index 73004cb27..8c86f8f8d 100644 --- a/pkg/arvo/app/lens.hoon +++ b/pkg/arvo/app/lens.hoon @@ -136,7 +136,7 @@ :: :_ this %+ give-simple-payload:app eyre-id.u.job.state - (json-response:gen (json-to-octs jon)) + (json-response:gen jon) :: ++ take-sole-effect |= fec=sole-effect @@ -186,7 +186,7 @@ %+ give-simple-payload:app eyre-id.u.job.state ?- -.u.out %json - (json-response:gen (json-to-octs json.u.out)) + (json-response:gen json.u.out) :: %mime =/ headers diff --git a/pkg/arvo/app/link-listen-hook.hoon b/pkg/arvo/app/link-listen-hook.hoon index 9fae8d98b..8fe602d2d 100644 --- a/pkg/arvo/app/link-listen-hook.hoon +++ b/pkg/arvo/app/link-listen-hook.hoon @@ -1,4 +1,6 @@ -:: link-listen-hook: get your friends' bookmarks +:: link-listen-hook [landscape]: +:: +:: get your friends' bookmarks :: :: keeps track of a listening=(set app-path). users can manually add to and :: remove from this set. @@ -118,7 +120,7 @@ /app-indices == |- - ?~ resources + ?~ resources upgrade-loop(old [%2 +.old]) =, i.resources =/ members=(set ship) diff --git a/pkg/arvo/app/link-proxy-hook.hoon b/pkg/arvo/app/link-proxy-hook.hoon index 98f61f6f8..4d325e11f 100644 --- a/pkg/arvo/app/link-proxy-hook.hoon +++ b/pkg/arvo/app/link-proxy-hook.hoon @@ -1,4 +1,6 @@ -:: link-proxy-hook: make local pages available to foreign ships +:: link-proxy-hook [landscape]: +:: +:: make local pages available to foreign ships :: :: this is a "proxy" style hook, relaying foreign subscriptions into local :: stores if permission conditions are met. diff --git a/pkg/arvo/app/link-store.hoon b/pkg/arvo/app/link-store.hoon index fed3d1206..8fe1c54a2 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 diff --git a/pkg/arvo/app/link-view.hoon b/pkg/arvo/app/link-view.hoon index 4e5fe6de7..3487e4e92 100644 --- a/pkg/arvo/app/link-view.hoon +++ b/pkg/arvo/app/link-view.hoon @@ -1,4 +1,6 @@ -:: link-view: frontend endpoints +:: link-view [landscape]: +:: +::frontend endpoints :: :: endpoints, mapping onto link-store's paths. p is for page as in pagination. :: only the /0/submissions endpoint provides updates. @@ -65,7 +67,7 @@ [%pass - %agent [our.bowl %invite-store] %watch -] :* %pass /srv %agent [our.bowl %file-server] %poke %file-server-action - !>([%serve-dir /'~link' /app/landscape %.n]) + !>([%serve-dir /'~link' /app/landscape %.n %.y]) == == :: @@ -81,7 +83,7 @@ :- [%pass /connect %arvo %e %disconnect [~ /'~link']] :~ :* %pass /srv %agent [our.bowl %file-server] %poke %file-server-action - !>([%serve-dir /'~link' /app/landscape %.n]) + !>([%serve-dir /'~link' /app/landscape %.n %.y]) == == == :: diff --git a/pkg/arvo/app/metadata-hook.hoon b/pkg/arvo/app/metadata-hook.hoon index e3acbd012..5cad89319 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 a0e88e2e6..0a6c52db6 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 266666a8c..535b54caf 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 7c55bdc68..ba715b64c 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 be9b2923e..0d051aac7 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 74e9385d3..e858112c5 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 2ed386584..cf0654df8 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 @@ -96,7 +100,7 @@ == :* %pass /srv %agent [our.bol %file-server] %poke %file-server-action - !>([%serve-dir /'~publish' /app/landscape %.n]) + !>([%serve-dir /'~publish' /app/landscape %.n %.y]) == [%pass /groups %agent [our.bol %group-store] %watch /groups] == @@ -126,7 +130,7 @@ [%pass /view-bind %arvo %e %connect [~ /'publish-view'] %publish] :* %pass /srv %agent [our.bol %file-server] %poke %file-server-action - !>([%serve-dir /'~publish' /app/landscape %.n]) + !>([%serve-dir /'~publish' /app/landscape %.n %.y]) == == =+ ^- [kick-cards=(list card) old-subs=(jug @tas @p)] kick-subs @@ -197,7 +201,7 @@ [%pass /view-bind %arvo %e %connect [~ /'publish-view'] %publish] :* %pass /srving %agent [our.bol %file-server] %poke %file-server-action - !>([%serve-dir /'~publish' /app/landscape %.n]) + !>([%serve-dir /'~publish' /app/landscape %.n %.y]) == == == :: @@ -2347,7 +2351,6 @@ :: all notebooks, short form [[[~ %json] [%'publish-view' %notebooks ~]] ~] %- json-response:gen - %- json-to-octs (notebooks-map:enjs our.bol books) :: :: notes pagination @@ -2366,7 +2369,6 @@ ?~ length not-found:gen %- json-response:gen - %- json-to-octs :- %o (notes-page:enjs notes.u.book u.start u.length) :: @@ -2390,7 +2392,6 @@ ?~ length not-found:gen %- json-response:gen - %- json-to-octs (comments-page:enjs comments.u.note u.start u.length) :: :: single notebook with initial 50 notes in short form, as json @@ -2409,7 +2410,7 @@ (~(put by p.notebook-json) %subscribers (get-subscribers-json book-name)) =. p.notebook-json (~(put by p.notebook-json) %writers (get-writers-json u.host book-name)) - (json-response:gen (json-to-octs (pairs notebook+notebook-json ~))) + (json-response:gen (pairs notebook+notebook-json ~)) :: :: single note, with initial 50 comments, as json [[[~ %json] [%'publish-view' @ @ @ ~]] ~] @@ -2424,7 +2425,7 @@ ?~ note not-found:gen =/ jon=json o+(note-presentation:enjs u.book note-name u.note) - (json-response:gen (json-to-octs jon)) + (json-response:gen jon) == :: -- diff --git a/pkg/arvo/app/s3-store.hoon b/pkg/arvo/app/s3-store.hoon index c9262ad9d..5b0bf0c70 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 ~ @@ -89,7 +93,18 @@ -- :: ++ on-leave on-leave:def -++ on-peek on-peek:def +++ on-peek + ~/ %s3-peek + |= =path + ^- (unit (unit cage)) + ?. (team:title our.bowl src.bowl) ~ + ?+ path [~ ~] + [%x %credentials ~] + [~ ~ %s3-update !>(`update`[%credentials credentials])] + :: + [%x %configuration ~] + [~ ~ %s3-update !>(`update`[%configuration configuration])] + == ++ on-agent on-agent:def ++ on-arvo on-arvo:def ++ on-fail on-fail:def diff --git a/pkg/arvo/app/soto.hoon b/pkg/arvo/app/soto.hoon index 606cdea28..2ca0c2620 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 @@ -29,7 +30,7 @@ :_ ~ :* %pass /srv %agent [our.bol %file-server] %poke %file-server-action - !>([%serve-dir /'~dojo' /app/landscape %.n]) + !>([%serve-dir /'~dojo' /app/landscape %.n %.y]) == ++ on-save !>(state) :: @@ -43,7 +44,7 @@ :~ [%pass /bind/soto %arvo %e %disconnect [~ /'~dojo']] :* %pass /srv %agent [our.bol %file-server] %poke %file-server-action - !>([%serve-dir /'~dojo' /app/landscape %.n]) + !>([%serve-dir /'~dojo' /app/landscape %.n %.y]) == == :: diff --git a/pkg/arvo/app/weather.hoon b/pkg/arvo/app/weather.hoon index 9c07b0fae..684956b15 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/chat-store.hoon b/pkg/arvo/lib/chat-store.hoon index 1ef713879..3b40c6c05 100644 --- a/pkg/arvo/lib/chat-store.hoon +++ b/pkg/arvo/lib/chat-store.hoon @@ -61,8 +61,9 @@ ^- json %+ frond %chat-update %- pairs - :~ - ?: ?=(%initial -.upd) + :_ ~ + ?- -.upd + %initial :- %initial %- pairs %+ turn ~(tap by inbox.upd) @@ -73,27 +74,37 @@ :~ [%envelopes [%a (turn envelopes.mailbox envelope)]] [%config (config config.mailbox)] == - ?: ?=(%message -.upd) - :- %message - %- pairs - :~ [%path (path path.upd)] - [%envelope (envelope envelope.upd)] - == - ?: ?=(%messages -.upd) - :- %messages - %- pairs - :~ [%path (path path.upd)] - [%start (numb start.upd)] - [%end (numb end.upd)] - [%envelopes [%a (turn envelopes.upd envelope)]] - == - ?: ?=(%read -.upd) - [%read (pairs [%path (path path.upd)]~)] - ?: ?=(%create -.upd) - [%create (pairs [%path (path path.upd)]~)] - ?: ?=(%delete -.upd) - [%delete (pairs [%path (path path.upd)]~)] - [*@t *json] + :: + %message + :- %message + %- pairs + :~ [%path (path path.upd)] + [%envelope (envelope envelope.upd)] + == + :: + %messages + :- %messages + %- pairs + :~ [%path (path path.upd)] + [%start (numb start.upd)] + [%end (numb end.upd)] + [%envelopes [%a (turn envelopes.upd envelope)]] + == + :: + %read + [%read (pairs [%path (path path.upd)]~)] + :: + %create + [%create (pairs [%path (path path.upd)]~)] + :: + %delete + [%delete (pairs [%path (path path.upd)]~)] + :: + %keys + :- %keys + :- %a + %+ turn ~(tap by keys.upd) + |= pax=^path (path pax) == -- ++ dejs diff --git a/pkg/arvo/lib/graph-store.hoon b/pkg/arvo/lib/graph-store.hoon index bf099c89f..4ed56c4a7 100644 --- a/pkg/arvo/lib/graph-store.hoon +++ b/pkg/arvo/lib/graph-store.hoon @@ -247,20 +247,24 @@ |% ++ decode %- of - :~ [%add-graph add-graph] - [%remove-graph remove-graph] - [%add-nodes add-nodes] + :~ [%add-nodes add-nodes] [%remove-nodes remove-nodes] [%add-signatures add-signatures] [%remove-signatures remove-signatures] + :: + [%add-graph add-graph] + [%remove-graph remove-graph] + :: [%add-tag add-tag] [%remove-tag remove-tag] + :: [%archive-graph archive-graph] [%unarchive-graph unarchive-graph] + [%run-updates run-updates] + :: [%keys keys] [%tags tags] [%tag-queries tag-queries] - [%run-updates run-updates] == :: ++ add-graph diff --git a/pkg/arvo/lib/pull-hook.hoon b/pkg/arvo/lib/pull-hook.hoon index 6fdbf1007..ce4954bb6 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,7 +262,9 @@ ++ add |= [=ship =resource] ~| resource - ?< (~(has by tracking) resource) + ?< |(=(our.bowl ship) =(our.bowl entity.resource)) + ?: (~(has by tracking) resource) + [~ state] =. tracking (~(put by tracking) resource ship) :_ state diff --git a/pkg/arvo/lib/push-hook.hoon b/pkg/arvo/lib/push-hook.hoon index 776aa0f59..bbe6f73c6 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/server.hoon b/pkg/arvo/lib/server.hoon index d51fa12c8..1f450c3e5 100644 --- a/pkg/arvo/lib/server.hoon +++ b/pkg/arvo/lib/server.hoon @@ -92,9 +92,9 @@ [[200 [['content-type' 'text/javascript'] max-1-da ~]] `octs] :: ++ json-response - |= =octs + |= =json ^- simple-payload:http - [[200 ['content-type' 'application/json']~] `octs] + [[200 ['content-type' 'application/json']~] `(json-to-octs json)] :: ++ css-response |= =octs diff --git a/pkg/arvo/sur/file-server.hoon b/pkg/arvo/sur/file-server.hoon index b29379c91..b2565d12e 100644 --- a/pkg/arvo/sur/file-server.hoon +++ b/pkg/arvo/sur/file-server.hoon @@ -1,7 +1,7 @@ /- glob |% +$ action - $% [%serve-dir url-base=path clay-base=path public=?] + $% [%serve-dir url-base=path clay-base=path public=? spa=?] [%serve-glob url-base=path =glob:glob public=?] [%unserve-dir url-base=path] [%toggle-permission url-base=path] diff --git a/pkg/arvo/ted/diff.hoon b/pkg/arvo/ted/diff.hoon new file mode 100644 index 000000000..cd9cdc796 --- /dev/null +++ b/pkg/arvo/ted/diff.hoon @@ -0,0 +1,30 @@ +/- spider +/+ strandio +=, strand=strand:spider +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +|^ +=+ !<([=a=path =b=path ~] arg) +=/ a-mark=mark -:(flop a-path) +=/ b-mark=mark -:(flop b-path) +?. =(a-mark b-mark) + (strand-fail:strandio %files-not-same-type ~) +=/ a-beam (need (de-beam:format a-path)) +;< =a=cage bind:m (get-file a-path) +;< =b=cage bind:m (get-file b-path) +;< =dais:clay bind:m (build-mark:strandio -.a-beam a-mark) +(pure:m (~(diff dais q.a-cage) q.b-cage)) +:: +++ get-file + |= =path + =/ m (strand ,cage) + ^- form:m + =/ beam (need (de-beam:format path)) + ;< =riot:clay bind:m + (warp:strandio p.beam q.beam ~ %sing %x r.beam (flop s.beam)) + ?~ riot + (strand-fail:strandio %file-not-found >path< ~) + (pure:m r.u.riot) +-- diff --git a/pkg/interface/README.md b/pkg/interface/README.md new file mode 100644 index 000000000..1af93b2b4 --- /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 28388c405..f379ea40f 100644 --- a/pkg/interface/package-lock.json +++ b/pkg/interface/package-lock.json @@ -1376,21 +1376,35 @@ "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, "@reach/auto-id": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@reach/auto-id/-/auto-id-0.10.1.tgz", - "integrity": "sha512-xGFW2v+L39M/mafdW7v+NhhsjT1LBnQJCGj64dm37T4IGNgAexlfMkRRwsqHOvuVvV38mR114YOy0xrlkqduRQ==", + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@reach/auto-id/-/auto-id-0.10.5.tgz", + "integrity": "sha512-we4/bwjFxJ3F+2eaddQ1HltbKvJ7AB8clkN719El7Zugpn/vOjfPMOVUiBqTmPGLUvkYrq4tpuFwLvk2HyOVHg==", "requires": { - "@reach/utils": "^0.10.1", - "tslib": "^1.11.1" + "@reach/utils": "0.10.5", + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", + "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==" + } } }, "@reach/descendants": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@reach/descendants/-/descendants-0.10.1.tgz", - "integrity": "sha512-Wh6VnCCDwqK/07GBx259fQsVGGwb+IT17GP3LYPtabo2L/t9Mw5oIiAkXZ6VVvw7zGpQGfm9cZYBxdYCbQOwuA==", + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@reach/descendants/-/descendants-0.10.5.tgz", + "integrity": "sha512-8HhN4DwS/HsPQ+Ym/Ft/XJ1spXBYdE8hqpnbYR9UcU7Nx3oDbTIdhjA6JXXt23t5avYIx2jRa8YHCtVKSHuiwA==", "requires": { - "@reach/utils": "^0.10.1", - "tslib": "^1.11.1" + "@reach/utils": "0.10.5", + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", + "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==" + } } }, "@reach/disclosure": { @@ -1430,53 +1444,81 @@ } }, "@reach/menu-button": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@reach/menu-button/-/menu-button-0.10.1.tgz", - "integrity": "sha512-GqROR7McvLdNdLe70a7aNSZaRmqttSqGdnOVkLs4NiihX1FFOw/k5CCTWmN6WEKLayVV/r4WaP/lUDdMa8w7nA==", + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@reach/menu-button/-/menu-button-0.10.5.tgz", + "integrity": "sha512-PQzFzexk9K7Q5qTGmXcg3qYp+F36H0MaeyzybR5t4lB1e56nAh1u/C2bocwpHssIoy25xOR8Nu+LVMVf6k6cUw==", "requires": { - "@reach/auto-id": "^0.10.1", - "@reach/descendants": "^0.10.1", - "@reach/popover": "^0.10.1", - "@reach/utils": "^0.10.1", + "@reach/auto-id": "0.10.5", + "@reach/descendants": "0.10.5", + "@reach/popover": "0.10.5", + "@reach/utils": "0.10.5", "prop-types": "^15.7.2", - "tslib": "^1.11.1" + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", + "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==" + } } }, "@reach/observe-rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.1.0.tgz", - "integrity": "sha512-kE+jvoj/OyJV24C03VvLt5zclb9ArJi04wWXMMFwQvdZjdHoBlN4g0ZQFjyy/ejPF1Z/dpUD5dhRdBiUmIGZTA==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz", + "integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==" }, "@reach/popover": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@reach/popover/-/popover-0.10.1.tgz", - "integrity": "sha512-CDRYWnCUfvn2WlTDVlDmWOV3TD0zYeJSfsd6daq2bqUX1+1jRddm3x/nk2Na6Fn8Nm9pjYUvatE+noin9iVvDw==", + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@reach/popover/-/popover-0.10.5.tgz", + "integrity": "sha512-S+qWIsjrN1yMpHjgELhjpdGc4Q3q1plJtXBGGQRxUAjmCUA/5OY7t5w5C8iqMNAEBwCvYXKvK/pLcXFxxLykSw==", "requires": { - "@reach/portal": "^0.10.1", - "@reach/rect": "^0.10.1", - "@reach/utils": "^0.10.1", + "@reach/portal": "0.10.5", + "@reach/rect": "0.10.5", + "@reach/utils": "0.10.5", "tabbable": "^4.0.0", - "tslib": "^1.11.1" + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", + "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==" + } } }, "@reach/portal": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@reach/portal/-/portal-0.10.1.tgz", - "integrity": "sha512-axap4IxA0xgsxluqyeyVuGZrStqaZ81iyiHmXFn+D+bjDNdd29colHm5GEB5mjGnkqktcXWyx5DQ+aRHIyGEkQ==", + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@reach/portal/-/portal-0.10.5.tgz", + "integrity": "sha512-K5K8gW99yqDPDCWQjEfSNZAbGOQWSx5AN2lpuR1gDVoz4xyWpTJ0k0LbetYJTDVvLP/InEcR7AU42JaDYDCXQw==", "requires": { - "@reach/utils": "^0.10.1", - "tslib": "^1.11.1" + "@reach/utils": "0.10.5", + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", + "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==" + } } }, "@reach/rect": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@reach/rect/-/rect-0.10.1.tgz", - "integrity": "sha512-jM172ZMUpdv4WeMjdO+A9Yg5doXWCq8SzRgk7Q7dK9x1y4czOmY0zanwYxDVs83r+mn0+QINnEDNcScpsOPAfQ==", + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@reach/rect/-/rect-0.10.5.tgz", + "integrity": "sha512-JBKs2HniYecq5zLO6UFReX28SUBPM3n0aizdNgHuvwZmDcTfNV4jsuJYQLqJ+FbCQsrSHkBxKZqWpfGXY9bUEg==", "requires": { - "@reach/observe-rect": "^1.1.0", - "@reach/utils": "^0.10.1", + "@reach/observe-rect": "1.2.0", + "@reach/utils": "0.10.5", "prop-types": "^15.7.2", - "tslib": "^1.11.1" + "tslib": "^2.0.0" + }, + "dependencies": { + "tslib": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", + "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==" + } } }, "@reach/tabs": { @@ -1527,15 +1569,38 @@ } }, "@reach/utils": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.10.1.tgz", - "integrity": "sha512-YzwZWVK+rSiUATNVtK7H2/ZkT/GhNKmkRjnj3hnVhSYLGxY9uQdfc+npetOqkh4hTAOXiErDa64ybVClR3h0TA==", + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.10.5.tgz", + "integrity": "sha512-5E/xxQnUbmpI/LrufBAOXjunl96DnqX6B4zC2MO2KH/dRzLug5gM5VuOwV26egsp0jvsSPxojwciOhS43px3qw==", "requires": { "@types/warning": "^3.0.0", - "tslib": "^1.11.1", + "tslib": "^2.0.0", "warning": "^4.0.3" + }, + "dependencies": { + "tslib": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.0.tgz", + "integrity": "sha512-lTqkx847PI7xEDYJntxZH89L2/aXInsyF2luSafe/+0fHOMjlBNXdH6th7f70qxLDhul7KZK0zC8V5ZIyHl0/g==" + } } }, + "@react-dnd/asap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz", + "integrity": "sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ==" + }, + "@react-dnd/invariant": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz", + "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==" + }, + "@react-dnd/shallowequal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", + "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==", + "dev": true + }, "@styled-system/background": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@styled-system/background/-/background-5.1.2.tgz", @@ -1689,6 +1754,16 @@ "integrity": "sha512-GRTZLeLJ8ia00ZH8mxMO8t0aC9M1N9bN461Z2eaRurJo6Fpa+utgCwLzI4jQHcrdzuzp5WPN9jRwpsCQ1VhJ5w==", "dev": true }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dev": true, + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/html-minifier-terser": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.0.tgz", @@ -1735,6 +1810,15 @@ "csstype": "^2.2.0" } }, + "@types/react-native": { + "version": "0.63.4", + "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.4.tgz", + "integrity": "sha512-IkQax0q5z5P4ttScELhrfrXtnFuADs/SP9kNwx2rfEuVjwF5xqhGjcY/YkiH2mSx+9QjI5S4zhxXOi3+kcnOkw==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-router": { "version": "5.1.7", "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.7.tgz", @@ -1762,6 +1846,43 @@ "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", "dev": true }, + "@types/styled-components": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.2.tgz", + "integrity": "sha512-HNocYLfrsnNNm8NTS/W53OERSjRA8dx5Bn6wBd2rXXwt4Z3s+oqvY6/PbVt3e6sgtzI63GX//WiWiRhWur08qQ==", + "dev": true, + "requires": { + "@types/hoist-non-react-statics": "*", + "@types/react": "*", + "@types/react-native": "*", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.2.tgz", + "integrity": "sha512-ofovWglpqoqbfLNOTBNZLSbMuGrblAf1efvvArGKOZMBrIoJeu5UsAipQolkijtyQx5MtAzT/J9IHj/CEY1mJw==", + "dev": true + } + } + }, + "@types/styled-system": { + "version": "5.1.10", + "resolved": "https://registry.npmjs.org/@types/styled-system/-/styled-system-5.1.10.tgz", + "integrity": "sha512-OmVjC9OzyUckAgdavJBc+t5oCJrNXTlzWl9vo2x47leqpX1REq2qJC49SEtzbu1OnWSzcD68Uq3Aj8TeX+Kvtg==", + "dev": true, + "requires": { + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.2.tgz", + "integrity": "sha512-ofovWglpqoqbfLNOTBNZLSbMuGrblAf1efvvArGKOZMBrIoJeu5UsAipQolkijtyQx5MtAzT/J9IHj/CEY1mJw==", + "dev": true + } + } + }, "@types/tapable": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.5.tgz", @@ -2326,6 +2447,48 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, + "aws-sdk": { + "version": "2.726.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.726.0.tgz", + "integrity": "sha512-QRQ7MaW5dprdr/T3vCTC+J8TeUfpM45yWsBuATPcCV/oO8afFHVySwygvGLY4oJuo5Mf4mJn3+JYTquo6CqiaA==", + "requires": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + }, + "dependencies": { + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, "babel-eslint": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", @@ -2462,8 +2625,7 @@ "base64-js": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", - "dev": true + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, "batch": { "version": "0.6.1", @@ -2685,7 +2847,6 @@ "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "dev": true, "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", @@ -2695,8 +2856,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" } } }, @@ -3111,9 +3271,9 @@ "dev": true }, "codemirror": { - "version": "5.53.2", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.53.2.tgz", - "integrity": "sha512-wvSQKS4E+P8Fxn/AQ+tQtJnF1qH5UOlxtugFLpubEZ5jcdH2iXTVinb+Xc/4QjshuOxRm4fUsU2QPF1JJKiyXA==" + "version": "5.57.0", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.57.0.tgz", + "integrity": "sha512-WGc6UL7Hqt+8a6ZAsj/f1ApQl3NPvHY/UQSzG6fB6l4BjExgVdhFaxd7mRTw1UCiYe/6q86zHP+kfvBQcZGvUg==" }, "collapse-white-space": { "version": "1.0.6", @@ -3700,6 +3860,21 @@ "randombytes": "^2.0.0" } }, + "dnd-core": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-11.1.3.tgz", + "integrity": "sha512-QugF55dNW+h+vzxVJ/LSJeTeUw9MCJ2cllhmVThVPEtF16ooBkxj0WBE5RB+AceFxMFo1rO6bJKXtqKl+JNnyA==", + "requires": { + "@react-dnd/asap": "^4.0.0", + "@react-dnd/invariant": "^2.0.0", + "redux": "^4.0.4" + } + }, + "dnd-multi-backend": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/dnd-multi-backend/-/dnd-multi-backend-6.0.0.tgz", + "integrity": "sha512-qfUO4V0IACs24xfE9m9OUnwIzoL+SWzSiFbKVIHE0pFddJeZ93BZOdHS1XEYr8X3HNh+CfnfjezXgOMgjvh74g==" + }, "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -4834,9 +5009,9 @@ "dev": true }, "formik": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/formik/-/formik-2.1.4.tgz", - "integrity": "sha512-oKz8S+yQBzuQVSEoxkqqJrKQS5XJASWGVn6mrs+oTWrBoHgByVwwI1qHiVc9GKDpZBU9vAxXYAKz2BvujlwunA==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.1.5.tgz", + "integrity": "sha512-bWpo3PiqVDYslvrRjTq0Isrm0mFXHiO33D8MS6t6dWcqSFGeYF52nlpCM2xwOJ6tRVRznDkL+zz/iHPL4LDuvQ==", "requires": { "deepmerge": "^2.1.1", "hoist-non-react-statics": "^3.3.0", @@ -5463,8 +5638,7 @@ "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, "iferr": { "version": "0.1.5", @@ -5920,6 +6094,11 @@ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6204,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", @@ -6628,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", @@ -6879,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", @@ -7537,8 +7724,7 @@ "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, "querystring-es3": { "version": "0.2.1", @@ -7617,6 +7803,53 @@ "resolved": "https://registry.npmjs.org/react-codemirror2/-/react-codemirror2-6.0.1.tgz", "integrity": "sha512-rutEKVgvFhWcy/GeVA1hFbqrO89qLqgqdhUr7YhYgIzdyICdlRQv+ztuNvOFQMXrO0fLt0VkaYOdMdYdQgsSUA==" }, + "react-dnd": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-11.1.3.tgz", + "integrity": "sha512-8rtzzT8iwHgdSC89VktwhqdKKtfXaAyC4wiqp0SywpHG12TTLvfOoL6xNEIUWXwIEWu+CFfDn4GZJyynCEuHIQ==", + "dev": true, + "requires": { + "@react-dnd/shallowequal": "^2.0.0", + "@types/hoist-non-react-statics": "^3.3.1", + "dnd-core": "^11.1.3", + "hoist-non-react-statics": "^3.3.0" + } + }, + "react-dnd-html5-backend": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-11.1.3.tgz", + "integrity": "sha512-/1FjNlJbW/ivkUxlxQd7o3trA5DE33QiRZgxent3zKme8DwF4Nbw3OFVhTRFGaYhHFNL1rZt6Rdj1D78BjnNLw==", + "requires": { + "dnd-core": "^11.1.3" + } + }, + "react-dnd-multi-backend": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/react-dnd-multi-backend/-/react-dnd-multi-backend-6.0.2.tgz", + "integrity": "sha512-SwpqRv0HkJYu244FbHf9NbvGzGy14Ir9wIAhm909uvOVaHgsOq6I1THMSWSgpwUI31J3Bo5uS19tuvGpVPjzZw==", + "requires": { + "dnd-multi-backend": "^6.0.0", + "prop-types": "^15.7.2", + "react-dnd-preview": "^6.0.2" + } + }, + "react-dnd-preview": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/react-dnd-preview/-/react-dnd-preview-6.0.2.tgz", + "integrity": "sha512-F2+uK4Be+q+7mZfNh9kaZols7wp1hX6G7UBTVaTpDsBpMhjFvY7/v7odxYSerSFBShh23MJl33a4XOVRFj1zoQ==", + "requires": { + "prop-types": "^15.7.2" + } + }, + "react-dnd-touch-backend": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-11.1.3.tgz", + "integrity": "sha512-8lz4fxfYwUuJ6Y2seQYwh8+OfwKcbBX0CIbz7AwXfBYz54Wg2nIDU6CP8Dyybt/Wyx4D3oXmTPEaOMB62uqJvQ==", + "requires": { + "@react-dnd/invariant": "^2.0.0", + "dnd-core": "^11.1.3" + } + }, "react-dom": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", @@ -7644,6 +7877,24 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" }, + "react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "requires": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + }, + "dependencies": { + "react-fast-compare": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", + "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" + } + } + }, "react-hot-loader": { "version": "4.12.21", "resolved": "https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.12.21.tgz", @@ -7694,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", @@ -7725,13 +7984,18 @@ "tiny-warning": "^1.0.0" } }, - "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-side-effect": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.0.tgz", + "integrity": "sha512-IgmcegOSi5SNX+2Snh1vqmF0Vg/CbkycU9XZbOHJlZ6kMzTmi3yc254oB1WCkgA7OQtIAoLmcSFuHTc/tlcqXg==" + }, + "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": { @@ -7754,6 +8018,15 @@ "picomatch": "^2.2.1" } }, + "redux": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", + "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -8003,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", @@ -8177,6 +8455,11 @@ "semver": "^6.3.0" } }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, "scheduler": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.18.0.tgz", @@ -9080,6 +9363,11 @@ "xml-reader": "2.4.3" } }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "synchronous-promise": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.13.tgz", @@ -11783,6 +12071,20 @@ "xml-lexer": "^0.2.2" } }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, "xregexp": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz", diff --git a/pkg/interface/package.json b/pkg/interface/package.json index 40c51cdd9..45e644e8b 100644 --- a/pkg/interface/package.json +++ b/pkg/interface/package.json @@ -6,12 +6,13 @@ "dependencies": { "@babel/runtime": "^7.10.5", "@reach/disclosure": "^0.10.5", - "@reach/menu-button": "^0.10.1", + "@reach/menu-button": "^0.10.5", "@reach/tabs": "^0.10.5", "@tlon/indigo-light": "^1.0.3", "@tlon/indigo-react": "^1.1.15", + "aws-sdk": "^2.726.0", "classnames": "^2.2.6", - "codemirror": "^5.51.0", + "codemirror": "^5.55.0", "css-loader": "^3.5.3", "formik": "^2.1.4", "lodash": "^4.17.15", @@ -19,13 +20,19 @@ "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", + "react-dnd-html5-backend": "^11.1.3", + "react-dnd-multi-backend": "^6.0.2", + "react-dnd-touch-backend": "^11.1.3", "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", @@ -47,6 +54,8 @@ "@types/lodash": "^4.14.155", "@types/react": "^16.9.38", "@types/react-router-dom": "^5.1.5", + "@types/styled-components": "^5.1.2", + "@types/styled-system": "^5.1.10", "@typescript-eslint/eslint-plugin": "^3.8.0", "@typescript-eslint/parser": "^3.8.0", "babel-eslint": "^10.1.0", @@ -58,6 +67,7 @@ "eslint-plugin-react": "^7.19.0", "file-loader": "^6.0.0", "html-webpack-plugin": "^4.2.0", + "react-dnd": "^11.1.3", "react-hot-loader": "^4.12.21", "sass": "^1.26.5", "sass-loader": "^8.0.2", diff --git a/pkg/interface/src/logic/api/chat.ts b/pkg/interface/src/logic/api/chat.ts index bd16529cf..ad6e5029d 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/global.ts b/pkg/interface/src/logic/api/global.ts index f30f53026..d724e0073 100644 --- a/pkg/interface/src/logic/api/global.ts +++ b/pkg/interface/src/logic/api/global.ts @@ -11,6 +11,8 @@ import GroupsApi from './groups'; import LaunchApi from './launch'; import LinksApi from './links'; import PublishApi from './publish'; +import GraphApi from './graph'; +import S3Api from './s3'; export default class GlobalApi extends BaseApi { chat = new ChatApi(this.ship, this.channel, this.store); @@ -22,10 +24,16 @@ export default class GlobalApi extends BaseApi { launch = new LaunchApi(this.ship, this.channel, this.store); links = new LinksApi(this.ship, this.channel, this.store); publish = new PublishApi(this.ship, this.channel, this.store); + s3 = new S3Api(this.ship, this.channel, this.store); + graph = new GraphApi(this.ship, this.channel, this.store); - constructor(public ship: Patp, public channel: any, public store: GlobalStore) { - super(ship,channel,store); + constructor( + public ship: Patp, + public channel: any, + public store: GlobalStore + ) { + super(ship, channel, store); } } diff --git a/pkg/interface/src/logic/api/graph.ts b/pkg/interface/src/logic/api/graph.ts new file mode 100644 index 000000000..d58332202 --- /dev/null +++ b/pkg/interface/src/logic/api/graph.ts @@ -0,0 +1,132 @@ +import BaseApi from './base'; +import { StoreState } from '../store/type'; +import { Patp, Path, PatpNoSig } from '~/types/noun'; + + +export const createPost = (contents: Object[], parentIndex: string = '') => { + return { + author: `~${window.ship}`, + index: parentIndex + '/' + Date.now(), + 'time-sent': Date.now(), + contents, + hash: null, + signatures: [] + }; +}; + +export default class GraphApi extends BaseApi { + + private storeAction(action: any): Promise { + return this.action('graph-store', 'graph-update', action) + } + + addGraph(ship: Patp, name: string, graph: any, mark: any) { + this.storeAction({ + 'add-graph': { + resource: { ship, name }, + graph, + mark + } + }); + } + + removeGraph(ship: Patp, name: string) { + this.storeAction({ + 'remove-graph': { + resource: { ship, name } + } + }); + } + + addPost(ship: Patp, name: string, post: Object) { + let nodes = {}; + nodes[post.index] = { + post, + children: { empty: null } + }; + + this.storeAction({ + 'add-nodes': { + resource: { ship, name }, + nodes + } + }); + } + + addNodes(ship: Patp, name: string, nodes: Object) { + this.storeAction({ + 'add-nodes': { + resource: { ship, name }, + nodes + } + }); + } + + removeNodes(ship: Patp, name: string, indices: string[]) { + this.storeAction({ + 'remove-nodes': { + resource: { ship, name }, + indices + } + }); + } + + getKeys() { + this.scry('graph-store', '/keys') + .then((keys) => { + this.store.handleEvent({ + data: keys + }); + }); + } + + getTags() { + this.scry('graph-store', '/tags') + .then((tags) => { + this.store.handleEvent({ + data: tags + }); + }); + } + + getTagQueries() { + this.scry('graph-store', '/tag-queries') + .then((tagQueries) => { + this.store.handleEvent({ + data: tagQueries + }); + }); + } + + getGraph(ship: string, resource: string) { + this.scry('graph-store', `/graph/${ship}/${resource}`) + .then((graph) => { + this.store.handleEvent({ + data: graph + }); + }); + } + + getGraphSubset(ship: string, resource: string, start: string, end: start) { + this.scry( + 'graph-store', + `/graph-subset/${ship}/${resource}/${end}/${start}` + ).then((subset) => { + this.store.handleEvent({ + data: subset + }); + }); + } + + getNode(ship: string, resource: string, index: string) { + this.scry( + 'graph-store', + `/node/${ship}/${resource}/${index}` + ).then((node) => { + this.store.handleEvent({ + data: node + }); + }); + } +} + diff --git a/pkg/interface/src/logic/api/launch.ts b/pkg/interface/src/logic/api/launch.ts index 1e6fa4a33..0eeca42e8 100644 --- a/pkg/interface/src/logic/api/launch.ts +++ b/pkg/interface/src/logic/api/launch.ts @@ -12,7 +12,7 @@ export default class LaunchApi extends BaseApi { this.launchAction({ remove: name }); } - changeOrder(orderedTiles = []) { + changeOrder(orderedTiles: string[] = []) { this.launchAction({ 'change-order': orderedTiles }); } diff --git a/pkg/interface/src/logic/api/links.ts b/pkg/interface/src/logic/api/links.ts index bb6e5812c..e3ad42f5d 100644 --- a/pkg/interface/src/logic/api/links.ts +++ b/pkg/interface/src/logic/api/links.ts @@ -44,8 +44,8 @@ export default class LinksApi extends BaseApi { this.fetchLink( endpoint, (res) => { - if (res.data.submission) { - callback(res.data.submission); + if (res.data?.['link-update']?.submission) { + callback(res.data?.['link-update']?.submission); } else { console.error('unexpected submission response', res); } diff --git a/pkg/interface/src/logic/api/local.ts b/pkg/interface/src/logic/api/local.ts index 3c1abfa83..3b874e060 100644 --- a/pkg/interface/src/logic/api/local.ts +++ b/pkg/interface/src/logic/api/local.ts @@ -1,5 +1,6 @@ import BaseApi from "./base"; import { StoreState } from "../store/type"; +import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from "../types/local-update"; export default class LocalApi extends BaseApi { getBaseHash() { @@ -38,4 +39,48 @@ export default class LocalApi extends BaseApi { }); } + setBackground(backgroundConfig: BackgroundConfig) { + this.store.handleEvent({ + data: { + local: { + backgroundConfig + } + } + }); + } + + hideAvatars(hideAvatars: boolean) { + this.store.handleEvent({ + data: { + local: { + hideAvatars + } + } + }); + } + + hideNicknames(hideNicknames: boolean) { + this.store.handleEvent({ + data: { + local: { + hideNicknames + } + } + }); + } + + 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 ced75c480..5b49aabfc 100644 --- a/pkg/interface/src/logic/api/publish.ts +++ b/pkg/interface/src/logic/api/publish.ts @@ -1,6 +1,7 @@ import BaseApi from './base'; + import { PublishResponse } from '~/types/publish-response'; -import { PatpNoSig } from '~/types/noun'; +import { PatpNoSig, Path } from '~/types/noun'; import { BookId, NoteId } from '~/types/publish-update'; export default class PublishApi extends BaseApi { @@ -80,5 +81,116 @@ export default class PublishApi extends BaseApi { publishAction(act: any) { 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: [], + 'use-preexisting': true, + 'make-managed': true + } : { + 'group-path': `/ship/~${window.ship}/${bookId}`, + invitees: [], + 'use-preexisting': false, + 'make-managed': false + }; + return this.publishAction({ + "new-book": { + book: bookId, + title: title, + about: description, + coms: true, + group: groupInfo + } + }); + } + + editBook(bookId: string, title: string, description: string, coms: boolean) { + return this.publishAction({ + "edit-book": { + book: bookId, + title: title, + about: description, + coms, + group: null + } + }); + } + + delBook(book: string) { + return this.publishAction({ + "del-book": { + book + } + }); + } + + newNote(who: PatpNoSig, book: string, note: string, title: string, body: string) { + return this.publishAction({ + 'new-note': { + who, + book, + note, + title, + body + } + }); + } + + editNote(who: PatpNoSig, book: string, note: string, title: string, body: string) { + return this.publishAction({ + 'edit-note': { + who, + book, + note, + title, + body + } + }); + } + + delNote(who: PatpNoSig, book: string, note: string) { + return this.publishAction({ + 'del-note': { + who, + book, + note + } + }); + } + + updateComment(who: PatpNoSig, book: string, note: string, comment: Path, body: string) { + return this.publishAction({ + 'edit-comment': { + who, + book, + note, + comment, + body + } + }); + } + + deleteComment(who: PatpNoSig, book: string, note: string, comment: Path ) { + return this.publishAction({ + "del-comment": { + who, + book, + note, + comment + }, + }); + } + } diff --git a/pkg/interface/src/logic/api/s3.ts b/pkg/interface/src/logic/api/s3.ts new file mode 100644 index 000000000..e5b67595a --- /dev/null +++ b/pkg/interface/src/logic/api/s3.ts @@ -0,0 +1,37 @@ +import BaseApi from './base'; +import { StoreState } from '../store/type'; +import {S3Update} from '../../types/s3-update'; + + +export default class S3Api extends BaseApi { + + setCurrentBucket(bucket: string) { + this.s3Action({ 'set-current-bucket': bucket }); + } + + addBucket(bucket: string) { + this.s3Action({ 'add-bucket': bucket }); + } + + removeBucket(bucket: string) { + this.s3Action({ 'remove-bucket': bucket }); + } + + setEndpoint(endpoint: string) { + this.s3Action({ 'set-endpoint': endpoint }); + } + + setAccessKeyId(accessKeyId: string) { + this.s3Action({ 'set-access-key-id': accessKeyId }); + } + + setSecretAccessKey(secretAccessKey: string) { + this.s3Action({ 'set-secret-access-key': secretAccessKey }); + } + + private s3Action(data: any) { + this.action('s3-store', 's3-action', data); + } + +} + diff --git a/pkg/interface/src/logic/lib/omnibox.js b/pkg/interface/src/logic/lib/omnibox.js index 8485c6128..ee616ddc9 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 @@ -40,6 +42,7 @@ const commandIndex = function () { commands.push(obj); } }); + return commands; }; @@ -51,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, @@ -67,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 @@ -96,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 { @@ -104,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); } @@ -115,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/s3.js b/pkg/interface/src/logic/lib/s3.js index 1df495741..64bda11be 100644 --- a/pkg/interface/src/logic/lib/s3.js +++ b/pkg/interface/src/logic/lib/s3.js @@ -1,3 +1,5 @@ +import S3 from 'aws-sdk/clients/s3'; + export default class S3Client { constructor() { this.s3 = null; @@ -8,27 +10,20 @@ export default class S3Client { } setCredentials(endpoint, accessKeyId, secretAccessKey) { - if (!window.AWS) { - setTimeout(() => { - this.setCredentials(endpoint, accessKeyId, secretAccessKey); - }, 2000); - return; - } - this.endpoint = new window.AWS.Endpoint(endpoint); + this.endpoint = endpoint; this.accessKeyId = accessKeyId; this.secretAccessKey = secretAccessKey; - this.s3 = - new window.AWS.S3({ - endpoint: this.endpoint, - credentials: new window.AWS.Credentials({ - accessKeyId: this.accessKeyId, - secretAccessKey: this.secretAccessKey - }) - }); + this.s3 = new S3({ + endpoint: endpoint, + credentials: { + accessKeyId: this.accessKeyId, + secretAccessKey: this.secretAccessKey + } + }); } - upload(bucket, filename, buffer) { + async upload(bucket, filename, buffer) { const params = { Bucket: bucket, Key: filename, @@ -36,19 +31,11 @@ export default class S3Client { ACL: 'public-read', ContentType: buffer.type }; - return new Promise((resolve, reject) => { - if (!this.s3) { - reject({ error: 'S3 not initialized!' }); - return; - } - this.s3.upload(params, (error, data) => { - if (error) { - reject({ error }); - } else { - resolve(data); - } - }); - }); + + if(!this.s3) { + throw new Error('S3 not initialized'); + } + return this.s3.upload(params).promise(); } } diff --git a/pkg/interface/src/logic/lib/sigil.js b/pkg/interface/src/logic/lib/sigil.js index bcdee52bb..8e5066eec 100644 --- a/pkg/interface/src/logic/lib/sigil.js +++ b/pkg/interface/src/logic/lib/sigil.js @@ -1,48 +1,39 @@ -import React, { Component } from 'react'; +import React, { memo } from 'react'; import { sigil, reactRenderer } from 'urbit-sigil-js'; -export class Sigil extends Component { - static foregroundFromBackground(background) { - const rgb = { - r: parseInt(background.slice(1, 3), 16), - g: parseInt(background.slice(3, 5), 16), - b: parseInt(background.slice(5, 7), 16) - }; - const brightness = ((299 * rgb.r) + (587 * rgb.g) + (114 * rgb.b)) / 1000; - const whiteBrightness = 255; +export const foregroundFromBackground = (background) => { + const rgb = { + r: parseInt(background.slice(1, 3), 16), + g: parseInt(background.slice(3, 5), 16), + b: parseInt(background.slice(5, 7), 16) + }; + const brightness = ((299 * rgb.r) + (587 * rgb.g) + (114 * rgb.b)) / 1000; + const whiteBrightness = 255; - return ((whiteBrightness - brightness) < 50) ? 'black' : 'white'; - } - - render() { - const { props } = this; - - const classes = props.classes || ''; - - const foreground = Sigil.foregroundFromBackground(props.color); - - if (props.ship.length > 14) { - return ( -
- ); - } else { - return ( -
- {sigil({ - patp: props.ship, - renderer: reactRenderer, - size: props.size, - colors: [props.color, foreground], - class: props.svgClass - })} -
- ); - } - } + return ((whiteBrightness - brightness) < 50) ? 'black' : 'white'; } + +export const Sigil = memo(({ classes = '', color, ship, size, svgClass = '' }) => { + return ship.length > 14 + ? (
+
) + : (
+ {sigil({ + patp: ship, + renderer: reactRenderer, + size: size, + colors: [ + color, + foregroundFromBackground(color) + ], + class: svgClass + })} +
) +}) + +export default Sigil; \ No newline at end of file diff --git a/pkg/interface/src/logic/lib/tokenizeMessage.js b/pkg/interface/src/logic/lib/tokenizeMessage.js new file mode 100644 index 000000000..6f43c421c --- /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/useDropdown.ts b/pkg/interface/src/logic/lib/useDropdown.ts new file mode 100644 index 000000000..ee72efd37 --- /dev/null +++ b/pkg/interface/src/logic/lib/useDropdown.ts @@ -0,0 +1,57 @@ +import { useState, useEffect, useMemo, useCallback } from "react"; + +export function useDropdown( + candidates: C[], + key: (c: C) => string, + searchPred: (query: string, c: C) => boolean +) { + const [options, setOptions] = useState(candidates); + const [selected, setSelected] = useState(); + const search = useCallback( + (s: string) => { + const opts = candidates.filter((c) => searchPred(s, c)); + setOptions(opts); + if (selected) { + const idx = opts.findIndex((c) => key(c) === key(selected)); + if (idx < 0) { + setSelected(undefined); + } + } + }, + [candidates, searchPred, key, selected, setOptions, setSelected] + ); + + const changeSelection = useCallback( + (backward = false) => { + const select = (idx: number) => { + setSelected(options[idx]); + }; + if(!selected) { select(0); return false; } + + const idx = options.findIndex((c) => key(c) === key(selected)); + if ( + idx === -1 || + (options.length - 1 <= idx && !backward) + ) { + select(0); + } else if (idx === 0 && backward) { + select(options.length - 1); + } else { + select(idx + (backward ? -1 : 1)); + } + return false; + }, + [options, setSelected, selected] + ); + + const next = useCallback(() => changeSelection(), [changeSelection]); + const back = useCallback(() => changeSelection(true), [changeSelection]); + + return { + next, + back, + search, + selected, + options, + }; +} diff --git a/pkg/interface/src/logic/lib/useLocalStorageState.ts b/pkg/interface/src/logic/lib/useLocalStorageState.ts new file mode 100644 index 000000000..a3f81c666 --- /dev/null +++ b/pkg/interface/src/logic/lib/useLocalStorageState.ts @@ -0,0 +1,22 @@ +import { useState, useCallback } from 'react'; + +export function useLocalStorageState(key: string, initial: T) { + const [state, _setState] = useState(() => { + const s = localStorage.getItem(key); + if(s) { + return JSON.parse(s) as T; + } + return initial; + + }); + + const setState = useCallback((s: T) => { + _setState(s); + localStorage.setItem(key, JSON.stringify(s)); + + }, [_setState]); + + return [state, setState] as const; +} + + diff --git a/pkg/interface/src/logic/lib/useQuery.ts b/pkg/interface/src/logic/lib/useQuery.ts new file mode 100644 index 000000000..0957ca098 --- /dev/null +++ b/pkg/interface/src/logic/lib/useQuery.ts @@ -0,0 +1,30 @@ +import { useMemo, useCallback } from "react"; +import { useLocation } from "react-router-dom"; +import _ from 'lodash'; + +export function useQuery() { + const { search } = useLocation(); + + const query = useMemo(() => new URLSearchParams(search), [search]); + + const appendQuery = useCallback( + (q: Record) => { + const newQuery = new URLSearchParams(search); + _.forIn(q, (value, key) => { + if (!value) { + newQuery.delete(key); + } else { + newQuery.append(key, value); + } + }); + + return newQuery.toString(); + }, + [search] + ); + + return { + query, + appendQuery, + }; +} diff --git a/pkg/interface/src/logic/lib/useS3.ts b/pkg/interface/src/logic/lib/useS3.ts new file mode 100644 index 000000000..f040924dd --- /dev/null +++ b/pkg/interface/src/logic/lib/useS3.ts @@ -0,0 +1,51 @@ +import { useCallback, useMemo, useEffect, useRef } from "react"; +import { S3State } from "../../types/s3-update"; +import S3 from "aws-sdk/clients/s3"; + +export function useS3(s3: S3State) { + const { configuration, credentials } = s3; + + const client = useRef(null); + + useEffect(() => { + if (!credentials) { + return; + } + client.current = new S3({ credentials, endpoint: credentials.endpoint }); + }, [credentials]); + + const canUpload = useMemo( + () => + (client && credentials && configuration.currentBucket !== "") || false, + [credentials, configuration.currentBucket, client] + ); + + const uploadDefault = useCallback(async (file: File) => { + if (configuration.currentBucket === "") { + throw new Error("current bucket not set"); + } + return upload(file, configuration.currentBucket); + }, []); + + const upload = useCallback( + async (file: File, bucket: string) => { + if (!client.current) { + throw new Error("S3 not ready"); + } + + const params = { + Bucket: bucket, + Key: file.name, + Body: file, + ACL: "public-read", + ContentType: file.type, + }; + + const { Location } = await client.current.upload(params).promise(); + return Location; + }, + [client] + ); + + return { canUpload, upload, uploadDefault }; +} diff --git a/pkg/interface/src/logic/lib/useWaitForProps.ts b/pkg/interface/src/logic/lib/useWaitForProps.ts new file mode 100644 index 000000000..80e92e92a --- /dev/null +++ b/pkg/interface/src/logic/lib/useWaitForProps.ts @@ -0,0 +1,38 @@ +import { useState, useEffect, useCallback } from 'react'; + + +export function useWaitForProps

(props: P, timeout: number = 0) { + const [resolve, setResolve] = useState<() => void>(() => () => {}); + const [ready, setReady] = useState<(p: P) => boolean | undefined>(); + + useEffect(() => { + if (typeof ready === "function" && ready(props)) { + resolve(); + } + }, [props, ready, resolve]); + + /** + * Waits until some predicate is true + * + * @param r - Predicate to wait for + * @returns A promise that resolves when `r` returns true, or rejects if the + * waiting times out + * + */ + const waiter = useCallback( + (r: (props: P) => boolean) => { + setReady(() => r); + return new Promise((resolve, reject) => { + setResolve(() => resolve); + if(timeout > 0) { + setTimeout(() => { + reject(new Error("Timed out")); + }, timeout); + } + }); + }, + [setResolve, setReady, timeout] + ); + + return waiter; +} diff --git a/pkg/interface/src/logic/lib/util.js b/pkg/interface/src/logic/lib/util.js index 4f6162f42..1c35d5e44 100644 --- a/pkg/interface/src/logic/lib/util.js +++ b/pkg/interface/src/logic/lib/util.js @@ -1,5 +1,7 @@ import _ from 'lodash'; +export const MOBILE_BROWSER_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i; + export function resourceAsPath(resource) { const { name, ship } = resource; return `/ship/~${ship}/${name}`; @@ -75,6 +77,14 @@ export function uxToHex(ux) { return value; } +export function hexToUx(hex) { + const ux = _.chain(hex.split("")) + .chunk(4) + .map((x) => _.dropWhile(x, (y) => y === 0).join("")) + .join("."); + return `0x${ux}`; +} + function hexToDec(hex) { const alphabet = '0123456789ABCDEF'.split(''); return hex.reverse().reduce((acc, digit, idx) => { diff --git a/pkg/interface/src/logic/reducers/chat-update.ts b/pkg/interface/src/logic/reducers/chat-update.ts index 292175038..531d97453 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 new file mode 100644 index 000000000..c8253f229 --- /dev/null +++ b/pkg/interface/src/logic/reducers/graph-update.js @@ -0,0 +1,164 @@ +import _ from 'lodash'; + + +export const GraphReducer = (json, state) => { + const data = _.get(json, 'graph-update', false); + if (data) { + keys(data, state); + addGraph(data, state); + removeGraph(data, state); + addNodes(data, state); + removeNodes(data, state); + } +}; + +const keys = (json, state) => { + const data = _.get(json, 'keys', false); + if (data) { + state.graphKeys = new Set(data.map((res) => { + let resource = res.ship + '/' + res.name; + + if (!(resource in state.graphs)) { + state.graphs[resource] = new Map(); + } + + return resource; + })); + } +}; + +const addGraph = (json, state) => { + + const _processNode = (node) => { + // is empty + if (!node.children) { + node.children = new Map(); + return node; + } + + // is graph + let converted = new Map(); + for (let i in node.children) { + let item = node.children[i]; + let index = item[0].split('/').slice(1).map((ind) => { + return parseInt(ind, 10); + }); + + if (index.length === 0) { break; } + + converted.set( + index[index.length - 1], + _processNode(item[1]) + ); + } + node.children = converted; + return node; + }; + + const data = _.get(json, 'add-graph', false); + if (data) { + if (!('graphs' in state)) { + state.graphs = {}; + } + + let resource = data.resource.ship + '/' + data.resource.name; + state.graphs[resource] = new Map(); + + for (let i in data.graph) { + let item = data.graph[i]; + let index = item[0].split('/').slice(1).map((ind) => { + return parseInt(ind, 10); + }); + + if (index.length === 0) { break; } + + let node = _processNode(item[1]); + state.graphs[resource].set(index[index.length - 1], node); + } + state.graphKeys.add(resource); + } + +}; + +const removeGraph = (json, state) => { + const data = _.get(json, 'remove-graph', false); + if (data) { + if (!('graphs' in state)) { + state.graphs = {}; + } + let resource = data.resource.ship + '/' + data.resource.name; + delete state.graphs[resource]; + } +}; + +const addNodes = (json, state) => { + const _addNode = (graph, index, node) => { + // set child of graph + if (index.length === 1) { + graph.set(index[0], node); + return graph; + } + + // set parent of graph + let parNode = graph.get(index[0]); + if (!parNode) { + console.error('parent node does not exist, cannot add child'); + return; + } + parNode.children = _addNode(parNode.children, index.slice(1), node); + graph.set(index[0], parNode); + return graph; + }; + + const data = _.get(json, 'add-nodes', false); + if (data) { + if (!('graphs' in state)) { return; } + + let resource = data.resource.ship + '/' + data.resource.name; + if (!(resource in state.graphs)) { return; } + + for (let i in data.nodes) { + let item = data.nodes[i]; + if (item[0].split('/').length === 0) { return; } + + let index = item[0].split('/').slice(1).map((ind) => { + return parseInt(ind, 10); + }); + + if (index.length === 0) { return; } + + // TODO: support adding nodes with children + item[1].children = new Map(); + + state.graphs[resource] = _addNode( + state.graphs[resource], + index, + item[1] + ); + } + } +}; + +const removeNodes = (json, state) => { + const data = _.get(json, 'remove-nodes', false); + if (data) { + console.log(data); + if (!(data.resource in state.graphs)) { return; } + + data.indices.forEach((index) => { + console.log(index); + if (index.split('/').length === 0) { return; } + 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 + } + + }); + } +}; + diff --git a/pkg/interface/src/logic/reducers/group-update.ts b/pkg/interface/src/logic/reducers/group-update.ts index 09adb35b2..5862aaca1 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 e2dee6bad..89a3fa822 100644 --- a/pkg/interface/src/logic/reducers/local.ts +++ b/pkg/interface/src/logic/reducers/local.ts @@ -1,18 +1,37 @@ import _ from 'lodash'; -import { StoreState } from '../../store/type'; +import { StoreState } from '~/store/type'; import { Cage } from '~/types/cage'; -import { LocalUpdate } from '~/types/local-update'; +import { LocalUpdate, BackgroundConfig } from '~/types/local-update'; -type LocalState = Pick; +type LocalState = Pick; export default class LocalReducer { + rehydrate(state: S) { + try { + const json = JSON.parse(localStorage.getItem('localReducer') || ''); + _.forIn(json, (value, key) => { + state[key] = value; + }); + } catch (e) { + console.warn('Failed to rehydrate localStorage state', e); + } + } + + dehydrate(state: S) { + const json = _.pick(state, ['hideNicknames' , 'hideAvatars' , 'background', 'remoteContentPolicy']); + localStorage.setItem('localReducer', JSON.stringify(json)); + } reduce(json: Cage, state: S) { const data = json['local']; if (data) { this.sidebarToggle(data, state); this.setDark(data, state); this.baseHash(data, state); + this.backgroundConfig(data, state) + this.hideAvatars(data, state) + this.hideNicknames(data, state) this.omniboxShown(data, state); + this.remoteContentPolicy(data, state); } } baseHash(obj: LocalUpdate, state: S) { @@ -45,4 +64,28 @@ export default class LocalReducer { state.dark = obj.setDark; } } + + backgroundConfig(obj: LocalUpdate, state: S) { + if('backgroundConfig' in obj) { + state.background = obj.backgroundConfig; + } + } + + 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; + } + } + + hideNicknames(obj: LocalUpdate, state: S) { + if( 'hideNicknames' in obj) { + state.hideNicknames = obj.hideNicknames; + } + } } diff --git a/pkg/interface/src/logic/reducers/publish-response.ts b/pkg/interface/src/logic/reducers/publish-response.ts index a1cfb85ae..073efa399 100644 --- a/pkg/interface/src/logic/reducers/publish-response.ts +++ b/pkg/interface/src/logic/reducers/publish-response.ts @@ -78,6 +78,8 @@ export default class PublishResponseReducer { json.data.notebook["subscribers-group-path"]; state.notebooks[json.host][json.notebook]["writers-group-path"] = json.data.notebook["writers-group-path"]; + state.notebooks[json.host][json.notebook].about = + json.data.notebook.about; if (state.notebooks[json.host][json.notebook].notes) { for (var key in json.data.notebook.notes) { let oldNote = state.notebooks[json.host][json.notebook].notes[key]; diff --git a/pkg/interface/src/logic/reducers/s3-update.ts b/pkg/interface/src/logic/reducers/s3-update.ts index 0a4741cc7..d05f741b3 100644 --- a/pkg/interface/src/logic/reducers/s3-update.ts +++ b/pkg/interface/src/logic/reducers/s3-update.ts @@ -40,7 +40,7 @@ export default class S3Reducer { currentBucket(json: S3Update, state: S) { const data = _.get(json, 'setCurrentBucket', false); if (data && state.s3) { - + state.s3.configuration.currentBucket = data; } } diff --git a/pkg/interface/src/logic/store/base.ts b/pkg/interface/src/logic/store/base.ts index da026c33e..9a48ebd80 100644 --- a/pkg/interface/src/logic/store/base.ts +++ b/pkg/interface/src/logic/store/base.ts @@ -5,6 +5,10 @@ export default class BaseStore { this.state = this.initialState(); } + dehydrate() {} + + rehydrate() {} + initialState() { return {} as S; } diff --git a/pkg/interface/src/logic/store/links.js b/pkg/interface/src/logic/store/links.js index d8d7fd7dd..eb395603f 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 137720cb6..8bb5794d9 100644 --- a/pkg/interface/src/logic/store/store.ts +++ b/pkg/interface/src/logic/store/store.ts @@ -9,6 +9,7 @@ import { Cage } from '~/types/cage'; import ContactReducer from '../reducers/contact-update'; import LinkUpdateReducer from '../reducers/link-update'; import S3Reducer from '../reducers/s3-update'; +import { GraphReducer } from '../reducers/graph-update'; import GroupReducer from '../reducers/group-update'; import PermissionReducer from '../reducers/permission-update'; import PublishUpdateReducer from '../reducers/publish-update'; @@ -34,6 +35,14 @@ export default class GlobalStore extends BaseStore { launchReducer = new LaunchReducer(); connReducer = new ConnectionReducer(); + rehydrate() { + this.localReducer.rehydrate(this.state); + } + + dehydrate() { + this.localReducer.dehydrate(this.state); + } + initialState(): StoreState { return { @@ -44,6 +53,15 @@ export default class GlobalStore extends BaseStore { omniboxShown: false, suspendedFocus: null, baseHash: null, + background: undefined, + remoteContentPolicy: { + imageShown: true, + audioShown: true, + videoShown: true, + oembedShown: true, + }, + hideAvatars: false, + hideNicknames: false, invites: {}, associations: { chat: {}, @@ -53,6 +71,8 @@ export default class GlobalStore extends BaseStore { }, groups: {}, groupKeys: new Set(), + graphs: {}, + graphKeys: new Set(), launch: { firstTime: false, tileOrdering: [], @@ -95,5 +115,6 @@ export default class GlobalStore extends BaseStore { this.launchReducer.reduce(data, this.state); this.linkListenReducer.reduce(data, this.state); this.connReducer.reduce(data, this.state); + GraphReducer(data, this.state); } } diff --git a/pkg/interface/src/logic/store/type.ts b/pkg/interface/src/logic/store/type.ts index cddd1c80f..ffb989fa5 100644 --- a/pkg/interface/src/logic/store/type.ts +++ b/pkg/interface/src/logic/store/type.ts @@ -11,6 +11,7 @@ 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, LocalUpdateRemoteContentPolicy } from '~/types/local-update'; export interface StoreState { // local state @@ -20,6 +21,10 @@ export interface StoreState { dark: boolean; connection: ConnectionStatus; baseHash: string | null; + background: BackgroundConfig; + remoteContentPolicy: LocalUpdateRemoteContentPolicy; + hideAvatars: boolean; + hideNicknames: boolean; // invite state invites: Invites; // metadata state @@ -31,6 +36,8 @@ export interface StoreState { groupKeys: Set; permissions: Permissions; s3: S3State; + graphs: Object; + graphKeys: Set; // App specific states diff --git a/pkg/interface/src/logic/subscription/global.ts b/pkg/interface/src/logic/subscription/global.ts index 6a3bb3701..8a07dde07 100644 --- a/pkg/interface/src/logic/subscription/global.ts +++ b/pkg/interface/src/logic/subscription/global.ts @@ -27,12 +27,18 @@ const groupSubscriptions: AppSubscription[] = [ ['/synced', 'contact-hook'] ]; -type AppName = 'publish' | 'chat' | 'link' | 'groups'; +const graphSubscriptions: AppSubscription[] = [ + ['/keys', 'graph-store'], + ['/updates', 'graph-store'] +]; + +type AppName = 'publish' | 'chat' | 'link' | 'groups' | 'graph'; const appSubscriptions: Record = { chat: chatSubscriptions, publish: publishSubscriptions, link: linkSubscriptions, - groups: groupSubscriptions + groups: groupSubscriptions, + graph: graphSubscriptions }; export default class GlobalSubscription extends BaseSubscription { @@ -40,8 +46,10 @@ export default class GlobalSubscription extends BaseSubscription { chat: [], publish: [], link: [], - groups: [] + groups: [], + graph: [] }; + start() { this.subscribe('/all', 'invite-store'); this.subscribe('/groups', 'group-store'); @@ -67,7 +75,8 @@ export default class GlobalSubscription extends BaseSubscription { console.log(`${app} already started`); return; } - this.openSubscriptions[app] = appSubscriptions[app].map(([path, agent]) => this.subscribe(path, agent)); + this.openSubscriptions[app] = + appSubscriptions[app].map(([path, agent]) => this.subscribe(path, agent)); } stopApp(app: AppName) { diff --git a/pkg/interface/src/types/chat-update.ts b/pkg/interface/src/types/chat-update.ts index 63b23eee1..6d03048f4 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/index.ts b/pkg/interface/src/types/index.ts new file mode 100644 index 000000000..3f7cda54c --- /dev/null +++ b/pkg/interface/src/types/index.ts @@ -0,0 +1,18 @@ +export * from './cage'; +export * from './chat-hook-update'; +export * from './chat-update'; +export * from './connection'; +export * from './contact-update'; +export * from './global'; +export * from './group-update'; +export * from './invite-update'; +export * from './launch-update'; +export * from './link-listen-update'; +export * from './link-update'; +export * from './local-update'; +export * from './metadata-update'; +export * from './noun'; +export * from './permission-update'; +export * from './publish-response'; +export * from './publish-update'; +export * from './s3-update'; diff --git a/pkg/interface/src/types/launch-update.ts b/pkg/interface/src/types/launch-update.ts index b8a337b5e..099987845 100644 --- a/pkg/interface/src/types/launch-update.ts +++ b/pkg/interface/src/types/launch-update.ts @@ -33,14 +33,14 @@ export interface LaunchState { } } -interface Tile { +export interface Tile { isShown: boolean; type: TileType; } type TileType = TileTypeBasic | TileTypeCustom; -interface TileTypeBasic { +export interface TileTypeBasic { basic: { iconUrl: string; linkedUrl: string; diff --git a/pkg/interface/src/types/local-update.ts b/pkg/interface/src/types/local-update.ts index 0ea170da0..a5739fd21 100644 --- a/pkg/interface/src/types/local-update.ts +++ b/pkg/interface/src/types/local-update.ts @@ -1,9 +1,3 @@ -export type LocalUpdate = - LocalUpdateSidebarToggle -| LocalUpdateSetDark -| LocalUpdateSetOmniboxShown -| LocalUpdateBaseHash; - interface LocalUpdateSidebarToggle { sidebarToggle: boolean; } @@ -16,6 +10,47 @@ interface LocalUpdateBaseHash { baseHash: string; } +interface LocalUpdateBackgroundConfig { + backgroundConfig: BackgroundConfig; +} + +interface LocalUpdateHideAvatars { + hideAvatars: boolean; +} + +interface LocalUpdateHideNicknames { + hideNicknames: boolean; +} + interface LocalUpdateSetOmniboxShown { omniboxShown: boolean; } + +export interface LocalUpdateRemoteContentPolicy { + imageShown: boolean; + audioShown: boolean; + videoShown: boolean; + oembedShown: boolean; +} + +interface BackgroundConfigUrl { + type: 'url'; + url: string; +} + +interface BackgroundConfigColor { + type: 'color'; + color: string; +} + +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/types/publish-update.ts b/pkg/interface/src/types/publish-update.ts index 7dec8a82b..f3f1e3d4d 100644 --- a/pkg/interface/src/types/publish-update.ts +++ b/pkg/interface/src/types/publish-update.ts @@ -129,7 +129,7 @@ export interface Notebook { 'writers-group-path': Path; } -type Notes = { +export type Notes = { [id in NoteId]: Note; }; @@ -148,7 +148,7 @@ export interface Note { title: string; } -interface Comment { +export interface Comment { [date: string]: { author: Patp; content: string; diff --git a/pkg/interface/src/views/App.js b/pkg/interface/src/views/App.js index ffbfd356d..e72d0b1d1 100644 --- a/pkg/interface/src/views/App.js +++ b/pkg/interface/src/views/App.js @@ -1,9 +1,10 @@ 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'; import Mousetrap from 'mousetrap'; import 'mousetrap-global-bind'; @@ -15,14 +16,13 @@ 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'; import GlobalApi from '~/logic/api/global'; import { uxToHex } from '~/logic/lib/util'; -import { Sigil } from '~/logic/lib/sigil'; +import { foregroundFromBackground } from '~/logic/lib/sigil'; const Root = styled.div` font-family: ${p => p.theme.fonts.sans}; @@ -30,6 +30,15 @@ const Root = styled.div` width: 100%; padding: 0; margin: 0; + ${p => p.background?.type === 'url' ? ` + background-image: url('${p.background?.url}'); + background-size: cover; + ` : p.background?.type === 'color' ? ` + background-color: ${p.background.color}; + ` : '' + } + display: flex; + flex-flow: column nowrap; `; const StatusBarWithRouter = withRouter(StatusBar); @@ -48,7 +57,7 @@ class App extends React.Component { new GlobalSubscription(this.store, this.api, this.appChannel); this.updateTheme = this.updateTheme.bind(this); - this.setFavicon = this.setFavicon.bind(this); + this.faviconString = this.faviconString.bind(this); } componentDidMount() { @@ -57,44 +66,36 @@ class App extends React.Component { this.api.local.setDark(this.themeWatcher.matches); this.themeWatcher.addListener(this.updateTheme); this.api.local.getBaseHash(); + this.store.rehydrate(); Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => { e.preventDefault(); e.stopImmediatePropagation(); this.api.local.setOmnibox(); }); - this.setFavicon(); } componentWillUnmount() { this.themeWatcher.removeListener(this.updateTheme); } - componentDidUpdate(prevProps, prevState, snapshot) { - this.setFavicon(); - } - updateTheme(e) { this.api.local.setDark(e.matches); } - setFavicon() { - if (window.ship.length < 14) { - let background = '#ffffff'; - if (this.state.contacts.hasOwnProperty('/~/default')) { - background = `#${uxToHex(this.state.contacts['/~/default'][window.ship].color)}`; - } - const foreground = Sigil.foregroundFromBackground(background); - const svg = sigiljs({ - patp: window.ship, - renderer: stringRenderer, - size: 16, - colors: [background, foreground] - }); - const dataurl = 'data:image/svg+xml;base64,' + btoa(svg); - const favicon = document.querySelector('[rel=icon]'); - favicon.href = dataurl; - favicon.type = 'image/svg+xml'; + faviconString() { + let background = '#ffffff'; + if (this.state.contacts.hasOwnProperty('/~/default')) { + background = `#${uxToHex(this.state.contacts['/~/default'][window.ship].color)}`; } + const foreground = foregroundFromBackground(background); + const svg = sigiljs({ + patp: window.ship, + renderer: stringRenderer, + size: 16, + colors: [background, foreground] + }); + const dataurl = 'data:image/svg+xml;base64,' + btoa(svg); + return dataurl; } render() { @@ -102,10 +103,16 @@ class App extends React.Component { const associations = state.associations ? state.associations : { contacts: {} }; const theme = state.dark ? dark : light; + const { background } = state; return ( - + + {window.ship.length < 14 + ? + : null} + + + {...state} + /> @@ -134,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 7f7239ee6..45790f203 100644 --- a/pkg/interface/src/views/apps/chat/app.tsx +++ b/pkg/interface/src/views/apps/chat/app.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; +import Helmet from 'react-helmet'; 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'; @@ -24,14 +24,11 @@ type ChatAppProps = StoreState & { }; export default class ChatApp extends React.Component { - totalUnreads = 0; - constructor(props) { super(props); } componentDidMount() { - document.title = 'OS1 - Chat'; // preload spinner asset new Image().src = '/~landscape/img/Spinner.png'; @@ -79,12 +76,6 @@ export default class ChatApp extends React.Component { } }); - if (totalUnreads !== this.totalUnreads) { - document.title = - totalUnreads > 0 ? `(${totalUnreads}) OS1 - Chat` : 'OS1 - Chat'; - this.totalUnreads = totalUnreads; - } - const { invites, s3, @@ -95,7 +86,10 @@ export default class ChatApp extends React.Component { api, chatInitialized, pendingMessages, - groups + groups, + hideAvatars, + hideNicknames, + remoteContentPolicy } = props; const renderChannelSidebar = (props, station?) => ( @@ -113,88 +107,97 @@ export default class ChatApp extends React.Component { ); return ( - - { - return ( - -

-
-

- Select, create, or join a chat to begin. -

+ <> + + {totalUnreads > 0 ? `(${totalUnreads}) ` : ''}OS1 - Chat + + + { + return ( + +
+
+

+ Select, create, or join a chat to begin. +

+
-
- - ); - }} - /> - { - const ship = props.match.params.ship; + + ); + }} + /> + { + const ship = props.match.params.ship; - return ( - - - - ); - }} - /> - { - return ( - - - - ); - }} - /> - { - let station = `/${props.match.params.ship}/${props.match.params.station}`; + return ( + + + + ); + }} + /> + { + return ( + + + + ); + }} + /> + { + let station = `/${props.match.params.ship}/${props.match.params.station}`; + + // ensure we know joined chats + if(!chatInitialized) { + return null; + } return ( { @@ -261,7 +264,7 @@ export default class ChatApp extends React.Component { association={association} api={api} read={mailbox.config.read} - length={mailbox.config.length} + mailboxSize={mailbox.config.length} envelopes={mailbox.envelopes} inbox={inbox} contacts={roomContacts} @@ -271,50 +274,54 @@ export default class ChatApp extends React.Component { popout={popout} sidebarShown={sidebarShown} chatInitialized={chatInitialized} + hideAvatars={hideAvatars} + hideNicknames={hideNicknames} + remoteContentPolicy={remoteContentPolicy} {...props} /> ); }} - /> - { - let station = `/${props.match.params.ship}/${props.match.params.station}`; - const popout = props.match.url.includes('/popout/'); + /> + { + let station = `/${props.match.params.ship}/${props.match.params.station}`; + const popout = props.match.url.includes('/popout/'); - const association = - station in associations['chat'] ? associations.chat[station] : {}; - const group = groups[association['group-path']] || groupBunts.group(); + const association = + station in associations['chat'] ? associations.chat[station] : {}; + const group = groups[association['group-path']] || groupBunts.group(); - return ( - - - - ); - }} - /> - + sidebar={renderChannelSidebar(props, station)} + > + + + ); + }} + /> + + ); } } diff --git a/pkg/interface/src/views/apps/chat/components/chat.tsx b/pkg/interface/src/views/apps/chat/components/chat.tsx index ab9a4da13..2fabcbf75 100644 --- a/pkg/interface/src/views/apps/chat/components/chat.tsx +++ b/pkg/interface/src/views/apps/chat/components/chat.tsx @@ -8,13 +8,15 @@ import { ChatHeader } from './lib/chat-header'; import { ChatInput } from "./lib/chat-input"; import { deSig } from "~/logic/lib/util"; import { ChatHookUpdate } from "~/types/chat-hook-update"; -import ChatApi from "~/logic/api/chat"; import { Inbox, Envelope } from "~/types/chat-update"; import { Contacts } from "~/types/contact-update"; import { Path, Patp } from "~/types/noun"; import GlobalApi from "~/logic/api/global"; import { Association } from "~/types/metadata-update"; import {Group} from "~/types/group-update"; +import { LocalUpdateRemoteContentPolicy } from "~/types"; +import { S3Upload, SubmitDragger } from '~/views/components/s3-upload'; +import { IUnControlledCodeMirror } from "react-codemirror2"; type ChatScreenProps = RouteComponentProps<{ @@ -26,7 +28,7 @@ type ChatScreenProps = RouteComponentProps<{ association: Association; api: GlobalApi; read: number; - length: number; + mailboxSize: number; inbox: Inbox; contacts: Contacts; group: Group; @@ -36,13 +38,18 @@ type ChatScreenProps = RouteComponentProps<{ sidebarShown: boolean; chatInitialized: boolean; envelopes: Envelope[]; + hideAvatars: boolean; + hideNicknames: boolean; + remoteContentPolicy: LocalUpdateRemoteContentPolicy; }; interface ChatScreenState { messages: Map; + dragover: boolean; } export class ChatScreen extends Component { + private chatInput: React.RefObject; lastNumPending = 0; activityTimeout: NodeJS.Timeout | null = null; @@ -51,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]", @@ -65,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; @@ -95,39 +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} /> { deleteMessage={() => this.setState({ messages: this.state.messages.set(props.station, "") })} + hideAvatars={props.hideAvatars} />
); diff --git a/pkg/interface/src/views/apps/chat/components/join.js b/pkg/interface/src/views/apps/chat/components/join.js index b2dae0a77..556bbe79a 100644 --- a/pkg/interface/src/views/apps/chat/components/join.js +++ b/pkg/interface/src/views/apps/chat/components/join.js @@ -25,7 +25,6 @@ const schema = Yup.object().shape({ export class JoinScreen extends Component { constructor(props) { super(props); - this.state = { awaiting: false }; @@ -38,17 +37,17 @@ export class JoinScreen extends Component { } onSubmit(values) { - console.log(values); + const { props } = this; this.setState({ awaiting: true }, () => { - console.log(values); const station = values.station.trim(); - if (`/${station}` in this.props.chatSynced) { - this.props.history.push(`/~chat/room/${station}`); + if (`/${station}` in props.chatSynced) { + props.history.push(`/~chat/room${station}`); return; } + const ship = station.substr(1).slice(0,station.substr(1).indexOf('/')); - props.api.chat.join(ship, station, true) - this.props.history.push(`/~chat/room/${station}`); + props.api.chat.join(ship, station, true); + props.history.push(`/~chat/room${station}`); }); } @@ -84,7 +83,7 @@ export class JoinScreen extends Component { mt={4} id="station" placeholder="~zod/chatroom" - fontFamily="mono" + fontFamily="mono" caption="Chat names use lowercase, hyphens, and slashes." /> { this.submit(); + }, + 'Esc': () => { + this.editor?.getInputField().blur(); } } }; return (
+ className={ + 'chat fr h-100 flex bg-gray0-d lh-copy pl2 w-100 items-center' + + (inCodeMode ? ' code' : '') + } + style={{ flexGrow: 1, maxHeight: '224px', width: 'calc(100% - 72px)' }}> this.messageChange(e, d, v)} editorDidMount={(editor) => { this.editor = editor; - if (!(BROWSER_REGEX.test(navigator.userAgent))) { + if (!MOBILE_BROWSER_REGEX.test(navigator.userAgent)) { editor.focus(); } }} + {...props} />
); diff --git a/pkg/interface/src/views/apps/chat/components/lib/chat-header.js b/pkg/interface/src/views/apps/chat/components/lib/chat-header.js index df43d08ac..ab63978f2 100644 --- a/pkg/interface/src/views/apps/chat/components/lib/chat-header.js +++ b/pkg/interface/src/views/apps/chat/components/lib/chat-header.js @@ -1,58 +1,61 @@ -import React, { Component, Fragment } from "react"; -import { Link } from "react-router-dom"; - -import { ChatTabBar } from "./chat-tabbar"; -import { SidebarSwitcher } from "~/views/components/SidebarSwitch"; -import { deSig } from "~/logic/lib/util"; +import React, { Fragment } from 'react'; +import { Link } from 'react-router-dom'; +import { TabBar } from '~/views/components/chat-link-tabbar'; +import { SidebarSwitcher } from '~/views/components/SidebarSwitch'; +import { deSig } from '~/logic/lib/util'; export const ChatHeader = (props) => { - const isInPopout = props.popout ? "popout/" : ""; + const isInPopout = props.popout ? 'popout/' : ''; const group = Array.from(props.group.members); let title = props.station.substr(1); - if (props.association && - "metadata" in props.association && - props.association.metadata.tile !== "") { - title = props.association.metadata.title + if (props.association && + 'metadata' in props.association && + props.association.metadata.tile !== '') { + title = props.association.metadata.title; } return (
- {"⟵ All Chats"} + style={{ height: '1rem' }} + > + {'⟵ All Chats'}
+ style={{ height: 48 }} + > + to={'/~chat/' + isInPopout + 'room' + props.station} + className="pt2 white-d" + >

+ style={{ width: 'max-content' }} + > {title}

-
); -} +}; 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 4e49680a1..000000000 --- a/pkg/interface/src/views/apps/chat/components/lib/chat-input.js +++ /dev/null @@ -1,265 +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)) - ? - : ; - - 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 000000000..ba7671463 --- /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 84b985417..e29e18802 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,83 +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 - } = 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 74580f995..000000000 --- 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 36a3a2c45..9cfd6d2d8 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,137 +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} /> - )) - } - + 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/code.js b/pkg/interface/src/views/apps/chat/components/lib/content/code.js index 99ca099d8..33df0a7ce 100644 --- a/pkg/interface/src/views/apps/chat/components/lib/content/code.js +++ b/pkg/interface/src/views/apps/chat/components/lib/content/code.js @@ -11,14 +11,14 @@ export default class CodeContent extends Component { (Boolean(content.code.output) && content.code.output.length && content.code.output.length > 0) ? ( -
+        
           {content.code.output[0].join('\n')}
         
) : null; - + return (
-
+        
           {content.code.expression}
         
{outputElement} diff --git a/pkg/interface/src/views/apps/chat/components/lib/content/text.js b/pkg/interface/src/views/apps/chat/components/lib/content/text.js index 8ddda4ef8..19b5aa423 100644 --- a/pkg/interface/src/views/apps/chat/components/lib/content/text.js +++ b/pkg/interface/src/views/apps/chat/components/lib/content/text.js @@ -3,10 +3,10 @@ import { Link } from 'react-router-dom'; import ReactMarkdown from 'react-markdown'; import RemarkDisableTokenizers from 'remark-disable-tokenizers'; import urbitOb from 'urbit-ob'; +import { Box } from '@tlon/indigo-react'; const DISABLED_BLOCK_TOKENS = [ 'indentedCode', - 'blockquote', 'atxHeading', 'thematicBreak', 'list', @@ -56,9 +56,9 @@ export default class TextContent extends Component { ); } else { 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 a6f1f989a..000000000 --- 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 cb7b75ae7..1abb19f7e 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 1f0e200b9..6f047232a 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,11 +1,13 @@ import React, { Component } from 'react'; import { Link } from 'react-router-dom'; import { ChannelItem } from './channel-item'; +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(`(( <-> )?~(?:${window.ship}|${deSig(cite(window.ship))})( <-> )?)`); let title = association['app-path'] ? association['app-path'] : 'Direct Messages'; if (association.metadata && association.metadata.title) { @@ -47,9 +49,13 @@ export class GroupItem extends Component { each in props.chatMetadata && props.chatMetadata[each].metadata ) { - title = props.chatMetadata[each].metadata.title - ? props.chatMetadata[each].metadata.title - : each.substr(1); + if (props.chatMetadata[each].metadata.title) { + title = props.chatMetadata[each].metadata.title + } + } + + if (DEFAULT_TITLE_REGEX.test(title) && props.index === "dm") { + title = title.replace(DEFAULT_TITLE_REGEX, ''); } const selected = props.station === each; 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 865ab3af2..41321470a 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 4e311df9f..a093bf237 100644 --- a/pkg/interface/src/views/apps/chat/components/lib/message.js +++ b/pkg/interface/src/views/apps/chat/components/lib/message.js @@ -4,17 +4,22 @@ 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 ( @@ -23,14 +28,14 @@ export const Message = (props) => { minHeight: 'min-content' }}> { - props.renderSigil ? ( + renderSigil ? ( renderWithSigil(props, timestamp) ) : (

{timestamp}

- +
) @@ -40,75 +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; - let name = `~${props.msg.author}`; - let color = '#000000'; - let sigilClass = 'mix-blend-diff'; - if (contact) { - name = (contact.nickname.length > 0) - ? 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'); + + 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); } - - 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} -

-
- -
-
- ); }; + 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/overlay-sigil.js b/pkg/interface/src/views/apps/chat/components/lib/overlay-sigil.js index 4a7990822..59e04394f 100644 --- a/pkg/interface/src/views/apps/chat/components/lib/overlay-sigil.js +++ b/pkg/interface/src/views/apps/chat/components/lib/overlay-sigil.js @@ -62,8 +62,9 @@ export class OverlaySigil extends Component { render() { const { props, state } = this; + const { hideAvatars } = props; - const img = (props.contact && (props.contact.avatar !== null)) + const img = (props.contact && (props.contact.avatar !== null) && !hideAvatars) ? : )} {img} diff --git a/pkg/interface/src/views/apps/chat/components/lib/profile-overlay.js b/pkg/interface/src/views/apps/chat/components/lib/profile-overlay.js index d05108278..3e550fc3b 100644 --- a/pkg/interface/src/views/apps/chat/components/lib/profile-overlay.js +++ b/pkg/interface/src/views/apps/chat/components/lib/profile-overlay.js @@ -34,7 +34,7 @@ export class ProfileOverlay extends Component { } render() { - const { contact, ship, color, topSpace, bottomSpace, group, association } = this.props; + const { contact, ship, color, topSpace, bottomSpace, group, association, hideNicknames, hideAvatars } = this.props; let top, bottom; if (topSpace < OVERLAY_HEIGHT / 2) { @@ -51,10 +51,10 @@ export class ProfileOverlay extends Component { const isOwn = window.ship === ship; const identityHref = group.hidden - ? '/~groups/me' + ? '/~profile/identity' : `/~groups/view${association['group-path']}/${window.ship}`; - let img = (contact && (contact.avatar !== null)) + let img = contact?.avatar && !hideAvatars ? : ; + const showNickname = contact?.nickname && !hideNicknames; if (!group.hidden) { img = {img}; @@ -78,7 +79,7 @@ export class ProfileOverlay extends Component { {img}
- {contact && contact.nickname && ( + {showNickname && (
{contact.nickname}
)}
{cite(`~${ship}`)}
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 b497413cf..000000000 --- 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 2724d23c9..954ed3357 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/new-dm.js b/pkg/interface/src/views/apps/chat/components/new-dm.js index 5e69eaa6f..3f2798b16 100644 --- a/pkg/interface/src/views/apps/chat/components/new-dm.js +++ b/pkg/interface/src/views/apps/chat/components/new-dm.js @@ -3,7 +3,7 @@ import { Spinner } from '~/views/components/Spinner'; import { Link } from 'react-router-dom'; import { InviteSearch } from '~/views/components/InviteSearch'; import urbitOb from 'urbit-ob'; -import { deSig } from '~/logic/lib/util'; +import { deSig, cite } from '~/logic/lib/util'; export class NewDmScreen extends Component { constructor(props) { @@ -91,7 +91,7 @@ export class NewDmScreen extends Component { const aud = state.ship !== window.ship ? [`~${state.ships[0]}`] : []; - let title = `~${window.ship} <-> ~${state.ships[0]}`; + let title = `${cite(window.ship)} <-> ${cite(state.ships[0])}`; if (state.title !== '') { title = state.title; diff --git a/pkg/interface/src/views/apps/chat/components/settings.js b/pkg/interface/src/views/apps/chat/components/settings.js index 9b1b33936..a79011ded 100644 --- a/pkg/interface/src/views/apps/chat/components/settings.js +++ b/pkg/interface/src/views/apps/chat/components/settings.js @@ -1,15 +1,11 @@ import React, { Component, Fragment } from 'react'; import { deSig } from '~/logic/lib/util'; -import { Link } from 'react-router-dom'; import { ChatHeader } from './lib/chat-header'; -import { MetadataSettings } from './lib/metadata-settings'; +import { MetadataSettings } from '~/views/components/metadata/settings'; import { DeleteButton } from './lib/delete-button'; import { GroupifyButton } from './lib/groupify-button'; import { Spinner } from '~/views/components/Spinner'; -import { ChatTabBar } from './lib/chat-tabbar'; -import SidebarSwitcher from '~/views/components/SidebarSwitch'; - export class SettingsScreen extends Component { constructor(props) { @@ -89,13 +85,17 @@ export class SettingsScreen extends Component { isOwner={isOwner} changeLoading={this.changeLoading} station={station} + association={association} + contacts={contacts} api={api} /> + resource="chat" + app="chat" + /> - +

{/* app window borders */} -
+
{/* sidebar skeleton, hidden on mobile when in chat panel */}
pre { - overflow-x: auto; -} - .mono { font-family: "Source Code Pro", monospace; } @@ -120,20 +116,8 @@ h2 { 100% {transform: rotate(360deg);} } -/* embeds */ -.embed-container { - position: relative; - height: 0; - overflow: hidden; - padding-bottom: 28.125%; -} - -.embed-container iframe, .embed-container object, .embed-container embed { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; +.embed-container iframe { + max-width: 100%; } .mh-16 { @@ -192,9 +176,6 @@ h2 { .h-100-minus-96-s { height: calc(100% - 96px); } - .embed-container { - padding-bottom: 56.25%; - } .unread-notice { top: 96px; } @@ -204,18 +185,12 @@ h2 { .flex-basis-250-m { flex-basis: 250px; } - .embed-container { - padding-bottom: 56.25%; - } } @media all and (min-width: 46.875em) and (max-width: 60em) { .flex-basis-250-l { flex-basis: 250px; } - .embed-container { - padding-bottom: 37.5%; - } } @media all and (min-width: 60em) { @@ -256,9 +231,21 @@ blockquote { font-family: 'Inter'; } -.chat .CodeMirror.cm-s-code.chat .cm-s-tlon * { - font-family: 'Source Code Pro'; +pre, code { + background-color: var(--light-gray); +} +pre code { + background-color: transparent; + white-space: pre-wrap; +} + +code, .code, .chat.code .react-codemirror2 .CodeMirror * { + font-family: 'Source Code Pro'; +} + +.chat .CodeMirror.cm-s-code.chat * { + font-family: 'Source Code Pro'; } .chat .CodeMirror-selected { background:#BAE3FE !important; color: black; } @@ -267,6 +254,7 @@ pre.CodeMirror-placeholder.CodeMirror-line-like { color: var(--gray); } .chat .cm-s-tlon span { font-family: "Inter"} .chat .cm-s-tlon span.cm-meta { color: var(--gray); } .chat .cm-s-tlon span.cm-number { color: var(--gray); } +.chat .cm-s-tlon span.cm-quote { color: var(--gray); } .chat .cm-s-tlon span.cm-keyword { line-height: 1em; font-weight: bold; color: var(--gray); } .chat .cm-s-tlon span.cm-atom { font-weight: bold; color: var(--gray); } .chat .cm-s-tlon span.cm-def { color: black; } diff --git a/pkg/interface/src/views/apps/dojo/app.js b/pkg/interface/src/views/apps/dojo/app.js index e67ac497e..3c4c5a6ee 100644 --- a/pkg/interface/src/views/apps/dojo/app.js +++ b/pkg/interface/src/views/apps/dojo/app.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import { Route, Link } from 'react-router-dom'; import classnames from 'classnames'; +import Helmet from 'react-helmet'; import { Popout } from './components/lib/icons/popout'; import { History } from './components/history'; @@ -28,8 +29,6 @@ export default class DojoApp extends Component { } componentDidMount() { - document.title = 'OS1 - Dojo'; - const channel = new window.channel(); this.api = new Api(this.props.ship, channel); this.store.api = this.api; @@ -46,56 +45,60 @@ export default class DojoApp extends Component { render() { return ( -
- { - const popout = Boolean(props.match.params.popout); + <> + + OS1 - Dojo + +
+ { + const popout = Boolean(props.match.params.popout); - const popoutClasses = classnames({ - 'mh4-m mh4-l mh4-xl': !popout, - 'mb4-m mb4-l mb4-xl': !popout, - 'ba-m ba-l ba-xl': !popout - }); + const popoutClasses = classnames({ + 'mh4-m mh4-l mh4-xl': !popout, + 'mb4-m mb4-l mb4-xl': !popout, + 'ba-m ba-l ba-xl': !popout + }); - return ( -
-
+ return ( +
+
+
+
+ + + +
-
- - - -
-
- ); - }} - /> -
+ ); + }} + /> +
+ ); } } diff --git a/pkg/interface/src/views/apps/dojo/components/input.js b/pkg/interface/src/views/apps/dojo/components/input.js index 198b8ff5a..a3a910541 100644 --- a/pkg/interface/src/views/apps/dojo/components/input.js +++ b/pkg/interface/src/views/apps/dojo/components/input.js @@ -80,7 +80,7 @@ export class Input extends Component { render() { return (
-
{cite(this.props.ship)}:dojo +
{cite(this.props.ship)}:dojo
{this.props.prompt} diff --git a/pkg/interface/src/views/apps/groups/app.tsx b/pkg/interface/src/views/apps/groups/app.tsx index 5e73f024b..a4e45043e 100644 --- a/pkg/interface/src/views/apps/groups/app.tsx +++ b/pkg/interface/src/views/apps/groups/app.tsx @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import { Route, Switch } from 'react-router-dom'; +import Helmet from 'react-helmet'; import './css/custom.css'; @@ -25,7 +26,6 @@ type GroupsAppProps = StoreState & { export default class GroupsApp extends Component { componentDidMount() { - document.title = 'OS1 - Groups'; // preload spinner asset new Image().src = '/~landscape/img/Spinner.png'; @@ -55,6 +55,10 @@ export default class GroupsApp extends Component { return ( + <> + + OS1 - Groups + { @@ -329,34 +333,8 @@ export default class GroupsApp extends Component { ); }} /> - { - const me = defaultContacts[window.ship] || {}; - - return ( - - - - ); - }} - /> + ); } } diff --git a/pkg/interface/src/views/apps/groups/components/lib/ContactCard.tsx b/pkg/interface/src/views/apps/groups/components/lib/ContactCard.tsx new file mode 100644 index 000000000..f8438417d --- /dev/null +++ b/pkg/interface/src/views/apps/groups/components/lib/ContactCard.tsx @@ -0,0 +1,124 @@ +import React, { Component } from "react"; +import { Sigil } from "~/logic/lib/sigil"; +import * as Yup from "yup"; + +import { Link } from "react-router-dom"; +import { EditElement } from "./edit-element"; +import { Spinner } from "~/views/components/Spinner"; +import { uxToHex } from "~/logic/lib/util"; +import { Col, Input, Box, Text, Row } from "@tlon/indigo-react"; +import { Formik, Form, FormikHelpers } from "formik"; +import { Contact } from "~/types/contact-update"; +import { AsyncButton } from "~/views/components/AsyncButton"; +import { ColorInput } from "~/views/components/ColorInput"; +import GlobalApi from "~/logic/api/global"; +import { ImageInput } from "~/views/components/ImageInput"; +import { S3State } from "~/types"; + +interface ContactCardProps { + contact: Contact; + path: string; + api: GlobalApi; + s3: S3State; +} + +const formSchema = Yup.object({ + color: Yup.string(), + nickname: Yup.string(), + email: Yup.string().matches( + new RegExp( + String( + /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*/.source + ) + + /@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/ + .source + ), + "Not a valid email" + ), + phone: Yup.string().matches( + new RegExp( + String(/^\s*(?:\+?(\d{1,3}))?/.source) + + /([-. (]*(\d{3})[-. )]*)?((\d{3})[-. ]*(\d{2,4})(?:[-.x ]*(\d+))?)\s*$/ + .source + ), + "Not a valid phone" + ), + + website: Yup.string().matches( + new RegExp( + String(/[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}/.source) + + /\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/.source + ), + "Not a valid website" + ), +}); + +export function ContactCard(props: ContactCardProps) { + const us = `~${window.ship}`; + const { contact } = props; + const onSubmit = async (values: Contact, actions: FormikHelpers) => { + try { + await Object.keys(values).reduce((acc, key) => { + const newValue = key !== "color" ? values[key] : uxToHex(values[key]); + if (newValue !== contact[key]) { + if (key === "avatar") { + return acc.then(() => + props.api.contacts.edit(props.path, us, { + avatar: { url: newValue }, + } as any) + ); + } + + return acc.then(() => + props.api.contacts.edit(props.path, us, { + [key]: newValue, + } as any) + ); + } + return acc; + }, Promise.resolve()); + actions.setStatus({ success: null }); + } catch (e) { + console.error(e); + actions.setStatus({ error: e.message }); + } + }; + + const hexColor = contact.color ? `#${uxToHex(contact.color)}` : "#000000"; + + return ( + + +
+ + + + + {us} + + + + + + + + + + + Save + + + +
+
+ ); +} diff --git a/pkg/interface/src/views/apps/groups/components/lib/contact-card.js b/pkg/interface/src/views/apps/groups/components/lib/contact-card.js index 572f2f2f0..40d44ae69 100644 --- a/pkg/interface/src/views/apps/groups/components/lib/contact-card.js +++ b/pkg/interface/src/views/apps/groups/components/lib/contact-card.js @@ -5,7 +5,7 @@ import { Link } from 'react-router-dom'; import { EditElement } from './edit-element'; import { Spinner } from '~/views/components/Spinner'; import { uxToHex } from '~/logic/lib/util'; -import { S3Upload } from './s3-upload'; +import { S3Upload } from '~/views/components/s3-upload'; export class ContactCard extends Component { constructor(props) { @@ -492,7 +492,15 @@ export class ContactCard extends Component { credentials={props.s3.credentials} uploadSuccess={this.uploadSuccess.bind(this)} uploadError={this.uploadError.bind(this)} - /> + 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 0fb197aa7..4d7b6ff94 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 @@ -19,6 +19,7 @@ export class GroupDetail extends Component { this.changeTitle = this.changeTitle.bind(this); this.changeDescription = this.changeDescription.bind(this); this.changePolicy = this.changePolicy.bind(this); + this.getShortcode = this.getShortcode.bind(this); } componentDidMount() { @@ -185,6 +186,41 @@ export class GroupDetail extends Component { ); } + getShortcode(group, path) { + if (group?.policy?.open) { + return ( +
+

Share

+

+ Share a shortcode to join this group +

+
+ + { + writeText(path.substr(6)); + this.refs.copy.innerText = 'Copied'; + }}> + Copy + +
+
+ ); + } else { + return
; + }; + } + renderSettings() { const { props } = this; @@ -201,33 +237,8 @@ export class GroupDetail extends Component { { description: 'Janitor', tag: 'janitor', addDescription: 'Make Janitor' } ]; - let shortcode =
; + const shortcode = this.getShortcode(group, props.path); - if (group?.policy?.open) { - shortcode =
-

Share

-

Share a shortcode to join this group

-
- - { - writeText(props.path.substr(6)); - this.refs.copy.innerText = 'Copied'; - }} - > - Copy - -
-
; - } return (
@@ -313,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/group-sidebar.js b/pkg/interface/src/views/apps/groups/components/lib/group-sidebar.js index e4d0ccf79..b617a417e 100644 --- a/pkg/interface/src/views/apps/groups/components/lib/group-sidebar.js +++ b/pkg/interface/src/views/apps/groups/components/lib/group-sidebar.js @@ -18,30 +18,6 @@ export class GroupSidebar extends Component { const selectedClass = (props.selected === 'me') ? 'bg-gray4 bg-gray1-d' : 'bg-white bg-gray0-d'; - const rootIdentity = -
- -

- {cite(window.ship)} -

-
- ; - const inviteItems = Object.keys(props.invites) .map((uid) => { @@ -127,8 +103,6 @@ export class GroupSidebar extends Component {

Join Group

-

Your Identity

- {rootIdentity} {inviteItems}

Groups

{groupItems} 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 b94b14728..000000000 --- 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/groups/components/new.tsx b/pkg/interface/src/views/apps/groups/components/new.tsx index 528794737..a613ec192 100644 --- a/pkg/interface/src/views/apps/groups/components/new.tsx +++ b/pkg/interface/src/views/apps/groups/components/new.tsx @@ -103,7 +103,7 @@ export class NewScreen extends Component { }, }; - const { groupName } = this.state; + const groupName = this.state.groupName.trim(); this.setState( { invites: { ships: [], groups: [] }, @@ -115,7 +115,7 @@ export class NewScreen extends Component { .then(() => { this.setState({ awaiting: false }); props.history.push( - `/~groups/ship/~${window.ship}/${state.groupName}` + `/~groups/ship/~${window.ship}/${groupName}` ); }); } diff --git a/pkg/interface/src/views/apps/groups/components/skeleton.js b/pkg/interface/src/views/apps/groups/components/skeleton.js index 08e006869..24820add6 100644 --- a/pkg/interface/src/views/apps/groups/components/skeleton.js +++ b/pkg/interface/src/views/apps/groups/components/skeleton.js @@ -10,7 +10,7 @@ export class Skeleton extends Component { return (
-
+
-
- - -
-
{props.baseHash}
-
+ <> + + OS1 - Home + +
+
+ + +
+ + {props.baseHash} + +
+ ); } } diff --git a/pkg/interface/src/views/apps/launch/components/tiles/clock.js b/pkg/interface/src/views/apps/launch/components/tiles/clock.js index f84104b1e..648483d16 100644 --- a/pkg/interface/src/views/apps/launch/components/tiles/clock.js +++ b/pkg/interface/src/views/apps/launch/components/tiles/clock.js @@ -192,7 +192,7 @@ class Clock extends React.Component { const newX = cx + (ctr - 15) * Math.cos(this.angle); const newY = cy + (ctr - 15) * Math.sin(this.angle); - // Initial background + // Center white circle with time and date circle( ctx, ctr, @@ -333,18 +333,6 @@ class Clock extends React.Component { ); // Outer borders - circleOutline( - ctx, - ctr, - ctr, - ctr, - -1, - 2 * Math.PI, - background, - 1 - ); - - // Center white circle with time and date circle( ctx, ctr, @@ -352,7 +340,8 @@ class Clock extends React.Component { ctr/1.85, -1, 2 * Math.PI, - background + background, + 1 ); // Center white circle border @@ -401,7 +390,7 @@ export default class ClockTile extends React.Component { renderWrapper(child) { return ( - + {child} ); diff --git a/pkg/interface/src/views/apps/launch/components/tiles/tile.js b/pkg/interface/src/views/apps/launch/components/tiles/tile.js index 5c723a40a..c99017b1c 100644 --- a/pkg/interface/src/views/apps/launch/components/tiles/tile.js +++ b/pkg/interface/src/views/apps/launch/components/tiles/tile.js @@ -3,10 +3,12 @@ import React from 'react'; export default class Tile extends React.Component { render() { + const { transparent } = this.props; + const bgClasses = transparent ? ' ' : ' bg-white bg-gray0-d '; return ( -
- {this.props.children} +
+ {this.props.children}
); } diff --git a/pkg/interface/src/views/apps/links/app.js b/pkg/interface/src/views/apps/links/app.js index 983b15b67..080aefae7 100644 --- a/pkg/interface/src/views/apps/links/app.js +++ b/pkg/interface/src/views/apps/links/app.js @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import { Switch, Route } from 'react-router-dom'; +import Helmet from 'react-helmet'; import _ from 'lodash'; @@ -7,31 +8,29 @@ import './css/custom.css'; import { Skeleton } from './components/skeleton'; import { NewScreen } from './components/new'; -import { MemberScreen } from './components/member'; import { SettingsScreen } from './components/settings'; import { MessageScreen } from './components/lib/message-screen'; import { Links } from './components/links-list'; import { LinkDetail } from './components/link'; + import { makeRoutePath, amOwnerOfGroup, base64urlDecode } from '~/logic/lib/util'; - export class LinksApp extends Component { constructor(props) { super(props); - this.totalUnseen = 0; } componentDidMount() { - document.title = 'OS1 - Links'; // preload spinner asset new Image().src = '/~landscape/img/Spinner.png'; this.props.api.links.getPage('', 0); this.props.subscription.startApp('link'); + this.props.subscription.startApp('graph'); if (!this.props.sidebarShown) { this.props.api.local.sidebarToggle(); } @@ -39,203 +38,101 @@ export class LinksApp extends Component { componentWillUnmount() { this.props.subscription.stopApp('link'); + this.props.subscription.stopApp('graph'); } - render() { const { props } = this; - const contacts = props.contacts ? props.contacts : {}; - const groups = props.groups ? props.groups : {}; - const associations = props.associations ? props.associations : { link: {}, contacts: {} }; const links = props.links ? props.links : {}; const comments = props.linkComments ? props.linkComments : {}; - const seen = props.linksSeen ? props.linksSeen : {}; - const totalUnseen = _.reduce( links, (acc, collection) => acc + collection.unseenCount, 0 ); - if(totalUnseen !== this.totalUnseen) { - document.title = totalUnseen !== 0 ? `(${totalUnseen}) OS1 - Links` : 'OS1 - Links'; - this.totalUnseen = totalUnseen; - } - const invites = props.invites ? props.invites : {}; - const listening = props.linkListening; - - const { api, sidebarShown } = this.props; + const { api, sidebarShown, hideAvatars, hideNicknames, s3, remoteContentPolicy } = this.props; return ( - - { - return ( - - - - ); - }} - /> - { - return ( - - - - ); - }} - /> - { - const resourcePath = '/' + props.match.params.resource; - - const autoJoin = () => { - try { - api.links.joinCollection(resourcePath); - props.history.push(makeRoutePath(resourcePath)); - } catch(err) { - setTimeout(autoJoin, 2000); - } - }; - autoJoin(); - }} - /> - { - const popout = props.match.url.includes('/popout/'); - const resourcePath = '/' + props.match.params.resource; - const resource = associations.link[resourcePath] || { metadata: {} }; - - const contactDetails = contacts[resource['group-path']] || {}; - const group = groups[resource['group-path']] || new Set([]); - const amOwner = amOwnerOfGroup(resource['group-path']); - - return ( - - - - ); - }} - /> - { - const popout = props.match.url.includes('/popout/'); - const resourcePath = '/' + props.match.params.resource; - const resource = associations.link[resourcePath] || false; - - const contactDetails = contacts[resource['group-path']] || {}; - const group = groups[resource['group-path']] || new Set([]); - const amOwner = amOwnerOfGroup(resource['group-path']); - - return ( - - - - ); - }} - /> - + + {totalUnseen > 0 ? `(${totalUnseen}) ` : ''}OS1 - Links + + + { + return ( + + + + ); + }} + /> + { + return ( + + + + ); + }} + /> + { + const resourcePath = '/' + props.match.params.resource; + + const autoJoin = () => { + try { + api.links.joinCollection(resourcePath); + props.history.push(makeRoutePath(resourcePath)); + } catch(err) { + setTimeout(autoJoin, 2000); + } + }; + autoJoin(); + }} + /> + { + const popout = props.match.url.includes('/popout/'); const resourcePath = '/' + props.match.params.resource; const resource = associations.link[resourcePath] || { metadata: {} }; - const amOwner = amOwnerOfGroup(resource['group-path']); - const contactDetails = contacts[resource['group-path']] || {}; - - const page = props.match.params.page || 0; - - const popout = props.match.url.includes('/popout/'); - - const channelLinks = links[resourcePath] - ? links[resourcePath] - : { local: {} }; - - const channelComments = comments[resourcePath] - ? comments[resourcePath] - : {}; - - const channelSeen = seen[resourcePath] - ? seen[resourcePath] - : {}; + const group = groups[resource['group-path']] || new Set([]); + const amOwner = amOwnerOfGroup(resource['group-path']); return ( - ); }} /> - { - const resourcePath = '/' + props.match.params.resource; - const resource = associations.link[resourcePath] || { metadata: {} }; - - const amOwner = amOwnerOfGroup(resource['group-path']); - const popout = props.match.url.includes('/popout/'); + const resourcePath = '/' + props.match.params.resource; + const resource = associations.link[resourcePath] || false; const contactDetails = contacts[resource['group-path']] || {}; - - const index = props.match.params.index || 0; - const page = props.match.params.page || 0; - const url = base64urlDecode(props.match.params.encodedUrl); - - const data = links[resourcePath] - ? links[resourcePath][page] - ? links[resourcePath][page][index] - : {} - : {}; - const coms = !comments[resourcePath] - ? undefined - : comments[resourcePath][url]; - - const commentPage = props.match.params.commentpage || 0; + const group = groups[resource['group-path']] || new Set([]); + const amOwner = amOwnerOfGroup(resource['group-path']); return ( - ); }} /> - + { + const resourcePath = '/' + props.match.params.resource; + const resource = associations.link[resourcePath] || { metadata: {} }; + + const amOwner = amOwnerOfGroup(resource['group-path']); + + const contactDetails = contacts[resource['group-path']] || {}; + + const page = props.match.params.page || 0; + + const popout = props.match.url.includes('/popout/'); + + const channelLinks = links[resourcePath] + ? links[resourcePath] + : { local: {} }; + + const channelComments = comments[resourcePath] + ? comments[resourcePath] + : {}; + + const channelSeen = seen[resourcePath] + ? seen[resourcePath] + : {}; + + return ( + + + + ); + }} + /> + { + const resourcePath = '/' + props.match.params.resource; + const resource = associations.link[resourcePath] || { metadata: {} }; + + const amOwner = amOwnerOfGroup(resource['group-path']); + + const popout = props.match.url.includes('/popout/'); + + const contactDetails = contacts[resource['group-path']] || {}; + + const index = props.match.params.index || 0; + const page = props.match.params.page || 0; + const url = base64urlDecode(props.match.params.encodedUrl); + + const data = links[resourcePath] + ? links[resourcePath][page] + ? links[resourcePath][page][index] + : {} + : {}; + const coms = !comments[resourcePath] + ? undefined + : comments[resourcePath][url]; + + const commentPage = props.match.params.commentpage || 0; + + return ( + + + + ); + }} + /> + + ); } } 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 4693c5f61..dfe9b3403 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 @@ -2,6 +2,8 @@ import React, { Component } from 'react'; 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 class CommentItem extends Component { constructor(props) { @@ -35,9 +37,9 @@ export class CommentItem extends Component { const member = props.member || false; - const pending = props.pending ? 'o-60' : ''; - - const img = (props.avatar) + const showAvatar = props.avatar && !props.hideAvatars; + const showNickname = props.nickname && !props.hideNicknames; + const img = showAvatar ? : ; return ( -
-
- {img} -

- - {props.nickname ? props.nickname : cite(props.ship)} - - + + + {img} + + + {showNickname ? props.nickname : cite(props.ship)} + + {this.state.timeSinceComment} - -

-
-

{props.content}

-
+ + + + {props.content} + ); } } diff --git a/pkg/interface/src/views/apps/links/components/lib/comments.js b/pkg/interface/src/views/apps/links/components/lib/comments.js index b6a55daa4..8982867fd 100644 --- a/pkg/interface/src/views/apps/links/components/lib/comments.js +++ b/pkg/interface/src/views/apps/links/components/lib/comments.js @@ -40,6 +40,8 @@ export class Comments extends Component { ? props.comments.totalPages : 1; + const { hideNicknames, hideAvatars, remoteContentPolicy } = props; + const commentsList = Object.keys(commentsPage) .map((entry) => { const commentObj = commentsPage[entry]; @@ -51,7 +53,7 @@ export class Comments extends Component { const { nickname, color, member, avatar } = getContactDetails(contacts[ship]); - const nameClass = nickname ? 'inter' : 'mono'; + const nameClass = nickname && !hideNicknames ? 'inter' : 'mono'; return( ); }); diff --git a/pkg/interface/src/views/apps/links/components/lib/invite-element.js b/pkg/interface/src/views/apps/links/components/lib/invite-element.js deleted file mode 100644 index 297f69359..000000000 --- a/pkg/interface/src/views/apps/links/components/lib/invite-element.js +++ /dev/null @@ -1,78 +0,0 @@ -import React, { Component } from 'react'; -import { InviteSearch } from '~/views/components/InviteSearch'; -import { Spinner } from '~/views/components/Spinner'; - -export class InviteElement extends Component { - constructor(props) { - super(props); - this.state = { - members: [], - error: false, - success: false, - awaiting: false - }; - this.setInvite = this.setInvite.bind(this); - } - - modifyMembers() { - const { props, state } = this; - - const aud = state.members.map(mem => `~${mem}`); - - if (state.members.length === 0) { - this.setState({ - error: true, - success: false - }); - return; - } - - this.setState({ awaiting: true }); - - this.setState({ - error: false, - success: true, - members: [] - }, () => { - props.api.links.inviteToCollection(props.resourcePath, aud).then(() => { - this.setState({ awaiting: false }); - }); - }); - } - - setInvite(invite) { - this.setState({ members: invite.ships }); - } - - render() { - const { props, state } = this; - - let modifyButtonClasses = 'mt4 db f9 ba pa2 white-d bg-gray0-d b--black b--gray2-d pointer'; - if (state.error) { - modifyButtonClasses = modifyButtonClasses + ' gray3'; - } - - return ( -
- - - -
- ); - } -} diff --git a/pkg/interface/src/views/apps/links/components/lib/link-detail-preview.js b/pkg/interface/src/views/apps/links/components/lib/link-detail-preview.js index 712870cb8..bc7d286d7 100644 --- a/pkg/interface/src/views/apps/links/components/lib/link-detail-preview.js +++ b/pkg/interface/src/views/apps/links/components/lib/link-detail-preview.js @@ -2,6 +2,8 @@ import React, { Component } from 'react'; import { cite } from '~/logic/lib/util'; import moment from 'moment'; +import RemoteContent from "~/views/components/RemoteContent"; + export class LinkPreview extends Component { constructor(props) { super(props); @@ -27,26 +29,6 @@ export class LinkPreview extends Component { timeSinceLinkPost: this.getTimeSinceLinkPost() }); }, 60000); - - // check for soundcloud for fetching embed - const soundcloudRegex = new RegExp(String(/(https?:\/\/(?:www.)?soundcloud.com\/[\w-]+\/?(?:sets\/)?[\w-]+)/.source) - ); - - const isSoundcloud = soundcloudRegex.exec(this.props.url); - - if (isSoundcloud && this.state.embed === '') { - fetch( - 'https://soundcloud.com/oembed?format=json&url=' + - encodeURIComponent(this.props.url)) - .then((response) => { - return response.json(); - }) - .then((json) => { - this.setState({ embed: json.html }); - }); - } else if (!isSoundcloud) { - this.setState({ embed: '' }); - } } componentWillUnmount() { @@ -66,6 +48,16 @@ export class LinkPreview extends Component { render() { const { props } = this; + const embed = ( + + ); + const URLparser = new RegExp( /((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/ ); @@ -76,50 +68,16 @@ export class LinkPreview extends Component { hostname = hostname[4]; } - const imgMatch = /(jpg|img|png|gif|tiff|jpeg|JPG|IMG|PNG|TIFF|GIF|webp|WEBP|webm|WEBM)$/.exec( - props.url - ); + const showNickname = props.nickname && !props.hideNicknames; - const youTubeRegex = 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 - ); - - const ytMatch = youTubeRegex.exec(props.url); - - let embed = ''; - - if (imgMatch) { - embed = - - ; - } - - if (ytMatch) { - embed = ( - - ); - } - - const nameClass = props.nickname ? 'inter' : 'mono'; + const nameClass = showNickname ? 'inter' : 'mono'; return (
- {embed ||
} + {embed}
@@ -132,7 +90,7 @@ export class LinkPreview extends Component { - {props.nickname ? props.nickname : cite(props.ship)} + {showNickname ? props.nickname : cite(props.ship)} {this.state.timeSinceLinkPost} diff --git a/pkg/interface/src/views/apps/links/components/lib/link-item.js b/pkg/interface/src/views/apps/links/components/lib/link-item.js index fce08e7b7..f30aa59ce 100644 --- a/pkg/interface/src/views/apps/links/components/lib/link-item.js +++ b/pkg/interface/src/views/apps/links/components/lib/link-item.js @@ -40,8 +40,6 @@ export class LinkItem extends Component { render() { const props = this.props; - const mono = (props.nickname) ? 'inter white-d' : 'mono white-d'; - const URLparser = new RegExp(/((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/); let hostname = URLparser.exec(props.url); @@ -58,7 +56,12 @@ export class LinkItem extends Component { const member = this.props.member || false; - const img = (this.props.avatar) + const showAvatar = props.avatar && !props.hideAvatars; + const showNickname = props.nickname && !props.hideNicknames; + + const mono = showNickname ? 'inter white-d' : 'mono white-d'; + + const img = showAvatar ? : - {(props.nickname) + {showNickname ? props.nickname : cite(props.ship)} diff --git a/pkg/interface/src/views/apps/links/components/lib/link-submit.js b/pkg/interface/src/views/apps/links/components/lib/link-submit.js deleted file mode 100644 index fd132571e..000000000 --- a/pkg/interface/src/views/apps/links/components/lib/link-submit.js +++ /dev/null @@ -1,134 +0,0 @@ -import React, { Component } from 'react'; -import { Spinner } from '~/views/components/Spinner'; - -export class LinkSubmit extends Component { - constructor() { - super(); - this.state = { - linkValue: '', - linkTitle: '', - linkValid: false, - submitFocus: false, - disabled: false - }; - this.setLinkValue = this.setLinkValue.bind(this); - this.setLinkTitle = this.setLinkTitle.bind(this); - } - - onClickPost() { - const link = this.state.linkValue; - const title = this.state.linkTitle - ? this.state.linkTitle - : this.state.linkValue; - this.setState({ disabled: true }); - this.props.api.links.postLink(this.props.resourcePath, link, title).then((r) => { - this.setState({ - disabled: false, - linkValue: '', - linkTitle: '', - linkValid: false - }); - }); - } - - setLinkValid(link) { - const URLparser = new RegExp( - /((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/ - ); - - const validURL = URLparser.exec(link); - - if (!validURL) { - const checkProtocol = URLparser.exec('http://' + link); - if (checkProtocol) { - this.setState({ linkValid: true }); - this.setState({ linkValue: 'http://' + link }); - } else { - this.setState({ linkValid: false }); - } - } else if (validURL) { - this.setState({ linkValid: true }); - } - } - - setLinkValue(event) { - this.setState({ linkValue: event.target.value }); - this.setLinkValid(event.target.value); - } - - setLinkTitle(event) { - this.setState({ linkTitle: event.target.value }); - } - - render() { - const activeClasses = (this.state.linkValid && !this.state.disabled) - ? 'green2 pointer' : 'gray2'; - - const focus = (this.state.submitFocus) - ? 'b--black b--white-d' - : 'b--gray4 b--gray2-d'; - - return ( -
- -)); - -CommentInput.displayName = 'commentInput'; - -export default CommentInput; diff --git a/pkg/interface/src/views/apps/publish/components/lib/comment-item.js b/pkg/interface/src/views/apps/publish/components/lib/comment-item.js deleted file mode 100644 index dfd4b43ca..000000000 --- a/pkg/interface/src/views/apps/publish/components/lib/comment-item.js +++ /dev/null @@ -1,155 +0,0 @@ -import React, { Component } from 'react'; -import moment from 'moment'; -import { Sigil } from '~/logic/lib/sigil'; -import CommentInput from './comment-input'; -import { uxToHex, cite } from '~/logic/lib/util'; - -export class CommentItem extends Component { - constructor(props) { - super(props); - - this.state = { - commentBody: '' - }; - - this.commentChange = this.commentChange.bind(this); - this.commentEdit = this.commentEdit.bind(this); - moment.updateLocale('en', { - relativeTime: { - past: function(input) { - return input === 'just now' - ? input - : input + ' ago'; - }, - s : 'just now', - future : 'in %s', - m : '1m', - mm : '%dm', - h : '1h', - hh : '%dh', - d : '1d', - dd : '%dd', - M : '1 month', - MM : '%d months', - y : '1 year', - yy : '%d years' - } - }); - } - - commentEdit() { - const commentPath = Object.keys(this.props.comment)[0]; - const commentBody = this.props.comment[commentPath].content; - this.setState({ commentBody }); - this.props.onEdit(); - } - - focusTextArea(text) { - text && text.focus(); - } - - commentChange(e) { - this.setState({ - commentBody: e.target.value - }); - } - - onUpdate() { - this.props.onUpdate(this.state.commentBody); - } - - render() { - const pending = this.props.pending ? 'o-60' : ''; - const commentData = this.props.comment[Object.keys(this.props.comment)[0]]; - const content = commentData.content.split('\n').map((line, i) => { - return ( -

{line}

- ); - }); - const date = moment(commentData['date-created']).fromNow(); - - const contact = commentData.author.substr(1) in this.props.contacts - ? this.props.contacts[commentData.author.substr(1)] : false; - - let name = commentData.author; - let color = '#000000'; - let classes = 'mix-blend-diff'; - let avatar = null; - if (contact) { - name = (contact.nickname.length > 0) - ? contact.nickname : commentData.author; - color = `#${uxToHex(contact.color)}`; - classes = ''; - avatar = contact.avatar; - } - - const img = (avatar !== null) - ? - : ; - - if (name === commentData.author) { - name = cite(commentData.author); - } - - const { editing } = this.props; - - const disabled = this.props.pending - || window.ship !== commentData.author.slice(1); - - return ( -
-
- {img} -
- {name} -
-
{date}
- { !editing && !disabled && ( - <> -
- Edit -
-
- Delete -
- - ) } -
-
- { !editing && content } - { editing && ( - { - this.focusTextArea(el); - }} - onChange={this.commentChange} - value={this.state.commentBody} - onSubmit={this.onUpdate.bind(this)} - > - - )} -
- { editing && ( -
-
- Submit -
-
- Cancel -
-
- )} -
- ); - } -} - -export default CommentItem; diff --git a/pkg/interface/src/views/apps/publish/components/lib/comments.js b/pkg/interface/src/views/apps/publish/components/lib/comments.js deleted file mode 100644 index fbbe9011f..000000000 --- a/pkg/interface/src/views/apps/publish/components/lib/comments.js +++ /dev/null @@ -1,200 +0,0 @@ -import React, { Component } from 'react'; -import { CommentItem } from './comment-item'; -import CommentInput from './comment-input'; -import { dateToDa } from '~/logic/lib/util'; -import { Spinner } from '~/views/components/Spinner'; - -export class Comments extends Component { - constructor(props) { - super(props); - this.state = { - commentBody: '', - pending: new Set(), - awaiting: null, - editing: null - }; - this.commentSubmit = this.commentSubmit.bind(this); - this.commentChange = this.commentChange.bind(this); - } - - componentDidUpdate(prevProps) { - const previousComments = prevProps.comments[0] || {}; - const currentComments = this.props.comments[0] || {}; - const previous = Object.keys(previousComments) || []; - const current = Object.keys(currentComments) || []; - if ((prevProps.comments && this.props.comments) && - (previous !== current)) { - const pendingSet = this.state.pending; - Object.keys(currentComments).map((com) => { - const obj = currentComments[com]; - for (const each of pendingSet.values()) { - if (obj.content === each['new-comment'].body) { - pendingSet.delete(each); - this.setState({ pending: pendingSet }); - } - } - }); - } - } - - commentSubmit(evt) { - const comment = { - 'new-comment': { - who: this.props.ship.slice(1), - book: this.props.book, - note: this.props.note, - body: this.state.commentBody - } - }; - - const pendingState = this.state.pending; - pendingState.add(comment); - this.setState({ pending: pendingState }); - - this.textArea.value = ''; - this.setState({ commentBody: '', awaiting: 'new' }); - const submit = this.props.api.publish.publishAction(comment); - submit.then(() => { - this.setState({ awaiting: null }); - }); - } - - commentChange(evt) { - this.setState({ - commentBody: evt.target.value - }); - } - - commentEdit(idx) { - this.setState({ editing: idx }); - } - - commentEditCancel() { - this.setState({ editing: null }); - } - - commentUpdate(idx, body) { - const path = Object.keys(this.props.comments[idx])[0]; - const comment = { - 'edit-comment': { - who: this.props.ship.slice(1), - book: this.props.book, - note: this.props.note, - body: body, - comment: path - } - }; - - this.setState({ awaiting: 'edit' }); - - this.props.api.publish - .publishAction(comment) - .then(() => { - this.setState({ awaiting: null, editing: null }); - }); - } - - commentDelete(idx) { - const path = Object.keys(this.props.comments[idx])[0]; - const comment = { - 'del-comment': { - who: this.props.ship.slice(1), - book: this.props.book, - note: this.props.note, - comment: path - } - }; - - this.setState({ awaiting: { kind: 'del', what: idx } }); - this.props.api.publish - .publishAction(comment) - .then(() => { - this.setState({ awaiting: null }); -}); - } - - render() { - if (!this.props.enabled) { - return null; - } - - const { editing } = this.state; - - const pendingArray = Array.from(this.state.pending).map((com, i) => { - const da = dateToDa(new Date()); - const comment = { - [da]: { - author: `~${window.ship}`, - content: com['new-comment'].body, - 'date-created': Math.round(new Date().getTime()) - } - }; - return ( - - ); - }); - - const commentArray = this.props.comments.map((com, i) => { - return ( - this.commentUpdate(i, u)} - onDelete={() => this.commentDelete(i)} - onEdit={() => this.commentEdit(i)} - onEditCancel={this.commentEditCancel.bind(this)} - editing={i === editing} - disabled={Boolean(this.state.awaiting) || editing} - /> - ); - }); - - const disableComment = ((this.state.commentBody === '') || (Boolean(this.state.awaiting))); - const commentClass = (disableComment) - ? 'bg-transparent f9 pa2 br1 ba b--gray2 gray2' - : 'bg-transparent f9 pa2 br1 ba b--gray2 black white-d pointer'; - - const spinnerText = - this.state.awaiting === 'new' - ? 'Posting commment...' - : this.state.awaiting === 'edit' - ? 'Updating comment...' - : 'Deleting comment...'; - - return ( -
-
-
- { - this.textArea = el; - }} - onChange={this.commentChange} - value={this.state.commentBody} - disabled={Boolean(this.state.editing)} - onSubmit={this.commentSubmit} - > - -
- - -
- {pendingArray} - {commentArray} -
- ); - } -} - -export default Comments; diff --git a/pkg/interface/src/views/apps/publish/components/lib/dropdown.js b/pkg/interface/src/views/apps/publish/components/lib/dropdown.js deleted file mode 100644 index 7f08bc3ab..000000000 --- a/pkg/interface/src/views/apps/publish/components/lib/dropdown.js +++ /dev/null @@ -1,89 +0,0 @@ -import React, { Component } from 'react'; - -export class Dropdown extends Component { - constructor(props) { - super(props); - - this.toggleDropdown = this.toggleDropdown.bind(this); - this.handleClickOutside = this.handleClickOutside.bind(this); - this.collapseAndDispatch = this.collapseAndDispatch.bind(this); - this.state = { - open: false - }; - } - - componentDidMount() { - document.addEventListener('mousedown', this.handleClickOutside); - } - - componentWillUnmount() { - document.removeEventListener('mousedown', this.handleClickOutside); - } - - handleClickOutside(evt) { - if (this.optsList && !this.optsList.contains(evt.target) && - this.optsButton && !this.optsButton.contains(evt.target)) { - this.setState({ open: false }); - } - } - - toggleDropdown() { - this.setState({ open: !this.state.open }); - } - - collapseAndDispatch(action) { - this.setState({ open: false }, action); - } - - render() { - const alignment = (this.props.align) - ? this.props.align : 'right'; - - const display = (this.state.open) - ? 'block' : 'none'; - - const optionsClass = (this.state.open) - ? 'open' : 'closed'; - - let leftAlign = ''; - let rightAlign = '0'; - - if (alignment === 'left') { - leftAlign = '0'; - rightAlign = ''; - } - - const optionsList = this.props.options.map((val, i) => { - return ( - - ); - }); - - return ( -
{ - this.optsButton = el; - }} - onClick={this.toggleDropdown} - > - -
{ - this.optsList = el; - }} - style={{ left: leftAlign, right: rightAlign, width:this.props.width, display: display }} - > - {optionsList} -
-
- ); - } -} - -export default Dropdown; diff --git a/pkg/interface/src/views/apps/publish/components/lib/edit-post.js b/pkg/interface/src/views/apps/publish/components/lib/edit-post.js deleted file mode 100644 index a259bbda3..000000000 --- a/pkg/interface/src/views/apps/publish/components/lib/edit-post.js +++ /dev/null @@ -1,145 +0,0 @@ -import React, { Component } from 'react'; -import { SidebarSwitcher } from '~/views/components/SidebarSwitch'; -import { Spinner } from '~/views/components/Spinner'; -import { Link } from 'react-router-dom'; -import { Controlled as CodeMirror } from 'react-codemirror2'; -import { dateToDa } from '~/logic/lib/util'; - -import 'codemirror/mode/markdown/markdown'; - -export class EditPost extends Component { - constructor(props) { - super(props); - this.state = { - body: '', - submit: false, - awaiting: false - }; - this.postSubmit = this.postSubmit.bind(this); - this.bodyChange = this.bodyChange.bind(this); - } - - componentDidMount() { - this.componentDidUpdate(); - } - - componentDidUpdate(prevProps) { - const { props, state } = this; - const contents = props.notebooks[props.ship]?.[props.book]?.notes?.[props.note]?.file; - if (prevProps && prevProps.api !== props.api) { - if (!contents) { - props.api?.fetchNote(props.ship, props.book, props.note); - } - } - if (contents && state.body === '') { - const notebook = props.notebooks[props.ship][props.book]; - const note = notebook.notes[props.note]; - const file = note.file; - const body = file.slice(file.indexOf(';>') + 3); - this.setState({ body: body }); - } - } - - postSubmit() { - const { props, state } = this; - const notebook = props.notebooks[props.ship][props.book]; - const note = notebook.notes[props.note]; - const title = note.title; - const editNote = { - 'edit-note': { - who: props.ship.slice(1), - book: props.book, - note: props.note, - title: title, - body: state.body - } - }; - this.setState({ awaiting: true }); - this.props.api.publish.publishAction(editNote).then(() => { - const editIndex = props.location.pathname.indexOf('/edit'); - const noteHref = props.location.pathname.slice(0, editIndex); - this.setState({ awaiting: false }); - props.history.push(noteHref); - }); - } - - bodyChange(editor, data, value) { - const submit = !(value === ''); - this.setState({ body: value, submit: submit }); - } - - render() { - const { props, state } = this; - const notebook = props.notebooks[props.ship][props.book]; - const note = notebook.notes[props.note]; - const title = note.title; - let date = dateToDa(new Date(note['date-created'])); - date = date.slice(1, -10); - - const submitStyle = (state.submit) - ? { color: '#2AA779', cursor: 'pointer' } - : { color: '#B1B2B3', cursor: 'auto' }; - - const hrefIndex = props.location.pathname.indexOf('/note/'); - const publishsubStr = props.location.pathname.substr(hrefIndex); - const popoutHref = `/~publish/popout${publishsubStr}`; - - const hiddenOnPopout = (props.popout) - ? '' : 'dib-m dib-l dib-xl'; - - const options = { - mode: 'markdown', - theme: 'tlon', - lineNumbers: false, - lineWrapping: true, - scrollbarStyle: null, - cursorHeight: 0.85 - }; - - return ( -
-
- - - - - -
-
-
-
{date}
-
-
- this.bodyChange(e, d, v)} - onChange={(editor, data, value) => {}} - /> - -
-
-
- ); - } -} - -export default EditPost; diff --git a/pkg/interface/src/views/apps/publish/components/lib/join.js b/pkg/interface/src/views/apps/publish/components/lib/join.js deleted file mode 100644 index 0269c5d51..000000000 --- a/pkg/interface/src/views/apps/publish/components/lib/join.js +++ /dev/null @@ -1,176 +0,0 @@ -import React, { Component } from 'react'; -import { Link } from 'react-router-dom'; -import { Spinner } from '~/views/components/Spinner'; -import urbitOb from 'urbit-ob'; - -export class JoinScreen extends Component { - constructor(props) { - super(props); - - this.state = { - book: '', - error: false, - awaiting: null, - disable: false - }; - - this.bookChange = this.bookChange.bind(this); - } - - componentDidMount() { - this.componentDidUpdate(); - } - - componentDidUpdate(prevProps) { - if ((this.props.ship) && (this.props.notebook)) { - const incomingBook = `${this.props.ship}/${this.props.notebook}`; - if (this.props.api && (prevProps?.api !== this.props.api)) { - this.setState({ book: incomingBook }, () => { - this.onClickJoin(); - }); - } - } - // redirect to notebook when we have it - if (this.props.notebooks) { - if (this.state.awaiting) { - const book = this.state.awaiting.split('/'); - const ship = book[0]; - const notebook = book[1]; - if ((ship in this.props.notebooks) && - (notebook in this.props.notebooks[ship])) { - this.setState({ disable: false, book: '/' }); - this.props.history.push(`/~publish/notebook/${ship}/${notebook}`); - } - } - } - } - - notebooksInclude(text, notebookObj) { - let verdict = false; - let keyPair = []; - // validate that it's a worthwhile thing to check - // certainly a unit would be nice here - if (text.indexOf('/') === -1) { - return verdict; - } else { - keyPair = text.split('/'); - }; - // check both levels of object - if (keyPair[0] in notebookObj) { - if (keyPair[1] in notebookObj[keyPair[0]]) { - verdict = true; - } - } - return verdict; - } - - onClickJoin() { - const { props, state } = this; - - const text = state.book; - - let book = text.split('/'); - const ship = book[0]; - book.splice(0, 1); - book = '/' + book.join('/'); - - if (this.notebooksInclude(state.book, props.notebooks)) { - const href = `/~publish/notebook/${ship}${book}`; - return props.history.push(href); - } - - if (book.length < 2 || !urbitOb.isValidPatp(ship)) { - this.setState({ - error: true - }); - return; - } - - const actionData = { - subscribe: { - who: ship.replace('~',''), - book: /\/?(.*)/.exec(book)[1] - } - }; - - // TODO: askHistory setting - this.setState({ disable: true }); - this.props.api.publish.publishAction(actionData).catch((err) => { - console.log(err); - }).then(() => { - this.setState({ awaiting: text }); - }); - } - - bookChange(event) { - this.setState({ - book: event.target.value - }); - } - - render() { - const { state } = this; - - let joinClasses = 'db f9 green2 ba pa2 b--green2 bg-gray0-d pointer'; - if ((state.disable) || (!state.book) || (state.book === '/')) { - joinClasses = 'db f9 gray2 ba pa2 b--gray3 bg-gray0-d'; - } - - let errElem = (); - if (state.error) { - errElem = ( - - Notebook must have a valid name. - - ); - } - - return ( -
-
- {'⟵ All Notebooks'} -
-

Subscribe to an Existing Notebook

-
-

Enter a ~ship/notebook-name

-

Notebook names use lowercase, hyphens, and slashes.

-