Merge branch 'release/next-userspace' into mp/chat/copy-notice

This commit is contained in:
Matilde Park 2020-09-14 21:05:58 -04:00
commit 28c019c9cb
227 changed files with 8271 additions and 5893 deletions

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6cd7246753c12c7acb757e1a6ee54c177806c20a137ad8fb4300c000ac146a0f
size 6260139
oid sha256:3f5741b71f11a562d443fc619eb1b6bb1ccf419375aa2f1eebbd1c06dce20cd0
size 6268477

View File

@ -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

View File

@ -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

View File

@ -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 ~

View File

@ -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

View File

@ -1,4 +1,6 @@
:: clock: deprecated, should be removed
:: clock [landscape]:
::
:: deprecated, should be removed
::
/+ *server, default-agent, verb, dbug
=, format

View File

@ -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

View File

@ -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)

View File

@ -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])
==
==
::

View File

@ -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?}

View File

@ -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)])
--
::

View File

@ -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]

View File

@ -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

View File

@ -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])
==

View File

@ -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: {<path>}"
~
~
=/ pax=^path
?: =('~' i.path)
t.path

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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.

View File

@ -1,3 +1,4 @@
:: invite-store [landscape]
/+ *invite-json, default-agent, dbug
|%
+$ card card:agent:gall

View File

@ -1,3 +1,7 @@
:: invite-view [landscape]:
::
:: deprecated
::
/+ default-agent
^- agent:gall
|_ =bowl:gall

View File

@ -23,7 +23,7 @@
<div id="root"></div>
<script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.js"></script>
<script src="/~landscape/js/bundle/index.f58fbbc4b037bb976a2a.js"></script>
<script src="/~landscape/js/bundle/index.ecc81763be57d23e6028.js"></script>
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js"></script>
</body>
</html>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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

View File

@ -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])
== ==
==
::

View File

@ -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)

View File

@ -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

View File

@ -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
::

View File

@ -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

View File

@ -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

View File

@ -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
::

View File

@ -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)
==
::
--

View File

@ -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

View File

@ -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])
==
==
::

View File

@ -1,3 +1,7 @@
:: weather [landscape]:
::
:: holds latlong, gets weather data from API, passes it on to subscribers
::
/+ *server, default-agent, verb, dbug
=, format
::

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]

30
pkg/arvo/ted/diff.hoon Normal file
View File

@ -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)
--

17
pkg/interface/README.md Normal file
View File

@ -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=

View File

@ -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",

View File

@ -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",

View File

@ -12,7 +12,7 @@ export default class ChatApi extends BaseApi<StoreState> {
* 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({

View File

@ -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<StoreState> {
chat = new ChatApi(this.ship, this.channel, this.store);
@ -22,10 +24,16 @@ export default class GlobalApi extends BaseApi<StoreState> {
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);
}
}

View File

@ -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<StoreState> {
private storeAction(action: any): Promise<any> {
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<any>('graph-store', '/keys')
.then((keys) => {
this.store.handleEvent({
data: keys
});
});
}
getTags() {
this.scry<any>('graph-store', '/tags')
.then((tags) => {
this.store.handleEvent({
data: tags
});
});
}
getTagQueries() {
this.scry<any>('graph-store', '/tag-queries')
.then((tagQueries) => {
this.store.handleEvent({
data: tagQueries
});
});
}
getGraph(ship: string, resource: string) {
this.scry<any>('graph-store', `/graph/${ship}/${resource}`)
.then((graph) => {
this.store.handleEvent({
data: graph
});
});
}
getGraphSubset(ship: string, resource: string, start: string, end: start) {
this.scry<any>(
'graph-store',
`/graph-subset/${ship}/${resource}/${end}/${start}`
).then((subset) => {
this.store.handleEvent({
data: subset
});
});
}
getNode(ship: string, resource: string, index: string) {
this.scry<any>(
'graph-store',
`/node/${ship}/${resource}/${index}`
).then((node) => {
this.store.handleEvent({
data: node
});
});
}
}

View File

@ -12,7 +12,7 @@ export default class LaunchApi extends BaseApi<StoreState> {
this.launchAction({ remove: name });
}
changeOrder(orderedTiles = []) {
changeOrder(orderedTiles: string[] = []) {
this.launchAction({ 'change-order': orderedTiles });
}

View File

@ -44,8 +44,8 @@ export default class LinksApi extends BaseApi<StoreState> {
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);
}

View File

@ -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<StoreState> {
getBaseHash() {
@ -38,4 +39,48 @@ export default class LocalApi extends BaseApi<StoreState> {
});
}
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();
}
}

