mirror of
https://github.com/urbit/shrub.git
synced 2024-12-01 06:35:32 +03:00
Merge branch 'release/next-userspace' into lt/link-migration
This commit is contained in:
commit
6c0589e32f
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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.
|
4
.github/ISSUE_TEMPLATE/os1-bug-report.md
vendored
4
.github/ISSUE_TEMPLATE/os1-bug-report.md
vendored
@ -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
|
||||
|
@ -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
|
43
README.md
43
README.md
@ -1,27 +1,38 @@
|
||||
# Urbit
|
||||
|
||||
A personal server operating function.
|
||||
[Urbit](https://urbit.org) is a personal server stack built from scratch. It
|
||||
has an identity layer (Azimuth), virtual machine (Vere), and operating system
|
||||
(Arvo).
|
||||
|
||||
> The Urbit address space, Azimuth, is now live on the Ethereum blockchain. You
|
||||
> can find it at [`0x223c067f8cf28ae173ee5cafea60ca44c335fecb`][azim] or
|
||||
> [`azimuth.eth`][aens]. Owners of Azimuth points (galaxies, stars, or planets)
|
||||
> can view or manage them using [Bridge][brid], and can also use them to boot
|
||||
> [Arvo][arvo], the Urbit OS.
|
||||
A running Urbit "ship" is designed to operate with other ships peer-to-peer.
|
||||
Urbit is a general-purpose, peer-to-peer computer and network.
|
||||
|
||||
This repository contains:
|
||||
|
||||
- The [Arvo OS][arvo]
|
||||
- [herb][herb], a tool for Unix control of an Urbit ship
|
||||
- Source code for [Landscape's web interface][land]
|
||||
- Source code for the [vere][vere] virtual machine.
|
||||
|
||||
For more on the identity layer, see [Azimuth][azim]. To manage your Urbit
|
||||
identity, use [Bridge][brid].
|
||||
|
||||
[azim]: https://etherscan.io/address/0x223c067f8cf28ae173ee5cafea60ca44c335fecb
|
||||
[aens]: https://etherscan.io/address/azimuth.eth
|
||||
[brid]: https://github.com/urbit/bridge
|
||||
[arvo]: https://github.com/urbit/urbit/tree/master/pkg/arvo
|
||||
[azim]: https://github.com/urbit/azimuth
|
||||
[brid]: https://github.com/urbit/bridge
|
||||
[herb]: https://github.com/urbit/urbit/tree/master/pkg/herb
|
||||
[land]: https://github.com/urbit/urbit/tree/master/pkg/interface
|
||||
[vere]: https://github.com/urbit/urbit/tree/master/pkg/urbit
|
||||
|
||||
## Install
|
||||
|
||||
To install and run Urbit, please follow the instructions at
|
||||
[urbit.org/docs/getting-started/][start]. You'll be on the live network in a
|
||||
[urbit.org/using/install][start]. You'll be on the live network in a
|
||||
few minutes.
|
||||
|
||||
If you're interested in Urbit development, keep reading.
|
||||
|
||||
[start]: https://urbit.org/docs/getting-started/
|
||||
[start]: https://urbit.org/using/install/
|
||||
|
||||
## Development
|
||||
|
||||
@ -38,7 +49,7 @@ The Makefile in the project's root directory contains useful phony targets for
|
||||
building, installing, testing, and so on. You can use it to avoid dealing with
|
||||
Nix explicitly.
|
||||
|
||||
To build Urbit, for example, use:
|
||||
To build the Urbit virtual machine binary, for example, use:
|
||||
|
||||
```
|
||||
make build
|
||||
@ -68,12 +79,10 @@ Contributions of any form are more than welcome! Please take a look at our
|
||||
[contributing guidelines][cont] for details on our git practices, coding
|
||||
styles, how we manage issues, and so on.
|
||||
|
||||
You might also be interested in:
|
||||
For instructions on contributing to Landscape, see [its][lcont] guidelines.
|
||||
|
||||
- joining the [urbit-dev][list] mailing list.
|
||||
- [applying to Hoon School][mail], a course we run to teach the Hoon
|
||||
programming language and Urbit application development.
|
||||
You might also be interested in joining the [urbit-dev][list] mailing list.
|
||||
|
||||
[list]: https://groups.google.com/a/urbit.org/forum/#!forum/dev
|
||||
[mail]: mailto:support@urbit.org
|
||||
[cont]: https://github.com/urbit/urbit/blob/master/CONTRIBUTING.md
|
||||
[lcont]: https://github.com/urbit/urbit/blob/master/pkg/interface/CONTRIBUTING.md
|
@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6cd7246753c12c7acb757e1a6ee54c177806c20a137ad8fb4300c000ac146a0f
|
||||
size 6260139
|
||||
oid sha256:3f5741b71f11a562d443fc619eb1b6bb1ccf419375aa2f1eebbd1c06dce20cd0
|
||||
size 6268477
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 ~
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,6 @@
|
||||
:: clock: deprecated, should be removed
|
||||
:: clock [landscape]:
|
||||
::
|
||||
:: deprecated, should be removed
|
||||
::
|
||||
/+ *server, default-agent, verb, dbug
|
||||
=, format
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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])
|
||||
==
|
||||
==
|
||||
::
|
||||
|
@ -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?}
|
||||
|
@ -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)])
|
||||
--
|
||||
::
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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-subset @ @ @ @ ~]
|
||||
@ -534,7 +555,7 @@
|
||||
=/ update-log=(unit update-log:store) (~(get by update-logs) [ship term])
|
||||
?~ update-log [~ ~]
|
||||
=/ result=(unit [time update:store])
|
||||
(peek:orm-log:store u.update-log)
|
||||
(peek:orm-log:store u.update-log)
|
||||
?~ result [~ ~]
|
||||
``noun+!>([~ -.u.result])
|
||||
==
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -1,3 +1,4 @@
|
||||
:: invite-store [landscape]
|
||||
/+ *invite-json, default-agent, dbug
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
|
@ -1,3 +1,7 @@
|
||||
:: invite-view [landscape]:
|
||||
::
|
||||
:: deprecated
|
||||
::
|
||||
/+ default-agent
|
||||
^- agent:gall
|
||||
|_ =bowl:gall
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
::
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
::
|
||||
|
@ -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)
|
||||
==
|
||||
::
|
||||
--
|
||||
|
@ -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
|
||||
|
@ -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])
|
||||
==
|
||||
==
|
||||
::
|
||||
|
@ -1,3 +1,7 @@
|
||||
:: weather [landscape]:
|
||||
::
|
||||
:: holds latlong, gets weather data from API, passes it on to subscribers
|
||||
::
|
||||
/+ *server, default-agent, verb, dbug
|
||||
=, format
|
||||
::
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
30
pkg/arvo/ted/diff.hoon
Normal 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
17
pkg/interface/README.md
Normal 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=
|
440
pkg/interface/package-lock.json
generated
440
pkg/interface/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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({
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
132
pkg/interface/src/logic/api/graph.ts
Normal file
132
pkg/interface/src/logic/api/graph.ts
Normal 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
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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() {
|
||||
@ -28,7 +29,7 @@ export default class LocalApi extends BaseApi<StoreState> {
|
||||
});
|
||||
}
|
||||
|
||||
setOmnibox() {
|
||||
setOmnibox() {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
37
pkg/interface/src/logic/api/s3.ts
Normal file
37
pkg/interface/src/logic/api/s3.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
67
pkg/interface/src/logic/lib/tokenizeMessage.js
Normal file
67
pkg/interface/src/logic/lib/tokenizeMessage.js
Normal 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 };
|
57
pkg/interface/src/logic/lib/useDropdown.ts
Normal file
57
pkg/interface/src/logic/lib/useDropdown.ts
Normal 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,
|
||||
};
|
||||
}
|
22
pkg/interface/src/logic/lib/useLocalStorageState.ts
Normal file
22
pkg/interface/src/logic/lib/useLocalStorageState.ts
Normal 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;
|
||||
}
|
||||
|
||||
|
30
pkg/interface/src/logic/lib/useQuery.ts
Normal file
30
pkg/interface/src/logic/lib/useQuery.ts
Normal 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,
|
||||
};
|
||||
}
|
51
pkg/interface/src/logic/lib/useS3.ts
Normal file
51
pkg/interface/src/logic/lib/useS3.ts
Normal 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 };
|
||||
}
|
38
pkg/interface/src/logic/lib/useWaitForProps.ts
Normal file
38
pkg/interface/src/logic/lib/useWaitForProps.ts
Normal 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;
|
||||
}
|
@ -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) => {
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
164
pkg/interface/src/logic/reducers/graph-update.js
Normal file
164
pkg/interface/src/logic/reducers/graph-update.js
Normal 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
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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'>;
|
||||
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) {
|
||||
@ -24,6 +43,13 @@ export default class LocalReducer<S extends LocalState> {
|
||||
omniboxShown(obj: LocalUpdate, state: S) {
|
||||
if ('omniboxShown' in obj) {
|
||||
state.omniboxShown = !state.omniboxShown;
|
||||
if (state.suspendedFocus) {
|
||||
state.suspendedFocus.focus();
|
||||
state.suspendedFocus = null;
|
||||
} else {
|
||||
state.suspendedFocus = document.activeElement;
|
||||
document.activeElement?.blur();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,10 @@ export default class BaseStore<S extends object> {
|
||||
this.state = this.initialState();
|
||||
}
|
||||
|
||||
dehydrate() {}
|
||||
|
||||
rehydrate() {}
|
||||
|
||||
initialState() {
|
||||
return {} as S;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
@ -42,7 +51,17 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
connection: 'connected',
|
||||
sidebarShown: true,
|
||||
omniboxShown: false,
|
||||
suspendedFocus: null,
|
||||
baseHash: null,
|
||||
background: undefined,
|
||||
remoteContentPolicy: {
|
||||
imageShown: true,
|
||||
audioShown: true,
|
||||
videoShown: true,
|
||||
oembedShown: true,
|
||||
},
|
||||
hideAvatars: false,
|
||||
hideNicknames: false,
|
||||
invites: {},
|
||||
associations: {
|
||||
chat: {},
|
||||
@ -52,6 +71,8 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
},
|
||||
groups: {},
|
||||
groupKeys: new Set(),
|
||||
graphs: {},
|
||||
graphKeys: new Set(),
|
||||
launch: {
|
||||
firstTime: false,
|
||||
tileOrdering: [],
|
||||
@ -94,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);
|
||||
}
|
||||
}
|
||||
|
@ -11,14 +11,20 @@ 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
|
||||
sidebarShown: boolean;
|
||||
omniboxShown: boolean;
|
||||
suspendedFocus: HTMLInputElement | null;
|
||||
dark: boolean;
|
||||
connection: ConnectionStatus;
|
||||
baseHash: string | null;
|
||||
background: BackgroundConfig;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
// invite state
|
||||
invites: Invites;
|
||||
// metadata state
|
||||
@ -30,6 +36,8 @@ export interface StoreState {
|
||||
groupKeys: Set<Path>;
|
||||
permissions: Permissions;
|
||||
s3: S3State;
|
||||
graphs: Object;
|
||||
graphKeys: Set<String>;
|
||||
|
||||
|
||||
// App specific states
|
||||
|
@ -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) {
|
||||
|
@ -73,6 +73,10 @@ export interface Envelope {
|
||||
letter: Letter;
|
||||
}
|
||||
|
||||
export type IMessage = Envelope & {
|
||||
pending?: boolean
|
||||
};
|
||||
|
||||
interface LetterText {
|
||||
text: string;
|
||||
}
|
||||
|
18
pkg/interface/src/types/index.ts
Normal file
18
pkg/interface/src/types/index.ts
Normal 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';
|
@ -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;
|
||||
|
@ -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;
|
@ -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;
|
||||
|
@ -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,43 +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() {
|
||||
@ -101,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}
|
||||
@ -113,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}
|
||||
@ -125,7 +134,8 @@ class App extends React.Component {
|
||||
ship={this.ship}
|
||||
api={this.api}
|
||||
subscription={this.subscription}
|
||||
{...state} />
|
||||
{...state}
|
||||
/>
|
||||
</Router>
|
||||
</Root>
|
||||
</ThemeProvider>
|
||||
@ -133,6 +143,5 @@ class App extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default process.env.NODE_ENV === 'production' ? App : hot(App);
|
||||
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
255
pkg/interface/src/views/apps/chat/components/lib/chat-input.tsx
Normal file
255
pkg/interface/src/views/apps/chat/components/lib/chat-input.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,106 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Button } from '@tlon/indigo-react';
|
||||
|
||||
const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i);
|
||||
|
||||
const YOUTUBE_REGEX =
|
||||
new RegExp(
|
||||
String(/(?:https?:\/\/(?:[a-z]+.)?)/.source) // protocol
|
||||
+ /(?:youtu\.?be(?:\.com)?\/)(?:embed\/)?/.source // short and long-links
|
||||
+ /(?:(?:(?:(?:watch\?)?(?:time_continue=(?:[0-9]+))?.+v=)?([a-zA-Z0-9_-]+))(?:\?t\=(?:[0-9a-zA-Z]+))?)/.source // id
|
||||
);
|
||||
|
||||
export default class UrlContent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
unfold: false,
|
||||
copied: false
|
||||
};
|
||||
this.unfoldEmbed = this.unfoldEmbed.bind(this);
|
||||
}
|
||||
|
||||
unfoldEmbed(id) {
|
||||
let unfoldState = this.state.unfold;
|
||||
unfoldState = !unfoldState;
|
||||
this.setState({ unfold: unfoldState });
|
||||
this.iframe.setAttribute('src', this.iframe.dataset.src);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const content = props.content;
|
||||
const imgMatch = IMAGE_REGEX.exec(props.content.url);
|
||||
const ytMatch = YOUTUBE_REGEX.exec(props.content.url);
|
||||
|
||||
let contents = content.url;
|
||||
if (imgMatch) {
|
||||
contents = (
|
||||
<img
|
||||
className="o-80-d"
|
||||
src={content.url}
|
||||
style={{
|
||||
maxWidth: '18rem'
|
||||
}}
|
||||
></img>
|
||||
);
|
||||
return (
|
||||
<a className='f7 lh-copy v-top word-break-all'
|
||||
href={content.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{contents}
|
||||
</a>
|
||||
);
|
||||
} else if (ytMatch) {
|
||||
contents = (
|
||||
<div className={'embed-container mb2 w-100 w-75-l w-50-xl ' +
|
||||
((this.state.unfold === true)
|
||||
? 'db' : 'dn')}
|
||||
>
|
||||
<iframe
|
||||
ref={(el) => {
|
||||
this.iframe = el;
|
||||
}}
|
||||
width="560"
|
||||
height="315"
|
||||
data-src={`https://www.youtube.com/embed/${ytMatch[1]}`}
|
||||
frameBorder="0" allow="picture-in-picture, fullscreen"
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<a href={content.url}
|
||||
className='f7 lh-copy v-top bb b--white-d word-break-all'
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{content.url}
|
||||
</a>
|
||||
<Button
|
||||
border={1}
|
||||
style={{ display: 'inline-flex', height: '1.66em' }} // Height is hacked to line-height until Button supports proper size
|
||||
ml={1}
|
||||
onClick={e => this.unfoldEmbed()}
|
||||
>
|
||||
{this.state.unfold ? 'collapse' : 'embed'}
|
||||
</Button>
|
||||
{contents}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<a className='f7 lh-copy v-top bb b--white-d b--black word-break-all'
|
||||
href={content.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{contents}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,51 +1,54 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
|
||||
export const DeleteButton = memo(({ isOwner, station, changeLoading, association, contacts, api }) => {
|
||||
const leaveButtonClasses = (!isOwner) ? 'pointer' : 'c-default';
|
||||
const deleteButtonClasses = (isOwner) ?
|
||||
'b--red2 red2 pointer bg-gray0-d' :
|
||||
'b--gray3 gray3 bg-gray0-d c-default';
|
||||
|
||||
export const DeleteButton = (props) => {
|
||||
const { isOwner, station, changeLoading, api } = props;
|
||||
const leaveButtonClasses = (!isOwner) ? 'pointer' : 'c-default';
|
||||
const deleteButtonClasses = (isOwner) ?
|
||||
'b--red2 red2 pointer bg-gray0-d' :
|
||||
'b--gray3 gray3 bg-gray0-d c-default';
|
||||
|
||||
const deleteChat = () => {
|
||||
changeLoading(
|
||||
true,
|
||||
true,
|
||||
isOwner ? 'Deleting chat...' : 'Leaving chat...',
|
||||
() => {
|
||||
api.chat.delete(station);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-100 cf">
|
||||
<div className={'w-100 fl mt3 ' + ((isOwner) ? 'o-30' : '')}>
|
||||
<p className="f8 mt3 lh-copy db">Leave Chat</p>
|
||||
<p className="f9 gray2 db mb4">
|
||||
Remove this chat from your chat list.{' '}
|
||||
You will need to request for access again.
|
||||
</p>
|
||||
<a onClick={(!isOwner) ? deleteChat : null}
|
||||
className={
|
||||
'dib f9 black gray4-d bg-gray0-d ba pa2 b--black b--gray1-d ' +
|
||||
leaveButtonClasses
|
||||
}>
|
||||
Leave this chat
|
||||
</a>
|
||||
</div>
|
||||
<div className={'w-100 fl mt3 ' + ((!isOwner) ? 'o-30' : '')}>
|
||||
<p className="f8 mt3 lh-copy db">Delete Chat</p>
|
||||
<p className="f9 gray2 db mb4">
|
||||
Permanently delete this chat.{' '}
|
||||
All current members will no longer see this chat.
|
||||
</p>
|
||||
<a onClick={(isOwner) ? deleteChat : null}
|
||||
className={'dib f9 ba pa2 ' + deleteButtonClasses}
|
||||
>Delete this chat</a>
|
||||
</div>
|
||||
</div>
|
||||
const deleteChat = () => {
|
||||
changeLoading(
|
||||
true,
|
||||
true,
|
||||
isOwner ? 'Deleting chat...' : 'Leaving chat...',
|
||||
() => {
|
||||
api.chat.delete(station);
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const groupPath = association['group-path'];
|
||||
const unmanagedVillage = !contacts[groupPath];
|
||||
|
||||
return (
|
||||
<div className="w-100 cf">
|
||||
<div className={'w-100 fl mt3 ' + ((isOwner) ? 'o-30' : '')}>
|
||||
<p className="f8 mt3 lh-copy db">Leave Chat</p>
|
||||
<p className="f9 gray2 db mb4">
|
||||
Remove this chat from your chat list.{' '}
|
||||
{unmanagedVillage
|
||||
? 'You will need to request for access again'
|
||||
: 'You will need to join again from the group page.'
|
||||
}
|
||||
</p>
|
||||
<a onClick={(!isOwner) ? deleteChat : null}
|
||||
className={
|
||||
'dib f9 black gray4-d bg-gray0-d ba pa2 b--black b--gray1-d ' +
|
||||
leaveButtonClasses
|
||||
}>
|
||||
Leave this chat
|
||||
</a>
|
||||
</div>
|
||||
<div className={'w-100 fl mt3 ' + ((!isOwner) ? 'o-30' : '')}>
|
||||
<p className="f8 mt3 lh-copy db">Delete Chat</p>
|
||||
<p className="f9 gray2 db mb4">
|
||||
Permanently delete this chat.{' '}
|
||||
All current members will no longer see this chat.
|
||||
</p>
|
||||
<a onClick={(isOwner) ? deleteChat : null}
|
||||
className={'dib f9 ba pa2 ' + deleteButtonClasses}
|
||||
>Delete this chat</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
@ -1,11 +1,13 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ChannelItem } from './channel-item';
|
||||
import { deSig, cite } from "~/logic/lib/util";
|
||||
|
||||
export class GroupItem extends Component {
|
||||
render() {
|
||||
const { props } = this;
|
||||
const association = props.association ? props.association : {};
|
||||
const DEFAULT_TITLE_REGEX = new RegExp(`(( <-> )?~(?:${window.ship}|${deSig(cite(window.ship))})( <-> )?)`);
|
||||
|
||||
let title = association['app-path'] ? association['app-path'] : 'Direct Messages';
|
||||
if (association.metadata && association.metadata.title) {
|
||||
@ -47,9 +49,13 @@ export class GroupItem extends Component {
|
||||
each in props.chatMetadata &&
|
||||
props.chatMetadata[each].metadata
|
||||
) {
|
||||
title = props.chatMetadata[each].metadata.title
|
||||
? props.chatMetadata[each].metadata.title
|
||||
: each.substr(1);
|
||||
if (props.chatMetadata[each].metadata.title) {
|
||||
title = props.chatMetadata[each].metadata.title
|
||||
}
|
||||
}
|
||||
|
||||
if (DEFAULT_TITLE_REGEX.test(title) && props.index === "dm") {
|
||||
title = title.replace(DEFAULT_TITLE_REGEX, '');
|
||||
}
|
||||
const selected = props.station === each;
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user