mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-15 18:12:47 +03:00
Merge branch 'master' into lf/global-skeleton
This commit is contained in:
commit
94050f150e
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
|
name: Landscape bug report
|
||||||
about: 'Use this template to file a bug for any OS1 app: Chat, Publish, Links, Groups,
|
about: 'Use this template to file a bug for any Landscape app: Chat, Publish, Links, Groups,
|
||||||
Weather or Clock'
|
Weather or Clock'
|
||||||
title: ''
|
title: ''
|
||||||
labels: landscape
|
labels: landscape
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
Thank you for your interest in contributing to Urbit.
|
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
|
instructions. You may also want to subscribe to [urbit-dev][list], the Urbit
|
||||||
development mailing list. For specific information on contributing to the Urbit
|
development mailing list. For specific information on contributing to the Urbit
|
||||||
interface, see its [contribution guidelines][interface].
|
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
|
[interface]: /pkg/interface/CONTRIBUTING.md
|
||||||
|
|
||||||
## Fake ships
|
## 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
|
`master` when commencing new work; similarly, when we pull in your
|
||||||
contribution, we'll do so by merging it to `master`.
|
contribution, we'll do so by merging it to `master`.
|
||||||
|
|
||||||
Since we use GitHub, it's helpful (though not required) to contribute via a
|
Since we use GitHub, we request you contribute via a GitHub pull request. Tag
|
||||||
GitHub pull request. You can also post patches to the [mailing list][list],
|
the [maintainer][main] for the component. If you have a question for the
|
||||||
email them to maintainers, or request a maintainer pull from your tree directly
|
maintainer, you can direct message them from your Urbit ship using that
|
||||||
-- but note that some maintainers will be more receptive to these methods than
|
information.
|
||||||
others.
|
|
||||||
|
|
||||||
When contributing changes, via whatever means, make sure you describe them
|
When contributing changes, via whatever means, make sure you describe them
|
||||||
appropriately. You should attach a reasonably high-level summary of what the
|
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.
|
is a good example of a pull request with a useful, concise description.
|
||||||
|
|
||||||
If your changes replace significant extant functionality, be sure to compare
|
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,
|
them with the thing you're replacing. You may also want to cc reviewers,
|
||||||
reviewers, or other parties who might have a particular interest in what you're
|
or other parties who might have a particular interest in what you're
|
||||||
contributing.
|
contributing.
|
||||||
|
|
||||||
[jbpr]: https://github.com/urbit/urbit/pull/1782
|
[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/
|
[reba]: https://git-rebase.io/
|
||||||
[issu]: https://github.com/urbit/urbit/issues
|
[issu]: https://github.com/urbit/urbit/issues
|
||||||
[hoon]: https://urbit.org/docs/learn/hoon/style/
|
[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
|
# 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
|
A running Urbit "ship" is designed to operate with other ships peer-to-peer.
|
||||||
> can find it at [`0x223c067f8cf28ae173ee5cafea60ca44c335fecb`][azim] or
|
Urbit is a general-purpose, peer-to-peer computer and network.
|
||||||
> [`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
|
This repository contains:
|
||||||
> [Arvo][arvo], the Urbit OS.
|
|
||||||
|
- 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
|
[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
|
## Install
|
||||||
|
|
||||||
To install and run Urbit, please follow the instructions at
|
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.
|
few minutes.
|
||||||
|
|
||||||
If you're interested in Urbit development, keep reading.
|
If you're interested in Urbit development, keep reading.
|
||||||
|
|
||||||
[start]: https://urbit.org/docs/getting-started/
|
[start]: https://urbit.org/using/install/
|
||||||
|
|
||||||
## Development
|
## 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
|
building, installing, testing, and so on. You can use it to avoid dealing with
|
||||||
Nix explicitly.
|
Nix explicitly.
|
||||||
|
|
||||||
To build Urbit, for example, use:
|
To build the Urbit virtual machine binary, for example, use:
|
||||||
|
|
||||||
```
|
```
|
||||||
make build
|
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
|
[contributing guidelines][cont] for details on our git practices, coding
|
||||||
styles, how we manage issues, and so on.
|
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.
|
You might also be interested in 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.
|
|
||||||
|
|
||||||
[list]: https://groups.google.com/a/urbit.org/forum/#!forum/dev
|
[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
|
[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
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:cfb556a9e6b473f6cf6c75b30a3b12cb986e57df1600dad4383b9d3380cffdb6
|
oid sha256:06808af2c089441d2cb497fc95e3292b6229b3dfa034272d46c7c41f34eb6a3b
|
||||||
size 6263010
|
size 6268465
|
||||||
|
@ -41,20 +41,20 @@ Most parts of Arvo have dedicated maintainers.
|
|||||||
|
|
||||||
* `/sys/hoon`: @pilfer-pandex (~pilfer-pandex)
|
* `/sys/hoon`: @pilfer-pandex (~pilfer-pandex)
|
||||||
* `/sys/zuse`: @pilfer-pandex (~pilfer-pandex)
|
* `/sys/zuse`: @pilfer-pandex (~pilfer-pandex)
|
||||||
* `/sys/arvo`: @jtobin (~nidsut-tomdun)
|
* `/sys/arvo`: @joemfb (~master-morzod)
|
||||||
* `/sys/vane/ames`: @belisarius222 (~rovnys-ricfer) & @joemfb (~master-morzod)
|
* `/sys/vane/ames`: @belisarius222 (~rovnys-ricfer) & @philipcmonk (~wicdev-wisryt)
|
||||||
* `/sys/vane/behn`: @belisarius222 (~rovnys-ricfer)
|
* `/sys/vane/behn`: @belisarius222 (~rovnys-ricfer)
|
||||||
* `/sys/vane/clay`: @philipcmonk (~wicdev-wisryt)
|
* `/sys/vane/clay`: @philipcmonk (~wicdev-wisryt) & @belisarius222 (~rovnys-ricfer)
|
||||||
* `/sys/vane/dill`: @bernardodelaplaz (~rigdyn-sondur)
|
* `/sys/vane/dill`: @joemfb (~master-morzod)
|
||||||
* `/sys/vane/eyre`: @eglaysher (~littel-ponnys)
|
* `/sys/vane/eyre`: @eglaysher (~littel-ponnys)
|
||||||
* `/sys/vane/ford`: @belisarius222 (~rovnys-ricfer) & @eglaysher (~littel-ponnys)
|
* `/sys/vane/gall`: @philipcmonk (~wicdev-wisryt)
|
||||||
* `/sys/vane/gall`: @jtobin (~nidsut-tomdun)
|
* `/sys/vane/jael`: @fang- (~palfun-foslup) & @philipcmonk (~wicdev-wisryt)
|
||||||
* `/sys/vane/jael`: @fang- (~palfun-foslup) & @joemfb (~master-morzod)
|
|
||||||
* `/app/acme`: @joemfb (~master-morzod)
|
* `/app/acme`: @joemfb (~master-morzod)
|
||||||
* `/app/dns`: @joemfb (~master-morzod)
|
* `/app/dns`: @joemfb (~master-morzod)
|
||||||
* `/app/hall`: @fang- (~palfun-foslup)
|
|
||||||
* `/app/talk`: @fang- (~palfun-foslup)
|
|
||||||
* `/app/aqua`: @philipcmonk (~wicdev-wisryt)
|
* `/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)
|
* `/lib/test`: @eglaysher (~littel-ponnys)
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
:: chat-hook:
|
:: chat-hook [landscape]:
|
||||||
:: mirror chat data from foreign to local based on read permissions
|
:: mirror chat data from foreign to local based on read permissions
|
||||||
:: allow sending chat messages to foreign paths based on write perms
|
:: allow sending chat messages to foreign paths based on write perms
|
||||||
::
|
::
|
||||||
|
@ -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
|
/+ store=chat-store, default-agent, verb, dbug, group-store
|
||||||
~% %chat-store-top ..is ~
|
~% %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
|
:: into semantic actions for the UI
|
||||||
::
|
::
|
||||||
/- *permission-store,
|
/- *permission-store,
|
||||||
@ -193,7 +195,6 @@
|
|||||||
=/ pax t.t.t.t.site.url
|
=/ pax t.t.t.t.site.url
|
||||||
=/ envelopes (envelope-scry [(scot %ud start) (scot %ud end) pax])
|
=/ envelopes (envelope-scry [(scot %ud start) (scot %ud end) pax])
|
||||||
%- json-response:gen
|
%- json-response:gen
|
||||||
%- json-to-octs
|
|
||||||
%- update:enjs:store
|
%- update:enjs:store
|
||||||
[%messages pax start end envelopes]
|
[%messages pax start end envelopes]
|
||||||
==
|
==
|
||||||
@ -296,6 +297,7 @@
|
|||||||
~[(chat-hook-poke %add-synced ship.act app-path.act ask-history.act)]
|
~[(chat-hook-poke %add-synced ship.act app-path.act ask-history.act)]
|
||||||
=/ rid=resource
|
=/ rid=resource
|
||||||
(de-path:resource ship+app-path.act)
|
(de-path:resource ship+app-path.act)
|
||||||
|
?: =(our.bol entity.rid) ~
|
||||||
=/ =cage
|
=/ =cage
|
||||||
:- %group-update
|
:- %group-update
|
||||||
!> ^- action:group-store
|
!> ^- action:group-store
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
:: clock: deprecated, should be removed
|
:: clock [landscape]:
|
||||||
|
::
|
||||||
|
:: deprecated, should be removed
|
||||||
::
|
::
|
||||||
/+ *server, default-agent, verb, dbug
|
/+ *server, default-agent, verb, dbug
|
||||||
=, format
|
=, format
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
:: contact-hook:
|
:: contact-hook [landscape]
|
||||||
|
::
|
||||||
::
|
::
|
||||||
/- group-hook,
|
/- group-hook,
|
||||||
*contact-hook,
|
*contact-hook,
|
||||||
|
@ -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
|
/+ *contact-json, default-agent, dbug
|
||||||
|%
|
|%
|
||||||
|
@ -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
|
:: into semantic actions for the UI
|
||||||
::
|
::
|
||||||
/-
|
/-
|
||||||
|
@ -148,9 +148,7 @@
|
|||||||
::
|
::
|
||||||
=; json=(unit json)
|
=; json=(unit json)
|
||||||
?~ json not-found:gen
|
?~ json not-found:gen
|
||||||
%- json-response:gen
|
(json-response:gen u.json)
|
||||||
=, html
|
|
||||||
(as-octt:mimes (en-json u.json))
|
|
||||||
=, enjs:format
|
=, enjs:format
|
||||||
?+ site ~
|
?+ site ~
|
||||||
:: /apps.json: {appname: running?}
|
:: /apps.json: {appname: running?}
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
:: file-server [landscape]:
|
||||||
|
::
|
||||||
|
:: mounts HTTP endpoints for Landscape (and third-party) user applications
|
||||||
|
::
|
||||||
/- srv=file-server, glob
|
/- srv=file-server, glob
|
||||||
/+ *server, default-agent, verb, dbug
|
/+ *server, default-agent, verb, dbug
|
||||||
|%
|
|%
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
:: glob [landscape]:
|
||||||
|
::
|
||||||
|
:: prompts content delivery and Gall state storage for Landscape JS blob
|
||||||
|
::
|
||||||
/- glob
|
/- glob
|
||||||
/+ default-agent, verb, dbug
|
/+ default-agent, verb, dbug
|
||||||
|%
|
|%
|
||||||
++ hash 0v6.8fpt6.7mcjg.nb019.df3fo.haav6
|
++ hash 0v3.u1ets.ipgbo.eo23m.md70h.djpj0
|
||||||
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
||||||
+$ all-states
|
+$ all-states
|
||||||
$% state-0
|
$% state-0
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
:: graph-store [landscape]
|
||||||
|
::
|
||||||
|
::
|
||||||
/+ store=graph-store, sigs=signatures, res=resource, default-agent, dbug
|
/+ store=graph-store, sigs=signatures, res=resource, default-agent, dbug
|
||||||
~% %graph-store-top ..is ~
|
~% %graph-store-top ..is ~
|
||||||
|%
|
|%
|
||||||
@ -447,16 +450,29 @@
|
|||||||
|^
|
|^
|
||||||
?> (team:title our.bowl src.bowl)
|
?> (team:title our.bowl src.bowl)
|
||||||
?+ path (on-peek:def path)
|
?+ path (on-peek:def path)
|
||||||
[%x %keys ~] ``noun+!>(~(key by graphs))
|
[%x %keys ~]
|
||||||
[%x %tags ~] ``noun+!>(~(key by tag-queries))
|
:- ~ :- ~ :- %graph-update
|
||||||
[%x %tag-queries ~] ``noun+!>(tag-queries)
|
!>(`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 @ @ ~]
|
[%x %graph @ @ ~]
|
||||||
=/ =ship (slav %p i.t.t.path)
|
=/ =ship (slav %p i.t.t.path)
|
||||||
=/ =term i.t.t.t.path
|
=/ =term i.t.t.t.path
|
||||||
=/ result=(unit marked-graph:store)
|
=/ result=(unit marked-graph:store)
|
||||||
(~(get by graphs) [ship term])
|
(~(get by graphs) [ship term])
|
||||||
?~ result [~ ~]
|
?~ 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 @ @ @ @ ~]
|
[%x %graph-subset @ @ @ @ ~]
|
||||||
=/ =ship (slav %p i.t.t.path)
|
=/ =ship (slav %p i.t.t.path)
|
||||||
@ -466,7 +482,16 @@
|
|||||||
=/ graph=(unit marked-graph:store)
|
=/ graph=(unit marked-graph:store)
|
||||||
(~(get by graphs) [ship term])
|
(~(get by graphs) [ship term])
|
||||||
?~ graph [~ ~]
|
?~ 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 @ @ @ *]
|
[%x %node @ @ @ *]
|
||||||
=/ =ship (slav %p i.t.t.path)
|
=/ =ship (slav %p i.t.t.path)
|
||||||
@ -475,28 +500,13 @@
|
|||||||
(turn t.t.t.t.path |=(=cord (slav %ud cord)))
|
(turn t.t.t.t.path |=(=cord (slav %ud cord)))
|
||||||
=/ node=(unit node:store) (get-node ship term index)
|
=/ node=(unit node:store) (get-node ship term index)
|
||||||
?~ node [~ ~]
|
?~ node [~ ~]
|
||||||
``noun+!>(u.node)
|
:- ~ :- ~ :- %graph-update
|
||||||
::
|
!> ^- update:store
|
||||||
[%x %post @ @ @ *]
|
:+ %0
|
||||||
=/ =ship (slav %p i.t.t.path)
|
now.bowl
|
||||||
=/ =term i.t.t.t.path
|
:+ %add-nodes
|
||||||
=/ =index:store
|
[ship term]
|
||||||
(turn t.t.t.t.path |=(=cord (slav %ud cord)))
|
(~(gas by *(map index:store node:store)) [index u.node] ~)
|
||||||
=/ 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)
|
|
||||||
==
|
|
||||||
::
|
::
|
||||||
[%x %node-children-subset @ @ @ @ @ *]
|
[%x %node-children-subset @ @ @ @ @ *]
|
||||||
=/ =ship (slav %p i.t.t.path)
|
=/ =ship (slav %p i.t.t.path)
|
||||||
@ -509,7 +519,18 @@
|
|||||||
?~ node [~ ~]
|
?~ node [~ ~]
|
||||||
?- -.children.u.node
|
?- -.children.u.node
|
||||||
%empty [~ ~]
|
%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 @ @ ~]
|
[%x %update-log @ @ ~]
|
||||||
|
@ -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
|
/- *group, hook=group-hook, *invite-store
|
||||||
/+ default-agent, verb, dbug, store=group-store, grpl=group, pull-hook, push-hook, resource
|
/+ default-agent, verb, dbug, store=group-store, grpl=group, pull-hook, push-hook, resource
|
||||||
|
@ -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
|
/- *group, hook=group-hook, *invite-store, *resource
|
||||||
/+ default-agent, verb, dbug, store=group-store, grpl=group, pull-hook
|
/+ 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
|
/- *group, hook=group-hook, *invite-store
|
||||||
/+ default-agent, verb, dbug, store=group-store, grpl=group, push-hook,
|
/+ 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
|
:: 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
|
:: associated with a group. The current model of group-store rolls
|
||||||
|
@ -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.
|
:: 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.
|
:: 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
|
/+ *invite-json, default-agent, dbug
|
||||||
|%
|
|%
|
||||||
+$ card card:agent:gall
|
+$ card card:agent:gall
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
:: invite-view [landscape]:
|
||||||
|
::
|
||||||
|
:: deprecated
|
||||||
|
::
|
||||||
/+ default-agent
|
/+ default-agent
|
||||||
^- agent:gall
|
^- agent:gall
|
||||||
|_ =bowl:gall
|
|_ =bowl:gall
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script src="/~landscape/js/channel.js"></script>
|
<script src="/~landscape/js/channel.js"></script>
|
||||||
<script src="/~landscape/js/session.js"></script>
|
<script src="/~landscape/js/session.js"></script>
|
||||||
<script src="/~landscape/js/bundle/index.5c70804296e13e8973c3.js"></script>
|
<script src="/~landscape/js/bundle/index.df81e597349a655b83f2.js"></script>
|
||||||
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js"></script>
|
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -113,7 +113,7 @@
|
|||||||
++ json-response
|
++ json-response
|
||||||
|= [eyre-id=@ta jon=json]
|
|= [eyre-id=@ta jon=json]
|
||||||
^- (list card)
|
^- (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
|
++ give-rpc-notification
|
||||||
|= res=out:notification:lsp-sur
|
|= res=out:notification:lsp-sur
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
:: launch [landscape]:
|
||||||
|
::
|
||||||
|
:: registers Landscape (and third party) applications, tiles
|
||||||
|
::
|
||||||
/+ store=launch-store, default-agent, dbug
|
/+ store=launch-store, default-agent, dbug
|
||||||
|%
|
|%
|
||||||
+$ card card:agent:gall
|
+$ card card:agent:gall
|
||||||
|
@ -136,7 +136,7 @@
|
|||||||
::
|
::
|
||||||
:_ this
|
:_ this
|
||||||
%+ give-simple-payload:app eyre-id.u.job.state
|
%+ give-simple-payload:app eyre-id.u.job.state
|
||||||
(json-response:gen (json-to-octs jon))
|
(json-response:gen jon)
|
||||||
::
|
::
|
||||||
++ take-sole-effect
|
++ take-sole-effect
|
||||||
|= fec=sole-effect
|
|= fec=sole-effect
|
||||||
@ -186,7 +186,7 @@
|
|||||||
%+ give-simple-payload:app eyre-id.u.job.state
|
%+ give-simple-payload:app eyre-id.u.job.state
|
||||||
?- -.u.out
|
?- -.u.out
|
||||||
%json
|
%json
|
||||||
(json-response:gen (json-to-octs json.u.out))
|
(json-response:gen json.u.out)
|
||||||
::
|
::
|
||||||
%mime
|
%mime
|
||||||
=/ headers
|
=/ headers
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
:: link-listen-hook: get your friends' bookmarks
|
:: link-listen-hook [landscape]:
|
||||||
|
::
|
||||||
|
:: get your friends' bookmarks
|
||||||
::
|
::
|
||||||
:: keeps track of a listening=(set app-path). users can manually add to and
|
:: keeps track of a listening=(set app-path). users can manually add to and
|
||||||
:: remove from this set.
|
:: remove from this set.
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
:: link-proxy-hook: make local pages available to foreign ships
|
:: link-proxy-hook [landscape]:
|
||||||
|
::
|
||||||
|
:: make local pages available to foreign ships
|
||||||
::
|
::
|
||||||
:: this is a "proxy" style hook, relaying foreign subscriptions into local
|
:: this is a "proxy" style hook, relaying foreign subscriptions into local
|
||||||
:: stores if permission conditions are met.
|
:: stores if permission conditions are met.
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
:: link: social bookmarking
|
:: link [landscape]:
|
||||||
|
::
|
||||||
|
:: social bookmarking
|
||||||
::
|
::
|
||||||
:: the paths under which links are submitted are generally expected to
|
:: the paths under which links are submitted are generally expected to
|
||||||
:: correspond to existing group paths. for strictly-local collections of
|
:: correspond to existing group paths. for strictly-local collections of
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
:: link-view: frontend endpoints
|
:: link-view [landscape]:
|
||||||
|
::
|
||||||
|
::frontend endpoints
|
||||||
::
|
::
|
||||||
:: endpoints, mapping onto link-store's paths. p is for page as in pagination.
|
:: endpoints, mapping onto link-store's paths. p is for page as in pagination.
|
||||||
:: only the /0/submissions endpoint provides updates.
|
:: only the /0/submissions endpoint provides updates.
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
:: metadata-hook: allow syncing foreign metadata
|
:: metadata-hook [landscape]:
|
||||||
|
::
|
||||||
|
:: allow syncing foreign metadata
|
||||||
::
|
::
|
||||||
:: watch paths:
|
:: watch paths:
|
||||||
:: /group/%group-path all updates related to this group
|
:: /group/%group-path all updates related to this group
|
||||||
|
@ -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
|
:: between groups and resources within applications
|
||||||
::
|
::
|
||||||
:: group-paths are expected to be an existing group path
|
:: 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
|
:: 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.
|
:: allows mirroring permissions between local and foreign ships.
|
||||||
:: local permission path are exposed according to the permssion paths
|
:: 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
|
/- *permission-store
|
||||||
/+ default-agent, verb, dbug
|
/+ 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
|
:: 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
|
/- *publish
|
||||||
/- *group
|
/- *group
|
||||||
/- group-hook
|
/- group-hook
|
||||||
@ -2347,7 +2351,6 @@
|
|||||||
:: all notebooks, short form
|
:: all notebooks, short form
|
||||||
[[[~ %json] [%'publish-view' %notebooks ~]] ~]
|
[[[~ %json] [%'publish-view' %notebooks ~]] ~]
|
||||||
%- json-response:gen
|
%- json-response:gen
|
||||||
%- json-to-octs
|
|
||||||
(notebooks-map:enjs our.bol books)
|
(notebooks-map:enjs our.bol books)
|
||||||
::
|
::
|
||||||
:: notes pagination
|
:: notes pagination
|
||||||
@ -2366,7 +2369,6 @@
|
|||||||
?~ length
|
?~ length
|
||||||
not-found:gen
|
not-found:gen
|
||||||
%- json-response:gen
|
%- json-response:gen
|
||||||
%- json-to-octs
|
|
||||||
:- %o
|
:- %o
|
||||||
(notes-page:enjs notes.u.book u.start u.length)
|
(notes-page:enjs notes.u.book u.start u.length)
|
||||||
::
|
::
|
||||||
@ -2390,7 +2392,6 @@
|
|||||||
?~ length
|
?~ length
|
||||||
not-found:gen
|
not-found:gen
|
||||||
%- json-response:gen
|
%- json-response:gen
|
||||||
%- json-to-octs
|
|
||||||
(comments-page:enjs comments.u.note u.start u.length)
|
(comments-page:enjs comments.u.note u.start u.length)
|
||||||
::
|
::
|
||||||
:: single notebook with initial 50 notes in short form, as json
|
:: 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))
|
(~(put by p.notebook-json) %subscribers (get-subscribers-json book-name))
|
||||||
=. p.notebook-json
|
=. p.notebook-json
|
||||||
(~(put by p.notebook-json) %writers (get-writers-json u.host book-name))
|
(~(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
|
:: single note, with initial 50 comments, as json
|
||||||
[[[~ %json] [%'publish-view' @ @ @ ~]] ~]
|
[[[~ %json] [%'publish-view' @ @ @ ~]] ~]
|
||||||
@ -2424,7 +2425,7 @@
|
|||||||
?~ note not-found:gen
|
?~ note not-found:gen
|
||||||
=/ jon=json
|
=/ jon=json
|
||||||
o+(note-presentation:enjs u.book note-name u.note)
|
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
|
||||||
/+ s3-json, default-agent, verb, dbug
|
/+ s3-json, default-agent, verb, dbug
|
||||||
~% %s3-top ..is ~
|
~% %s3-top ..is ~
|
||||||
|
@ -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
|
:: Relays sole-effects to subscribers and forwards sole-action pokes
|
||||||
::
|
::
|
||||||
/- sole
|
/- sole
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
:: weather [landscape]:
|
||||||
|
::
|
||||||
|
:: holds latlong, gets weather data from API, passes it on to subscribers
|
||||||
|
::
|
||||||
/+ *server, default-agent, verb, dbug
|
/+ *server, default-agent, verb, dbug
|
||||||
=, format
|
=, format
|
||||||
::
|
::
|
||||||
|
@ -247,20 +247,24 @@
|
|||||||
|%
|
|%
|
||||||
++ decode
|
++ decode
|
||||||
%- of
|
%- of
|
||||||
:~ [%add-graph add-graph]
|
:~ [%add-nodes add-nodes]
|
||||||
[%remove-graph remove-graph]
|
|
||||||
[%add-nodes add-nodes]
|
|
||||||
[%remove-nodes remove-nodes]
|
[%remove-nodes remove-nodes]
|
||||||
[%add-signatures add-signatures]
|
[%add-signatures add-signatures]
|
||||||
[%remove-signatures remove-signatures]
|
[%remove-signatures remove-signatures]
|
||||||
|
::
|
||||||
|
[%add-graph add-graph]
|
||||||
|
[%remove-graph remove-graph]
|
||||||
|
::
|
||||||
[%add-tag add-tag]
|
[%add-tag add-tag]
|
||||||
[%remove-tag remove-tag]
|
[%remove-tag remove-tag]
|
||||||
|
::
|
||||||
[%archive-graph archive-graph]
|
[%archive-graph archive-graph]
|
||||||
[%unarchive-graph unarchive-graph]
|
[%unarchive-graph unarchive-graph]
|
||||||
|
[%run-updates run-updates]
|
||||||
|
::
|
||||||
[%keys keys]
|
[%keys keys]
|
||||||
[%tags tags]
|
[%tags tags]
|
||||||
[%tag-queries tag-queries]
|
[%tag-queries tag-queries]
|
||||||
[%run-updates run-updates]
|
|
||||||
==
|
==
|
||||||
::
|
::
|
||||||
++ add-graph
|
++ 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
|
/- *pull-hook
|
||||||
/+ default-agent, resource
|
/+ default-agent, resource
|
||||||
::
|
::
|
||||||
@ -5,6 +25,12 @@
|
|||||||
|%
|
|%
|
||||||
+$ card card:agent:gall
|
+$ 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
|
+$ config
|
||||||
$: store-name=term
|
$: store-name=term
|
||||||
update=mold
|
update=mold
|
||||||
@ -12,6 +38,12 @@
|
|||||||
push-hook-name=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
|
+$ state-0
|
||||||
$: %0
|
$: %0
|
||||||
tracking=(map resource ship)
|
tracking=(map resource ship)
|
||||||
@ -37,7 +69,29 @@
|
|||||||
|* config
|
|* config
|
||||||
$_ ^|
|
$_ ^|
|
||||||
|_ bowl:gall
|
|_ 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
|
++ on-init
|
||||||
*[(list card) _^|(..on-init)]
|
*[(list card) _^|(..on-init)]
|
||||||
::
|
::
|
||||||
@ -75,26 +129,6 @@
|
|||||||
++ on-fail
|
++ on-fail
|
||||||
|~ [term tang]
|
|~ [term tang]
|
||||||
*[(list card) _^|(..on-init)]
|
*[(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
|
++ agent
|
||||||
|* =config
|
|* =config
|
||||||
@ -209,7 +243,10 @@
|
|||||||
=^ cards pull-hook
|
=^ cards pull-hook
|
||||||
(on-fail:og term tang)
|
(on-fail:og term tang)
|
||||||
[cards this]
|
[cards this]
|
||||||
++ on-peek on-peek:def
|
++ on-peek
|
||||||
|
|= =path
|
||||||
|
^- (unit (unit cage))
|
||||||
|
(on-peek:og path)
|
||||||
--
|
--
|
||||||
|_ =bowl:gall
|
|_ =bowl:gall
|
||||||
+* og ~(. pull-hook bowl)
|
+* og ~(. pull-hook bowl)
|
||||||
@ -225,6 +262,7 @@
|
|||||||
++ add
|
++ add
|
||||||
|= [=ship =resource]
|
|= [=ship =resource]
|
||||||
~| resource
|
~| resource
|
||||||
|
?< |(=(our.bowl ship) =(our.bowl entity.resource))
|
||||||
?: (~(has by tracking) resource)
|
?: (~(has by tracking) resource)
|
||||||
[~ state]
|
[~ state]
|
||||||
=. tracking
|
=. tracking
|
||||||
|
@ -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
|
/- *push-hook
|
||||||
/+ default-agent, resource
|
/+ default-agent, resource
|
||||||
|%
|
|%
|
||||||
+$ card card:agent:gall
|
+$ 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
|
+$ config
|
||||||
$: store-name=term
|
$: store-name=term
|
||||||
store-path=path
|
store-path=path
|
||||||
@ -10,6 +43,12 @@
|
|||||||
update-mark=term
|
update-mark=term
|
||||||
pull-hook-name=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
|
+$ state-0
|
||||||
$: %0
|
$: %0
|
||||||
sharing=(set resource)
|
sharing=(set resource)
|
||||||
@ -21,6 +60,48 @@
|
|||||||
$_ ^|
|
$_ ^|
|
||||||
|_ bowl:gall
|
|_ 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
|
++ on-init
|
||||||
*[(list card) _^|(..on-init)]
|
*[(list card) _^|(..on-init)]
|
||||||
::
|
::
|
||||||
@ -58,35 +139,6 @@
|
|||||||
++ on-fail
|
++ on-fail
|
||||||
|~ [term tang]
|
|~ [term tang]
|
||||||
*[(list card) _^|(..on-init)]
|
*[(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
|
++ agent
|
||||||
|* =config
|
|* =config
|
||||||
|
@ -92,9 +92,9 @@
|
|||||||
[[200 [['content-type' 'text/javascript'] max-1-da ~]] `octs]
|
[[200 [['content-type' 'text/javascript'] max-1-da ~]] `octs]
|
||||||
::
|
::
|
||||||
++ json-response
|
++ json-response
|
||||||
|= =octs
|
|= =json
|
||||||
^- simple-payload:http
|
^- simple-payload:http
|
||||||
[[200 ['content-type' 'application/json']~] `octs]
|
[[200 ['content-type' 'application/json']~] `(json-to-octs json)]
|
||||||
::
|
::
|
||||||
++ css-response
|
++ css-response
|
||||||
|= =octs
|
|= =octs
|
||||||
|
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=
|
43
pkg/interface/package-lock.json
generated
43
pkg/interface/package-lock.json
generated
@ -6383,11 +6383,6 @@
|
|||||||
"p-is-promise": "^2.0.0"
|
"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": {
|
"memory-fs": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
|
||||||
@ -6807,6 +6802,11 @@
|
|||||||
"tslib": "^1.10.0"
|
"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": {
|
"node-forge": {
|
||||||
"version": "0.9.0",
|
"version": "0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz",
|
||||||
@ -7058,6 +7058,14 @@
|
|||||||
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
|
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
|
||||||
"dev": true
|
"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": {
|
"omit-deep": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/omit-deep/-/omit-deep-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/omit-deep/-/omit-deep-0.3.0.tgz",
|
||||||
@ -7937,6 +7945,14 @@
|
|||||||
"xtend": "^4.0.1"
|
"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": {
|
"react-router": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",
|
||||||
@ -7973,13 +7989,13 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.0.tgz",
|
||||||
"integrity": "sha512-IgmcegOSi5SNX+2Snh1vqmF0Vg/CbkycU9XZbOHJlZ6kMzTmi3yc254oB1WCkgA7OQtIAoLmcSFuHTc/tlcqXg=="
|
"integrity": "sha512-IgmcegOSi5SNX+2Snh1vqmF0Vg/CbkycU9XZbOHJlZ6kMzTmi3yc254oB1WCkgA7OQtIAoLmcSFuHTc/tlcqXg=="
|
||||||
},
|
},
|
||||||
"react-window": {
|
"react-virtuoso": {
|
||||||
"version": "1.8.5",
|
"version": "0.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-0.20.0.tgz",
|
||||||
"integrity": "sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q==",
|
"integrity": "sha512-h+U6t/+m91AzfUe6bBfaacdLLJl1y8v7CfcXwPgQ/Dic+vNlgQmi6cIKTq18zuF+kI8Q7QN0ojIeqPHWbU8TZA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.0.0",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"memoize-one": ">=3.1.1 <6"
|
"tslib": "^1.11.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"readable-stream": {
|
"readable-stream": {
|
||||||
@ -8260,6 +8276,11 @@
|
|||||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||||
"dev": true
|
"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": {
|
"resolve": {
|
||||||
"version": "1.17.0",
|
"version": "1.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
"moment": "^2.20.1",
|
"moment": "^2.20.1",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
"mousetrap-global-bind": "^1.1.0",
|
"mousetrap-global-bind": "^1.1.0",
|
||||||
|
"oembed-parser": "^1.4.1",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"react": "^16.5.2",
|
"react": "^16.5.2",
|
||||||
"react-codemirror2": "^6.0.1",
|
"react-codemirror2": "^6.0.1",
|
||||||
@ -29,8 +30,9 @@
|
|||||||
"react-dom": "^16.8.6",
|
"react-dom": "^16.8.6",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-markdown": "^4.3.1",
|
"react-markdown": "^4.3.1",
|
||||||
|
"react-oembed-container": "^1.0.0",
|
||||||
"react-router-dom": "^5.0.0",
|
"react-router-dom": "^5.0.0",
|
||||||
"react-window": "^1.8.5",
|
"react-virtuoso": "^0.20.0",
|
||||||
"remark-disable-tokenizers": "^1.0.24",
|
"remark-disable-tokenizers": "^1.0.24",
|
||||||
"style-loader": "^1.2.1",
|
"style-loader": "^1.2.1",
|
||||||
"styled-components": "^5.1.0",
|
"styled-components": "^5.1.0",
|
||||||
|
@ -12,7 +12,7 @@ export default class ChatApi extends BaseApi<StoreState> {
|
|||||||
* Fetch backlog
|
* Fetch backlog
|
||||||
*/
|
*/
|
||||||
fetchMessages(start: number, end: number, path: Path) {
|
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(response => response.json())
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
this.store.handleEvent({
|
this.store.handleEvent({
|
||||||
|
@ -11,6 +11,7 @@ import GroupsApi from './groups';
|
|||||||
import LaunchApi from './launch';
|
import LaunchApi from './launch';
|
||||||
import LinksApi from './links';
|
import LinksApi from './links';
|
||||||
import PublishApi from './publish';
|
import PublishApi from './publish';
|
||||||
|
import GraphApi from './graph';
|
||||||
import S3Api from './s3';
|
import S3Api from './s3';
|
||||||
|
|
||||||
export default class GlobalApi extends BaseApi<StoreState> {
|
export default class GlobalApi extends BaseApi<StoreState> {
|
||||||
@ -24,10 +25,15 @@ export default class GlobalApi extends BaseApi<StoreState> {
|
|||||||
links = new LinksApi(this.ship, this.channel, this.store);
|
links = new LinksApi(this.ship, this.channel, this.store);
|
||||||
publish = new PublishApi(this.ship, this.channel, this.store);
|
publish = new PublishApi(this.ship, this.channel, this.store);
|
||||||
s3 = new S3Api(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) {
|
constructor(
|
||||||
super(ship,channel,store);
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -44,8 +44,8 @@ export default class LinksApi extends BaseApi<StoreState> {
|
|||||||
this.fetchLink(
|
this.fetchLink(
|
||||||
endpoint,
|
endpoint,
|
||||||
(res) => {
|
(res) => {
|
||||||
if (res.data.submission) {
|
if (res.data?.['link-update']?.submission) {
|
||||||
callback(res.data.submission);
|
callback(res.data?.['link-update']?.submission);
|
||||||
} else {
|
} else {
|
||||||
console.error('unexpected submission response', res);
|
console.error('unexpected submission response', res);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import BaseApi from "./base";
|
import BaseApi from "./base";
|
||||||
import { StoreState } from "../store/type";
|
import { StoreState } from "../store/type";
|
||||||
import { BackgroundConfig } from "../types/local-update";
|
import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from "../types/local-update";
|
||||||
|
|
||||||
export default class LocalApi extends BaseApi<StoreState> {
|
export default class LocalApi extends BaseApi<StoreState> {
|
||||||
getBaseHash() {
|
getBaseHash() {
|
||||||
@ -69,6 +69,16 @@ export default class LocalApi extends BaseApi<StoreState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setRemoteContentPolicy(policy: LocalUpdateRemoteContentPolicy) {
|
||||||
|
this.store.handleEvent({
|
||||||
|
data: {
|
||||||
|
local: {
|
||||||
|
remoteContentPolicy: policy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
dehydrate() {
|
dehydrate() {
|
||||||
this.store.dehydrate();
|
this.store.dehydrate();
|
||||||
}
|
}
|
||||||
|
@ -82,6 +82,17 @@ export default class PublishApi extends BaseApi {
|
|||||||
return this.action('publish', 'publish-action', act);
|
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) {
|
newBook(bookId: string, title: string, description: string, group?: Path) {
|
||||||
const groupInfo = group ? { 'group-path': group,
|
const groupInfo = group ? { 'group-path': group,
|
||||||
invitees: [],
|
invitees: [],
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import defaultApps from './default-apps';
|
import defaultApps from './default-apps';
|
||||||
|
import { cite } from '~/logic/lib/util';
|
||||||
|
|
||||||
const indexes = new Map([
|
const indexes = new Map([
|
||||||
['commands', []],
|
['commands', []],
|
||||||
['subscriptions', []],
|
['subscriptions', []],
|
||||||
['groups', []],
|
['groups', []],
|
||||||
['apps', []]
|
['apps', []],
|
||||||
|
['other', []]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// result schematic
|
// result schematic
|
||||||
@ -41,8 +43,6 @@ const commandIndex = function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
commands.push(result('Profile', '/~profile', 'profile', null));
|
|
||||||
|
|
||||||
return commands;
|
return commands;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -54,6 +54,9 @@ const appIndex = function (apps) {
|
|||||||
.filter((e) => {
|
.filter((e) => {
|
||||||
return apps[e]?.type?.basic;
|
return apps[e]?.type?.basic;
|
||||||
})
|
})
|
||||||
|
.sort((a,b) => {
|
||||||
|
return a.localeCompare(b);
|
||||||
|
})
|
||||||
.map((e) => {
|
.map((e) => {
|
||||||
const obj = result(
|
const obj = result(
|
||||||
apps[e].type.basic.title,
|
apps[e].type.basic.title,
|
||||||
@ -70,6 +73,14 @@ const appIndex = function (apps) {
|
|||||||
return applications;
|
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) {
|
export default function index(associations, apps) {
|
||||||
// all metadata from all apps is indexed
|
// all metadata from all apps is indexed
|
||||||
// into subscriptions and groups
|
// into subscriptions and groups
|
||||||
@ -99,7 +110,7 @@ export default function index(associations, apps) {
|
|||||||
title,
|
title,
|
||||||
`/~${app}${each['app-path']}`,
|
`/~${app}${each['app-path']}`,
|
||||||
app.charAt(0).toUpperCase() + app.slice(1),
|
app.charAt(0).toUpperCase() + app.slice(1),
|
||||||
shipStart.slice(0, shipStart.indexOf('/'))
|
cite(shipStart.slice(0, shipStart.indexOf('/')))
|
||||||
);
|
);
|
||||||
groups.push(obj);
|
groups.push(obj);
|
||||||
} else {
|
} else {
|
||||||
@ -107,7 +118,7 @@ export default function index(associations, apps) {
|
|||||||
title,
|
title,
|
||||||
`/~${each['app-name']}/join${each['app-path']}`,
|
`/~${each['app-name']}/join${each['app-path']}`,
|
||||||
app.charAt(0).toUpperCase() + app.slice(1),
|
app.charAt(0).toUpperCase() + app.slice(1),
|
||||||
shipStart.slice(0, shipStart.indexOf('/'))
|
(associations?.contacts?.[each['group-path']]?.metadata?.title || null)
|
||||||
);
|
);
|
||||||
subscriptions.push(obj);
|
subscriptions.push(obj);
|
||||||
}
|
}
|
||||||
@ -118,6 +129,7 @@ export default function index(associations, apps) {
|
|||||||
indexes.set('subscriptions', subscriptions);
|
indexes.set('subscriptions', subscriptions);
|
||||||
indexes.set('groups', groups);
|
indexes.set('groups', groups);
|
||||||
indexes.set('apps', appIndex(apps));
|
indexes.set('apps', appIndex(apps));
|
||||||
|
indexes.set('other', otherIndex());
|
||||||
|
|
||||||
return indexes;
|
return indexes;
|
||||||
};
|
};
|
||||||
|
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 };
|
@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
|
||||||
export function useWaitForProps<P>(props: P, timeout: number) {
|
export function useWaitForProps<P>(props: P, timeout: number = 0) {
|
||||||
const [resolve, setResolve] = useState<() => void>(() => () => {});
|
const [resolve, setResolve] = useState<() => void>(() => () => {});
|
||||||
const [ready, setReady] = useState<(p: P) => boolean | undefined>();
|
const [ready, setReady] = useState<(p: P) => boolean | undefined>();
|
||||||
|
|
||||||
@ -24,9 +24,11 @@ export function useWaitForProps<P>(props: P, timeout: number) {
|
|||||||
setReady(() => r);
|
setReady(() => r);
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
setResolve(() => resolve);
|
setResolve(() => resolve);
|
||||||
setTimeout(() => {
|
if(timeout > 0) {
|
||||||
reject(new Error("Timed out"));
|
setTimeout(() => {
|
||||||
}, timeout);
|
reject(new Error("Timed out"));
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setResolve, setReady, timeout]
|
[setResolve, setReady, timeout]
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { StoreState } from '../../../store/type';
|
import { StoreState } from '~/logic/store/type';
|
||||||
import { Cage } from '~/types/cage';
|
import { Cage } from '~/types/cage';
|
||||||
import { ChatUpdate } from '~/types/chat-update';
|
import { ChatUpdate } from '~/types/chat-update';
|
||||||
import { ChatHookUpdate } from '~/types/chat-hook-update';
|
import { ChatHookUpdate } from '~/types/chat-hook-update';
|
||||||
|
import { Envelope } from "~/types/chat-update";
|
||||||
|
|
||||||
type ChatState = Pick<StoreState, 'chatInitialized' | 'chatSynced' | 'inbox' | 'pendingMessages'>;
|
type ChatState = Pick<StoreState, 'chatInitialized' | 'chatSynced' | 'inbox' | 'pendingMessages'>;
|
||||||
|
|
||||||
@ -49,8 +50,11 @@ export default class ChatReducer<S extends ChatState> {
|
|||||||
messages(json: ChatUpdate, state: S) {
|
messages(json: ChatUpdate, state: S) {
|
||||||
const data = _.get(json, 'messages', false);
|
const data = _.get(json, 'messages', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
state.inbox[data.path].envelopes =
|
state.inbox[data.path].envelopes = _.unionBy(
|
||||||
state.inbox[data.path].envelopes.concat(data.envelopes);
|
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.addGroup(data, state);
|
||||||
this.removeGroup(data, state);
|
this.removeGroup(data, state);
|
||||||
this.changePolicy(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) {
|
private inviteChangePolicy(diff: InvitePolicyDiff, policy: InvitePolicy) {
|
||||||
if ('addInvites' in diff) {
|
if ('addInvites' in diff) {
|
||||||
const { addInvites } = diff;
|
const { addInvites } = diff;
|
||||||
|
@ -3,7 +3,7 @@ import { StoreState } from '~/store/type';
|
|||||||
import { Cage } from '~/types/cage';
|
import { Cage } from '~/types/cage';
|
||||||
import { LocalUpdate, BackgroundConfig } from '~/types/local-update';
|
import { LocalUpdate, BackgroundConfig } from '~/types/local-update';
|
||||||
|
|
||||||
type LocalState = Pick<StoreState, 'sidebarShown' | 'omniboxShown' | 'baseHash' | 'hideAvatars' | 'hideNicknames' | 'background' | 'dark' | 'suspendedFocus'>;
|
type LocalState = Pick<StoreState, 'sidebarShown' | 'omniboxShown' | 'baseHash' | 'hideAvatars' | 'hideNicknames' | 'background' | 'dark' | 'suspendedFocus' | 'remoteContentPolicy'>;
|
||||||
|
|
||||||
export default class LocalReducer<S extends LocalState> {
|
export default class LocalReducer<S extends LocalState> {
|
||||||
rehydrate(state: S) {
|
rehydrate(state: S) {
|
||||||
@ -18,7 +18,7 @@ export default class LocalReducer<S extends LocalState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dehydrate(state: S) {
|
dehydrate(state: S) {
|
||||||
const json = _.pick(state, ['hideNicknames' , 'hideAvatars' , 'background']);
|
const json = _.pick(state, ['hideNicknames' , 'hideAvatars' , 'background', 'remoteContentPolicy']);
|
||||||
localStorage.setItem('localReducer', JSON.stringify(json));
|
localStorage.setItem('localReducer', JSON.stringify(json));
|
||||||
}
|
}
|
||||||
reduce(json: Cage, state: S) {
|
reduce(json: Cage, state: S) {
|
||||||
@ -31,6 +31,7 @@ export default class LocalReducer<S extends LocalState> {
|
|||||||
this.hideAvatars(data, state)
|
this.hideAvatars(data, state)
|
||||||
this.hideNicknames(data, state)
|
this.hideNicknames(data, state)
|
||||||
this.omniboxShown(data, state);
|
this.omniboxShown(data, state);
|
||||||
|
this.remoteContentPolicy(data, state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
baseHash(obj: LocalUpdate, state: S) {
|
baseHash(obj: LocalUpdate, state: S) {
|
||||||
@ -70,6 +71,12 @@ export default class LocalReducer<S extends LocalState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remoteContentPolicy(obj: LocalUpdate, state: S) {
|
||||||
|
if('remoteContentPolicy' in obj) {
|
||||||
|
state.remoteContentPolicy = obj.remoteContentPolicy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
hideAvatars(obj: LocalUpdate, state: S) {
|
hideAvatars(obj: LocalUpdate, state: S) {
|
||||||
if('hideAvatars' in obj) {
|
if('hideAvatars' in obj) {
|
||||||
state.hideAvatars = obj.hideAvatars;
|
state.hideAvatars = obj.hideAvatars;
|
||||||
|
@ -6,6 +6,7 @@ import InviteReducer from '../reducers/invite-update';
|
|||||||
import LinkReducer from '../reducers/link-update';
|
import LinkReducer from '../reducers/link-update';
|
||||||
import ListenReducer from '../reducers/listen-update';
|
import ListenReducer from '../reducers/listen-update';
|
||||||
import LocalReducer from '../reducers/local';
|
import LocalReducer from '../reducers/local';
|
||||||
|
import S3Reducer from '../reducers/s3-update';
|
||||||
|
|
||||||
import BaseStore from './base';
|
import BaseStore from './base';
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ export default class LinksStore extends BaseStore {
|
|||||||
this.localReducer = new LocalReducer();
|
this.localReducer = new LocalReducer();
|
||||||
this.linkReducer = new LinkReducer();
|
this.linkReducer = new LinkReducer();
|
||||||
this.listenReducer = new ListenReducer();
|
this.listenReducer = new ListenReducer();
|
||||||
|
this.s3Reducer = new S3Reducer();
|
||||||
}
|
}
|
||||||
|
|
||||||
initialState() {
|
initialState() {
|
||||||
@ -37,6 +39,7 @@ export default class LinksStore extends BaseStore {
|
|||||||
comments: {},
|
comments: {},
|
||||||
seen: {},
|
seen: {},
|
||||||
permissions: {},
|
permissions: {},
|
||||||
|
s3: {},
|
||||||
sidebarShown: true
|
sidebarShown: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -50,6 +53,7 @@ export default class LinksStore extends BaseStore {
|
|||||||
this.localReducer.reduce(data, this.state);
|
this.localReducer.reduce(data, this.state);
|
||||||
this.linkReducer.reduce(data, this.state);
|
this.linkReducer.reduce(data, this.state);
|
||||||
this.listenReducer.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 ContactReducer from '../reducers/contact-update';
|
||||||
import LinkUpdateReducer from '../reducers/link-update';
|
import LinkUpdateReducer from '../reducers/link-update';
|
||||||
import S3Reducer from '../reducers/s3-update';
|
import S3Reducer from '../reducers/s3-update';
|
||||||
|
import { GraphReducer } from '../reducers/graph-update';
|
||||||
import GroupReducer from '../reducers/group-update';
|
import GroupReducer from '../reducers/group-update';
|
||||||
import PermissionReducer from '../reducers/permission-update';
|
import PermissionReducer from '../reducers/permission-update';
|
||||||
import PublishUpdateReducer from '../reducers/publish-update';
|
import PublishUpdateReducer from '../reducers/publish-update';
|
||||||
@ -53,6 +54,12 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
|||||||
suspendedFocus: null,
|
suspendedFocus: null,
|
||||||
baseHash: null,
|
baseHash: null,
|
||||||
background: undefined,
|
background: undefined,
|
||||||
|
remoteContentPolicy: {
|
||||||
|
imageShown: true,
|
||||||
|
audioShown: true,
|
||||||
|
videoShown: true,
|
||||||
|
oembedShown: true,
|
||||||
|
},
|
||||||
hideAvatars: false,
|
hideAvatars: false,
|
||||||
hideNicknames: false,
|
hideNicknames: false,
|
||||||
invites: {},
|
invites: {},
|
||||||
@ -64,6 +71,8 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
|||||||
},
|
},
|
||||||
groups: {},
|
groups: {},
|
||||||
groupKeys: new Set(),
|
groupKeys: new Set(),
|
||||||
|
graphs: {},
|
||||||
|
graphKeys: new Set(),
|
||||||
launch: {
|
launch: {
|
||||||
firstTime: false,
|
firstTime: false,
|
||||||
tileOrdering: [],
|
tileOrdering: [],
|
||||||
@ -106,5 +115,6 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
|||||||
this.launchReducer.reduce(data, this.state);
|
this.launchReducer.reduce(data, this.state);
|
||||||
this.linkListenReducer.reduce(data, this.state);
|
this.linkListenReducer.reduce(data, this.state);
|
||||||
this.connReducer.reduce(data, this.state);
|
this.connReducer.reduce(data, this.state);
|
||||||
|
GraphReducer(data, this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import { Permissions } from '~/types/permission-update';
|
|||||||
import { LaunchState, WeatherState } from '~/types/launch-update';
|
import { LaunchState, WeatherState } from '~/types/launch-update';
|
||||||
import { LinkComments, LinkCollections, LinkSeen } from '~/types/link-update';
|
import { LinkComments, LinkCollections, LinkSeen } from '~/types/link-update';
|
||||||
import { ConnectionStatus } from '~/types/connection';
|
import { ConnectionStatus } from '~/types/connection';
|
||||||
import { BackgroundConfig } from '~/types/local-update';
|
import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from '~/types/local-update';
|
||||||
|
|
||||||
export interface StoreState {
|
export interface StoreState {
|
||||||
// local state
|
// local state
|
||||||
@ -22,6 +22,7 @@ export interface StoreState {
|
|||||||
connection: ConnectionStatus;
|
connection: ConnectionStatus;
|
||||||
baseHash: string | null;
|
baseHash: string | null;
|
||||||
background: BackgroundConfig;
|
background: BackgroundConfig;
|
||||||
|
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||||
hideAvatars: boolean;
|
hideAvatars: boolean;
|
||||||
hideNicknames: boolean;
|
hideNicknames: boolean;
|
||||||
// invite state
|
// invite state
|
||||||
@ -35,6 +36,8 @@ export interface StoreState {
|
|||||||
groupKeys: Set<Path>;
|
groupKeys: Set<Path>;
|
||||||
permissions: Permissions;
|
permissions: Permissions;
|
||||||
s3: S3State;
|
s3: S3State;
|
||||||
|
graphs: Object;
|
||||||
|
graphKeys: Set<String>;
|
||||||
|
|
||||||
|
|
||||||
// App specific states
|
// App specific states
|
||||||
|
@ -27,12 +27,18 @@ const groupSubscriptions: AppSubscription[] = [
|
|||||||
['/synced', 'contact-hook']
|
['/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[]> = {
|
const appSubscriptions: Record<AppName, AppSubscription[]> = {
|
||||||
chat: chatSubscriptions,
|
chat: chatSubscriptions,
|
||||||
publish: publishSubscriptions,
|
publish: publishSubscriptions,
|
||||||
link: linkSubscriptions,
|
link: linkSubscriptions,
|
||||||
groups: groupSubscriptions
|
groups: groupSubscriptions,
|
||||||
|
graph: graphSubscriptions
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class GlobalSubscription extends BaseSubscription<StoreState> {
|
export default class GlobalSubscription extends BaseSubscription<StoreState> {
|
||||||
@ -40,8 +46,10 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
|
|||||||
chat: [],
|
chat: [],
|
||||||
publish: [],
|
publish: [],
|
||||||
link: [],
|
link: [],
|
||||||
groups: []
|
groups: [],
|
||||||
|
graph: []
|
||||||
};
|
};
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
this.subscribe('/all', 'invite-store');
|
this.subscribe('/all', 'invite-store');
|
||||||
this.subscribe('/groups', 'group-store');
|
this.subscribe('/groups', 'group-store');
|
||||||
@ -67,7 +75,8 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
|
|||||||
console.log(`${app} already started`);
|
console.log(`${app} already started`);
|
||||||
return;
|
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) {
|
stopApp(app: AppName) {
|
||||||
|
@ -69,10 +69,14 @@ export interface Envelope {
|
|||||||
uid: string;
|
uid: string;
|
||||||
number: number;
|
number: number;
|
||||||
author: Patp;
|
author: Patp;
|
||||||
when: string;
|
when: number;
|
||||||
letter: Letter;
|
letter: Letter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IMessage = Envelope & {
|
||||||
|
pending?: boolean
|
||||||
|
};
|
||||||
|
|
||||||
interface LetterText {
|
interface LetterText {
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,3 @@
|
|||||||
export type LocalUpdate =
|
|
||||||
LocalUpdateSidebarToggle
|
|
||||||
| LocalUpdateSetDark
|
|
||||||
| LocalUpdateBaseHash
|
|
||||||
| LocalUpdateBackgroundConfig
|
|
||||||
| LocalUpdateHideAvatars
|
|
||||||
| LocalUpdateHideNicknames
|
|
||||||
| LocalUpdateSetOmniboxShown;
|
|
||||||
|
|
||||||
interface LocalUpdateSidebarToggle {
|
interface LocalUpdateSidebarToggle {
|
||||||
sidebarToggle: boolean;
|
sidebarToggle: boolean;
|
||||||
}
|
}
|
||||||
@ -31,7 +22,16 @@ interface LocalUpdateHideNicknames {
|
|||||||
hideNicknames: boolean;
|
hideNicknames: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BackgroundConfig = BackgroundConfigUrl | BackgroundConfigColor | undefined;
|
interface LocalUpdateSetOmniboxShown {
|
||||||
|
omniboxShown: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocalUpdateRemoteContentPolicy {
|
||||||
|
imageShown: boolean;
|
||||||
|
audioShown: boolean;
|
||||||
|
videoShown: boolean;
|
||||||
|
oembedShown: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface BackgroundConfigUrl {
|
interface BackgroundConfigUrl {
|
||||||
type: 'url';
|
type: 'url';
|
||||||
@ -43,6 +43,14 @@ interface BackgroundConfigColor {
|
|||||||
color: string;
|
color: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LocalUpdateSetOmniboxShown {
|
export type BackgroundConfig = BackgroundConfigUrl | BackgroundConfigColor | undefined;
|
||||||
omniboxShown: boolean;
|
|
||||||
}
|
export type LocalUpdate =
|
||||||
|
LocalUpdateSidebarToggle
|
||||||
|
| LocalUpdateSetDark
|
||||||
|
| LocalUpdateBaseHash
|
||||||
|
| LocalUpdateBackgroundConfig
|
||||||
|
| LocalUpdateHideAvatars
|
||||||
|
| LocalUpdateHideNicknames
|
||||||
|
| LocalUpdateSetOmniboxShown
|
||||||
|
| LocalUpdateRemoteContentPolicy;
|
@ -1,7 +1,7 @@
|
|||||||
import { hot } from 'react-hot-loader/root';
|
import { hot } from 'react-hot-loader/root';
|
||||||
import 'react-hot-loader';
|
import 'react-hot-loader';
|
||||||
import * as React from 'react';
|
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 styled, { ThemeProvider, createGlobalStyle } from 'styled-components';
|
||||||
import { sigil as sigiljs, stringRenderer } from 'urbit-sigil-js';
|
import { sigil as sigiljs, stringRenderer } from 'urbit-sigil-js';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
@ -16,8 +16,7 @@ import dark from './themes/old-dark';
|
|||||||
|
|
||||||
import { Content } from './components/Content';
|
import { Content } from './components/Content';
|
||||||
import StatusBar from './components/StatusBar';
|
import StatusBar from './components/StatusBar';
|
||||||
import Omnibox from './components/Omnibox';
|
import Omnibox from './components/leap/Omnibox';
|
||||||
import ErrorComponent from './components/Error';
|
|
||||||
|
|
||||||
import GlobalStore from '~/logic/store/store';
|
import GlobalStore from '~/logic/store/store';
|
||||||
import GlobalSubscription from '~/logic/subscription/global';
|
import GlobalSubscription from '~/logic/subscription/global';
|
||||||
@ -36,7 +35,7 @@ const Root = styled.div`
|
|||||||
background-size: cover;
|
background-size: cover;
|
||||||
` : p.background?.type === 'color' ? `
|
` : p.background?.type === 'color' ? `
|
||||||
background-color: ${p.background.color};
|
background-color: ${p.background.color};
|
||||||
` : ``
|
` : ''
|
||||||
}
|
}
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
@ -135,7 +134,8 @@ class App extends React.Component {
|
|||||||
ship={this.ship}
|
ship={this.ship}
|
||||||
api={this.api}
|
api={this.api}
|
||||||
subscription={this.subscription}
|
subscription={this.subscription}
|
||||||
{...state} />
|
{...state}
|
||||||
|
/>
|
||||||
</Router>
|
</Router>
|
||||||
</Root>
|
</Root>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
@ -143,6 +143,5 @@ class App extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default process.env.NODE_ENV === 'production' ? App : hot(App);
|
export default process.env.NODE_ENV === 'production' ? App : hot(App);
|
||||||
|
|
||||||
|
@ -4,10 +4,8 @@ import { Col } from "@tlon/indigo-react";
|
|||||||
|
|
||||||
import { Association } from "~/types/metadata-update";
|
import { Association } from "~/types/metadata-update";
|
||||||
import { StoreState } from "~/logic/store/type";
|
import { StoreState } from "~/logic/store/type";
|
||||||
import { ChatScreen } from "./components/chat";
|
import ChatWindow from "./components/lib/ChatWindow";
|
||||||
import { ChatWindow } from "./components/lib/chat-window";
|
import ChatInput from "./components/lib/ChatInput";
|
||||||
import { ChatHeader } from "./components/lib/chat-header";
|
|
||||||
import { ChatInput } from "./components/lib/chat-input";
|
|
||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from "~/logic/api/global";
|
||||||
import { deSig } from "~/logic/lib/util";
|
import { deSig } from "~/logic/lib/util";
|
||||||
|
|
||||||
@ -27,8 +25,8 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
const { read, length } = config;
|
const { read, length } = config;
|
||||||
|
|
||||||
const groupPath = props.association["group-path"];
|
const groupPath = props.association["group-path"];
|
||||||
const group = props.groups[groupPath] || groupBunts.group();
|
const group = props.groups[groupPath];
|
||||||
const contacts = props.contacts[groupPath];
|
const contacts = props.contacts[groupPath] || {};
|
||||||
|
|
||||||
const pendingMessages = (props.pendingMessages.get(props.station) || []).map(
|
const pendingMessages = (props.pendingMessages.get(props.station) || []).map(
|
||||||
(value) => ({
|
(value) => ({
|
||||||
@ -41,25 +39,23 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
props.chatInitialized &&
|
props.chatInitialized &&
|
||||||
!(station in props.inbox) &&
|
!(station in props.inbox) &&
|
||||||
props.chatSynced &&
|
props.chatSynced &&
|
||||||
!(station in props.chatSynced);
|
!(station in props.chatSynced) || false;
|
||||||
|
|
||||||
const isChatLoading =
|
const isChatLoading =
|
||||||
props.chatInitialized &&
|
props.chatInitialized &&
|
||||||
!(station in props.inbox) &&
|
!(station in props.inbox) &&
|
||||||
props.chatSynced &&
|
props.chatSynced &&
|
||||||
station in props.chatSynced;
|
station in props.chatSynced || false;
|
||||||
|
|
||||||
const isChatUnsynced =
|
const isChatUnsynced =
|
||||||
props.chatSynced && !(station in props.chatSynced) && envelopes.length > 0;
|
props.chatSynced && !(station in props.chatSynced) && envelopes.length > 0 || false;
|
||||||
|
|
||||||
const unreadCount = length - read;
|
const unreadCount = length - read;
|
||||||
const unreadMsg = unreadCount > 0 && envelopes[unreadCount - 1];
|
const unreadMsg = unreadCount > 0 && envelopes[unreadCount - 1];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const roomContacts = contacts[groupPath] || {};
|
|
||||||
|
|
||||||
const popout = props.match.url.includes("/popout/");
|
|
||||||
const [, owner, name] = station.split("/");
|
const [, owner, name] = station.split("/");
|
||||||
const ownerContact = contacts?.[deSig(owner)];
|
const ownerContact = contacts?.[deSig(owner)];
|
||||||
const lastMsgNum = 0;
|
const lastMsgNum = 0;
|
||||||
@ -67,16 +63,18 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
return (
|
return (
|
||||||
<Col overflow="hidden" position="relative">
|
<Col overflow="hidden" position="relative">
|
||||||
<ChatWindow
|
<ChatWindow
|
||||||
|
remoteContentPolicy={props.remoteContentPolicy}
|
||||||
|
mailboxSize={length}
|
||||||
|
match={props.match as any}
|
||||||
|
stationPendingMessages={[]}
|
||||||
history={props.history}
|
history={props.history}
|
||||||
isChatMissing={isChatMissing}
|
isChatMissing={isChatMissing}
|
||||||
isChatLoading={isChatLoading}
|
isChatLoading={isChatLoading}
|
||||||
isChatUnsynced={isChatUnsynced}
|
isChatUnsynced={isChatUnsynced}
|
||||||
unreadCount={unreadCount}
|
unreadCount={unreadCount}
|
||||||
unreadMsg={unreadMsg}
|
unreadMsg={unreadMsg}
|
||||||
pendingMessages={pendingMessages}
|
envelopes={envelopes || []}
|
||||||
messages={envelopes}
|
contacts={contacts}
|
||||||
length={length}
|
|
||||||
contacts={roomContacts}
|
|
||||||
association={props.association}
|
association={props.association}
|
||||||
group={group}
|
group={group}
|
||||||
ship={owner}
|
ship={owner}
|
||||||
@ -84,6 +82,7 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
api={props.api}
|
api={props.api}
|
||||||
hideNicknames={props.hideNicknames}
|
hideNicknames={props.hideNicknames}
|
||||||
hideAvatars={props.hideAvatars}
|
hideAvatars={props.hideAvatars}
|
||||||
|
location={props.location}
|
||||||
/>
|
/>
|
||||||
<ChatInput
|
<ChatInput
|
||||||
api={props.api}
|
api={props.api}
|
||||||
@ -91,14 +90,15 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
station={station}
|
station={station}
|
||||||
owner={deSig(owner)}
|
owner={deSig(owner)}
|
||||||
ownerContact={ownerContact}
|
ownerContact={ownerContact}
|
||||||
envelopes={envelopes}
|
envelopes={envelopes || []}
|
||||||
contacts={roomContacts}
|
contacts={contacts}
|
||||||
onUnmount={(msg: string) => {
|
onUnmount={(msg: string) => {
|
||||||
/*this.setState({
|
/*this.setState({
|
||||||
messages: this.state.messages.set(props.station, msg),
|
messages: this.state.messages.set(props.station, msg),
|
||||||
}) */
|
}) */
|
||||||
}}
|
}}
|
||||||
s3={props.s3}
|
s3={props.s3}
|
||||||
|
hideAvatars={props.hideAvatars}
|
||||||
placeholder="Message..."
|
placeholder="Message..."
|
||||||
message={"" || ""}
|
message={"" || ""}
|
||||||
deleteMessage={() => {}}
|
deleteMessage={() => {}}
|
||||||
|
@ -7,7 +7,6 @@ import './css/custom.css';
|
|||||||
import { Skeleton } from './components/skeleton';
|
import { Skeleton } from './components/skeleton';
|
||||||
import { Sidebar } from './components/sidebar';
|
import { Sidebar } from './components/sidebar';
|
||||||
import { ChatScreen } from './components/chat';
|
import { ChatScreen } from './components/chat';
|
||||||
import { MemberScreen } from './components/member';
|
|
||||||
import { SettingsScreen } from './components/settings';
|
import { SettingsScreen } from './components/settings';
|
||||||
import { NewScreen } from './components/new';
|
import { NewScreen } from './components/new';
|
||||||
import { JoinScreen } from './components/join';
|
import { JoinScreen } from './components/join';
|
||||||
@ -89,7 +88,8 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
|
|||||||
pendingMessages,
|
pendingMessages,
|
||||||
groups,
|
groups,
|
||||||
hideAvatars,
|
hideAvatars,
|
||||||
hideNicknames
|
hideNicknames,
|
||||||
|
remoteContentPolicy
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const renderChannelSidebar = (props, station?) => (
|
const renderChannelSidebar = (props, station?) => (
|
||||||
@ -108,7 +108,7 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet defer={false}>
|
||||||
<title>{totalUnreads > 0 ? `(${totalUnreads}) ` : ''}OS1 - Chat</title>
|
<title>{totalUnreads > 0 ? `(${totalUnreads}) ` : ''}OS1 - Chat</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Switch>
|
<Switch>
|
||||||
@ -194,6 +194,11 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
|
|||||||
render={(props) => {
|
render={(props) => {
|
||||||
let station = `/${props.match.params.ship}/${props.match.params.station}`;
|
let station = `/${props.match.params.ship}/${props.match.params.station}`;
|
||||||
|
|
||||||
|
// ensure we know joined chats
|
||||||
|
if(!chatInitialized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
associations={associations}
|
associations={associations}
|
||||||
@ -226,56 +231,57 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
|
|||||||
envelopes: []
|
envelopes: []
|
||||||
};
|
};
|
||||||
|
|
||||||
let roomContacts = {};
|
let roomContacts = {};
|
||||||
const associatedGroup =
|
const associatedGroup =
|
||||||
station in associations['chat'] &&
|
station in associations['chat'] &&
|
||||||
'group-path' in associations.chat[station]
|
'group-path' in associations.chat[station]
|
||||||
? associations.chat[station]['group-path']
|
? associations.chat[station]['group-path']
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
if (associations.chat[station] && associatedGroup in contacts) {
|
if (associations.chat[station] && associatedGroup in contacts) {
|
||||||
roomContacts = contacts[associatedGroup];
|
roomContacts = contacts[associatedGroup];
|
||||||
}
|
}
|
||||||
|
|
||||||
const association =
|
const association =
|
||||||
station in associations['chat'] ? associations.chat[station] : {};
|
station in associations['chat'] ? associations.chat[station] : {};
|
||||||
|
|
||||||
const group = groups[association['group-path']] || groupBunts.group();
|
const group = groups[association['group-path']] || groupBunts.group();
|
||||||
|
|
||||||
const popout = props.match.url.includes('/popout/');
|
const popout = props.match.url.includes('/popout/');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
associations={associations}
|
associations={associations}
|
||||||
invites={invites}
|
invites={invites}
|
||||||
sidebarHideOnMobile={true}
|
sidebarHideOnMobile={true}
|
||||||
|
popout={popout}
|
||||||
|
sidebarShown={sidebarShown}
|
||||||
|
sidebar={renderChannelSidebar(props, station)}
|
||||||
|
>
|
||||||
|
<ChatScreen
|
||||||
|
chatSynced={chatSynced || {}}
|
||||||
|
station={station}
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
read={mailbox.config.read}
|
||||||
|
mailboxSize={mailbox.config.length}
|
||||||
|
envelopes={mailbox.envelopes}
|
||||||
|
inbox={inbox}
|
||||||
|
contacts={roomContacts}
|
||||||
|
group={group}
|
||||||
|
pendingMessages={pendingMessages}
|
||||||
|
s3={s3}
|
||||||
popout={popout}
|
popout={popout}
|
||||||
sidebarShown={sidebarShown}
|
sidebarShown={sidebarShown}
|
||||||
sidebar={renderChannelSidebar(props, station)}
|
chatInitialized={chatInitialized}
|
||||||
>
|
hideAvatars={hideAvatars}
|
||||||
<ChatScreen
|
hideNicknames={hideNicknames}
|
||||||
chatSynced={chatSynced || {}}
|
remoteContentPolicy={remoteContentPolicy}
|
||||||
station={station}
|
{...props}
|
||||||
association={association}
|
/>
|
||||||
api={api}
|
</Skeleton>
|
||||||
read={mailbox.config.read}
|
);
|
||||||
length={mailbox.config.length}
|
}}
|
||||||
envelopes={mailbox.envelopes}
|
|
||||||
inbox={inbox}
|
|
||||||
contacts={roomContacts}
|
|
||||||
group={group}
|
|
||||||
pendingMessages={pendingMessages}
|
|
||||||
s3={s3}
|
|
||||||
popout={popout}
|
|
||||||
sidebarShown={sidebarShown}
|
|
||||||
chatInitialized={chatInitialized}
|
|
||||||
hideAvatars={hideAvatars}
|
|
||||||
hideNicknames={hideNicknames}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</Skeleton>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
|
@ -1,20 +1,21 @@
|
|||||||
import React, { Component, Fragment } from "react";
|
import React, { Component } from "react";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { RouteComponentProps } from "react-router-dom";
|
||||||
|
|
||||||
import { Link, RouteComponentProps } from "react-router-dom";
|
|
||||||
|
|
||||||
import { ChatWindow } from './lib/chat-window';
|
|
||||||
import { ChatHeader } from './lib/chat-header';
|
|
||||||
import { ChatInput } from "./lib/chat-input";
|
|
||||||
import { deSig } from "~/logic/lib/util";
|
import { deSig } from "~/logic/lib/util";
|
||||||
import { ChatHookUpdate } from "~/types/chat-hook-update";
|
import { ChatHookUpdate } from "~/types/chat-hook-update";
|
||||||
import ChatApi from "~/logic/api/chat";
|
|
||||||
import { Inbox, Envelope } from "~/types/chat-update";
|
import { Inbox, Envelope } from "~/types/chat-update";
|
||||||
import { Contacts } from "~/types/contact-update";
|
import { Contacts } from "~/types/contact-update";
|
||||||
import { Path, Patp } from "~/types/noun";
|
import { Path, Patp } from "~/types/noun";
|
||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from "~/logic/api/global";
|
||||||
import { Association } from "~/types/metadata-update";
|
import { Association } from "~/types/metadata-update";
|
||||||
import {Group} from "~/types/group-update";
|
import {Group} from "~/types/group-update";
|
||||||
|
import { LocalUpdateRemoteContentPolicy } from "~/types";
|
||||||
|
import { SubmitDragger } from '~/views/components/s3-upload';
|
||||||
|
|
||||||
|
import ChatWindow from './lib/ChatWindow';
|
||||||
|
import ChatHeader from './lib/ChatHeader';
|
||||||
|
import ChatInput from "./lib/ChatInput";
|
||||||
|
|
||||||
|
|
||||||
type ChatScreenProps = RouteComponentProps<{
|
type ChatScreenProps = RouteComponentProps<{
|
||||||
@ -26,7 +27,7 @@ type ChatScreenProps = RouteComponentProps<{
|
|||||||
association: Association;
|
association: Association;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
read: number;
|
read: number;
|
||||||
length: number;
|
mailboxSize: number;
|
||||||
inbox: Inbox;
|
inbox: Inbox;
|
||||||
contacts: Contacts;
|
contacts: Contacts;
|
||||||
group: Group;
|
group: Group;
|
||||||
@ -38,13 +39,16 @@ type ChatScreenProps = RouteComponentProps<{
|
|||||||
envelopes: Envelope[];
|
envelopes: Envelope[];
|
||||||
hideAvatars: boolean;
|
hideAvatars: boolean;
|
||||||
hideNicknames: boolean;
|
hideNicknames: boolean;
|
||||||
|
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ChatScreenState {
|
interface ChatScreenState {
|
||||||
messages: Map<string, string>;
|
messages: Map<string, string>;
|
||||||
|
dragover: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
|
export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
|
||||||
|
private chatInput: React.RefObject<ChatInput>;
|
||||||
lastNumPending = 0;
|
lastNumPending = 0;
|
||||||
activityTimeout: NodeJS.Timeout | null = null;
|
activityTimeout: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
@ -53,8 +57,11 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
messages: new Map(),
|
messages: new Map(),
|
||||||
|
dragover: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.chatInput = React.createRef();
|
||||||
|
|
||||||
moment.updateLocale("en", {
|
moment.updateLocale("en", {
|
||||||
calendar: {
|
calendar: {
|
||||||
sameDay: "[Today]",
|
sameDay: "[Today]",
|
||||||
@ -67,6 +74,31 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readyToUpload(): boolean {
|
||||||
|
return Boolean(this.chatInput.current?.s3Uploader.current?.inputRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragEnter(event) {
|
||||||
|
if (!this.readyToUpload() || !event.dataTransfer.files.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState({ dragover: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onDrop(event: DragEvent) {
|
||||||
|
this.setState({ dragover: false });
|
||||||
|
event.preventDefault();
|
||||||
|
if (!event.dataTransfer || !event.dataTransfer.files.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.dataTransfer.items.length && !event.dataTransfer.files.length) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
this.chatInput.current?.uploadFiles(event.dataTransfer.files);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
|
|
||||||
@ -97,42 +129,42 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
|
|||||||
!(props.station in props.chatSynced) &&
|
!(props.station in props.chatSynced) &&
|
||||||
props.envelopes.length > 0;
|
props.envelopes.length > 0;
|
||||||
|
|
||||||
const unreadCount = props.length - props.read;
|
const unreadCount = props.mailboxSize - props.read;
|
||||||
const unreadMsg = unreadCount > 0 && props.envelopes[unreadCount - 1];
|
const unreadMsg = unreadCount > 0 && props.envelopes[unreadCount - 1];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={props.station}
|
key={props.station}
|
||||||
className="h-100 w-100 overflow-hidden flex flex-column relative">
|
className="h-100 w-100 overflow-hidden flex flex-column relative"
|
||||||
<ChatHeader
|
onDragEnter={this.onDragEnter.bind(this)}
|
||||||
match={props.match}
|
onDragOver={event => {
|
||||||
location={props.location}
|
event.preventDefault();
|
||||||
api={props.api}
|
if (
|
||||||
group={props.group}
|
!this.state.dragover
|
||||||
association={props.association}
|
&& (
|
||||||
station={props.station}
|
(event.dataTransfer.files.length && event.dataTransfer.files[0].kind === 'file')
|
||||||
sidebarShown={props.sidebarShown}
|
|| (event.dataTransfer.items.length && event.dataTransfer.items[0].kind === 'file')
|
||||||
popout={props.popout} />
|
)
|
||||||
|
) {
|
||||||
|
this.setState({ dragover: true });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onDragLeave={() => this.setState({ dragover: false })}
|
||||||
|
onDrop={this.onDrop.bind(this)}
|
||||||
|
>
|
||||||
|
{this.state.dragover ? <SubmitDragger /> : null}
|
||||||
|
<ChatHeader {...props} />
|
||||||
<ChatWindow
|
<ChatWindow
|
||||||
history={props.history}
|
|
||||||
isChatMissing={isChatMissing}
|
isChatMissing={isChatMissing}
|
||||||
isChatLoading={isChatLoading}
|
isChatLoading={isChatLoading}
|
||||||
isChatUnsynced={isChatUnsynced}
|
isChatUnsynced={isChatUnsynced}
|
||||||
unreadCount={unreadCount}
|
unreadCount={unreadCount}
|
||||||
unreadMsg={unreadMsg}
|
unreadMsg={unreadMsg}
|
||||||
pendingMessages={pendingMessages}
|
stationPendingMessages={pendingMessages}
|
||||||
messages={props.envelopes}
|
|
||||||
length={props.length}
|
|
||||||
contacts={props.contacts}
|
|
||||||
association={props.association}
|
|
||||||
group={props.group}
|
|
||||||
ship={props.match.params.ship}
|
ship={props.match.params.ship}
|
||||||
station={props.station}
|
{...props} />
|
||||||
api={props.api}
|
|
||||||
hideNicknames={props.hideNicknames}
|
|
||||||
hideAvatars={props.hideAvatars}
|
|
||||||
/>
|
|
||||||
<ChatInput
|
<ChatInput
|
||||||
|
ref={this.chatInput}
|
||||||
api={props.api}
|
api={props.api}
|
||||||
numMsgs={lastMsgNum}
|
numMsgs={lastMsgNum}
|
||||||
station={props.station}
|
station={props.station}
|
||||||
@ -149,6 +181,7 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
|
|||||||
deleteMessage={() => this.setState({
|
deleteMessage={() => this.setState({
|
||||||
messages: this.state.messages.set(props.station, "")
|
messages: this.state.messages.set(props.station, "")
|
||||||
})}
|
})}
|
||||||
|
hideAvatars={props.hideAvatars}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
const ChatHeader = (props) => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
</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 '
|
||||||
|
}
|
||||||
|
style={{ height: 48 }}
|
||||||
|
>
|
||||||
|
<SidebarSwitcher
|
||||||
|
sidebarShown={props.sidebarShown}
|
||||||
|
popout={props.popout}
|
||||||
|
api={props.api}
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
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' : '')
|
||||||
|
}
|
||||||
|
style={{ width: 'max-content' }}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
</Link>
|
||||||
|
<TabBar
|
||||||
|
location={props.location}
|
||||||
|
popoutHref={`/~chat/popout/room${props.station}`}
|
||||||
|
settings={`/~chat/${isInPopout}settings${props.station}`}
|
||||||
|
popout={props.popout}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatHeader;
|
255
pkg/interface/src/views/apps/chat/components/lib/ChatInput.tsx
Normal file
255
pkg/interface/src/views/apps/chat/components/lib/ChatInput.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 default 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
283
pkg/interface/src/views/apps/chat/components/lib/ChatMessage.tsx
Normal file
283
pkg/interface/src/views/apps/chat/components/lib/ChatMessage.tsx
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
import React, { Component, PureComponent } from "react";
|
||||||
|
import moment from "moment";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
import { OverlaySigil } from './overlay-sigil';
|
||||||
|
import { uxToHex, cite, writeText } from '~/logic/lib/util';
|
||||||
|
import { Envelope, IMessage } from "~/types/chat-update";
|
||||||
|
import { Group, Association, Contacts, LocalUpdateRemoteContentPolicy } from "~/types";
|
||||||
|
import TextContent from './content/text';
|
||||||
|
import CodeContent from './content/code';
|
||||||
|
import RemoteContent from '~/views/components/RemoteContent';
|
||||||
|
|
||||||
|
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
|
||||||
|
|
||||||
|
export const UnreadMarker = React.forwardRef(({ dayBreak, when, style }, ref) => (
|
||||||
|
<div ref={element => {
|
||||||
|
setTimeout(() => {
|
||||||
|
element.style.opacity = '1';
|
||||||
|
}, 250);
|
||||||
|
}} className="green2 flex items-center f9 absolute w-100" style={{...style, opacity: '0'}}>
|
||||||
|
<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(when).calendar()}</p>
|
||||||
|
: null}
|
||||||
|
<hr style={{ width: "calc(50% - 48px)" }} className="b--green2 ma0 bt-0" />
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
export const DayBreak = ({ when }) => (
|
||||||
|
<div className="pv3 gray2 b--gray2 flex items-center justify-center f9 w-100">
|
||||||
|
<p>{moment(when).calendar()}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ChatMessageProps {
|
||||||
|
measure(element): void;
|
||||||
|
msg: Envelope | IMessage;
|
||||||
|
previousMsg: Envelope | IMessage | undefined;
|
||||||
|
nextMsg: Envelope | IMessage | undefined;
|
||||||
|
isFirstUnread: boolean;
|
||||||
|
group: Group;
|
||||||
|
association: Association;
|
||||||
|
contacts: Contacts;
|
||||||
|
unreadRef: React.RefObject<HTMLDivElement>;
|
||||||
|
hideAvatars: boolean;
|
||||||
|
hideNicknames: boolean;
|
||||||
|
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||||
|
className: string;
|
||||||
|
isPending: boolean;
|
||||||
|
style?: any;
|
||||||
|
scrollWindow: HTMLDivElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ChatMessage extends Component<ChatMessageProps> {
|
||||||
|
private divRef: React.RefObject<HTMLDivElement>;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.divRef = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (this.divRef.current) {
|
||||||
|
this.props.measure(this.divRef.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
msg,
|
||||||
|
previousMsg,
|
||||||
|
nextMsg,
|
||||||
|
isFirstUnread,
|
||||||
|
group,
|
||||||
|
association,
|
||||||
|
contacts,
|
||||||
|
unreadRef,
|
||||||
|
hideAvatars,
|
||||||
|
hideNicknames,
|
||||||
|
remoteContentPolicy,
|
||||||
|
className = '',
|
||||||
|
isPending,
|
||||||
|
style,
|
||||||
|
measure,
|
||||||
|
scrollWindow
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const renderSigil = Boolean((nextMsg && msg.author !== nextMsg.author) || !nextMsg || msg.number === 1);
|
||||||
|
const dayBreak = nextMsg && new Date(msg.when).getDate() !== new Date(nextMsg.when).getDate();
|
||||||
|
|
||||||
|
const containerClass = `${renderSigil
|
||||||
|
? `w-100 flex flex-wrap cf pr3 f7 pt4 pl3 lh-copy`
|
||||||
|
: `w-100 flex flex-wrap cf pr3 hide-child`} ${isPending ? ' o-40' : ''} ${className}`
|
||||||
|
|
||||||
|
const timestamp = moment.unix(msg.when / 1000).format(renderSigil ? 'hh:mm a' : 'hh:mm');
|
||||||
|
|
||||||
|
const reboundMeasure = (event) => {
|
||||||
|
return measure(this.divRef.current);
|
||||||
|
};
|
||||||
|
|
||||||
|
const messageProps = {
|
||||||
|
msg,
|
||||||
|
timestamp,
|
||||||
|
contacts,
|
||||||
|
hideNicknames,
|
||||||
|
association,
|
||||||
|
group,
|
||||||
|
hideAvatars,
|
||||||
|
remoteContentPolicy,
|
||||||
|
measure: reboundMeasure.bind(this),
|
||||||
|
style,
|
||||||
|
containerClass,
|
||||||
|
isPending,
|
||||||
|
scrollWindow
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={this.divRef} className={containerClass} style={style} data-number={msg.number}>
|
||||||
|
{dayBreak && !isFirstUnread ? <DayBreak when={msg.when} /> : null}
|
||||||
|
{renderSigil
|
||||||
|
? <MessageWithSigil {...messageProps} />
|
||||||
|
: <MessageWithoutSigil {...messageProps} />}
|
||||||
|
{isFirstUnread
|
||||||
|
? <UnreadMarker ref={unreadRef} dayBreak={dayBreak} when={msg.when} style={{ marginTop: (renderSigil ? "-17px" : "-6px") }} />
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageProps {
|
||||||
|
msg: Envelope | IMessage;
|
||||||
|
timestamp: string;
|
||||||
|
group: Group;
|
||||||
|
association: Association;
|
||||||
|
contacts: Contacts;
|
||||||
|
hideAvatars: boolean;
|
||||||
|
hideNicknames: boolean;
|
||||||
|
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||||
|
containerClass: string;
|
||||||
|
isPending: boolean;
|
||||||
|
style: any;
|
||||||
|
measure(element): void;
|
||||||
|
scrollWindow: HTMLDivElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class MessageWithSigil extends PureComponent<MessageProps> {
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
msg,
|
||||||
|
timestamp,
|
||||||
|
contacts,
|
||||||
|
hideNicknames,
|
||||||
|
association,
|
||||||
|
group,
|
||||||
|
hideAvatars,
|
||||||
|
remoteContentPolicy,
|
||||||
|
measure,
|
||||||
|
scrollWindow
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const datestamp = moment.unix(msg.when / 1000).format(DATESTAMP_FORMAT);
|
||||||
|
const contact = msg.author in contacts ? contacts[msg.author] : false;
|
||||||
|
const showNickname = !hideNicknames && contact && contact.nickname;
|
||||||
|
const name = showNickname ? contact.nickname : cite(msg.author);
|
||||||
|
const color = contact ? `#${uxToHex(contact.color)}` : '#000000';
|
||||||
|
const sigilClass = contact ? '' : 'mix-blend-diff';
|
||||||
|
|
||||||
|
let nameSpan = null;
|
||||||
|
|
||||||
|
const copyNotice = (saveName) => {
|
||||||
|
if (nameSpan !== null) {
|
||||||
|
nameSpan.innerText = 'Copied';
|
||||||
|
setTimeout(() => {
|
||||||
|
nameSpan.innerText = saveName;
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<OverlaySigil
|
||||||
|
ship={msg.author}
|
||||||
|
contact={contact}
|
||||||
|
color={color}
|
||||||
|
sigilClass={sigilClass}
|
||||||
|
association={association}
|
||||||
|
group={group}
|
||||||
|
hideAvatars={hideAvatars}
|
||||||
|
hideNicknames={hideNicknames}
|
||||||
|
scrollWindow={scrollWindow}
|
||||||
|
className="fl pr3 v-top bg-white bg-gray0-d"
|
||||||
|
/>
|
||||||
|
<div className="fr clamp-message white-d" style={{ flexGrow: 1, marginTop: -8 }}>
|
||||||
|
<div className="hide-child" style={{ paddingTop: '6px' }}>
|
||||||
|
<p className="v-mid f9 gray2 dib mr3 c-default">
|
||||||
|
<span
|
||||||
|
className={`mw5 db truncate pointer ${showNickname ? '' : 'mono'}`}
|
||||||
|
ref={e => nameSpan = e}
|
||||||
|
onClick={() => {
|
||||||
|
writeText(msg.author);
|
||||||
|
copyNotice(name);
|
||||||
|
}}
|
||||||
|
title={`~${msg.author}`}
|
||||||
|
>{name}</span>
|
||||||
|
</p>
|
||||||
|
<p className="v-mid mono f9 gray2 dib">{timestamp}</p>
|
||||||
|
<p className="v-mid mono f9 gray2 dib ml2 child dn-s">{datestamp}</p>
|
||||||
|
</div>
|
||||||
|
<MessageContent content={msg.letter} remoteContentPolicy={remoteContentPolicy} measure={measure} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MessageWithoutSigil = ({ timestamp, msg, remoteContentPolicy, measure }) => (
|
||||||
|
<>
|
||||||
|
<p className="child pt2 pl2 pr1 mono f9 gray2 dib">{timestamp}</p>
|
||||||
|
<div className="fr f7 clamp-message white-d pr3 lh-copy" style={{ flexGrow: 1 }}>
|
||||||
|
<MessageContent content={msg.letter} remoteContentPolicy={remoteContentPolicy} measure={measure}/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const MessageContent = ({ content, remoteContentPolicy, measure }) => {
|
||||||
|
if ('code' in content) {
|
||||||
|
return <CodeContent content={content} />;
|
||||||
|
} else if ('url' in content) {
|
||||||
|
return (
|
||||||
|
<RemoteContent
|
||||||
|
url={content.url}
|
||||||
|
remoteContentPolicy={remoteContentPolicy}
|
||||||
|
onLoad={measure}
|
||||||
|
imageProps={{style: {
|
||||||
|
maxWidth: '18rem'
|
||||||
|
}}}
|
||||||
|
videoProps={{style: {
|
||||||
|
maxWidth: '18rem'
|
||||||
|
}}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if ('me' in content) {
|
||||||
|
return (
|
||||||
|
<p className='f7 i lh-copy v-top'>
|
||||||
|
{content.me}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if ('text' in content) {
|
||||||
|
return <TextContent content={content} />;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MessagePlaceholder = ({ 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>
|
||||||
|
);
|
296
pkg/interface/src/views/apps/chat/components/lib/ChatWindow.tsx
Normal file
296
pkg/interface/src/views/apps/chat/components/lib/ChatWindow.tsx
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
import React, { Component } from "react";
|
||||||
|
import { RouteComponentProps } from "react-router-dom";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
import GlobalApi from "~/logic/api/global";
|
||||||
|
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 { Envelope, IMessage } from "~/types/chat-update";
|
||||||
|
import { LocalUpdateRemoteContentPolicy } from "~/types";
|
||||||
|
|
||||||
|
import VirtualScroller from "~/views/components/VirtualScroller";
|
||||||
|
|
||||||
|
import ChatMessage, { MessagePlaceholder } from './ChatMessage';
|
||||||
|
import { UnreadNotice } from "./unread-notice";
|
||||||
|
import { ResubscribeElement } from "./resubscribe-element";
|
||||||
|
import { BacklogElement } from "./backlog-element";
|
||||||
|
|
||||||
|
const INITIAL_LOAD = 20;
|
||||||
|
const DEFAULT_BACKLOG_SIZE = 100;
|
||||||
|
const IDLE_THRESHOLD = 64;
|
||||||
|
|
||||||
|
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;
|
||||||
|
initialized: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ChatWindow extends Component<ChatWindowProps, ChatWindowState> {
|
||||||
|
private virtualList: VirtualScroller | null;
|
||||||
|
|
||||||
|
INITIALIZATION_MAX_TIME = 1500;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
fetchPending: false,
|
||||||
|
idle: true,
|
||||||
|
initialized: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.dismissUnread = this.dismissUnread.bind(this);
|
||||||
|
this.initialIndex = this.initialIndex.bind(this);
|
||||||
|
this.scrollToUnread = this.scrollToUnread.bind(this);
|
||||||
|
this.handleWindowBlur = this.handleWindowBlur.bind(this);
|
||||||
|
this.handleWindowFocus = this.handleWindowFocus.bind(this);
|
||||||
|
this.stayLockedIfActive = this.stayLockedIfActive.bind(this);
|
||||||
|
this.firstUnread = this.firstUnread.bind(this);
|
||||||
|
|
||||||
|
this.virtualList = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
window.addEventListener('blur', this.handleWindowBlur);
|
||||||
|
window.addEventListener('focus', this.handleWindowFocus);
|
||||||
|
this.initialFetch();
|
||||||
|
setTimeout(() => {
|
||||||
|
this.setState({ initialized: true });
|
||||||
|
}, this.INITIALIZATION_MAX_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener('blur', this.handleWindowBlur);
|
||||||
|
window.removeEventListener('focus', this.handleWindowFocus);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleWindowBlur() {
|
||||||
|
this.setState({ idle: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleWindowFocus() {
|
||||||
|
this.setState({ idle: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
initialIndex() {
|
||||||
|
const { mailboxSize, unreadCount } = this.props;
|
||||||
|
return Math.min(Math.max(mailboxSize - 1 < INITIAL_LOAD
|
||||||
|
? 0
|
||||||
|
: this.firstUnread(),
|
||||||
|
0), mailboxSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialFetch() {
|
||||||
|
const { envelopes, mailboxSize, unreadCount } = this.props;
|
||||||
|
if (envelopes.length > 0) {
|
||||||
|
const start = Math.min(mailboxSize - unreadCount, mailboxSize - DEFAULT_BACKLOG_SIZE);
|
||||||
|
this.stayLockedIfActive();
|
||||||
|
this.fetchMessages(start, start + DEFAULT_BACKLOG_SIZE, true).then(() => {
|
||||||
|
if (!this.virtualList) return;
|
||||||
|
const initialIndex = this.initialIndex();
|
||||||
|
this.virtualList.scrollToData(initialIndex).then(() => {
|
||||||
|
if (
|
||||||
|
initialIndex === mailboxSize
|
||||||
|
|| (this.virtualList && this.virtualList.window && this.virtualList.window.scrollTop === 0)
|
||||||
|
) {
|
||||||
|
this.setState({ idle: false });
|
||||||
|
this.dismissUnread();
|
||||||
|
}
|
||||||
|
this.setState({ initialized: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.initialFetch();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: ChatWindowProps, prevState) {
|
||||||
|
const { isChatMissing, history, envelopes, mailboxSize, stationPendingMessages } = this.props;
|
||||||
|
|
||||||
|
if (isChatMissing) {
|
||||||
|
history.push("/~chat");
|
||||||
|
} else if (envelopes.length !== prevProps.envelopes.length && this.state.fetchPending) {
|
||||||
|
this.setState({ fetchPending: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((mailboxSize !== prevProps.mailboxSize) || (envelopes.length !== prevProps.envelopes.length)) {
|
||||||
|
this.virtualList?.calculateVisibleItems();
|
||||||
|
this.stayLockedIfActive();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stationPendingMessages.length !== prevProps.stationPendingMessages.length) {
|
||||||
|
this.virtualList?.calculateVisibleItems();
|
||||||
|
this.virtualList?.scrollToData(mailboxSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.fetchPending && prevState.fetchPending) {
|
||||||
|
this.virtualList?.calculateVisibleItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stayLockedIfActive() {
|
||||||
|
if (this.virtualList && !this.state.idle) {
|
||||||
|
this.virtualList.resetScroll();
|
||||||
|
this.dismissUnread();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToUnread() {
|
||||||
|
const { mailboxSize, unreadCount } = this.props;
|
||||||
|
this.virtualList?.scrollToData(mailboxSize - unreadCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
dismissUnread() {
|
||||||
|
if (this.state.fetchPending) return;
|
||||||
|
if (this.props.unreadCount === 0) return;
|
||||||
|
this.props.api.chat.read(this.props.station);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchMessages(start, end, force = false): Promise<void> {
|
||||||
|
start = Math.max(start, 0);
|
||||||
|
end = Math.max(end, 0);
|
||||||
|
const { api, mailboxSize, station } = this.props;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(this.state.fetchPending ||
|
||||||
|
mailboxSize <= 0)
|
||||||
|
&& !force
|
||||||
|
) {
|
||||||
|
return new Promise((resolve, reject) => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ fetchPending: true });
|
||||||
|
|
||||||
|
return api.chat
|
||||||
|
.fetchMessages(Math.max(mailboxSize - end, 0), Math.min(mailboxSize - start, mailboxSize), station)
|
||||||
|
.finally(() => {
|
||||||
|
this.setState({ fetchPending: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUnread() {
|
||||||
|
const { mailboxSize, unreadCount } = this.props;
|
||||||
|
return mailboxSize - unreadCount + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
envelopes,
|
||||||
|
stationPendingMessages,
|
||||||
|
unreadCount,
|
||||||
|
unreadMsg,
|
||||||
|
isChatLoading,
|
||||||
|
isChatUnsynced,
|
||||||
|
api,
|
||||||
|
ship,
|
||||||
|
station,
|
||||||
|
association,
|
||||||
|
group,
|
||||||
|
contacts,
|
||||||
|
mailboxSize,
|
||||||
|
hideAvatars,
|
||||||
|
hideNicknames,
|
||||||
|
remoteContentPolicy,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const messages = new Map();
|
||||||
|
let lastMessage = 0;
|
||||||
|
|
||||||
|
[...envelopes]
|
||||||
|
.sort((a, b) => a.when - b.when)
|
||||||
|
.forEach(message => {
|
||||||
|
messages.set(message.number, message);
|
||||||
|
lastMessage = message.number;
|
||||||
|
});
|
||||||
|
|
||||||
|
stationPendingMessages
|
||||||
|
.sort((a, b) => a.when - b.when)
|
||||||
|
.forEach((message, index) => {
|
||||||
|
index = index + 1; // To 1-index it
|
||||||
|
messages.set(envelopes.length + index, message);
|
||||||
|
lastMessage = envelopes.length + index;
|
||||||
|
});
|
||||||
|
|
||||||
|
const messageProps = { association, group, contacts, hideAvatars, hideNicknames, remoteContentPolicy };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<UnreadNotice
|
||||||
|
unreadCount={unreadCount}
|
||||||
|
unreadMsg={unreadCount === 1 && unreadMsg && unreadMsg.author === window.ship ? false : unreadMsg}
|
||||||
|
dismissUnread={this.dismissUnread}
|
||||||
|
onClick={this.scrollToUnread}
|
||||||
|
/>
|
||||||
|
<BacklogElement isChatLoading={isChatLoading} />
|
||||||
|
<ResubscribeElement {...{ api, host: ship, station, isChatUnsynced}} />
|
||||||
|
<VirtualScroller
|
||||||
|
ref={list => {this.virtualList = list}}
|
||||||
|
origin="bottom"
|
||||||
|
style={{ height: '100%' }}
|
||||||
|
onStartReached={() => {
|
||||||
|
this.setState({ idle: false });
|
||||||
|
this.dismissUnread();
|
||||||
|
}}
|
||||||
|
onScroll={({ scrollTop }) => {
|
||||||
|
if (!this.state.idle && scrollTop > IDLE_THRESHOLD) {
|
||||||
|
this.setState({ idle: true });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
data={messages}
|
||||||
|
size={mailboxSize + stationPendingMessages.length}
|
||||||
|
renderer={({ index, measure, scrollWindow }) => {
|
||||||
|
const msg: Envelope | IMessage = messages.get(index);
|
||||||
|
if (!msg) return null;
|
||||||
|
if (!this.state.initialized) {
|
||||||
|
return <MessagePlaceholder key={index} height="64px" index={index} />;
|
||||||
|
}
|
||||||
|
const isPending: boolean = 'pending' in msg && Boolean(msg.pending);
|
||||||
|
const isFirstUnread: boolean = Boolean(unreadCount && index === this.firstUnread());
|
||||||
|
const isLastMessage: boolean = Boolean(index === lastMessage)
|
||||||
|
const props = { measure, scrollWindow, isPending, isFirstUnread, msg, ...messageProps };
|
||||||
|
return (
|
||||||
|
<ChatMessage
|
||||||
|
key={index}
|
||||||
|
previousMsg={messages.get(index + 1)}
|
||||||
|
nextMsg={messages.get(index - 1)}
|
||||||
|
className={isLastMessage ? 'pb3' : ''}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
loadRows={(start, end) => {
|
||||||
|
this.fetchMessages(start, end);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@ export const BacklogElement = (props) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="center mw6">
|
<div className="center mw6 absolute z-9999" style={{ left: 0, right: 0, top: 48}}>
|
||||||
<div className={
|
<div className={
|
||||||
"db pa3 ma3 ba b--gray4 bg-gray5 b--gray2-d bg-gray1-d " +
|
"db pa3 ma3 ba b--gray4 bg-gray5 b--gray2-d bg-gray1-d " +
|
||||||
"white-d flex items-center"
|
"white-d flex items-center"
|
||||||
|
@ -101,9 +101,14 @@ export default class ChatEditor extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { props } = this;
|
const {
|
||||||
|
inCodeMode,
|
||||||
|
placeholder,
|
||||||
|
message,
|
||||||
|
...props
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
const codeTheme = props.inCodeMode ? ' code' : '';
|
const codeTheme = inCodeMode ? ' code' : '';
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
mode: MARKDOWN_CONFIG,
|
mode: MARKDOWN_CONFIG,
|
||||||
@ -112,10 +117,13 @@ export default class ChatEditor extends Component {
|
|||||||
lineWrapping: true,
|
lineWrapping: true,
|
||||||
scrollbarStyle: 'native',
|
scrollbarStyle: 'native',
|
||||||
cursorHeight: 0.85,
|
cursorHeight: 0.85,
|
||||||
placeholder: props.inCodeMode ? 'Code...' : props.placeholder,
|
placeholder: inCodeMode ? 'Code...' : placeholder,
|
||||||
extraKeys: {
|
extraKeys: {
|
||||||
'Enter': () => {
|
'Enter': () => {
|
||||||
this.submit();
|
this.submit();
|
||||||
|
},
|
||||||
|
'Esc': () => {
|
||||||
|
this.editor?.getInputField().blur();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -124,11 +132,11 @@ export default class ChatEditor extends Component {
|
|||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'chat fr h-100 flex bg-gray0-d lh-copy pl2 w-100 items-center' +
|
'chat fr h-100 flex bg-gray0-d lh-copy pl2 w-100 items-center' +
|
||||||
(props.inCodeMode ? ' code' : '')
|
(inCodeMode ? ' code' : '')
|
||||||
}
|
}
|
||||||
style={{ flexGrow: 1, maxHeight: '224px', width: 'calc(100% - 72px)' }}>
|
style={{ flexGrow: 1, maxHeight: '224px', width: 'calc(100% - 72px)' }}>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={props.message}
|
value={message}
|
||||||
options={options}
|
options={options}
|
||||||
onChange={(e, d, v) => this.messageChange(e, d, v)}
|
onChange={(e, d, v) => this.messageChange(e, d, v)}
|
||||||
editorDidMount={(editor) => {
|
editorDidMount={(editor) => {
|
||||||
@ -137,6 +145,7 @@ export default class ChatEditor extends Component {
|
|||||||
editor.focus();
|
editor.focus();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
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";
|
|
||||||
|
|
||||||
|
|
||||||
export const ChatHeader = (props) => {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
</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 "
|
|
||||||
}
|
|
||||||
style={{ height: 48 }}>
|
|
||||||
<SidebarSwitcher
|
|
||||||
sidebarShown={props.sidebarShown}
|
|
||||||
popout={props.popout}
|
|
||||||
api={props.api}
|
|
||||||
/>
|
|
||||||
<Link
|
|
||||||
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" : "")
|
|
||||||
}
|
|
||||||
style={{ width: "max-content" }}>
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
</Link>
|
|
||||||
<ChatTabBar
|
|
||||||
location={props.location}
|
|
||||||
station={props.station}
|
|
||||||
isOwner={deSig(props.match.params.ship) === window.ship}
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
import React, { PureComponent, Fragment } from "react";
|
|
||||||
import moment from "moment";
|
|
||||||
|
|
||||||
import { Message } from "./message";
|
|
||||||
|
|
||||||
type IMessage = Envelope & { pending?: boolean };
|
|
||||||
|
|
||||||
|
|
||||||
export const ChatMessage = (props) => {
|
|
||||||
const {
|
|
||||||
msg,
|
|
||||||
previousMsg,
|
|
||||||
nextMsg,
|
|
||||||
isLastUnread,
|
|
||||||
group,
|
|
||||||
association,
|
|
||||||
contacts,
|
|
||||||
unreadRef,
|
|
||||||
hideAvatars,
|
|
||||||
hideNicknames
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
// Render sigil if previous message is not by the same sender
|
|
||||||
const aut = ["author"];
|
|
||||||
const renderSigil =
|
|
||||||
_.get(nextMsg, aut) !== _.get(msg, aut, msg.author);
|
|
||||||
const paddingTop = renderSigil;
|
|
||||||
const paddingBot =
|
|
||||||
_.get(previousMsg, aut) !== _.get(msg, aut, msg.author);
|
|
||||||
|
|
||||||
const when = ["when"];
|
|
||||||
const dayBreak =
|
|
||||||
moment(_.get(nextMsg, when)).format("YYYY.MM.DD") !==
|
|
||||||
moment(_.get(msg, when)).format("YYYY.MM.DD");
|
|
||||||
|
|
||||||
const messageElem = (
|
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
} 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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -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,198 +0,0 @@
|
|||||||
import React, { Component, Fragment } from "react";
|
|
||||||
|
|
||||||
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";
|
|
||||||
|
|
||||||
const MAX_BACKLOG_SIZE = 1000;
|
|
||||||
const DEFAULT_BACKLOG_SIZE = 200;
|
|
||||||
const PAGE_SIZE = 50;
|
|
||||||
const INITIAL_LOAD = 20;
|
|
||||||
|
|
||||||
|
|
||||||
export class ChatWindow extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
numPages: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.hasAskedForMessages = false;
|
|
||||||
|
|
||||||
this.dismissUnread = this.dismissUnread.bind(this);
|
|
||||||
this.scrollIsAtBottom = this.scrollIsAtBottom.bind(this);
|
|
||||||
this.scrollIsAtTop = this.scrollIsAtTop.bind(this);
|
|
||||||
|
|
||||||
this.scrollReference = React.createRef();
|
|
||||||
this.unreadReference = React.createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.initialFetch();
|
|
||||||
|
|
||||||
if (this.state.numPages === 1 && this.props.unreadCount < INITIAL_LOAD) {
|
|
||||||
this.dismissUnread();
|
|
||||||
this.scrollToBottom();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.initialFetch();
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
const { props, state } = this;
|
|
||||||
|
|
||||||
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 (this.state.numPages === numPages) {
|
|
||||||
if (props.unreadCount > 20) {
|
|
||||||
this.scrollToUnread();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.setState({ numPages }, () => {
|
|
||||||
if (props.unreadCount > 20) {
|
|
||||||
this.scrollToUnread();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
state.numPages === 1 &&
|
|
||||||
this.props.unreadCount < INITIAL_LOAD &&
|
|
||||||
this.props.unreadCount > 0
|
|
||||||
) {
|
|
||||||
this.dismissUnread();
|
|
||||||
this.scrollToBottom();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToBottom() {
|
|
||||||
if (this.scrollReference.current) {
|
|
||||||
this.scrollReference.current.scrollToBottom();
|
|
||||||
}
|
|
||||||
if (this.state.numPages !== 1) {
|
|
||||||
this.setState({ numPages: 1 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollToUnread() {
|
|
||||||
if (this.scrollReference.current && this.unreadReference.current) {
|
|
||||||
this.scrollReference.current.scrollToReference(this.unreadReference);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dismissUnread() {
|
|
||||||
this.props.api.chat.read(this.props.station);
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchBacklog(size) {
|
|
||||||
const { props } = this;
|
|
||||||
|
|
||||||
if (
|
|
||||||
props.messages.length >= props.length ||
|
|
||||||
this.hasAskedForMessages ||
|
|
||||||
props.length <= 0
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
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}
|
|
||||||
hideAvatars={props.hideAvatars}
|
|
||||||
hideNicknames={props.hideNicknames}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ChatScrollContainer>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -27,6 +27,21 @@ const DISABLED_INLINE_TOKENS = [
|
|||||||
const MessageMarkdown = React.memo(props => (
|
const MessageMarkdown = React.memo(props => (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
{...props}
|
{...props}
|
||||||
|
unwrapDisallowed={true}
|
||||||
|
allowNode={(node, index, parent) => {
|
||||||
|
if (
|
||||||
|
node.type === 'blockquote'
|
||||||
|
&& parent.type === 'root'
|
||||||
|
&& node.children.length
|
||||||
|
&& node.children[0].type === 'paragraph'
|
||||||
|
&& node.children[0].position.start.offset < 2
|
||||||
|
) {
|
||||||
|
node.children[0].children[0].value = '>' + node.children[0].children[0].value;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
plugins={[[
|
plugins={[[
|
||||||
RemarkDisableTokenizers,
|
RemarkDisableTokenizers,
|
||||||
{ block: DISABLED_BLOCK_TOKENS, inline: DISABLED_INLINE_TOKENS }
|
{ block: DISABLED_BLOCK_TOKENS, inline: DISABLED_INLINE_TOKENS }
|
||||||
|
@ -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 deleteChat = () => {
|
||||||
const { isOwner, station, changeLoading, api } = props;
|
changeLoading(
|
||||||
const leaveButtonClasses = (!isOwner) ? 'pointer' : 'c-default';
|
true,
|
||||||
const deleteButtonClasses = (isOwner) ?
|
true,
|
||||||
'b--red2 red2 pointer bg-gray0-d' :
|
isOwner ? 'Deleting chat...' : 'Leaving chat...',
|
||||||
'b--gray3 gray3 bg-gray0-d c-default';
|
() => {
|
||||||
|
api.chat.delete(station);
|
||||||
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 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 React, { Component } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { ChannelItem } from './channel-item';
|
import { ChannelItem } from './channel-item';
|
||||||
|
import { deSig, cite } from "~/logic/lib/util";
|
||||||
|
|
||||||
export class GroupItem extends Component {
|
export class GroupItem extends Component {
|
||||||
render() {
|
render() {
|
||||||
const { props } = this;
|
const { props } = this;
|
||||||
const association = props.association ? props.association : {};
|
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';
|
let title = association['app-path'] ? association['app-path'] : 'Direct Messages';
|
||||||
if (association.metadata && association.metadata.title) {
|
if (association.metadata && association.metadata.title) {
|
||||||
@ -26,19 +28,19 @@ export class GroupItem extends Component {
|
|||||||
|
|
||||||
return bWhen - aWhen;
|
return bWhen - aWhen;
|
||||||
} else {
|
} else {
|
||||||
const aAssociation = a in props.chatMetadata ? props.chatMetadata[a] : {};
|
const aAssociation = a in props.chatMetadata ? props.chatMetadata[a] : {};
|
||||||
const bAssociation = b in props.chatMetadata ? props.chatMetadata[b] : {};
|
const bAssociation = b in props.chatMetadata ? props.chatMetadata[b] : {};
|
||||||
let aTitle = a;
|
let aTitle = a;
|
||||||
let bTitle = b;
|
let bTitle = b;
|
||||||
if (aAssociation.metadata && aAssociation.metadata.title) {
|
if (aAssociation.metadata && aAssociation.metadata.title) {
|
||||||
aTitle = (aAssociation.metadata.title !== '')
|
aTitle = (aAssociation.metadata.title !== '')
|
||||||
? aAssociation.metadata.title : a;
|
? aAssociation.metadata.title : a;
|
||||||
}
|
}
|
||||||
if (bAssociation.metadata && bAssociation.metadata.title) {
|
if (bAssociation.metadata && bAssociation.metadata.title) {
|
||||||
bTitle =
|
bTitle =
|
||||||
bAssociation.metadata.title !== '' ? bAssociation.metadata.title : b;
|
bAssociation.metadata.title !== '' ? bAssociation.metadata.title : b;
|
||||||
}
|
}
|
||||||
return aTitle.toLowerCase().localeCompare(bTitle.toLowerCase());
|
return aTitle.toLowerCase().localeCompare(bTitle.toLowerCase());
|
||||||
}
|
}
|
||||||
}).map((each, i) => {
|
}).map((each, i) => {
|
||||||
const unread = props.unreads[each];
|
const unread = props.unreads[each];
|
||||||
@ -47,9 +49,13 @@ export class GroupItem extends Component {
|
|||||||
each in props.chatMetadata &&
|
each in props.chatMetadata &&
|
||||||
props.chatMetadata[each].metadata
|
props.chatMetadata[each].metadata
|
||||||
) {
|
) {
|
||||||
title = props.chatMetadata[each].metadata.title
|
if (props.chatMetadata[each].metadata.title) {
|
||||||
? props.chatMetadata[each].metadata.title
|
title = props.chatMetadata[each].metadata.title
|
||||||
: each.substr(1);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DEFAULT_TITLE_REGEX.test(title) && props.index === "dm") {
|
||||||
|
title = title.replace(DEFAULT_TITLE_REGEX, '');
|
||||||
}
|
}
|
||||||
const selected = props.station === each;
|
const selected = props.station === each;
|
||||||
|
|
||||||
@ -73,6 +79,7 @@ export class GroupItem extends Component {
|
|||||||
|
|
||||||
if (props.index === 'dm') {
|
if (props.index === 'dm') {
|
||||||
dmLink = <Link
|
dmLink = <Link
|
||||||
|
key="link"
|
||||||
className="absolute right-0 f9 top-0 mr4 green2 bg-gray5 bg-gray1-d b--transparent br1"
|
className="absolute right-0 f9 top-0 mr4 green2 bg-gray5 bg-gray1-d b--transparent br1"
|
||||||
to="/~chat/new/dm"
|
to="/~chat/new/dm"
|
||||||
style={{ padding: '0rem 0.2rem' }}
|
style={{ padding: '0rem 0.2rem' }}
|
||||||
@ -82,7 +89,7 @@ export class GroupItem extends Component {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={first + 'relative'}>
|
<div className={first + 'relative'}>
|
||||||
<p className="f9 ph4 gray3">{title}</p>
|
<p className="f9 ph4 gray3" key="p">{title}</p>
|
||||||
{dmLink}
|
{dmLink}
|
||||||
{channelItems}
|
{channelItems}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
import TextContent from './content/text';
|
|
||||||
import CodeContent from './content/code';
|
|
||||||
import UrlContent from './content/url';
|
|
||||||
|
|
||||||
|
|
||||||
export default class MessageContent extends Component {
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { props } = this;
|
|
||||||
|
|
||||||
const content = props.letter;
|
|
||||||
|
|
||||||
if ('code' in content) {
|
|
||||||
return <CodeContent content={content} />;
|
|
||||||
} else if ('url' in content) {
|
|
||||||
return <UrlContent content={content} />;
|
|
||||||
} else if ('me' in content) {
|
|
||||||
return (
|
|
||||||
<p className='f7 i lh-copy v-top'>
|
|
||||||
{content.me}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else if ('text' in content) {
|
|
||||||
return <TextContent content={content} />;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,106 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import { OverlaySigil } from './overlay-sigil';
|
|
||||||
import MessageContent from './message-content';
|
|
||||||
import { uxToHex, cite, writeText } from '~/logic/lib/util';
|
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
|
|
||||||
export const Message = (props) => {
|
|
||||||
const pending = props.msg.pending ? ' o-40' : '';
|
|
||||||
const containerClass =
|
|
||||||
props.renderSigil ?
|
|
||||||
`w-100 f7 pl3 pt4 pr3 cf flex lh-copy ` + pending :
|
|
||||||
'w-100 pr3 cf hide-child flex' + pending;
|
|
||||||
|
|
||||||
const timestamp =
|
|
||||||
moment.unix(props.msg.when / 1000).format(
|
|
||||||
props.renderSigil ? 'hh:mm a' : 'hh:mm'
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={containerClass}
|
|
||||||
style={{
|
|
||||||
minHeight: 'min-content'
|
|
||||||
}}>
|
|
||||||
{
|
|
||||||
props.renderSigil ? (
|
|
||||||
renderWithSigil(props, timestamp)
|
|
||||||
) : (
|
|
||||||
<div className="flex w-100">
|
|
||||||
<p className="child pt2 pl2 pr1 mono f9 gray2 dib">{timestamp}</p>
|
|
||||||
<div className="fr f7 clamp-message white-d pr3 lh-copy"
|
|
||||||
style={{ flexGrow: 1 }}>
|
|
||||||
<MessageContent letter={props.msg.letter} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderWithSigil = (props, timestamp) => {
|
|
||||||
const paddingTop = props.paddingTop ? { 'paddingTop': '6px' } : '';
|
|
||||||
const datestamp =
|
|
||||||
'~' + moment.unix(props.msg.when / 1000).format('YYYY.M.D');
|
|
||||||
|
|
||||||
const contact = props.msg.author in props.contacts
|
|
||||||
? props.contacts[props.msg.author] : false;
|
|
||||||
const showNickname = !props.hideNicknames && contact?.nickname;
|
|
||||||
let name = `~${props.msg.author}`;
|
|
||||||
let color = '#000000';
|
|
||||||
let sigilClass = 'mix-blend-diff';
|
|
||||||
if (contact) {
|
|
||||||
name = showNickname
|
|
||||||
? contact.nickname
|
|
||||||
: `~${props.msg.author}`;
|
|
||||||
color = `#${uxToHex(contact.color)}`;
|
|
||||||
sigilClass = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (`~${props.msg.author}` === name) {
|
|
||||||
name = cite(props.msg.author);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex w-100">
|
|
||||||
<OverlaySigil
|
|
||||||
ship={props.msg.author}
|
|
||||||
contact={contact}
|
|
||||||
color={color}
|
|
||||||
sigilClass={sigilClass}
|
|
||||||
association={props.association}
|
|
||||||
group={props.group}
|
|
||||||
hideAvatars={props.hideAvatars}
|
|
||||||
hideNicknames={props.hideNicknames}
|
|
||||||
className="fl pr3 v-top bg-white bg-gray0-d"
|
|
||||||
/>
|
|
||||||
<div className="fr clamp-message white-d"
|
|
||||||
style={{ flexGrow: 1, marginTop: -8 }}>
|
|
||||||
<div className="hide-child" style={paddingTop}>
|
|
||||||
<p className={`v-mid f9 gray2 dib mr3 c-default`}>
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
'mw5 db truncate pointer ' +
|
|
||||||
(showNickname ? '' : 'mono')
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
writeText(props.msg.author);
|
|
||||||
}}
|
|
||||||
title={`~${props.msg.author}`}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p className={`v-mid mono f9 gray2 dib`}>{timestamp}</p>
|
|
||||||
<p className={`v-mid mono f9 ml2 gray2 dib child dn-s`}>
|
|
||||||
{datestamp}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<MessageContent letter={props.msg.letter} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { Sigil } from '~/logic/lib/sigil';
|
import { Sigil } from '~/logic/lib/sigil';
|
||||||
import {
|
import {
|
||||||
ProfileOverlay,
|
ProfileOverlay,
|
||||||
OVERLAY_HEIGHT
|
OVERLAY_HEIGHT
|
||||||
} from './profile-overlay';
|
} from './profile-overlay';
|
||||||
|
|
||||||
export class OverlaySigil extends Component {
|
export class OverlaySigil extends PureComponent {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.state = {
|
this.state = {
|
||||||
@ -19,40 +19,29 @@ export class OverlaySigil extends Component {
|
|||||||
|
|
||||||
this.profileShow = this.profileShow.bind(this);
|
this.profileShow = this.profileShow.bind(this);
|
||||||
this.profileHide = this.profileHide.bind(this);
|
this.profileHide = this.profileHide.bind(this);
|
||||||
|
this.updateContainerOffset = this.updateContainerOffset.bind(this);
|
||||||
this.updateContainerInterval = null;
|
this.updateContainerInterval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
profileShow() {
|
profileShow() {
|
||||||
this.updateContainerOffset();
|
this.updateContainerOffset();
|
||||||
this.setState({ profileClicked: true });
|
this.setState({ profileClicked: true });
|
||||||
this.updateContainerInterval = setInterval(
|
this.props.scrollWindow.addEventListener('scroll', this.updateContainerOffset);
|
||||||
this.updateContainerOffset.bind(this),
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
profileHide() {
|
profileHide() {
|
||||||
this.setState({ profileClicked: false });
|
this.setState({ profileClicked: false });
|
||||||
if(this.updateContainerInterval) {
|
this.props.scrollWindow.removeEventListener('scroll', this.updateContainerOffset, true);
|
||||||
clearInterval(this.updateContainerInterval);
|
|
||||||
this.updateContainerInterval = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateContainerOffset() {
|
updateContainerOffset() {
|
||||||
if (this.containerRef && this.containerRef.current) {
|
if (this.containerRef && this.containerRef.current) {
|
||||||
const parent = this.containerRef.current.offsetParent;
|
const container = this.containerRef.current;
|
||||||
const { offsetTop } = this.containerRef.current;
|
const scrollWindow = this.props.scrollWindow;
|
||||||
|
|
||||||
let bottomSpace, topSpace;
|
const bottomSpace = scrollWindow.scrollHeight - container.offsetTop - scrollWindow.scrollTop;
|
||||||
|
const topSpace = scrollWindow.offsetHeight - bottomSpace - OVERLAY_HEIGHT;
|
||||||
|
|
||||||
if(navigator.userAgent.includes('Firefox')) {
|
|
||||||
topSpace = offsetTop - parent.scrollTop - OVERLAY_HEIGHT / 2;
|
|
||||||
bottomSpace = parent.clientHeight - topSpace - OVERLAY_HEIGHT;
|
|
||||||
} else {
|
|
||||||
topSpace = offsetTop + parent.scrollHeight - parent.clientHeight - parent.scrollTop;
|
|
||||||
bottomSpace = parent.clientHeight - topSpace - OVERLAY_HEIGHT;
|
|
||||||
}
|
|
||||||
this.setState({
|
this.setState({
|
||||||
topSpace,
|
topSpace,
|
||||||
bottomSpace
|
bottomSpace
|
||||||
@ -60,6 +49,10 @@ export class OverlaySigil extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.scrollWindow?.removeEventListener('scroll', this.updateContainerOffset, true);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
const { hideAvatars } = props;
|
const { hideAvatars } = props;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { cite } from '~/logic/lib/util';
|
import { cite } from '~/logic/lib/util';
|
||||||
import { Sigil } from '~/logic/lib/sigil';
|
import { Sigil } from '~/logic/lib/sigil';
|
||||||
|
|
||||||
export const OVERLAY_HEIGHT = 250;
|
export const OVERLAY_HEIGHT = 250;
|
||||||
|
|
||||||
export class ProfileOverlay extends Component {
|
export class ProfileOverlay extends PureComponent {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
@ -1,105 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import S3Client from '~/logic/lib/s3';
|
|
||||||
|
|
||||||
export class S3Upload extends Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.s3 = new S3Client();
|
|
||||||
this.setCredentials(props.credentials, props.configuration);
|
|
||||||
this.inputRef = React.createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
isReady(creds, config) {
|
|
||||||
return (
|
|
||||||
Boolean(creds) &&
|
|
||||||
'endpoint' in creds &&
|
|
||||||
'accessKeyId' in creds &&
|
|
||||||
'secretAccessKey' in creds &&
|
|
||||||
creds.endpoint !== '' &&
|
|
||||||
creds.accessKeyId !== '' &&
|
|
||||||
creds.secretAccessKey !== '' &&
|
|
||||||
Boolean(config) &&
|
|
||||||
'currentBucket' in config &&
|
|
||||||
config.currentBucket !== ''
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const { props } = this;
|
|
||||||
this.setCredentials(props.credentials, props.configuration);
|
|
||||||
}
|
|
||||||
|
|
||||||
setCredentials(credentials, configuration) {
|
|
||||||
if (!this.isReady(credentials, configuration)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.s3.setCredentials(
|
|
||||||
credentials.endpoint,
|
|
||||||
credentials.accessKeyId,
|
|
||||||
credentials.secretAccessKey
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getFileUrl(endpoint, filename) {
|
|
||||||
return endpoint + '/' + filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange() {
|
|
||||||
const { props } = this;
|
|
||||||
if (!this.inputRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const files = this.inputRef.current.files;
|
|
||||||
if (files.length <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = files.item(0);
|
|
||||||
const bucket = props.configuration.currentBucket;
|
|
||||||
|
|
||||||
this.s3.upload(bucket, file.name, file).then((data) => {
|
|
||||||
if (!data || !('Location' in data)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.props.uploadSuccess(data.Location);
|
|
||||||
}).catch((err) => {
|
|
||||||
console.error(err);
|
|
||||||
this.props.uploadError(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onClick() {
|
|
||||||
if (!this.inputRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.inputRef.current.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { props } = this;
|
|
||||||
if (!this.isReady(props.credentials, props.configuration)) {
|
|
||||||
return <div></div>;
|
|
||||||
} else {
|
|
||||||
const classes = props.className ?
|
|
||||||
'pointer ' + props.className : 'pointer';
|
|
||||||
return (
|
|
||||||
<div className={classes}>
|
|
||||||
<input className="dn"
|
|
||||||
type="file"
|
|
||||||
id="fileElement"
|
|
||||||
ref={this.inputRef}
|
|
||||||
accept="image/*"
|
|
||||||
onChange={this.onChange.bind(this)}
|
|
||||||
/>
|
|
||||||
<img className="invert-d"
|
|
||||||
src="/~chat/img/ImageUpload.png"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
onClick={this.onClick.bind(this)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
export const UnreadNotice = (props) => {
|
export const UnreadNotice = (props) => {
|
||||||
const { unreadCount, unreadMsg, dismissUnread } = props;
|
const { unreadCount, unreadMsg, dismissUnread, onClick } = props;
|
||||||
|
|
||||||
if (!unreadMsg || (unreadCount === 0)) {
|
if (!unreadMsg || (unreadCount === 0)) {
|
||||||
return null;
|
return null;
|
||||||
@ -22,7 +22,7 @@ export const UnreadNotice = (props) => {
|
|||||||
"ba b--green2 green2 bg-white bg-gray0-d flex items-center " +
|
"ba b--green2 green2 bg-white bg-gray0-d flex items-center " +
|
||||||
"pa2 f9 justify-between br1"
|
"pa2 f9 justify-between br1"
|
||||||
}>
|
}>
|
||||||
<p className="lh-copy db">
|
<p className="lh-copy db pointer" onClick={onClick}>
|
||||||
{unreadCount} new messages since{' '}
|
{unreadCount} new messages since{' '}
|
||||||
{datestamp && (
|
{datestamp && (
|
||||||
<>
|
<>
|
||||||
|
@ -3,7 +3,7 @@ import { Spinner } from '~/views/components/Spinner';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { InviteSearch } from '~/views/components/InviteSearch';
|
import { InviteSearch } from '~/views/components/InviteSearch';
|
||||||
import urbitOb from 'urbit-ob';
|
import urbitOb from 'urbit-ob';
|
||||||
import { deSig } from '~/logic/lib/util';
|
import { deSig, cite } from '~/logic/lib/util';
|
||||||
|
|
||||||
export class NewDmScreen extends Component {
|
export class NewDmScreen extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -91,7 +91,7 @@ export class NewDmScreen extends Component {
|
|||||||
|
|
||||||
const aud = state.ship !== window.ship ? [`~${state.ships[0]}`] : [];
|
const aud = state.ship !== window.ship ? [`~${state.ships[0]}`] : [];
|
||||||
|
|
||||||
let title = `~${window.ship} <-> ~${state.ships[0]}`;
|
let title = `${cite(window.ship)} <-> ${cite(state.ships[0])}`;
|
||||||
|
|
||||||
if (state.title !== '') {
|
if (state.title !== '') {
|
||||||
title = state.title;
|
title = state.title;
|
||||||
|
@ -1,15 +1,12 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { deSig } from '~/logic/lib/util';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { ChatHeader } from './lib/chat-header';
|
import { deSig } from '~/logic/lib/util';
|
||||||
import { MetadataSettings } from './lib/metadata-settings';
|
import { MetadataSettings } from '~/views/components/metadata/settings';
|
||||||
|
import { Spinner } from '~/views/components/Spinner';
|
||||||
|
|
||||||
|
import ChatHeader from './lib/ChatHeader';
|
||||||
import { DeleteButton } from './lib/delete-button';
|
import { DeleteButton } from './lib/delete-button';
|
||||||
import { GroupifyButton } from './lib/groupify-button';
|
import { GroupifyButton } from './lib/groupify-button';
|
||||||
import { Spinner } from '~/views/components/Spinner';
|
|
||||||
import { ChatTabBar } from './lib/chat-tabbar';
|
|
||||||
import SidebarSwitcher from '~/views/components/SidebarSwitch';
|
|
||||||
|
|
||||||
|
|
||||||
export class SettingsScreen extends Component {
|
export class SettingsScreen extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -89,13 +86,17 @@ export class SettingsScreen extends Component {
|
|||||||
isOwner={isOwner}
|
isOwner={isOwner}
|
||||||
changeLoading={this.changeLoading}
|
changeLoading={this.changeLoading}
|
||||||
station={station}
|
station={station}
|
||||||
|
association={association}
|
||||||
|
contacts={contacts}
|
||||||
api={api} />
|
api={api} />
|
||||||
<MetadataSettings
|
<MetadataSettings
|
||||||
isOwner={isOwner}
|
isOwner={isOwner}
|
||||||
changeLoading={this.changeLoading}
|
changeLoading={this.changeLoading}
|
||||||
api={api}
|
api={api}
|
||||||
association={association}
|
association={association}
|
||||||
station={station} />
|
resource="chat"
|
||||||
|
app="chat"
|
||||||
|
/>
|
||||||
<Spinner
|
<Spinner
|
||||||
awaiting={this.state.awaiting}
|
awaiting={this.state.awaiting}
|
||||||
classes="absolute right-2 bottom-2 ba pa2 b--gray1-d"
|
classes="absolute right-2 bottom-2 ba pa2 b--gray1-d"
|
||||||
|
@ -116,20 +116,8 @@ h2 {
|
|||||||
100% {transform: rotate(360deg);}
|
100% {transform: rotate(360deg);}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* embeds */
|
.embed-container iframe {
|
||||||
.embed-container {
|
max-width: 100%;
|
||||||
position: relative;
|
|
||||||
height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
padding-bottom: 28.125%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.embed-container iframe, .embed-container object, .embed-container embed {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mh-16 {
|
.mh-16 {
|
||||||
@ -188,9 +176,6 @@ h2 {
|
|||||||
.h-100-minus-96-s {
|
.h-100-minus-96-s {
|
||||||
height: calc(100% - 96px);
|
height: calc(100% - 96px);
|
||||||
}
|
}
|
||||||
.embed-container {
|
|
||||||
padding-bottom: 56.25%;
|
|
||||||
}
|
|
||||||
.unread-notice {
|
.unread-notice {
|
||||||
top: 96px;
|
top: 96px;
|
||||||
}
|
}
|
||||||
@ -200,18 +185,12 @@ h2 {
|
|||||||
.flex-basis-250-m {
|
.flex-basis-250-m {
|
||||||
flex-basis: 250px;
|
flex-basis: 250px;
|
||||||
}
|
}
|
||||||
.embed-container {
|
|
||||||
padding-bottom: 56.25%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (min-width: 46.875em) and (max-width: 60em) {
|
@media all and (min-width: 46.875em) and (max-width: 60em) {
|
||||||
.flex-basis-250-l {
|
.flex-basis-250-l {
|
||||||
flex-basis: 250px;
|
flex-basis: 250px;
|
||||||
}
|
}
|
||||||
.embed-container {
|
|
||||||
padding-bottom: 37.5%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (min-width: 60em) {
|
@media all and (min-width: 60em) {
|
||||||
@ -252,10 +231,15 @@ blockquote {
|
|||||||
font-family: 'Inter';
|
font-family: 'Inter';
|
||||||
}
|
}
|
||||||
|
|
||||||
code, pre.code {
|
pre, code {
|
||||||
background-color: var(--light-gray);
|
background-color: var(--light-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
code, .code, .chat.code .react-codemirror2 .CodeMirror * {
|
code, .code, .chat.code .react-codemirror2 .CodeMirror * {
|
||||||
font-family: 'Source Code Pro';
|
font-family: 'Source Code Pro';
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@ export default class DojoApp extends Component {
|
|||||||
this.store.setStateHandler(this.setState.bind(this));
|
this.store.setStateHandler(this.setState.bind(this));
|
||||||
|
|
||||||
this.state = this.store.state;
|
this.state = this.store.state;
|
||||||
this.resetControllers();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resetControllers() {
|
resetControllers() {
|
||||||
@ -29,6 +28,7 @@ export default class DojoApp extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
this.resetControllers();
|
||||||
const channel = new window.channel();
|
const channel = new window.channel();
|
||||||
this.api = new Api(this.props.ship, channel);
|
this.api = new Api(this.props.ship, channel);
|
||||||
this.store.api = this.api;
|
this.store.api = this.api;
|
||||||
|
@ -5,7 +5,7 @@ import { Link } from 'react-router-dom';
|
|||||||
import { EditElement } from './edit-element';
|
import { EditElement } from './edit-element';
|
||||||
import { Spinner } from '~/views/components/Spinner';
|
import { Spinner } from '~/views/components/Spinner';
|
||||||
import { uxToHex } from '~/logic/lib/util';
|
import { uxToHex } from '~/logic/lib/util';
|
||||||
import { S3Upload } from './s3-upload';
|
import { S3Upload } from '~/views/components/s3-upload';
|
||||||
|
|
||||||
export class ContactCard extends Component {
|
export class ContactCard extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@ -492,7 +492,15 @@ export class ContactCard extends Component {
|
|||||||
credentials={props.s3.credentials}
|
credentials={props.s3.credentials}
|
||||||
uploadSuccess={this.uploadSuccess.bind(this)}
|
uploadSuccess={this.uploadSuccess.bind(this)}
|
||||||
uploadError={this.uploadError.bind(this)}
|
uploadError={this.uploadError.bind(this)}
|
||||||
/>
|
accept="image/*"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="invert-d"
|
||||||
|
src="/~chat/img/ImageUpload.png"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
/>
|
||||||
|
</S3Upload>
|
||||||
</span>
|
</span>
|
||||||
<EditElement
|
<EditElement
|
||||||
className="fr w-80"
|
className="fr w-80"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { FixedSizeList as List } from 'react-window';
|
import { Virtuoso as VirtualList } from 'react-virtuoso';
|
||||||
|
|
||||||
import { ContactItem } from './contact-item';
|
import { ContactItem } from './contact-item';
|
||||||
import { ShareSheet } from './share-sheet';
|
import { ShareSheet } from './share-sheet';
|
||||||
@ -14,17 +14,17 @@ import { Groups, Group } from '~/types/group-update';
|
|||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
|
|
||||||
interface ContactSidebarProps {
|
interface ContactSidebarProps {
|
||||||
activeDrawer: 'contacts' | 'detail' | 'rightPanel';
|
activeDrawer: 'contacts' | 'detail' | 'rightPanel';
|
||||||
groups: Groups;
|
groups: Groups;
|
||||||
group: Group
|
group: Group
|
||||||
contacts: Contacts;
|
contacts: Contacts;
|
||||||
path: Path;
|
path: Path;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
defaultContacts: Contacts;
|
defaultContacts: Contacts;
|
||||||
selectedContact?: PatpNoSig;
|
selectedContact?: PatpNoSig;
|
||||||
}
|
}
|
||||||
interface ContactSidebarState {
|
interface ContactSidebarState {
|
||||||
awaiting: boolean;
|
awaiting: boolean;
|
||||||
memberboxHeight: number;
|
memberboxHeight: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,17 +57,17 @@ export class ContactSidebar extends Component<ContactSidebarProps, ContactSideba
|
|||||||
|
|
||||||
const group = props.groups[props.path];
|
const group = props.groups[props.path];
|
||||||
|
|
||||||
const members = new Set(group.members || []);
|
const members = new Set(group.members || []);
|
||||||
|
|
||||||
const me = (window.ship in props.contacts)
|
const me = (window.ship in props.contacts)
|
||||||
? props.contacts[window.ship]
|
? props.contacts[window.ship]
|
||||||
: (window.ship in props.defaultContacts)
|
: (window.ship in props.defaultContacts)
|
||||||
? props.defaultContacts[window.ship]
|
? props.defaultContacts[window.ship]
|
||||||
: { color: '0x0', nickname: null, avatar: null };
|
: { color: '0x0', nickname: null, avatar: null };
|
||||||
|
|
||||||
const shareSheet =
|
const shareSheet =
|
||||||
!(window.ship in props.contacts) ?
|
!(window.ship in props.contacts) ?
|
||||||
( <ShareSheet
|
(<ShareSheet
|
||||||
ship={window.ship}
|
ship={window.ship}
|
||||||
nickname={me.nickname}
|
nickname={me.nickname}
|
||||||
avatar={me.avatar}
|
avatar={me.avatar}
|
||||||
@ -75,57 +75,57 @@ export class ContactSidebar extends Component<ContactSidebarProps, ContactSideba
|
|||||||
path={props.path}
|
path={props.path}
|
||||||
selected={props.path + '/' + window.ship === props.selectedContact}
|
selected={props.path + '/' + window.ship === props.selectedContact}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<h2 className="f9 pt4 pr4 pb2 pl4 gray2 c-default">You</h2>
|
<h2 className="f9 pt4 pr4 pb2 pl4 gray2 c-default">You</h2>
|
||||||
<ContactItem
|
<ContactItem
|
||||||
ship={window.ship}
|
ship={window.ship}
|
||||||
nickname={me.nickname}
|
nickname={me.nickname}
|
||||||
avatar={me.avatar}
|
avatar={me.avatar}
|
||||||
color={me.color}
|
color={me.color}
|
||||||
path={props.path}
|
path={props.path}
|
||||||
selected={props.path + '/' + window.ship === props.selectedContact}
|
selected={props.path + '/' + window.ship === props.selectedContact}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
members.delete(window.ship);
|
members.delete(window.ship);
|
||||||
|
|
||||||
const contactItems =
|
const contactItems =
|
||||||
Object.keys(props.contacts)
|
Object.keys(props.contacts)
|
||||||
.filter(c => c !== window.ship)
|
.filter(c => c !== window.ship)
|
||||||
.map((contact) => {
|
.map((contact) => {
|
||||||
members.delete(contact);
|
members.delete(contact);
|
||||||
const path = props.path + '/' + contact;
|
const path = props.path + '/' + contact;
|
||||||
const obj = props.contacts[contact];
|
const obj = props.contacts[contact];
|
||||||
return (
|
return (
|
||||||
<ContactItem
|
<ContactItem
|
||||||
key={contact}
|
key={contact}
|
||||||
ship={contact}
|
ship={contact}
|
||||||
nickname={obj.nickname}
|
nickname={obj.nickname}
|
||||||
color={obj.color}
|
color={obj.color}
|
||||||
avatar={obj.avatar}
|
avatar={obj.avatar}
|
||||||
path={props.path}
|
path={props.path}
|
||||||
selected={path === props.selectedContact}
|
selected={path === props.selectedContact}
|
||||||
share={false}
|
share={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const role = roleForShip(group, window.ship);
|
const role = roleForShip(group, window.ship);
|
||||||
|
|
||||||
const resource = resourceFromPath(props.path);
|
const resource = resourceFromPath(props.path);
|
||||||
const groupItems =
|
const groupItems =
|
||||||
Array.from(members).map((member) => {
|
Array.from(members).map((member) => {
|
||||||
const memberRole = roleForShip(group, member);
|
const memberRole = roleForShip(group, member);
|
||||||
const adminOpt = (role === 'admin' && memberRole !== 'admin')
|
const adminOpt = (role === 'admin' && memberRole !== 'admin')
|
||||||
|| (role === 'moderator' &&
|
|| (role === 'moderator' &&
|
||||||
(memberRole !== 'admin' && memberRole !== 'moderator'))
|
(memberRole !== 'admin' && memberRole !== 'moderator'))
|
||||||
? 'dib' : 'dn';
|
? 'dib' : 'dn';
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={member}
|
key={member}
|
||||||
className={'pl4 pt1 pb1 f9 flex justify-start content-center ' +
|
className={'pl4 pt1 pb1 f9 flex justify-start content-center ' +
|
||||||
'bg-white bg-gray0-d relative'}
|
'bg-white bg-gray0-d relative'}
|
||||||
>
|
>
|
||||||
<Sigil
|
<Sigil
|
||||||
ship={member}
|
ship={member}
|
||||||
@ -160,8 +160,8 @@ export class ContactSidebar extends Component<ContactSidebarProps, ContactSideba
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={this.memberbox} className={'bn br-m br-l br-xl b--gray4 b--gray1-d lh-copy h-100 ' +
|
<div ref={this.memberbox} className={'bn br-m br-l br-xl b--gray4 b--gray1-d lh-copy h-100 ' +
|
||||||
'flex-basis-100-s flex-basis-30-ns mw5-m mw5-l mw5-xl relative ' +
|
'flex-basis-100-s flex-basis-30-ns mw5-m mw5-l mw5-xl relative ' +
|
||||||
'overflow-hidden flex-shrink-0 ' + responsiveClasses}
|
'overflow-hidden flex-shrink-0 ' + responsiveClasses}
|
||||||
>
|
>
|
||||||
<div className="pt3 pb5 pl3 f8 db dn-m dn-l dn-xl">
|
<div className="pt3 pb5 pl3 f8 db dn-m dn-l dn-xl">
|
||||||
<Link to="/~groups/">{'⟵ All Groups'}</Link>
|
<Link to="/~groups/">{'⟵ All Groups'}</Link>
|
||||||
@ -180,23 +180,21 @@ export class ContactSidebar extends Component<ContactSidebarProps, ContactSideba
|
|||||||
>Channels</Link>
|
>Channels</Link>
|
||||||
{shareSheet}
|
{shareSheet}
|
||||||
<h2 className="f9 pt4 pr4 pb2 pl4 gray2 c-default">Members</h2>
|
<h2 className="f9 pt4 pr4 pb2 pl4 gray2 c-default">Members</h2>
|
||||||
<List
|
<VirtualList
|
||||||
height={this.state.memberboxHeight}
|
style={{ height: this.state.memberboxHeight, width: '100%' }}
|
||||||
className="flex-auto"
|
className="flex-auto"
|
||||||
itemCount={contactItems.length + groupItems.length}
|
totalCount={contactItems.length + groupItems.length}
|
||||||
itemSize={44}
|
itemHeight={44} // We happen to know this
|
||||||
width="100%"
|
item={
|
||||||
>
|
(index) => index <= (contactItems.length - 1) // If the index is within the length of contact items,
|
||||||
{({ index, style }) => (<div style={style}>{
|
|
||||||
index <= (contactItems.length - 1) // If the index is within the length of contact items,
|
|
||||||
? contactItems[index] // show a contact item
|
? contactItems[index] // show a contact item
|
||||||
: groupItems[index - contactItems.length] // Otherwise show a group item
|
: groupItems[index - contactItems.length] // Otherwise show a group item
|
||||||
}</div>)}
|
}
|
||||||
</List>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<Spinner awaiting={this.state.awaiting} text="Removing from group..." classes="pa2 ba absolute right-1 bottom-1 b--gray1-d" />
|
<Spinner awaiting={this.state.awaiting} text="Removing from group..." classes="pa2 ba absolute right-1 bottom-1 b--gray1-d" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user