View File

@ -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
},
});
}
}

View File

@ -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<StoreState> {
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);
}
}

View File

@ -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;
};

View File

@ -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();
}
}

View File

@ -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 (
<div
className={'bg-black dib ' + classes}
style={{ width: props.size, height: props.size }}
></div>
);
} else {
return (
<div
className={'dib ' + classes}
style={{ flexBasis: props.size, backgroundColor: props.color }}
>
{sigil({
patp: props.ship,
renderer: reactRenderer,
size: props.size,
colors: [props.color, foreground],
class: props.svgClass
})}
</div>
);
}
}
return ((whiteBrightness - brightness) < 50) ? 'black' : 'white';
}
export const Sigil = memo(({ classes = '', color, ship, size, svgClass = '' }) => {
return ship.length > 14
? (<div
className={'bg-black dib ' + classes}
style={{ width: size, height: size }}>
</div>)
: (<div
className={'dib ' + classes}
style={{ flexBasis: size, backgroundColor: color }}
>
{sigil({
patp: ship,
renderer: reactRenderer,
size: size,
colors: [
color,
foregroundFromBackground(color)
],
class: svgClass
})}
</div>)
})
export default Sigil;

View File

@ -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 };

View File

@ -0,0 +1,57 @@
import { useState, useEffect, useMemo, useCallback } from "react";
export function useDropdown<C>(
candidates: C[],
key: (c: C) => string,
searchPred: (query: string, c: C) => boolean
) {
const [options, setOptions] = useState(candidates);
const [selected, setSelected] = useState<C | undefined>();
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,
};
}

View File

@ -0,0 +1,22 @@
import { useState, useCallback } from 'react';
export function useLocalStorageState<T>(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;
}

View File

@ -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<string, string>) => {
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,
};
}

View File

@ -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<S3 | null>(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 };
}

View File

@ -0,0 +1,38 @@
import { useState, useEffect, useCallback } from 'react';
export function useWaitForProps<P>(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<void>((resolve, reject) => {
setResolve(() => resolve);
if(timeout > 0) {
setTimeout(() => {
reject(new Error("Timed out"));
}, timeout);
}
});
},
[setResolve, setReady, timeout]
);
return waiter;
}

View File

@ -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) => {

View File

@ -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<StoreState, 'chatInitialized' | 'chatSynced' | 'inbox' | 'pendingMessages'>;
@ -49,8 +50,11 @@ export default class ChatReducer<S extends ChatState> {
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
);
}
}

View File

@ -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
}
});
}
};

View File

@ -78,6 +78,7 @@ export default class GroupReducer<S extends GroupState> {
this.addGroup(data, state);
this.removeGroup(data, state);
this.changePolicy(data, state);
this.expose(data, state);
}
}
@ -187,6 +188,15 @@ export default class GroupReducer<S extends GroupState> {
}
}
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;

View File

@ -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<StoreState, 'sidebarShown' | 'omniboxShown' | 'dark' | 'baseHash' | 'suspendedFocus'>;
type LocalState = Pick<StoreState, 'sidebarShown' | 'omniboxShown' | 'baseHash' | 'hideAvatars' | 'hideNicknames' | 'background' | 'dark' | 'suspendedFocus' | 'remoteContentPolicy'>;
export default class LocalReducer<S extends LocalState> {
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<S extends LocalState> {
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;
}
}
}

View File

@ -78,6 +78,8 @@ export default class PublishResponseReducer<S extends PublishState> {
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];

View File

@ -40,7 +40,7 @@ export default class S3Reducer<S extends S3State> {
currentBucket(json: S3Update, state: S) {
const data = _.get(json, 'setCurrentBucket', false);
if (data && state.s3) {
state.s3.configuration.currentBucket = data;
}
}

View File

@ -5,6 +5,10 @@ export default class BaseStore<S extends object> {
this.state = this.initialState();
}
dehydrate() {}
rehydrate() {}
initialState() {
return {} as S;
}

View File

@ -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);
}
}

View File

@ -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<StoreState> {
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<StoreState> {
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<StoreState> {
},
groups: {},
groupKeys: new Set(),
graphs: {},
graphKeys: new Set(),
launch: {
firstTime: false,
tileOrdering: [],
@ -95,5 +115,6 @@ export default class GlobalStore extends BaseStore<StoreState> {
this.launchReducer.reduce(data, this.state);
this.linkListenReducer.reduce(data, this.state);
this.connReducer.reduce(data, this.state);
GraphReducer(data, this.state);
}
}

View File

@ -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<Path>;
permissions: Permissions;
s3: S3State;
graphs: Object;
graphKeys: Set<String>;
// App specific states

View File

@ -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<AppName, AppSubscription[]> = {
chat: chatSubscriptions,
publish: publishSubscriptions,
link: linkSubscriptions,
groups: groupSubscriptions
groups: groupSubscriptions,
graph: graphSubscriptions
};
export default class GlobalSubscription extends BaseSubscription<StoreState> {
@ -40,8 +46,10 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
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<StoreState> {
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) {

View File

@ -73,6 +73,10 @@ export interface Envelope {
letter: Letter;
}
export type IMessage = Envelope & {
pending?: boolean
};
interface LetterText {
text: string;
}

View File

@ -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';

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 (
<ThemeProvider theme={theme}>
<Root>
<Helmet>
{window.ship.length < 14
? <link rel="icon" type="image/svg+xml" href={this.faviconString()} />
: null}
</Helmet>
<Root background={background} >
<Router>
<StatusBarWithRouter
props={this.props}
@ -114,6 +121,7 @@ class App extends React.Component {
api={this.api}
connection={this.state.connection}
subscription={this.subscription}
ship={this.ship}
/>
<Omnibox
associations={state.associations}
@ -126,7 +134,8 @@ class App extends React.Component {
ship={this.ship}
api={this.api}
subscription={this.subscription}
{...state} />
{...state}
/>
</Router>
</Root>
</ThemeProvider>
@ -134,6 +143,5 @@ class App extends React.Component {
}
}
export default process.env.NODE_ENV === 'production' ? App : hot(App);

View File

@ -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<ChatAppProps, {}> {
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<ChatAppProps, {}> {
}
});
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<ChatAppProps, {}> {
api,
chatInitialized,
pendingMessages,
groups
groups,
hideAvatars,
hideNicknames,
remoteContentPolicy
} = props;
const renderChannelSidebar = (props, station?) => (
@ -113,88 +107,97 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
);
return (
<Switch>
<Route
exact
path="/~chat"
render={(props) => {
return (
<Skeleton
associations={associations}
invites={invites}
chatHideonMobile={true}
sidebarShown={sidebarShown}
sidebar={renderChannelSidebar(props)}
>
<div className="h-100 w-100 overflow-x-hidden flex flex-column bg-white bg-gray0-d">
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
Select, create, or join a chat to begin.
</p>
<>
<Helmet defer={false}>
<title>{totalUnreads > 0 ? `(${totalUnreads}) ` : ''}OS1 - Chat</title>
</Helmet>
<Switch>
<Route
exact
path="/~chat"
render={(props) => {
return (
<Skeleton
associations={associations}
invites={invites}
chatHideonMobile={true}
sidebarShown={sidebarShown}
sidebar={renderChannelSidebar(props)}
>
<div className="h-100 w-100 overflow-x-hidden flex flex-column bg-white bg-gray0-d">
<div className="pl3 pr3 pt2 dt pb3 w-100 h-100">
<p className="f8 pt3 gray2 w-100 h-100 dtc v-mid tc">
Select, create, or join a chat to begin.
</p>
</div>
</div>
</div>
</Skeleton>
);
}}
/>
<Route
exact
path="/~chat/new/dm/:ship?"
render={(props) => {
const ship = props.match.params.ship;
</Skeleton>
);
}}
/>
<Route
exact
path="/~chat/new/dm/:ship?"
render={(props) => {
const ship = props.match.params.ship;
return (
<Skeleton
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
sidebar={renderChannelSidebar(props)}
sidebarShown={sidebarShown}
>
<NewDmScreen
api={api}
inbox={inbox}
groups={groups || {}}
contacts={contacts || {}}
associations={associations.contacts}
chatSynced={chatSynced || {}}
autoCreate={ship}
{...props}
/>
</Skeleton>
);
}}
/>
<Route
exact
path="/~chat/new"
render={(props) => {
return (
<Skeleton
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
sidebar={renderChannelSidebar(props)}
sidebarShown={sidebarShown}
>
<NewScreen
api={api}
inbox={inbox || {}}
groups={groups}
contacts={contacts || {}}
associations={associations.contacts}
chatSynced={chatSynced || {}}
{...props}
/>
</Skeleton>
);
}}
/>
<Route
exact
path="/~chat/join/:ship?/:station?"
render={(props) => {
let station = `/${props.match.params.ship}/${props.match.params.station}`;
return (
<Skeleton
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
sidebar={renderChannelSidebar(props)}
sidebarShown={sidebarShown}
>
<NewDmScreen
api={api}
inbox={inbox}
groups={groups || {}}
contacts={contacts || {}}
associations={associations.contacts}
chatSynced={chatSynced || {}}
autoCreate={ship}
{...props}
/>
</Skeleton>
);
}}
/>
<Route
exact
path="/~chat/new"
render={(props) => {
return (
<Skeleton
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
sidebar={renderChannelSidebar(props)}
sidebarShown={sidebarShown}
>
<NewScreen
api={api}
inbox={inbox || {}}
groups={groups}
contacts={contacts || {}}
associations={associations.contacts}
chatSynced={chatSynced || {}}
{...props}
/>
</Skeleton>
);
}}
/>
<Route
exact
path="/~chat/join/:ship?/:station?"
render={(props) => {
let station = `/${props.match.params.ship}/${props.match.params.station}`;
// ensure we know joined chats
if(!chatInitialized) {
return null;
}
return (
<Skeleton
@ -207,7 +210,7 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
<JoinScreen
api={api}
inbox={inbox}
autoJoin={station}
station={station}
chatSynced={chatSynced || {}}
{...props}
/>
@ -261,7 +264,7 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
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<ChatAppProps, {}> {
popout={popout}
sidebarShown={sidebarShown}
chatInitialized={chatInitialized}
hideAvatars={hideAvatars}
hideNicknames={hideNicknames}
remoteContentPolicy={remoteContentPolicy}
{...props}
/>
</Skeleton>
);
}}
/>
<Route
exact
path="/~chat/(popout)?/settings/(~)?/:ship/:station+"
render={(props) => {
let station = `/${props.match.params.ship}/${props.match.params.station}`;
const popout = props.match.url.includes('/popout/');
/>
<Route
exact
path="/~chat/(popout)?/settings/(~)?/:ship/:station+"
render={(props) => {
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 (
<Skeleton
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
popout={popout}
sidebarShown={sidebarShown}
sidebar={renderChannelSidebar(props, station)}
>
<SettingsScreen
{...props}
station={station}
association={association}
groups={groups || {}}
group={group}
contacts={contacts || {}}
associations={associations.contacts}
api={api}
inbox={inbox}
return (
<Skeleton
associations={associations}
invites={invites}
sidebarHideOnMobile={true}
popout={popout}
sidebarShown={sidebarShown}
/>
</Skeleton>
);
}}
/>
</Switch>
sidebar={renderChannelSidebar(props, station)}
>
<SettingsScreen
{...props}
station={station}
association={association}
groups={groups || {}}
group={group}
contacts={contacts || {}}
associations={associations.contacts}
api={api}
inbox={inbox}
popout={popout}
sidebarShown={sidebarShown}
/>
</Skeleton>
);
}}
/>
</Switch>
</>
);
}
}

View File

@ -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<string, string>;
dragover: boolean;
}
export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
private chatInput: React.RefObject<ChatInput>;
lastNumPending = 0;
activityTimeout: NodeJS.Timeout | null = null;
@ -51,8 +58,11 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
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<ChatScreenProps, ChatScreenState> {
});
}
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<ChatScreenProps, ChatScreenState> {
!(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 (
<div
key={props.station}
className="h-100 w-100 overflow-hidden flex flex-column relative">
<ChatHeader
match={props.match}
location={props.location}
api={props.api}
group={props.group}
association={props.association}
station={props.station}
sidebarShown={props.sidebarShown}
popout={props.popout} />
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 ? <SubmitDragger /> : null}
<ChatHeader {...props} />
<ChatWindow
history={props.history}
isChatMissing={isChatMissing}
isChatLoading={isChatLoading}
isChatUnsynced={isChatUnsynced}
unreadCount={unreadCount}
unreadMsg={unreadMsg}
pendingMessages={pendingMessages}
messages={props.envelopes}
length={props.length}
contacts={props.contacts}
association={props.association}
group={props.group}
stationPendingMessages={pendingMessages}
ship={props.match.params.ship}
station={props.station}
api={props.api} />
{...props} />
<ChatInput
ref={this.chatInput}
api={props.api}
numMsgs={lastMsgNum}
station={props.station}
@ -144,6 +171,7 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
deleteMessage={() => this.setState({
messages: this.state.messages.set(props.station, "")
})}
hideAvatars={props.hideAvatars}
/>
</div>
);

View File

@ -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." />
<Button>Join Chat</Button>
<Spinner

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react';
import { UnControlled as CodeEditor } from 'react-codemirror2';
import { MOBILE_BROWSER_REGEX } from "~/logic/lib/util";
import CodeMirror from 'codemirror';
import 'codemirror/mode/markdown/markdown';
@ -15,7 +16,7 @@ const MARKDOWN_CONFIG = {
name: 'markdown',
tokenTypeOverrides: {
header: 'presentation',
quote: 'presentation',
quote: 'quote',
list1: 'presentation',
list2: 'presentation',
list3: 'presentation',
@ -100,9 +101,14 @@ export default class ChatEditor extends Component {
}
render() {
const { props } = this;
const {
inCodeMode,
placeholder,
message,
...props
} = this.props;
const codeTheme = props.inCodeMode ? ' code' : '';
const codeTheme = inCodeMode ? ' code' : '';
const options = {
mode: MARKDOWN_CONFIG,
@ -111,29 +117,35 @@ export default class ChatEditor extends Component {
lineWrapping: true,
scrollbarStyle: 'native',
cursorHeight: 0.85,
placeholder: props.inCodeMode ? 'Code...' : props.placeholder,
placeholder: inCodeMode ? 'Code...' : placeholder,
extraKeys: {
'Enter': () => {
this.submit();
},
'Esc': () => {
this.editor?.getInputField().blur();
}
}
};
return (
<div
className="chat fr h-100 flex bg-gray0-d lh-copy pl2 w-100 items-center"
style={{ flexGrow: 1, maxHeight: '224px', width: 'calc(100% - 72px)' }}
>
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)' }}>
<CodeEditor
value={props.message}
value={message}
options={options}
onChange={(e, d, v) => 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}
/>
</div>
);

View File

@ -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 (
<Fragment>
<div
className="w-100 dn-m dn-l dn-xl inter pt4 pb6 pl3 f8"
style={{ height: "1rem" }}>
<Link to="/~chat/">{"⟵ All Chats"}</Link>
style={{ height: '1rem' }}
>
<Link to="/~chat/">{'⟵ All Chats'}</Link>
</div>
<div
className={
"pl4 pt2 bb b--gray4 b--gray1-d bg-gray0-d flex relative " +
"overflow-x-auto overflow-y-hidden flex-shrink-0 "
'pl4 pt2 bb b--gray4 b--gray1-d bg-gray0-d flex relative ' +
'overflow-x-auto overflow-y-hidden flex-shrink-0 '
}
style={{ height: 48 }}>
style={{ height: 48 }}
>
<SidebarSwitcher
sidebarShown={props.sidebarShown}
popout={props.popout}
api={props.api}
/>
<Link
to={"/~chat/" + isInPopout + "room" + props.station}
className="pt2 white-d">
to={'/~chat/' + isInPopout + 'room' + props.station}
className="pt2 white-d"
>
<h2
className={
"dib f9 fw4 lh-solid v-top " +
(title === props.station.substr(1) ? "mono" : "")
'dib f9 fw4 lh-solid v-top ' +
(title === props.station.substr(1) ? 'mono' : '')
}
style={{ width: "max-content" }}>
style={{ width: 'max-content' }}
>
{title}
</h2>
</Link>
<ChatTabBar
<TabBar
location={props.location}
station={props.station}
isOwner={deSig(props.match.params.ship) === window.ship}
popoutHref={`/~chat/popout/room${props.station}`}
settings={`/~chat/${isInPopout}settings${props.station}`}
popout={props.popout}
/>
</div>
</Fragment>
);
}
};

View File

@ -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))
? <img src={props.ownerContact.avatar} height={24} width={24} className="dib" />
: <Sigil
ship={window.ship}
size={24}
color={`#${color}`}
classes={sigilClass}
/>;
return (
<div className={
"pa3 cf flex black white-d bt b--gray4 b--gray1-d bg-white " +
"bg-gray0-d relative"
}
style={{ flexGrow: 1 }}>
<div className="fl"
style={{
marginTop: 6,
flexBasis: 24,
height: 24
}}>
{avatar}
</div>
<ChatEditor
inCodeMode={state.inCodeMode}
submit={this.submit}
onUnmount={props.onUnmount}
message={props.message}
placeholder='Message...' />
<div className="ml2 mr2"
style={{
height: '16px',
width: '16px',
flexBasis: 16,
marginTop: 10
}}>
<S3Upload
configuration={props.s3.configuration}
credentials={props.s3.credentials}
uploadSuccess={this.uploadSuccess.bind(this)}
uploadError={this.uploadError.bind(this)}
/>
</div>
<div style={{
height: '16px',
width: '16px',
flexBasis: 16,
marginTop: 10
}}>
<img style={{
filter: state.inCodeMode && 'invert(100%)',
height: '14px',
width: '14px',
}}
onClick={this.toggleCode}
src="/~chat/img/CodeEval.png"
className="contrast-10-d bg-white bg-none-d ba b--gray1-d br1" />
</div>
</div>
);
}
}

View File

@ -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<ChatInputProps, ChatInputState> {
public s3Uploader: React.RefObject<S3Upload>;
private chatEditor: React.RefObject<ChatEditor>;
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)
)
? <img src={props.ownerContact.avatar} height={24} width={24} className="dib" />
: <Sigil
ship={window.ship}
size={24}
color={`#${color}`}
classes={sigilClass}
/>;
return (
<div className={
"pa3 cf flex black white-d bt b--gray4 b--gray1-d bg-white " +
"bg-gray0-d relative"
}
style={{ flexGrow: 1 }}
>
<div className="fl"
style={{
marginTop: 6,
flexBasis: 24,
height: 24
}}>
{avatar}
</div>
<ChatEditor
ref={this.chatEditor}
inCodeMode={state.inCodeMode}
submit={this.submit}
onUnmount={props.onUnmount}
message={props.message}
onPaste={this.onPaste.bind(this)}
placeholder='Message...'
/>
<div className="ml2 mr2"
style={{
height: '16px',
width: '16px',
flexBasis: 16,
marginTop: 10
}}>
<S3Upload
ref={this.s3Uploader}
configuration={props.s3.configuration}
credentials={props.s3.credentials}
uploadSuccess={this.uploadSuccess.bind(this)}
uploadError={this.uploadError.bind(this)}
accept="*"
>
<img
className="invert-d"
src="/~chat/img/ImageUpload.png"
width="16"
height="16"
/>
</S3Upload>
</div>
<div style={{
height: '16px',
width: '16px',
flexBasis: 16,
marginTop: 10
}}>
<img style={{
filter: state.inCodeMode ? 'invert(100%)' : '',
height: '14px',
width: '14px',
}}
onClick={this.toggleCode}
src="/~chat/img/CodeEval.png"
className="contrast-10-d bg-white bg-none-d ba b--gray1-d br1" />
</div>
</div>
);
}
}

View File

@ -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 = (
<Message
key={msg.uid}
msg={msg}
renderSigil={renderSigil}
paddingTop={paddingTop}
paddingBot={paddingBot}
pending={Boolean(msg.pending)}
group={group}
contacts={contacts}
association={association}
/>
);
if (props.isLastUnread) {
return (
<Fragment key={msg.uid}>
{messageElem}
<div ref={unreadRef}
className="mv2 green2 flex items-center f9">
<hr className="dn-s ma0 w2 b--green2 bt-0" />
<p className="mh4">New messages below</p>
<hr className="ma0 flex-grow-1 b--green2 bt-0" />
{dayBreak && (
<p className="gray2 mh4">
{moment(_.get(msg, when)).calendar()}
</p>
)}
<hr
style={{ width: "calc(50% - 48px)" }}
className="b--green2 ma0 bt-0"
/>
</div>
</Fragment>
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 = (
<Message
key={msg.uid}
msg={msg}
renderSigil={renderSigil}
paddingTop={paddingTop}
paddingBot={paddingBot}
pending={Boolean(msg.pending)}
group={group}
contacts={contacts}
association={association}
hideNicknames={hideNicknames}
hideAvatars={hideAvatars}
remoteContentPolicy={remoteContentPolicy}
className={className}
/>
);
} else if (dayBreak) {
return (
<Fragment key={msg.uid}>
{messageElem}
<div
className="pv3 gray2 b--gray2 flex items-center justify-center f9 "
>
<p>{moment(_.get(msg, when)).calendar()}</p>
</div>
</Fragment>
);
} else {
return messageElem;
if (isFirstUnread) {
return (
<Fragment key={msg.uid}>
{messageElem}
<div ref={unreadRef}
className="mv2 green2 flex items-center f9">
<hr className="dn-s ma0 w2 b--green2 bt-0" />
<p className="mh4">New messages below</p>
<hr className="ma0 flex-grow-1 b--green2 bt-0" />
{dayBreak && (
<p className="gray2 mh4">
{moment(_.get(msg, when)).calendar()}
</p>
)}
<hr
style={{ width: "calc(50% - 48px)" }}
className="b--green2 ma0 bt-0"
/>
</div>
</Fragment>
);
} else if (dayBreak) {
return (
<Fragment key={msg.uid}>
<div
className="pv3 gray2 b--gray2 flex items-center justify-center f9 "
>
<p>{moment(_.get(msg, when)).calendar()}</p>
</div>
{messageElem}
</Fragment>
);
} else {
return messageElem;
}
}
};
}

View File

@ -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 (
<div
className="relative overflow-y-scroll h-100"
onScroll={this.containerDidScroll}
ref={this.containerRef}>
<div
className="bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
style={{ resize: "vertical" }}>
<div ref={this.scrollRef}></div>
{this.props.children}
</div>
</div>
);
}
normalScrollContainer() {
return (
<div
className={
"overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex " +
"flex-column-reverse relative"
}
style={{ height: "100%", resize: "vertical" }}
onScroll={this.containerDidScroll}>
<div ref={this.scrollRef}></div>
{this.props.children}
</div>
);
}
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
);
}
}
}

View File

@ -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 }) => (
<div className={`w-100 f7 pl3 pt4 pr3 cf flex lh-copy ${className}`} style={{ height, ...style }} {...props}>
<div className="fl pr3 v-top bg-white bg-gray0-d">
<span
className="db bg-gray2 bg-white-d"
style={{
width: "24px",
height: "24px",
borderRadius: "50%",
visibility: (index % 5 == 0) ? "initial" : "hidden",
}}
></span>
</div>
<div className="fr clamp-message white-d" style={{ flexGrow: 1, marginTop: -8 }}>
<div className="hide-child" style={{paddingTop: "6px", visibility: (index % 5 == 0) ? "initial" : "hidden" }}>
<p className={`v-mid f9 gray2 dib mr3 c-default`}>
<span className="mw5 db"><span className="bg-gray5 bg-gray1-d db w-100 h-100"></span></span>
</p>
<p className="v-mid mono f9 gray2 dib"><span className="bg-gray5 bg-gray1-d db w-100 h-100" style={{height: "1em", width: `${(index % 3 + 1) * 3}em`}}></span></p>
<p className="v-mid mono f9 ml2 gray2 dib child dn-s"><span className="bg-gray5 bg-gray1-d db w-100 h-100"></span></p>
</div>
<span className="bg-gray5 bg-gray1-d db w-100 h-100 db" style={{height: `1em`, width: `${(index % 5) * 20}%`}}></span>
</div>
</div>
);
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<ChatWindowProps, ChatWindowState> {
private unreadReference: React.RefObject<Component>;
private virtualList: React.RefObject<VirtuosoMethods>;
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 (
<Fragment>
<UnreadNotice
unreadCount={props.unreadCount}
unreadMsg={props.unreadMsg}
dismissUnread={this.dismissUnread} />
<ChatScrollContainer
ref={this.scrollReference}
scrollIsAtBottom={this.scrollIsAtBottom}
scrollIsAtTop={this.scrollIsAtTop}>
<BacklogElement isChatLoading={props.isChatLoading} />
<ResubscribeElement
api={props.api}
host={props.ship}
station={props.station}
isChatUnsynced={props.isChatUnsynced}
/>
{ messages.map((msg, i) => (
<ChatMessage
key={msg.uid}
unreadRef={this.unreadReference}
isLastUnread={
props.unreadCount > 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} />
))
}
</ChatScrollContainer>
unreadCount={unreadCount}
unreadMsg={this.state.idle ? unreadMsg : false}
dismissUnread={this.dismissUnread}
onClick={this.scrollToUnread}
/>
<BacklogElement isChatLoading={isChatLoading} />
<ResubscribeElement {...{ api, host: ship, station, isChatUnsynced}} />
{messages.length ? <VirtualList
ref={this.virtualList}
style={{height: '100%', width: '100%', visibility: this.state.initialized ? 'initial' : 'hidden'}}
totalCount={mailboxSize + stationPendingMessages.length}
followOutput={!this.state.idle}
endReached={this.dismissUnread}
scrollSeek={{
enter: velocity => Math.abs(velocity) > 2000,
exit: velocity => Math.abs(velocity) < 200,
change: (_velocity, _range) => {},
placeholder: this.state.initialized ? Placeholder : () => <div></div>
}}
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 <Placeholder index={number} height="0px" style={{overflow: 'hidden'}} />;
}
return <ChatMessage
key={number}
unreadRef={this.unreadReference}
isFirstUnread={
unreadCount
&& mailboxSize - unreadCount === number
&& this.state.idle
}
msg={msg}
previousMsg={messages[number + 1]}
nextMsg={messages[number - 1]}
association={association}
group={group}
contacts={contacts}
hideAvatars={hideAvatars}
hideNicknames={hideNicknames}
remoteContentPolicy={remoteContentPolicy}
className={number === mailboxSize + stationPendingMessages.length ? 'pb3' : ''}
/>
}}
/> : <div style={{height: '100%', width: '100%'}}></div>}
</Fragment>
);
}

View File

@ -11,14 +11,14 @@ export default class CodeContent extends Component {
(Boolean(content.code.output) &&
content.code.output.length && content.code.output.length > 0) ?
(
<pre className={`f7 clamp-attachment pa1 mt0 mb0 b--gray4 b--gray1-d bl br bb`}>
<pre className={`code f7 clamp-attachment pa1 mt0 mb0`}>
{content.code.output[0].join('\n')}
</pre>
) : null;
return (
<div className="mv2">
<pre className={`f7 clamp-attachment pa1 mt0 mb0 bg-light-gray b--gray4 b--gray1-d ba`}>
<pre className={`code f7 clamp-attachment pa1 mt0 mb0`}>
{content.code.expression}
</pre>
{outputElement}

View File

@ -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 (
<section className="chat-md-message">
<Box style={{ overflowWrap: 'break-word' }}>
<MessageMarkdown source={content.text} />
</section>
</Box>
);
}
}

Some files were not shown because too many files have changed in this diff Show More