mirror of
https://github.com/urbit/shrub.git
synced 2024-11-28 22:33:06 +03:00
Merge branch 'release/link' into lt/link-js
This commit is contained in:
commit
20832ab53a
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Landscape feature request
|
||||
url: https://github.com/urbit/landscape/issues/new?assignees=&labels=feature+request&template=feature_request.md&title=
|
||||
about: Landscape is comprised of Tlon's user applications and client for Urbit. Submit Landscape feature requests here.
|
||||
- name: urbit-dev mailing list
|
||||
url: https://groups.google.com/a/urbit.org/g/dev
|
||||
about: Developer questions and discussions also take place on the urbit-dev mailing list.
|
4
.github/ISSUE_TEMPLATE/os1-bug-report.md
vendored
4
.github/ISSUE_TEMPLATE/os1-bug-report.md
vendored
@ -1,6 +1,6 @@
|
||||
---
|
||||
name: OS1 Bug report
|
||||
about: 'Use this template to file a bug for any OS1 app: Chat, Publish, Links, Groups,
|
||||
name: Landscape bug report
|
||||
about: 'Use this template to file a bug for any Landscape app: Chat, Publish, Links, Groups,
|
||||
Weather or Clock'
|
||||
title: ''
|
||||
labels: landscape
|
||||
|
@ -2,12 +2,14 @@
|
||||
|
||||
Thank you for your interest in contributing to Urbit.
|
||||
|
||||
See [urbit.org/docs/getting-started][start] for basic orientation and usage
|
||||
See [urbit.org/using/install][start] for basic orientation and usage
|
||||
instructions. You may also want to subscribe to [urbit-dev][list], the Urbit
|
||||
development mailing list. For specific information on contributing to the Urbit
|
||||
interface, see its [contribution guidelines][interface].
|
||||
|
||||
[start]: https://urbit.org/docs/getting-started/#arvo
|
||||
For information on Arvo's maintainers, see [pkg/arvo][main].
|
||||
|
||||
[start]: https://urbit.org/using/install
|
||||
[interface]: /pkg/interface/CONTRIBUTING.md
|
||||
|
||||
## Fake ships
|
||||
@ -45,11 +47,10 @@ The canonical source tree is the `master` branch of
|
||||
`master` when commencing new work; similarly, when we pull in your
|
||||
contribution, we'll do so by merging it to `master`.
|
||||
|
||||
Since we use GitHub, it's helpful (though not required) to contribute via a
|
||||
GitHub pull request. You can also post patches to the [mailing list][list],
|
||||
email them to maintainers, or request a maintainer pull from your tree directly
|
||||
-- but note that some maintainers will be more receptive to these methods than
|
||||
others.
|
||||
Since we use GitHub, we request you contribute via a GitHub pull request. Tag
|
||||
the [maintainer][main] for the component. If you have a question for the
|
||||
maintainer, you can direct message them from your Urbit ship using that
|
||||
information.
|
||||
|
||||
When contributing changes, via whatever means, make sure you describe them
|
||||
appropriately. You should attach a reasonably high-level summary of what the
|
||||
@ -58,8 +59,8 @@ exist, e.g. a GitHub issue, a mailing list discussion, a UP, etc. [Here][jbpr]
|
||||
is a good example of a pull request with a useful, concise description.
|
||||
|
||||
If your changes replace significant extant functionality, be sure to compare
|
||||
them with the thing you're replacing. You may also want to cc maintainers,
|
||||
reviewers, or other parties who might have a particular interest in what you're
|
||||
them with the thing you're replacing. You may also want to cc reviewers,
|
||||
or other parties who might have a particular interest in what you're
|
||||
contributing.
|
||||
|
||||
[jbpr]: https://github.com/urbit/urbit/pull/1782
|
||||
@ -283,3 +284,4 @@ Questions or other communications about contributing to Urbit can go to
|
||||
[reba]: https://git-rebase.io/
|
||||
[issu]: https://github.com/urbit/urbit/issues
|
||||
[hoon]: https://urbit.org/docs/learn/hoon/style/
|
||||
[main]: https://github.com/urbit/urbit/tree/master/pkg/arvo#maintainers
|
43
README.md
43
README.md
@ -1,27 +1,38 @@
|
||||
# Urbit
|
||||
|
||||
A personal server operating function.
|
||||
[Urbit](https://urbit.org) is a personal server stack built from scratch. It
|
||||
has an identity layer (Azimuth), virtual machine (Vere), and operating system
|
||||
(Arvo).
|
||||
|
||||
> The Urbit address space, Azimuth, is now live on the Ethereum blockchain. You
|
||||
> can find it at [`0x223c067f8cf28ae173ee5cafea60ca44c335fecb`][azim] or
|
||||
> [`azimuth.eth`][aens]. Owners of Azimuth points (galaxies, stars, or planets)
|
||||
> can view or manage them using [Bridge][brid], and can also use them to boot
|
||||
> [Arvo][arvo], the Urbit OS.
|
||||
A running Urbit "ship" is designed to operate with other ships peer-to-peer.
|
||||
Urbit is a general-purpose, peer-to-peer computer and network.
|
||||
|
||||
This repository contains:
|
||||
|
||||
- The [Arvo OS][arvo]
|
||||
- [herb][herb], a tool for Unix control of an Urbit ship
|
||||
- Source code for [Landscape's web interface][land]
|
||||
- Source code for the [vere][vere] virtual machine.
|
||||
|
||||
For more on the identity layer, see [Azimuth][azim]. To manage your Urbit
|
||||
identity, use [Bridge][brid].
|
||||
|
||||
[azim]: https://etherscan.io/address/0x223c067f8cf28ae173ee5cafea60ca44c335fecb
|
||||
[aens]: https://etherscan.io/address/azimuth.eth
|
||||
[brid]: https://github.com/urbit/bridge
|
||||
[arvo]: https://github.com/urbit/urbit/tree/master/pkg/arvo
|
||||
[azim]: https://github.com/urbit/azimuth
|
||||
[brid]: https://github.com/urbit/bridge
|
||||
[herb]: https://github.com/urbit/urbit/tree/master/pkg/herb
|
||||
[land]: https://github.com/urbit/urbit/tree/master/pkg/interface
|
||||
[vere]: https://github.com/urbit/urbit/tree/master/pkg/urbit
|
||||
|
||||
## Install
|
||||
|
||||
To install and run Urbit, please follow the instructions at
|
||||
[urbit.org/docs/getting-started/][start]. You'll be on the live network in a
|
||||
[urbit.org/using/install][start]. You'll be on the live network in a
|
||||
few minutes.
|
||||
|
||||
If you're interested in Urbit development, keep reading.
|
||||
|
||||
[start]: https://urbit.org/docs/getting-started/
|
||||
[start]: https://urbit.org/using/install/
|
||||
|
||||
## Development
|
||||
|
||||
@ -38,7 +49,7 @@ The Makefile in the project's root directory contains useful phony targets for
|
||||
building, installing, testing, and so on. You can use it to avoid dealing with
|
||||
Nix explicitly.
|
||||
|
||||
To build Urbit, for example, use:
|
||||
To build the Urbit virtual machine binary, for example, use:
|
||||
|
||||
```
|
||||
make build
|
||||
@ -68,12 +79,10 @@ Contributions of any form are more than welcome! Please take a look at our
|
||||
[contributing guidelines][cont] for details on our git practices, coding
|
||||
styles, how we manage issues, and so on.
|
||||
|
||||
You might also be interested in:
|
||||
For instructions on contributing to Landscape, see [its][lcont] guidelines.
|
||||
|
||||
- joining the [urbit-dev][list] mailing list.
|
||||
- [applying to Hoon School][mail], a course we run to teach the Hoon
|
||||
programming language and Urbit application development.
|
||||
You might also be interested in joining the [urbit-dev][list] mailing list.
|
||||
|
||||
[list]: https://groups.google.com/a/urbit.org/forum/#!forum/dev
|
||||
[mail]: mailto:support@urbit.org
|
||||
[cont]: https://github.com/urbit/urbit/blob/master/CONTRIBUTING.md
|
||||
[lcont]: https://github.com/urbit/urbit/blob/master/pkg/interface/CONTRIBUTING.md
|
@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cfb556a9e6b473f6cf6c75b30a3b12cb986e57df1600dad4383b9d3380cffdb6
|
||||
size 6263010
|
||||
oid sha256:3f5741b71f11a562d443fc619eb1b6bb1ccf419375aa2f1eebbd1c06dce20cd0
|
||||
size 6268477
|
||||
|
@ -41,20 +41,20 @@ Most parts of Arvo have dedicated maintainers.
|
||||
|
||||
* `/sys/hoon`: @pilfer-pandex (~pilfer-pandex)
|
||||
* `/sys/zuse`: @pilfer-pandex (~pilfer-pandex)
|
||||
* `/sys/arvo`: @jtobin (~nidsut-tomdun)
|
||||
* `/sys/vane/ames`: @belisarius222 (~rovnys-ricfer) & @joemfb (~master-morzod)
|
||||
* `/sys/arvo`: @joemfb (~master-morzod)
|
||||
* `/sys/vane/ames`: @belisarius222 (~rovnys-ricfer) & @philipcmonk (~wicdev-wisryt)
|
||||
* `/sys/vane/behn`: @belisarius222 (~rovnys-ricfer)
|
||||
* `/sys/vane/clay`: @philipcmonk (~wicdev-wisryt)
|
||||
* `/sys/vane/dill`: @bernardodelaplaz (~rigdyn-sondur)
|
||||
* `/sys/vane/clay`: @philipcmonk (~wicdev-wisryt) & @belisarius222 (~rovnys-ricfer)
|
||||
* `/sys/vane/dill`: @joemfb (~master-morzod)
|
||||
* `/sys/vane/eyre`: @eglaysher (~littel-ponnys)
|
||||
* `/sys/vane/ford`: @belisarius222 (~rovnys-ricfer) & @eglaysher (~littel-ponnys)
|
||||
* `/sys/vane/gall`: @jtobin (~nidsut-tomdun)
|
||||
* `/sys/vane/jael`: @fang- (~palfun-foslup) & @joemfb (~master-morzod)
|
||||
* `/sys/vane/gall`: @philipcmonk (~wicdev-wisryt)
|
||||
* `/sys/vane/jael`: @fang- (~palfun-foslup) & @philipcmonk (~wicdev-wisryt)
|
||||
* `/app/acme`: @joemfb (~master-morzod)
|
||||
* `/app/dns`: @joemfb (~master-morzod)
|
||||
* `/app/hall`: @fang- (~palfun-foslup)
|
||||
* `/app/talk`: @fang- (~palfun-foslup)
|
||||
* `/app/aqua`: @philipcmonk (~wicdev-wisryt)
|
||||
* `/app/hood`: @belisarius222 (~rovnys-ricfer)
|
||||
* `/lib/hood/drum`: @philipcmonk (~wicdev-wisryt)
|
||||
* `/lib/hood/kiln`: @philipcmonk (~wicdev-wisryt)
|
||||
* `/lib/test`: @eglaysher (~littel-ponnys)
|
||||
|
||||
## Contributing
|
||||
|
@ -1,4 +1,4 @@
|
||||
:: chat-hook:
|
||||
:: chat-hook [landscape]:
|
||||
:: mirror chat data from foreign to local based on read permissions
|
||||
:: allow sending chat messages to foreign paths based on write perms
|
||||
::
|
||||
@ -114,7 +114,7 @@
|
||||
i.syncs
|
||||
?> ?=(^ pax)
|
||||
?. =('~' i.pax)
|
||||
$(syncs t.syncs)
|
||||
$(syncs t.syncs)
|
||||
=/ new-path=path
|
||||
t.pax
|
||||
=. synced.old
|
||||
|
@ -1,4 +1,6 @@
|
||||
:: chat-store: data store that holds linear sequences of chat messages
|
||||
:: chat-store [landscape]:
|
||||
::
|
||||
:: data store that holds linear sequences of chat messages
|
||||
::
|
||||
/+ store=chat-store, default-agent, verb, dbug, group-store
|
||||
~% %chat-store-top ..is ~
|
||||
|
@ -1,4 +1,6 @@
|
||||
:: chat-view: sets up chat JS client, paginates data, and combines commands
|
||||
:: chat-view [landscape]:
|
||||
::
|
||||
:: sets up chat JS client, paginates data, and combines commands
|
||||
:: into semantic actions for the UI
|
||||
::
|
||||
/- *permission-store,
|
||||
@ -157,7 +159,7 @@
|
||||
(on-arvo:def wire sign-arvo)
|
||||
::
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
++ on-load
|
||||
|= old-vase=vase
|
||||
^- (quip card _this)
|
||||
=/ old ((soft state-0) q.old-vase)
|
||||
@ -211,8 +213,8 @@
|
||||
?- -.act
|
||||
%create
|
||||
?> ?=(^ app-path.act)
|
||||
?> ?| =(+:group-path.act app-path.act)
|
||||
=(~(tap in members.act) ~)
|
||||
?> ?| =(+:group-path.act app-path.act)
|
||||
=(~(tap in members.act) ~)
|
||||
==
|
||||
?^ (chat-scry app-path.act)
|
||||
~& %chat-already-exists
|
||||
@ -295,6 +297,7 @@
|
||||
~[(chat-hook-poke %add-synced ship.act app-path.act ask-history.act)]
|
||||
=/ rid=resource
|
||||
(de-path:resource ship+app-path.act)
|
||||
?: =(our.bol entity.rid) ~
|
||||
=/ =cage
|
||||
:- %group-update
|
||||
!> ^- action:group-store
|
||||
|
@ -1,4 +1,6 @@
|
||||
:: clock: deprecated, should be removed
|
||||
:: clock [landscape]:
|
||||
::
|
||||
:: deprecated, should be removed
|
||||
::
|
||||
/+ *server, default-agent, verb, dbug
|
||||
=, format
|
||||
|
@ -1,4 +1,5 @@
|
||||
:: contact-hook:
|
||||
:: contact-hook [landscape]
|
||||
::
|
||||
::
|
||||
/- group-hook,
|
||||
*contact-hook,
|
||||
@ -54,7 +55,7 @@
|
||||
=/ old !<(versioned-state old-vase)
|
||||
=| cards=(list card)
|
||||
|^
|
||||
|- ^- (quip card _this)
|
||||
|- ^- (quip card _this)
|
||||
?: ?=(%3 -.old)
|
||||
[cards this(state old)]
|
||||
?: ?=(%2 -.old)
|
||||
@ -80,7 +81,7 @@
|
||||
%_ $
|
||||
-.old %2
|
||||
::
|
||||
synced.old
|
||||
synced.old
|
||||
%- malt
|
||||
%+ turn
|
||||
~(tap by synced.old)
|
||||
@ -126,7 +127,7 @@
|
||||
%json
|
||||
(poke-json:cc !<(json vase))
|
||||
::
|
||||
%contact-action
|
||||
%contact-action
|
||||
(poke-contact-action:cc !<(contact-action vase))
|
||||
::
|
||||
%contact-hook-action
|
||||
@ -149,7 +150,7 @@
|
||||
%kick [(kick:cc wire) this]
|
||||
%watch-ack
|
||||
=^ cards state
|
||||
(watch-ack:cc wire p.sign)
|
||||
(watch-ack:cc wire p.sign)
|
||||
[cards this]
|
||||
::
|
||||
%fact
|
||||
@ -301,8 +302,8 @@
|
||||
[%pass /group %agent [our.bol %group-store] %watch /groups]~
|
||||
::
|
||||
[%contacts @ *]
|
||||
=/ wir
|
||||
?: =(%ship i.t.wir)
|
||||
=/ wir
|
||||
?: =(%ship i.t.wir)
|
||||
wir
|
||||
(migrate wir)
|
||||
?> ?=([%contacts @ @ *] wir)
|
||||
|
@ -1,4 +1,6 @@
|
||||
:: contact-store: data store that holds group-based contact data
|
||||
:: contact-store [landscape]:
|
||||
::
|
||||
:: data store that holds group-based contact data
|
||||
::
|
||||
/+ *contact-json, default-agent, dbug
|
||||
|%
|
||||
@ -253,7 +255,7 @@
|
||||
++ send-diff
|
||||
|= [pax=path upd=contact-update]
|
||||
^- (list card)
|
||||
:~ :*
|
||||
:~ :*
|
||||
%give %fact
|
||||
~[/all /updates [%contacts pax]]
|
||||
%contact-update !>(upd)
|
||||
|
@ -1,4 +1,6 @@
|
||||
:: contact-view: sets up contact JS client and combines commands
|
||||
:: contact-view [landscape]:
|
||||
::
|
||||
:: sets up contact JS client and combines commands
|
||||
:: into semantic actions for the UI
|
||||
::
|
||||
/-
|
||||
|
@ -1,3 +1,7 @@
|
||||
:: file-server [landscape]:
|
||||
::
|
||||
:: mounts HTTP endpoints for Landscape (and third-party) user applications
|
||||
::
|
||||
/- srv=file-server, glob
|
||||
/+ *server, default-agent, verb, dbug
|
||||
|%
|
||||
@ -218,8 +222,8 @@
|
||||
::
|
||||
[~ %html]
|
||||
%. file
|
||||
%* . html-response:gen
|
||||
cache
|
||||
%* . html-response:gen
|
||||
cache
|
||||
!=(/app/landscape/index/html (slag 3 scry-path))
|
||||
==
|
||||
==
|
||||
|
@ -1,7 +1,11 @@
|
||||
:: glob [landscape]:
|
||||
::
|
||||
:: prompts content delivery and Gall state storage for Landscape JS blob
|
||||
::
|
||||
/- glob
|
||||
/+ default-agent, verb, dbug
|
||||
|%
|
||||
++ hash 0v6.8fpt6.7mcjg.nb019.df3fo.haav6
|
||||
++ hash 0v4.kdc52.27is2.c7mnh.7vsrb.ij4jo
|
||||
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
||||
+$ all-states
|
||||
$% state-0
|
||||
|
48
pkg/arvo/app/graph-pull-hook.hoon
Normal file
48
pkg/arvo/app/graph-pull-hook.hoon
Normal file
@ -0,0 +1,48 @@
|
||||
/- *resource
|
||||
/+ store=graph-store, graph, default-agent, verb, dbug, pull-hook
|
||||
~% %graph-pull-hook-top ..is ~
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
++ config
|
||||
^- config:pull-hook
|
||||
:* %graph-store
|
||||
update:store
|
||||
%graph-update
|
||||
%graph-push-hook
|
||||
==
|
||||
--
|
||||
::
|
||||
%- agent:dbug
|
||||
^- agent:gall
|
||||
%- (agent:pull-hook config)
|
||||
^- (pull-hook:pull-hook config)
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
dep ~(. (default:pull-hook this config) bowl)
|
||||
::
|
||||
++ on-init on-init:def
|
||||
++ on-save !>(~)
|
||||
++ on-load on-load:def
|
||||
++ on-poke on-poke:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-fail on-fail:def
|
||||
++ on-agent on-agent:def
|
||||
++ on-watch on-watch:def
|
||||
++ on-leave on-leave:def
|
||||
++ on-pull-nack
|
||||
|= [=resource =tang]
|
||||
^- (quip card _this)
|
||||
:_ this
|
||||
=- [%pass /pull-nack %agent [our.bowl %graph-store] %poke %graph-update -]~
|
||||
!> ^- update:store
|
||||
[%0 now.bowl [%archive-graph resource]]
|
||||
::
|
||||
++ on-pull-kick
|
||||
|= =resource
|
||||
^- (unit path)
|
||||
=/ maybe-time (peek-update-log:graph resource)
|
||||
?~ maybe-time `/
|
||||
`/(scot %da u.maybe-time)
|
||||
--
|
123
pkg/arvo/app/graph-push-hook.hoon
Normal file
123
pkg/arvo/app/graph-push-hook.hoon
Normal file
@ -0,0 +1,123 @@
|
||||
/+ store=graph-store
|
||||
/+ met=metadata
|
||||
/+ res=resource
|
||||
/+ graph
|
||||
/+ group
|
||||
/+ default-agent
|
||||
/+ dbug
|
||||
/+ push-hook
|
||||
~% %graph-push-hook-top ..is ~
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
++ config
|
||||
^- config:push-hook
|
||||
:* %graph-store
|
||||
/updates
|
||||
update:store
|
||||
%graph-update
|
||||
%graph-pull-hook
|
||||
==
|
||||
::
|
||||
+$ agent (push-hook:push-hook config)
|
||||
::
|
||||
++ is-member
|
||||
|= [=resource:res =bowl:gall]
|
||||
^- ?
|
||||
=/ grp ~(. group bowl)
|
||||
=/ group-paths (groups-from-resource:met [%graph (en-path:res resource)])
|
||||
?~ group-paths %.n
|
||||
(is-member:grp src.bowl i.group-paths)
|
||||
--
|
||||
::
|
||||
%- agent:dbug
|
||||
^- agent:gall
|
||||
%- (agent:push-hook config)
|
||||
^- agent
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
grp ~(. group bowl)
|
||||
gra ~(. graph bowl)
|
||||
::
|
||||
++ on-init on-init:def
|
||||
++ on-save !>(~)
|
||||
++ on-load on-load:def
|
||||
++ on-poke on-poke:def
|
||||
++ on-agent on-agent:def
|
||||
++ on-watch on-watch:def
|
||||
++ on-leave on-leave:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-fail on-fail:def
|
||||
::
|
||||
++ should-proxy-update
|
||||
|= =vase
|
||||
^- ?
|
||||
=/ =update:store !<(update:store vase)
|
||||
?- -.q.update
|
||||
%add-graph (is-member resource.q.update bowl)
|
||||
%remove-graph (is-member resource.q.update bowl)
|
||||
%add-nodes (is-member resource.q.update bowl)
|
||||
%remove-nodes (is-member resource.q.update bowl)
|
||||
%add-signatures (is-member resource.uid.q.update bowl)
|
||||
%remove-signatures (is-member resource.uid.q.update bowl)
|
||||
%archive-graph (is-member resource.q.update bowl)
|
||||
%unarchive-graph %.n
|
||||
%add-tag %.n
|
||||
%remove-tag %.n
|
||||
%keys %.n
|
||||
%tags %.n
|
||||
%tag-queries %.n
|
||||
%run-updates (is-member resource.q.update bowl)
|
||||
==
|
||||
::
|
||||
++ resource-for-update
|
||||
|= =vase
|
||||
^- (unit resource:res)
|
||||
=/ =update:store !<(update:store vase)
|
||||
?- -.q.update
|
||||
%add-graph `resource.q.update
|
||||
%remove-graph `resource.q.update
|
||||
%add-nodes `resource.q.update
|
||||
%remove-nodes `resource.q.update
|
||||
%add-signatures `resource.uid.q.update
|
||||
%remove-signatures `resource.uid.q.update
|
||||
%archive-graph `resource.q.update
|
||||
%unarchive-graph ~
|
||||
%add-tag ~
|
||||
%remove-tag ~
|
||||
%keys ~
|
||||
%tags ~
|
||||
%tag-queries ~
|
||||
%run-updates `resource.q.update
|
||||
==
|
||||
::
|
||||
++ initial-watch
|
||||
|= [=path =resource:res]
|
||||
^- vase
|
||||
?> (is-member resource bowl)
|
||||
!> ^- update:store
|
||||
?~ path
|
||||
:: new subscribe
|
||||
::
|
||||
(get-graph:gra resource)
|
||||
:: resubscribe
|
||||
::
|
||||
=/ =time (slav %da i.path)
|
||||
=/ =update-log:store (get-update-log-subset:gra resource time)
|
||||
[%0 now.bowl [%run-updates resource update-log]]
|
||||
::
|
||||
++ take-update
|
||||
|= =vase
|
||||
^- [(list card) agent]
|
||||
=/ =update:store !<(update:store vase)
|
||||
?+ -.q.update [~ this]
|
||||
%remove-graph
|
||||
:_ this
|
||||
[%give %kick ~[resource+(en-path:res resource.q.update)] ~]~
|
||||
::
|
||||
%archive-graph
|
||||
:_ this
|
||||
[%give %kick ~[resource+(en-path:res resource.q.update)] ~]~
|
||||
==
|
||||
--
|
@ -1,3 +1,6 @@
|
||||
:: graph-store [landscape]
|
||||
::
|
||||
::
|
||||
/+ store=graph-store, sigs=signatures, res=resource, default-agent, dbug
|
||||
~% %graph-store-top ..is ~
|
||||
|%
|
||||
@ -282,7 +285,7 @@
|
||||
?~ index graph
|
||||
=* atom i.index
|
||||
=/ =node:store
|
||||
~| "node does not exist to add signatures to!"
|
||||
~| "node does not exist to add signatures to!"
|
||||
(need (get:orm graph atom))
|
||||
:: last index in list
|
||||
::
|
||||
@ -327,7 +330,7 @@
|
||||
?~ index graph
|
||||
=* atom i.index
|
||||
=/ =node:store
|
||||
~| "node does not exist to add signatures to!"
|
||||
~| "node does not exist to add signatures to!"
|
||||
(need (get:orm graph atom))
|
||||
:: last index in list
|
||||
::
|
||||
@ -529,6 +532,15 @@
|
||||
^- [index:store node:store]
|
||||
[(snoc index atom) node]
|
||||
==
|
||||
::
|
||||
[%x %update-log-subset @ @ @ @ ~]
|
||||
=/ =ship (slav %p i.t.t.path)
|
||||
=/ =term i.t.t.t.path
|
||||
=/ start=(unit time) (slaw %da i.t.t.t.t.path)
|
||||
=/ end=(unit time) (slaw %da i.t.t.t.t.t.path)
|
||||
=/ update-log=(unit update-log:store) (~(get by update-logs) [ship term])
|
||||
?~ update-log [~ ~]
|
||||
``noun+!>((subset:orm-log u.update-log start end))
|
||||
::
|
||||
[%x %update-log @ @ ~]
|
||||
=/ =ship (slav %p i.t.t.path)
|
||||
@ -543,7 +555,7 @@
|
||||
=/ update-log=(unit update-log:store) (~(get by update-logs) [ship term])
|
||||
?~ update-log [~ ~]
|
||||
=/ result=(unit [time update:store])
|
||||
(peek:orm-log:store u.update-log)
|
||||
(peek:orm-log:store u.update-log)
|
||||
?~ result [~ ~]
|
||||
``noun+!>([~ -.u.result])
|
||||
==
|
||||
|
@ -1,4 +1,6 @@
|
||||
:: group-hook: allow syncing group data from foreign paths to local paths
|
||||
:: group-hook [landscape]:
|
||||
::
|
||||
:: allow syncing group data from foreign paths to local paths
|
||||
::
|
||||
/- *group, hook=group-hook, *invite-store
|
||||
/+ default-agent, verb, dbug, store=group-store, grpl=group, pull-hook, push-hook, resource
|
||||
@ -58,7 +60,7 @@
|
||||
:: ignore duplicate publish groups
|
||||
?: =(4 (lent path))
|
||||
~& "ignoring: {<path>}"
|
||||
~
|
||||
~
|
||||
=/ pax=^path
|
||||
?: =('~' i.path)
|
||||
t.path
|
||||
|
@ -1,5 +1,6 @@
|
||||
:: group-hook: allow syncing group data from foreign paths to local paths
|
||||
:: group-hook [landscape]:
|
||||
::
|
||||
:: allow syncing group data from foreign paths to local paths
|
||||
::
|
||||
/- *group, hook=group-hook, *invite-store, *resource
|
||||
/+ default-agent, verb, dbug, store=group-store, grpl=group, pull-hook
|
||||
|
@ -1,5 +1,6 @@
|
||||
:: group-hook: allow syncing group data from foreign paths to local paths
|
||||
:: group-hook [landscape]:
|
||||
::
|
||||
:: allow syncing group data from foreign paths to local paths
|
||||
::
|
||||
/- *group, hook=group-hook, *invite-store
|
||||
/+ default-agent, verb, dbug, store=group-store, grpl=group, push-hook,
|
||||
|
@ -1,4 +1,6 @@
|
||||
:: group-store: Store groups of ships
|
||||
:: group-store [landscape]:
|
||||
::
|
||||
:: Store groups of ships
|
||||
::
|
||||
:: group-store stores groups of ships, so that resources in other apps can be
|
||||
:: associated with a group. The current model of group-store rolls
|
||||
@ -128,7 +130,7 @@
|
||||
^- [resource group]
|
||||
=/ members=(set ship)
|
||||
(~(got by groups.old) pax)
|
||||
=| =invite:policy
|
||||
=| =invite:policy
|
||||
?> ?=(^ pax)
|
||||
=/ rid=resource
|
||||
(resource-from-old-path t.pax)
|
||||
@ -149,7 +151,7 @@
|
||||
|= pax=path
|
||||
=/ members
|
||||
(~(got by groups.old) pax)
|
||||
=| =invite:policy
|
||||
=| =invite:policy
|
||||
=/ rid=resource
|
||||
(resource-from-old-path pax)
|
||||
=/ =tags
|
||||
@ -239,7 +241,7 @@
|
||||
(~(has in members.group) ship)
|
||||
==
|
||||
%open
|
||||
?! ?|
|
||||
?! ?|
|
||||
(~(has in banned.policy) ship)
|
||||
(~(has in ban-ranks.policy) (clan:title ship))
|
||||
==
|
||||
@ -285,7 +287,7 @@
|
||||
^- resource
|
||||
?> ?=([@ @ *] path)
|
||||
:- (slav %p i.path)
|
||||
i.t.path
|
||||
i.t.path
|
||||
::
|
||||
++ add-new
|
||||
|= =permission:permission-store
|
||||
@ -293,7 +295,7 @@
|
||||
?: ?=(%black kind.permission)
|
||||
[~ ~ [%open ~ who.permission] %.y]
|
||||
[who.permission ~ [%invite ~] %.y]
|
||||
::
|
||||
::
|
||||
++ update-existing
|
||||
|= =permission:permission-store
|
||||
|= =group
|
||||
|
@ -2,7 +2,7 @@
|
||||
/+ drum=hood-drum, helm=hood-helm, kiln=hood-kiln
|
||||
|%
|
||||
+$ state
|
||||
$: %9
|
||||
$: %10
|
||||
drum=state:drum
|
||||
helm=state:helm
|
||||
kiln=state:kiln
|
||||
@ -12,6 +12,7 @@
|
||||
[ver=?(%1 %2 %3 %4 %5 %6) lac=(map @tas fin-any-state)]
|
||||
[%7 drum=state:drum helm=state:helm kiln=state:kiln]
|
||||
[%8 drum=state:drum helm=state:helm kiln=state:kiln]
|
||||
[%9 drum=state:drum helm=state:helm kiln=state:kiln]
|
||||
==
|
||||
+$ any-state-tuple
|
||||
$: drum=any-state:drum
|
||||
|
@ -1,4 +1,6 @@
|
||||
:: invite-hook: receive invites from any source
|
||||
:: invite-hook [landscape]:
|
||||
::
|
||||
:: receive invites from any source
|
||||
::
|
||||
:: only handles %invite actions. accepts json, but only from the host team.
|
||||
:: can be poked by the host team to send an invite out to someone.
|
||||
|
@ -1,3 +1,4 @@
|
||||
:: invite-store [landscape]
|
||||
/+ *invite-json, default-agent, dbug
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
|
@ -1,3 +1,7 @@
|
||||
:: invite-view [landscape]:
|
||||
::
|
||||
:: deprecated
|
||||
::
|
||||
/+ default-agent
|
||||
^- agent:gall
|
||||
|_ =bowl:gall
|
||||
|
@ -23,7 +23,7 @@
|
||||
<div id="root"></div>
|
||||
<script src="/~landscape/js/channel.js"></script>
|
||||
<script src="/~landscape/js/session.js"></script>
|
||||
<script src="/~landscape/js/bundle/index.5c70804296e13e8973c3.js"></script>
|
||||
<script src="/~landscape/js/bundle/index.ecc81763be57d23e6028.js"></script>
|
||||
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.1.12.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,3 +1,7 @@
|
||||
:: launch [landscape]:
|
||||
::
|
||||
:: registers Landscape (and third party) applications, tiles
|
||||
::
|
||||
/+ store=launch-store, default-agent, dbug
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
|
@ -1,644 +1,46 @@
|
||||
:: link-listen-hook: get your friends' bookmarks
|
||||
:: link-listen-hook: no longer in use
|
||||
::
|
||||
:: keeps track of a listening=(set app-path). users can manually add to and
|
||||
:: remove from this set.
|
||||
::
|
||||
:: for all ships in groups associated with those resources, we subscribe to
|
||||
:: their link's local-pages and annotations at the resource path (through
|
||||
:: link-proxy-hook), and forward all entries into our link-store as
|
||||
:: submissions and comments.
|
||||
::
|
||||
:: if a subscription to a target fails, we assume it's because their
|
||||
:: metadata+groups definition hasn't been updated to include us yet.
|
||||
:: we retry with exponential backoff, maxing out at one hour timeouts.
|
||||
:: to expede this process, we prod other potential listeners when we add
|
||||
:: them to our metadata+groups definition.
|
||||
::
|
||||
::
|
||||
/- listen-hook=link-listen-hook, *metadata-store, *group, *link
|
||||
/+ mdl=metadata, default-agent, verb, dbug, group-store, grpl=group, resource, store=link-store
|
||||
/+ default-agent, verb, dbug
|
||||
::
|
||||
~% %link-listen-hook-top ..is ~
|
||||
|%
|
||||
+$ versioned-state
|
||||
$% [%0 state-0]
|
||||
[%1 state-1]
|
||||
[%2 state-2]
|
||||
[%3 state-3]
|
||||
$% [%0 *]
|
||||
[%1 *]
|
||||
[%2 *]
|
||||
[%3 *]
|
||||
[%4 ~]
|
||||
==
|
||||
+$ state-3 state-1
|
||||
+$ state-2 state-1
|
||||
+$ state-1
|
||||
$: listening=(set app-path)
|
||||
state-0
|
||||
==
|
||||
+$ state-0
|
||||
$: retry-timers=(map target @dr)
|
||||
:: reasoning: the resources we're subscribed to,
|
||||
:: and the groups that cause that.
|
||||
::
|
||||
:: we don't strictly need to track this in state, but doing so heavily
|
||||
:: simplifies logic and reduces the amount of big scries we do.
|
||||
:: this also gives us the option to check & restore subscriptions,
|
||||
:: should we ever need that.
|
||||
::
|
||||
reasoning=(jug [ship app-path] group-path)
|
||||
==
|
||||
::
|
||||
+$ what-target ?(%local-pages %annotations)
|
||||
+$ target
|
||||
$: what=what-target
|
||||
who=ship
|
||||
where=path
|
||||
==
|
||||
++ wire-to-target
|
||||
|= =wire
|
||||
^- target
|
||||
?> ?=([what-target @ ^] wire)
|
||||
[i.wire (slav %p i.t.wire) t.t.wire]
|
||||
++ target-to-wire
|
||||
|= target
|
||||
^- wire
|
||||
[what (scot %p who) where]
|
||||
::
|
||||
+$ card card:agent:gall
|
||||
--
|
||||
::
|
||||
=| [%3 state-3]
|
||||
=| [%4 ~]
|
||||
=* state -
|
||||
::
|
||||
%- agent:dbug
|
||||
%+ verb |
|
||||
^- agent:gall
|
||||
=<
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
do ~(. +> bowl)
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
++ on-init
|
||||
^- (quip card _this)
|
||||
:_ this
|
||||
~[watch-metadata:do watch-groups:do]
|
||||
::
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= =vase
|
||||
^- (quip card _this)
|
||||
=/ old=versioned-state
|
||||
!<(versioned-state vase)
|
||||
=| cards=(list card)
|
||||
|-
|
||||
=* upgrade-loop $
|
||||
?- -.old
|
||||
%3 [cards this(state old)]
|
||||
::
|
||||
%2
|
||||
:_ this(state [%3 +.old])
|
||||
%+ welp cards
|
||||
:~ [%pass /groups %agent [our.bowl %group-store] %leave ~]
|
||||
watch-groups:do
|
||||
==
|
||||
::
|
||||
%1
|
||||
:: the upgrade from 0 left out local-only collections.
|
||||
:: here, we pull those back in.
|
||||
::
|
||||
=. listening.old
|
||||
(~(run in ~(key by reasoning.old)) tail)
|
||||
=/ resources=(list [=group-path =app-path])
|
||||
%~ tap in
|
||||
%. %link
|
||||
%~ get ju
|
||||
.^ (jug app-name [group-path app-path])
|
||||
%gy
|
||||
(scot %p our.bowl)
|
||||
%metadata-store
|
||||
(scot %da now.bowl)
|
||||
/app-indices
|
||||
==
|
||||
|-
|
||||
?~ resources
|
||||
upgrade-loop(old [%2 +.old])
|
||||
=, i.resources
|
||||
=/ members=(set ship)
|
||||
(members-from-path:grp:do group-path)
|
||||
:: if we're the only group member, this got incorrectly ignored
|
||||
:: during 0's upgrade logic. watch it now.
|
||||
::
|
||||
?. &(=(1 ~(wyt in members)) (~(has in members) our.bowl))
|
||||
$(resources t.resources)
|
||||
=^ more-cards state
|
||||
(handle-listen-action:do %watch app-path)
|
||||
$(resources t.resources, cards (weld more-cards cards))
|
||||
::
|
||||
%0
|
||||
=/ listening=(set app-path)
|
||||
(~(run in ~(key by reasoning.old)) tail)
|
||||
$(old [%1 listening +.old])
|
||||
==
|
||||
::
|
||||
++ on-agent
|
||||
|= [=wire =sign:agent:gall]
|
||||
^- (quip card _this)
|
||||
=^ cards state
|
||||
?+ wire ~|([dap.bowl %weird-agent-wire wire] !!)
|
||||
[%metadata ~]
|
||||
(take-metadata-sign:do sign)
|
||||
::
|
||||
[%groups ~]
|
||||
(take-groups-sign:do sign)
|
||||
::
|
||||
[%links ?(%local-pages %annotations) @ ^]
|
||||
(take-link-sign:do (wire-to-target t.wire) sign)
|
||||
::
|
||||
[%forward ^]
|
||||
(take-forward-sign:do t.wire sign)
|
||||
::
|
||||
[%prod *]
|
||||
?> ?=(%poke-ack -.sign)
|
||||
?~ p.sign [~ state]
|
||||
%- (slog leaf+"prod failed" u.p.sign)
|
||||
[~ state]
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
++ on-poke
|
||||
|= [=mark =vase]
|
||||
?+ mark (on-poke:def mark vase)
|
||||
%link-listen-poke
|
||||
=/ =path !<(path vase)
|
||||
:_ this
|
||||
%+ weld
|
||||
(take-retry:do %local-pages src.bowl path)
|
||||
(take-retry:do %annotations src.bowl path)
|
||||
::
|
||||
%link-listen-action
|
||||
?> (team:title [our src]:bowl)
|
||||
=^ cards state
|
||||
~| p.vase
|
||||
(handle-listen-action:do !<(action:listen-hook vase))
|
||||
[cards this]
|
||||
==
|
||||
::
|
||||
++ on-arvo
|
||||
|= [=wire =sign-arvo]
|
||||
^- (quip card _this)
|
||||
?+ sign-arvo (on-arvo:def wire sign-arvo)
|
||||
[%g %done *]
|
||||
?~ error.sign-arvo [~ this]
|
||||
=/ =tank leaf+"{(trip dap.bowl)}'s message went wrong!"
|
||||
%- (slog tank tang.u.error.sign-arvo)
|
||||
[~ this]
|
||||
::
|
||||
[%b %wake *]
|
||||
?> ?=([%retry @ @ ^] wire)
|
||||
?^ error.sign-arvo
|
||||
=/ =tank leaf+"wake on {(spud wire)} went wrong!"
|
||||
%- (slog tank u.error.sign-arvo)
|
||||
[~ this]
|
||||
:_ this
|
||||
(take-retry:do (wire-to-target t.wire))
|
||||
==
|
||||
::
|
||||
++ on-peek
|
||||
|= =path
|
||||
^- (unit (unit cage))
|
||||
?+ path ~
|
||||
[%x %listening ~] ``noun+!>(listening)
|
||||
[%x %listening ^] ``noun+!>((~(has in listening) t.t.path))
|
||||
==
|
||||
::
|
||||
++ on-watch
|
||||
|= =path
|
||||
^- (quip card _this)
|
||||
?. ?=([%listening ~] path) (on-watch:def path)
|
||||
?> (team:title [our src]:bowl)
|
||||
:_ this
|
||||
[%give %fact ~ %link-listen-update !>([%listening listening])]~
|
||||
::
|
||||
++ on-leave on-leave:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
::
|
||||
::
|
||||
|_ =bowl:gall
|
||||
+* md ~(. mdl bowl)
|
||||
++ grp ~(. grpl bowl)
|
||||
+* this .
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
:: user actions & updates
|
||||
::
|
||||
++ handle-listen-action
|
||||
|= =action:listen-hook
|
||||
^- (quip card _state)
|
||||
::NOTE no-opping where appropriate happens further down the call stack.
|
||||
:: we *could* no-op here, as %watch when we're already listening should
|
||||
:: result in no-ops all the way down, but walking through everything
|
||||
:: makes this a nice "resurrect if broken unexpectedly" option.
|
||||
::
|
||||
=* app-path path.action
|
||||
=^ cards listening
|
||||
^- (quip card _listening)
|
||||
=/ had=? (~(has in listening) app-path)
|
||||
?- -.action
|
||||
%watch
|
||||
:_ (~(put in listening) app-path)
|
||||
?:(had ~ [(send-update action)]~)
|
||||
::
|
||||
%leave
|
||||
:_ (~(del in listening) app-path)
|
||||
?.(had ~ [(send-update action)]~)
|
||||
==
|
||||
=/ groups=(list group-path)
|
||||
(groups-from-resource:md %link app-path)
|
||||
|-
|
||||
?~ groups [cards state]
|
||||
=^ more-cards state
|
||||
?- -.action
|
||||
%watch (listen-to-group app-path i.groups)
|
||||
%leave (leave-from-group app-path i.groups)
|
||||
==
|
||||
$(cards (weld cards more-cards), groups t.groups)
|
||||
::
|
||||
++ send-update
|
||||
|= =update:listen-hook
|
||||
++ on-init [~ this]
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= =vase
|
||||
^- (quip card _this)
|
||||
:_ this
|
||||
:- [%pass /groups %agent [our.bowl %group-store] %leave ~]
|
||||
%+ turn ~(tap in ~(key by wex.bowl))
|
||||
|= [=wire =ship =term]
|
||||
^- card
|
||||
[%give %fact ~[/listening] %link-listen-update !>(update)]
|
||||
[%pass wire %agent [ship term] %leave ~]
|
||||
::
|
||||
:: metadata subscription
|
||||
::
|
||||
++ watch-metadata
|
||||
^- card
|
||||
[%pass /metadata %agent [our.bowl %metadata-store] %watch /app-name/link]
|
||||
::
|
||||
++ take-metadata-sign
|
||||
|= =sign:agent:gall
|
||||
^- (quip card _state)
|
||||
?- -.sign
|
||||
%poke-ack ~|([dap.bowl %unexpected-poke-ack /metadata] !!)
|
||||
%kick [[watch-metadata]~ state]
|
||||
::
|
||||
%watch-ack
|
||||
?~ p.sign [~ state]
|
||||
=/ =tank
|
||||
:- %leaf
|
||||
"{(trip dap.bowl)} failed subscribe to metadata store. very wrong!"
|
||||
%- (slog tank u.p.sign)
|
||||
[~ state]
|
||||
::
|
||||
%fact
|
||||
=* mark p.cage.sign
|
||||
=* vase q.cage.sign
|
||||
?. ?=(%metadata-update mark)
|
||||
~| [dap.bowl %unexpected-mark mark]
|
||||
!!
|
||||
%- handle-metadata-update
|
||||
!<(metadata-update vase)
|
||||
==
|
||||
::
|
||||
++ handle-metadata-update
|
||||
|= upd=metadata-update
|
||||
^- (quip card _state)
|
||||
?+ -.upd [~ state]
|
||||
%add
|
||||
?> =(%link app-name.resource.upd)
|
||||
:: auto-listen to collections in unmanaged groups only
|
||||
::
|
||||
=/ rid=resource
|
||||
(de-path:resource group-path.upd)
|
||||
=/ =group
|
||||
(need (scry-group:grp rid))
|
||||
?. hidden.group
|
||||
[~ state]
|
||||
=, resource.upd
|
||||
=^ update listening
|
||||
^- (quip card _listening)
|
||||
?: (~(has in listening) app-path)
|
||||
[~ listening]
|
||||
:- [(send-update %watch app-path)]~
|
||||
(~(put in listening) app-path)
|
||||
=^ cards state
|
||||
(listen-to-group app-path group-path.upd)
|
||||
[(weld update cards) state]
|
||||
::
|
||||
%remove
|
||||
?> =(%link app-name.resource.upd)
|
||||
=? listening
|
||||
?=(~ (groups-from-resource:md %link app-path.resource.upd))
|
||||
(~(del in listening) app-path.resource.upd)
|
||||
(leave-from-group app-path.resource.upd group-path.upd)
|
||||
==
|
||||
::
|
||||
:: groups subscriptions
|
||||
::
|
||||
++ watch-groups
|
||||
^- card
|
||||
[%pass /groups %agent [our.bowl %group-store] %watch /groups]
|
||||
::
|
||||
++ take-groups-sign
|
||||
|= =sign:agent:gall
|
||||
^- (quip card _state)
|
||||
?- -.sign
|
||||
%poke-ack ~|([dap.bowl %unexpected-poke-ack /groups] !!)
|
||||
%kick [[watch-groups]~ state]
|
||||
::
|
||||
%watch-ack
|
||||
?~ p.sign [~ state]
|
||||
=/ =tank
|
||||
:- %leaf
|
||||
"{(trip dap.bowl)} failed subscribe to groups. very wrong!"
|
||||
%- (slog tank u.p.sign)
|
||||
[~ state]
|
||||
::
|
||||
%fact
|
||||
=* mark p.cage.sign
|
||||
=* vase q.cage.sign
|
||||
?+ mark ~|([dap.bowl %unexpected-mark mark] !!)
|
||||
%group-initial [~ state] ::NOTE initial handled using metadata
|
||||
%group-update (handle-group-update !<(update:group-store vase))
|
||||
==
|
||||
==
|
||||
::
|
||||
++ handle-group-update
|
||||
|= upd=update:group-store
|
||||
^- (quip card _state)
|
||||
?. ?=(?(%add-members %initial-group %remove-members) -.upd)
|
||||
[~ state]
|
||||
=/ =path
|
||||
(en-path:resource resource.upd)
|
||||
=/ socs=(list app-path)
|
||||
(app-paths-from-group:md %link path)
|
||||
=/ whos=(list ship)
|
||||
?- -.upd
|
||||
%add-members ~(tap in ships.upd)
|
||||
%remove-members ~(tap in ships.upd)
|
||||
%initial-group ~(tap in members.group.upd)
|
||||
==
|
||||
=| cards=(list card)
|
||||
|-
|
||||
=* loop-socs $
|
||||
?~ socs [cards state]
|
||||
?. (~(has in listening) i.socs)
|
||||
loop-socs(socs t.socs)
|
||||
|-
|
||||
=* loop-whos $
|
||||
?~ whos loop-socs(socs t.socs)
|
||||
=^ caz state
|
||||
?. ?=(%remove-members -.upd)
|
||||
(listen-to-peer i.socs path i.whos)
|
||||
?: =(our.bowl i.whos)
|
||||
(handle-listen-action %leave i.socs)
|
||||
(leave-from-peer i.socs path i.whos)
|
||||
loop-whos(whos t.whos, cards (weld cards caz))
|
||||
::
|
||||
:: link subscriptions
|
||||
::
|
||||
++ listen-to-group
|
||||
|= [=app-path =group-path]
|
||||
^- (quip card _state)
|
||||
=/ peers=(list ship)
|
||||
~| group-path
|
||||
%~ tap in
|
||||
(members-from-path:grp group-path)
|
||||
=| cards=(list card)
|
||||
|-
|
||||
?~ peers [cards state]
|
||||
=^ caz state
|
||||
(listen-to-peer app-path group-path i.peers)
|
||||
$(peers t.peers, cards (weld cards caz))
|
||||
::
|
||||
++ leave-from-group
|
||||
|= [=app-path =group-path]
|
||||
^- (quip card _state)
|
||||
=/ peers=(list ship)
|
||||
%~ tap in
|
||||
(members-from-path:grp group-path)
|
||||
=| cards=(list card)
|
||||
|-
|
||||
?~ peers [cards state]
|
||||
=^ caz state
|
||||
(leave-from-peer app-path group-path i.peers)
|
||||
$(peers t.peers, cards (weld cards caz))
|
||||
::
|
||||
++ listen-to-peer
|
||||
|= [=app-path =group-path who=ship]
|
||||
^- (quip card _state)
|
||||
?: =(our.bowl who)
|
||||
[~ state]
|
||||
:_ =- state(reasoning -)
|
||||
(~(put ju reasoning) [who app-path] group-path)
|
||||
:- (prod-other-listener who app-path)
|
||||
?^ (~(get ju reasoning) [who app-path])
|
||||
~
|
||||
(start-link-subscriptions who app-path)
|
||||
::
|
||||
++ leave-from-peer
|
||||
|= [=app-path =group-path who=ship]
|
||||
^- (quip card _state)
|
||||
?: =(our.bowl who)
|
||||
[~ state]
|
||||
=. reasoning (~(del ju reasoning) [who app-path] group-path)
|
||||
::NOTE leaving is always safe, so we just do it unconditionally
|
||||
(end-link-subscriptions who app-path)
|
||||
::
|
||||
++ start-link-subscriptions
|
||||
|= [=ship =app-path]
|
||||
^- (list card)
|
||||
:~ (start-link-subscription %local-pages ship app-path)
|
||||
(start-link-subscription %annotations ship app-path)
|
||||
==
|
||||
::
|
||||
++ start-link-subscription
|
||||
|= =target
|
||||
^- card
|
||||
:* %pass
|
||||
[%links (target-to-wire target)]
|
||||
%agent
|
||||
[who.target %link-proxy-hook]
|
||||
%watch
|
||||
?- what.target
|
||||
%local-pages [what where]:target
|
||||
%annotations [what %$ where]:target
|
||||
==
|
||||
==
|
||||
::
|
||||
++ end-link-subscriptions
|
||||
|= [who=ship where=path]
|
||||
^- (quip card _state)
|
||||
=. retry-timers (~(del by retry-timers) [%local-pages who where])
|
||||
=. retry-timers (~(del by retry-timers) [%annotations who where])
|
||||
:_ state
|
||||
|^ ~[(end %local-pages) (end %annotations)]
|
||||
::
|
||||
++ end
|
||||
|= what=what-target
|
||||
:* %pass
|
||||
[%links (target-to-wire what who where)]
|
||||
%agent
|
||||
[who %link-proxy-hook]
|
||||
%leave
|
||||
~
|
||||
==
|
||||
--
|
||||
::
|
||||
++ prod-other-listener
|
||||
|= [who=ship where=path]
|
||||
^- card
|
||||
:* %pass
|
||||
[%prod (scot %p who) where]
|
||||
%agent
|
||||
[who %link-listen-hook]
|
||||
%poke
|
||||
%link-listen-poke
|
||||
!>(where)
|
||||
==
|
||||
::
|
||||
++ take-link-sign
|
||||
|= [=target =sign:agent:gall]
|
||||
^- (quip card _state)
|
||||
?- -.sign
|
||||
%poke-ack ~|([dap.bowl %unexpected-poke-ack /links target] !!)
|
||||
%kick [[(start-link-subscription target)]~ state]
|
||||
::
|
||||
%watch-ack
|
||||
?~ p.sign
|
||||
=. retry-timers (~(del by retry-timers) target)
|
||||
[~ state]
|
||||
:: our subscription request got rejected,
|
||||
:: most likely because our group definition is out of sync with theirs.
|
||||
:: set timer for retry.
|
||||
::
|
||||
(start-retry target)
|
||||
::
|
||||
%fact
|
||||
=* mark p.cage.sign
|
||||
=* vase q.cage.sign
|
||||
?+ mark ~|([dap.bowl %unexpected-mark mark] !!)
|
||||
%link-initial
|
||||
%- handle-link-initial
|
||||
[who.target where.target !<(initial:store vase)]
|
||||
::
|
||||
%link-update
|
||||
%- handle-link-update
|
||||
[who.target where.target !<(update:store vase)]
|
||||
==
|
||||
==
|
||||
::
|
||||
++ start-retry
|
||||
|= =target
|
||||
^- (quip card _state)
|
||||
=/ timer=@dr
|
||||
%+ min ~h1
|
||||
%+ mul 2
|
||||
(~(gut by retry-timers) target ~s15)
|
||||
=. retry-timers
|
||||
(~(put by retry-timers) target timer)
|
||||
:_ state
|
||||
:_ ~
|
||||
:* %pass
|
||||
[%retry (target-to-wire target)]
|
||||
[%arvo %b %wait (add now.bowl timer)]
|
||||
==
|
||||
::
|
||||
++ take-retry
|
||||
|= =target
|
||||
^- (list card)
|
||||
:: relevant: whether :who is still associated with resource :where
|
||||
::
|
||||
=; relevant=?
|
||||
?. relevant ~
|
||||
[(start-link-subscription target)]~
|
||||
?. (~(has in listening) where.target)
|
||||
|
|
||||
?: %- ~(has by wex.bowl)
|
||||
[[%links (target-to-wire target)] who.target %link-proxy-hook]
|
||||
|
|
||||
%+ lien (groups-from-resource:md %link where.target)
|
||||
|= =group-path
|
||||
^- ?
|
||||
%. who.target
|
||||
~(has in (members-from-path:grp group-path))
|
||||
|
||||
::
|
||||
++ do-link-action
|
||||
|= [=wire =action:store]
|
||||
^- card
|
||||
:* %pass
|
||||
wire
|
||||
%agent
|
||||
[our.bowl %link-store]
|
||||
%poke
|
||||
%link-action
|
||||
!>(action)
|
||||
==
|
||||
::
|
||||
++ handle-link-initial
|
||||
|= [who=ship where=path =initial:store]
|
||||
^- (quip card _state)
|
||||
?> =(src.bowl who)
|
||||
?+ -.initial ~|([dap.bowl %unexpected-initial -.initial] !!)
|
||||
%local-pages
|
||||
=/ =pages (~(got by pages.initial) where)
|
||||
(handle-link-update who where [%local-pages where pages])
|
||||
::
|
||||
%annotations
|
||||
=/ urls=(list [=url =notes])
|
||||
~(tap by (~(got by notes.initial) where))
|
||||
=| cards=(list card)
|
||||
|- ^- (quip card _state)
|
||||
?~ urls [cards state]
|
||||
=^ caz state
|
||||
^- (quip card _state)
|
||||
=, i.urls
|
||||
(handle-link-update who where [%annotations where url notes])
|
||||
$(urls t.urls, cards (weld cards caz))
|
||||
==
|
||||
::
|
||||
++ handle-link-update
|
||||
|= [who=ship where=path =update:store]
|
||||
^- (quip card _state)
|
||||
?> =(src.bowl who)
|
||||
:_ state
|
||||
?+ -.update ~|([dap.bowl %unexpected-update -.update] !!)
|
||||
%local-pages
|
||||
%+ turn pages.update
|
||||
|= =page
|
||||
%+ do-link-action
|
||||
[%forward %local-page (scot %p who) where]
|
||||
[%hear where who page]
|
||||
::
|
||||
%annotations
|
||||
%+ turn notes.update
|
||||
|= =note
|
||||
^- card
|
||||
%+ do-link-action
|
||||
`wire`[%forward %annotation (scot %p who) where]
|
||||
`action:store`[%read where url.update `comment`[who note]]
|
||||
==
|
||||
::
|
||||
++ take-forward-sign
|
||||
|= [=wire =sign:agent:gall]
|
||||
^- (quip card _state)
|
||||
~| [%unexpected-sign on=[%forward wire] -.sign]
|
||||
?> ?=(%poke-ack -.sign)
|
||||
?~ p.sign [~ state]
|
||||
=/ =tank
|
||||
:- %leaf
|
||||
;: weld
|
||||
(trip dap.bowl)
|
||||
" failed to save submission from "
|
||||
(spud wire)
|
||||
==
|
||||
%- (slog tank u.p.sign)
|
||||
[~ state]
|
||||
::
|
||||
++ scry-for
|
||||
|* [=mold =app-name =path]
|
||||
.^ mold
|
||||
%gx
|
||||
(scot %p our.bowl)
|
||||
app-name
|
||||
(scot %da now.bowl)
|
||||
(snoc `^path`path %noun)
|
||||
==
|
||||
++ on-agent on-agent:def
|
||||
++ on-poke on-poke:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-watch on-watch:def
|
||||
++ on-leave on-leave:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
|
@ -1,337 +1,46 @@
|
||||
:: link-proxy-hook: make local pages available to foreign ships
|
||||
:: link-proxy-hook: no longer in use
|
||||
::
|
||||
:: this is a "proxy" style hook, relaying foreign subscriptions into local
|
||||
:: stores if permission conditions are met.
|
||||
:: the patterns herein should one day be generalized into a proxy-hook lib.
|
||||
::
|
||||
:: this uses metadata-store to discover resources and their associated
|
||||
:: groups. it sets the permission condition to be that a ship must be in a
|
||||
:: group associated with the resource it's subscribing to.
|
||||
:: we check this on-watch, but also subscribe to metadata & groups so that
|
||||
:: we can kick subscriptions if needed (eg ship removed from group).
|
||||
::
|
||||
:: we deduplicate incoming subscriptions on the same path, ensuring we have
|
||||
:: exactly one local subscription per unique incoming subscription path.
|
||||
:: this comes at the cost of assuming that the store's initial response is
|
||||
:: whatever's returned by the scry at that path, but perhaps that should
|
||||
:: become part of the stores standard anyway.
|
||||
::
|
||||
:: when adding support for new paths, the only things you'll likely want
|
||||
:: to touch are +permitted, +initial-response, & +kick-proxies.
|
||||
::
|
||||
/- *link, *metadata-store, *group
|
||||
/+ metadata, default-agent, verb, dbug, group-store, grpl=group,
|
||||
resource, store=link-store
|
||||
/+ default-agent, verb, dbug
|
||||
~% %link-proxy-hook-top ..is ~
|
||||
|%
|
||||
+$ state-0
|
||||
$: %0
|
||||
::TODO we use this to detect "first sub started" and "last sub left",
|
||||
:: but can't we use [wex sup]:bowl for that?
|
||||
active=(map path (set ship))
|
||||
==
|
||||
+$ state-1
|
||||
$: %1
|
||||
active=(map path (set ship))
|
||||
==
|
||||
::
|
||||
+$ versioned-state
|
||||
$% state-0
|
||||
state-1
|
||||
$% [%0 *]
|
||||
[%1 *]
|
||||
[%2 ~]
|
||||
==
|
||||
::
|
||||
+$ card card:agent:gall
|
||||
--
|
||||
::
|
||||
=| state-1
|
||||
=| [%2 ~]
|
||||
=* state -
|
||||
::
|
||||
%- agent:dbug
|
||||
%+ verb |
|
||||
^- agent:gall
|
||||
=<
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
do ~(. +> bowl)
|
||||
def ~(. (default-agent this %&) bowl)
|
||||
::
|
||||
++ on-init
|
||||
^- (quip card _this)
|
||||
:_ this
|
||||
~[watch-groups:do watch-metadata:do]
|
||||
::
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= old-vase=vase
|
||||
^- (quip card _this)
|
||||
=/ old
|
||||
!<(versioned-state old-vase)
|
||||
?- -.old
|
||||
%1 [~ this(state old)]
|
||||
::
|
||||
%0
|
||||
:_ this(state [%1 +.old])
|
||||
:~ [%pass /groups %agent [our.bowl %group-store] %leave ~]
|
||||
watch-groups:do
|
||||
==
|
||||
==
|
||||
|
||||
::
|
||||
++ on-watch
|
||||
|= =path
|
||||
^- (quip card _this)
|
||||
:: the local ship should just use link-store directly
|
||||
::TODO do we want to allow this anyway, to avoid client-side target checks?
|
||||
::
|
||||
?< (team:title [our src]:bowl)
|
||||
?> (permitted:do src.bowl path)
|
||||
=^ cards state
|
||||
(start-proxy:do src.bowl path)
|
||||
[cards this]
|
||||
::
|
||||
++ on-leave
|
||||
|= =path
|
||||
^- (quip card _this)
|
||||
=^ cards state
|
||||
(stop-proxy:do src.bowl path)
|
||||
[cards this]
|
||||
::
|
||||
++ on-agent
|
||||
|= [=wire =sign:agent:gall]
|
||||
^- (quip card _this)
|
||||
?: ?=([%groups ~] wire)
|
||||
=^ cards state
|
||||
(take-groups-sign:do sign)
|
||||
[cards this]
|
||||
?: ?=([%proxy ^] wire)
|
||||
=^ cards state
|
||||
(handle-proxy-sign t.wire sign)
|
||||
[cards this]
|
||||
~| [dap.bowl %weird-wire wire]
|
||||
!!
|
||||
::
|
||||
++ on-poke on-poke:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
::
|
||||
|_ =bowl:gall
|
||||
+* md ~(. metadata bowl)
|
||||
grp ~(. grpl bowl)
|
||||
+* this .
|
||||
def ~(. (default-agent this %&) bowl)
|
||||
::
|
||||
:: permissions
|
||||
++ on-init on-init:def
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= old-vase=vase
|
||||
^- (quip card _this)
|
||||
=/ paths
|
||||
%+ turn ~(val by sup.bowl)
|
||||
|=([=ship =path] path)
|
||||
:_ this
|
||||
:- [%pass /groups %agent [our.bowl %group-store] %leave ~]
|
||||
?~ paths ~
|
||||
[%give %kick paths ~]~
|
||||
::
|
||||
++ permitted
|
||||
|= [who=ship =path]
|
||||
^- ?
|
||||
:: we only expose /local-pages and /annotations,
|
||||
:: to ships in the groups associated with the resource.
|
||||
:: (no url-specific annotations subscriptions, either.)
|
||||
::
|
||||
=/ target=(unit ^path)
|
||||
?: ?=([%local-pages ^] path)
|
||||
`t.path
|
||||
?: ?=([%annotations ~ ^] path)
|
||||
`t.t.path
|
||||
~
|
||||
?~ target |
|
||||
%+ lien (groups-from-resource:md %link u.target)
|
||||
|= =group-path
|
||||
^- ?
|
||||
(~(has in (members-from-path:grp group-path)) who)
|
||||
::
|
||||
++ kick-revoked-permissions
|
||||
|= [=path who=(list ship)]
|
||||
^- (list card)
|
||||
%+ murn who
|
||||
|= =ship
|
||||
^- (unit card)
|
||||
:: no need to remove to ourselves
|
||||
::
|
||||
?: =(our.bowl ship) ~
|
||||
?: (permitted ship path) ~
|
||||
`(kick-proxies ship path)
|
||||
::
|
||||
:: metadata subscription
|
||||
::
|
||||
++ watch-metadata
|
||||
^- card
|
||||
[%pass /metadata %agent [our.bowl %metadata-store] %watch /app-name/link]
|
||||
::
|
||||
++ take-metadata-sign
|
||||
|= =sign:agent:gall
|
||||
^- (quip card _state)
|
||||
?- -.sign
|
||||
%poke-ack ~|([dap.bowl %unexpected-poke-ack /metadata] !!)
|
||||
%kick [[watch-metadata]~ state]
|
||||
::
|
||||
%watch-ack
|
||||
?~ p.sign [~ state]
|
||||
=/ =tank
|
||||
:- %leaf
|
||||
"{(trip dap.bowl)} failed subscribe to metadata store. very wrong!"
|
||||
%- (slog tank u.p.sign)
|
||||
[~ state]
|
||||
::
|
||||
%fact
|
||||
=* mark p.cage.sign
|
||||
=* vase q.cage.sign
|
||||
?. ?=(%metadata-update mark)
|
||||
~| [dap.bowl %unexpected-mark mark]
|
||||
!!
|
||||
%- handle-metadata-update
|
||||
!<(metadata-update vase)
|
||||
==
|
||||
::
|
||||
++ handle-metadata-update
|
||||
|= upd=metadata-update
|
||||
^- (quip card _state)
|
||||
:_ state
|
||||
?. ?=(%remove -.upd) ~
|
||||
?> =(%link app-name.resource.upd)
|
||||
:: if a group is no longer associated with a resource,
|
||||
:: we need to re-check permissions for everyone in that group.
|
||||
::
|
||||
%+ kick-revoked-permissions
|
||||
app-path.resource.upd
|
||||
%~ tap in
|
||||
(members-from-path:grp group-path.upd)
|
||||
::
|
||||
:: groups subscription
|
||||
::TODO largely copied from link-listen-hook. maybe make a store-listener lib?
|
||||
::
|
||||
++ watch-groups
|
||||
^- card
|
||||
[%pass /groups %agent [our.bowl %group-store] %watch /groups]
|
||||
::
|
||||
++ take-groups-sign
|
||||
|= =sign:agent:gall
|
||||
^- (quip card _state)
|
||||
?- -.sign
|
||||
%poke-ack ~|([dap.bowl %unexpected-poke-ack /groups] !!)
|
||||
%kick [[watch-groups]~ state]
|
||||
::
|
||||
%watch-ack
|
||||
?~ p.sign [~ state]
|
||||
=/ =tank
|
||||
:- %leaf
|
||||
"{(trip dap.bowl)} failed subscribe to group store. very wrong!"
|
||||
%- (slog tank u.p.sign)
|
||||
[~ state]
|
||||
::
|
||||
%fact
|
||||
=* mark p.cage.sign
|
||||
=* vase q.cage.sign
|
||||
?+ mark ~|([dap.bowl %unexpected-mark mark] !!)
|
||||
%group-initial [~ state]
|
||||
%group-update (handle-group-update !<(update:group-store vase))
|
||||
==
|
||||
==
|
||||
::
|
||||
++ handle-group-update
|
||||
|= upd=update:group-store
|
||||
^- (quip card _state)
|
||||
:_ state
|
||||
?. ?=(%remove-members -.upd) ~
|
||||
:: if someone was removed from a group, find all link resources associated
|
||||
:: with that group, then kick their subscriptions if they're no longer
|
||||
::
|
||||
%- zing
|
||||
%+ turn (app-paths-from-group:md %link (en-path:resource resource.upd))
|
||||
|= =app-path
|
||||
^- (list card)
|
||||
%+ kick-revoked-permissions
|
||||
app-path
|
||||
~(tap in ships.upd)
|
||||
::
|
||||
:: proxy subscriptions
|
||||
::
|
||||
++ kick-proxies
|
||||
|= [who=ship =path]
|
||||
^- card
|
||||
=- [%give %kick - `who]
|
||||
:~ [%local-pages path]
|
||||
[%annotations %$ path]
|
||||
==
|
||||
::
|
||||
++ handle-proxy-sign
|
||||
|= [=wire =sign:agent:gall]
|
||||
^- (quip card _state)
|
||||
?- -.sign
|
||||
%poke-ack ~|([dap.bowl %unexpected-poke-ack wire] !!)
|
||||
%fact [[%give %fact ~[wire] cage.sign]~ state]
|
||||
%kick [[(proxy-pass-link-store wire %watch wire)]~ state]
|
||||
::
|
||||
%watch-ack
|
||||
?~ p.sign [~ state]
|
||||
=/ =tank
|
||||
:- %leaf
|
||||
"{(trip dap.bowl)} failed subscribe to link-store. very wrong!"
|
||||
%- (slog tank u.p.sign)
|
||||
[~ state]
|
||||
==
|
||||
::
|
||||
++ proxy-pass-link-store
|
||||
|= [=path =task:agent:gall]
|
||||
^- card
|
||||
:* %pass
|
||||
[%proxy path]
|
||||
%agent
|
||||
[our.bowl %link-store]
|
||||
task
|
||||
==
|
||||
::
|
||||
++ initial-response
|
||||
|= =path
|
||||
^- card
|
||||
=; =initial:store
|
||||
[%give %fact ~ %link-initial !>(initial)]
|
||||
?+ path !!
|
||||
[%local-pages ^]
|
||||
[%local-pages (scry-for (map ^path pages) %link-store path)]
|
||||
::
|
||||
[%annotations %$ ^]
|
||||
[%annotations (scry-for (per-path-url notes) %link-store path)]
|
||||
==
|
||||
::
|
||||
++ start-proxy
|
||||
|= [who=ship =path]
|
||||
^- (quip card _state)
|
||||
:_ state(active (~(put ju active) path who))
|
||||
:_ ~
|
||||
:: if we already have a local subscription open,
|
||||
::
|
||||
?. =(~ (~(get ju active) path))
|
||||
:: gather the initial response ourselves, and send that.
|
||||
::
|
||||
(initial-response path)
|
||||
:: else, open a local subscription,
|
||||
:: sending outward its initial response when we hear it.
|
||||
::
|
||||
(proxy-pass-link-store path %watch path)
|
||||
::
|
||||
++ stop-proxy
|
||||
|= [who=ship =path]
|
||||
^- (quip card _state)
|
||||
=. active (~(del ju active) path who)
|
||||
:_ state
|
||||
:: if there are still subscriptions remaining, do nothing.
|
||||
::
|
||||
?. =(~ (~(get ju active) path)) ~
|
||||
:: else, close the local subscription.
|
||||
::
|
||||
[(proxy-pass-link-store path %leave ~)]~
|
||||
::
|
||||
:: helpers
|
||||
::
|
||||
++ scry-for
|
||||
|* [=mold =app-name =path]
|
||||
.^ mold
|
||||
%gx
|
||||
(scot %p our.bowl)
|
||||
app-name
|
||||
(scot %da now.bowl)
|
||||
(snoc `^path`path %noun)
|
||||
==
|
||||
++ on-watch on-watch:def
|
||||
++ on-leave on-leave:def
|
||||
++ on-agent on-agent:def
|
||||
++ on-poke on-poke:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
:: link: social bookmarking
|
||||
:: link [landscape]:
|
||||
::
|
||||
:: social bookmarking
|
||||
::
|
||||
:: the paths under which links are submitted are generally expected to
|
||||
:: correspond to existing group paths. for strictly-local collections of
|
||||
@ -50,10 +52,12 @@
|
||||
:: ?
|
||||
:: /seen/wood-url/some-path have we seen this here
|
||||
::
|
||||
/- *link
|
||||
/+ store=link-store, default-agent, verb, dbug
|
||||
/- *link, gra=graph-store, *resource
|
||||
/+ store=link-store, graph-store, default-agent, verb, dbug
|
||||
::
|
||||
|%
|
||||
+$ state-any $%(state-1 state-0)
|
||||
+$ state-1 [%1 ~]
|
||||
+$ state-0
|
||||
$: %0
|
||||
by-group=(map path links)
|
||||
@ -76,414 +80,107 @@
|
||||
+$ card card:agent:gall
|
||||
--
|
||||
::
|
||||
=| state-0
|
||||
=| state-1
|
||||
=* state -
|
||||
::
|
||||
%- agent:dbug
|
||||
%+ verb |
|
||||
^- agent:gall
|
||||
=<
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
do ~(. +> bowl)
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
++ on-init on-init:def
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= old=vase
|
||||
^- (quip card _this)
|
||||
[~ this(state !<(state-0 old))]
|
||||
::
|
||||
++ on-poke
|
||||
|= [=mark =vase]
|
||||
^- (quip card _this)
|
||||
?> (team:title [our src]:bowl) ::TODO /lib/store
|
||||
=^ cards state
|
||||
?+ mark (on-poke:def mark vase)
|
||||
::TODO move json conversion into mark once mark performance improves
|
||||
%json (do-action:do (action:dejs:store !<(json vase)))
|
||||
%link-action (do-action:do !<(action:store vase))
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
++ on-peek
|
||||
|= =path
|
||||
^- (unit (unit cage))
|
||||
?+ path (on-peek:def path)
|
||||
[%y ?(%local-pages %submissions) ~]
|
||||
``noun+!>(~(key by by-group))
|
||||
::
|
||||
[%x %local-pages *]
|
||||
``noun+!>((get-local-pages:do t.t.path))
|
||||
::
|
||||
[%x %submissions *]
|
||||
``noun+!>((get-submissions:do t.t.path))
|
||||
::
|
||||
[%y ?(%annotations %discussions) *]
|
||||
=/ [spath=^path surl=url]
|
||||
(break-discussion-path:store t.t.path)
|
||||
=- ``noun+!>(-)
|
||||
::
|
||||
?: =(~ surl)
|
||||
:: no url, provide urls that have comments
|
||||
::
|
||||
^- (set url)
|
||||
?~ spath
|
||||
:: no path, find urls accross all paths
|
||||
::
|
||||
%- ~(rep by discussions)
|
||||
|= [[* discussions=(map url discussion)] urls=(set url)]
|
||||
%- ~(uni in urls)
|
||||
~(key by discussions)
|
||||
:: specified path, find urls for that specific path
|
||||
::
|
||||
%~ key by
|
||||
(~(gut by discussions) spath *(map url *))
|
||||
:: specified url and path, nothing to list here
|
||||
::
|
||||
?^ spath !!
|
||||
:: no path, find paths with comments for this url
|
||||
::
|
||||
^- (set ^path)
|
||||
%- ~(rep by discussions)
|
||||
|= [[=^path urls=(map url discussion)] paths=(set ^path)]
|
||||
?. (~(has by urls) surl) paths
|
||||
(~(put in paths) path)
|
||||
::
|
||||
[%x %annotations *]
|
||||
``noun+!>((get-annotations:do t.t.path))
|
||||
::
|
||||
[%x %discussions *]
|
||||
``noun+!>((get-discussions:do t.t.path))
|
||||
::
|
||||
[%x %seen @ ^]
|
||||
``noun+!>((is-seen:do t.t.path))
|
||||
::
|
||||
[%x %unseen ~]
|
||||
``noun+!>(get-all-unseen:do)
|
||||
::
|
||||
[%x %unseen ^]
|
||||
``noun+!>((get-unseen:do t.t.path))
|
||||
==
|
||||
::
|
||||
++ on-watch
|
||||
|= =path
|
||||
^- (quip card _this)
|
||||
?> (team:title [our src]:bowl) ::TODO /lib/store
|
||||
:_ this
|
||||
|^ ?+ path (on-watch:def path)
|
||||
[%local-pages *]
|
||||
%+ give %link-initial
|
||||
^- initial:store
|
||||
[%local-pages (get-local-pages:do t.path)]
|
||||
::
|
||||
[%submissions *]
|
||||
%+ give %link-initial
|
||||
^- initial:store
|
||||
[%submissions (get-submissions:do t.path)]
|
||||
::
|
||||
[%annotations *]
|
||||
%+ give %link-initial
|
||||
^- initial:store
|
||||
[%annotations (get-annotations:do t.path)]
|
||||
::
|
||||
[%discussions *]
|
||||
%+ give %link-initial
|
||||
^- initial:store
|
||||
[%discussions (get-discussions:do t.path)]
|
||||
::
|
||||
[%seen ~]
|
||||
~
|
||||
==
|
||||
::
|
||||
++ give
|
||||
|* [=mark =noun]
|
||||
^- (list card)
|
||||
[%give %fact ~ mark !>(noun)]~
|
||||
::
|
||||
++ give-single
|
||||
|* [=mark =noun]
|
||||
^- card
|
||||
[%give %fact ~ mark !>(noun)]
|
||||
--
|
||||
::
|
||||
++ on-leave on-leave:def
|
||||
++ on-agent on-agent:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
::
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
do ~(. +> bowl)
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
:: writing
|
||||
::
|
||||
++ do-action
|
||||
|= =action:store
|
||||
^- (quip card _state)
|
||||
?- -.action
|
||||
%save (save-page +.action)
|
||||
%note (note-note +.action)
|
||||
%seen (seen-submission +.action)
|
||||
++ on-init on-init:def
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= old=vase
|
||||
^- (quip card _this)
|
||||
=/ s !<(state-any old)
|
||||
?: ?=(%1 -.s)
|
||||
[~ this(state s)]
|
||||
::
|
||||
%hear (hear-submission +.action)
|
||||
%read (read-comment +.action)
|
||||
==
|
||||
:: +save-page: save a page ourselves
|
||||
::
|
||||
++ save-page
|
||||
|= [=path title=@t =url]
|
||||
^- (quip card _state)
|
||||
?< |(=(~ path) =(~ title) =(~ url))
|
||||
:: add page to group ours
|
||||
::
|
||||
=/ =links (~(gut by by-group) path *links)
|
||||
=/ =page [title url now.bowl]
|
||||
=. ours.links [page ours.links]
|
||||
=. by-group (~(put by by-group) path links)
|
||||
:: do generic submission logic
|
||||
::
|
||||
=^ submission-cards state
|
||||
(hear-submission path [our.bowl page])
|
||||
:: mark page as seen (because we submitted it ourselves)
|
||||
::
|
||||
=^ seen-cards state
|
||||
(seen-submission path `url)
|
||||
:: send updates to subscribers
|
||||
::
|
||||
:_ state
|
||||
:_ (weld submission-cards seen-cards)
|
||||
:+ %give %fact
|
||||
:+ :~ /local-pages
|
||||
[%local-pages path]
|
||||
==
|
||||
%link-update
|
||||
!>([%local-pages path [page]~])
|
||||
:: +note-note: save a note for a url
|
||||
::
|
||||
++ note-note
|
||||
|= [=path =url udon=@t]
|
||||
^- (quip card _state)
|
||||
?< |(=(~ path) =(~ url) =(~ udon))
|
||||
:: add note to discussion ours
|
||||
::
|
||||
=/ urls (~(gut by discussions) path *(map ^url discussion))
|
||||
=/ =discussion (~(gut by urls) url *discussion)
|
||||
=/ =note [now.bowl udon]
|
||||
=. ours.discussion [note ours.discussion]
|
||||
=. urls (~(put by urls) url discussion)
|
||||
=. discussions (~(put by discussions) path urls)
|
||||
:: do generic comment logic
|
||||
::
|
||||
=^ cards state
|
||||
(read-comment path url [our.bowl note])
|
||||
:: send updates to subscribers
|
||||
::
|
||||
:_ state
|
||||
:_ this(state *state-1)
|
||||
=/ orm orm:graph-store
|
||||
|^ ^- (list card)
|
||||
%- zing
|
||||
%+ turn ~(tap by by-group.s)
|
||||
|= [=path =links]
|
||||
^- (list card)
|
||||
:_ cards
|
||||
:+ %give %fact
|
||||
:+ :~ /annotations
|
||||
[%annotations %$ path]
|
||||
[%annotations (build-discussion-path:store url)]
|
||||
[%annotations (build-discussion-path:store path url)]
|
||||
?. ?=([@ @ *] path)
|
||||
(on-bad-path path links)
|
||||
=/ =resource [(slav %p i.path) i.t.path]
|
||||
:_ [(archive-graph resource)]~
|
||||
%+ add-graph resource
|
||||
^- graph:gra
|
||||
%+ gas:orm ~
|
||||
=/ comments (~(gut by discussions.s) path *(map url discussion))
|
||||
%+ turn submissions.links
|
||||
|= sub=submission
|
||||
^- [atom node:gra]
|
||||
:- time.sub
|
||||
=/ contents ~[text+title.sub url+url.sub]
|
||||
=/ parent-hash `@ux`(sham ~ ship.sub time.sub contents)
|
||||
:- ^- post:gra
|
||||
:* author=ship.sub
|
||||
index=~[time.sub]
|
||||
time-sent=time.sub
|
||||
contents
|
||||
hash=`parent-hash
|
||||
signatures=~
|
||||
==
|
||||
%link-update
|
||||
!>([%annotations path url [note]~])
|
||||
:: +seen-submission: mark url as seen/read
|
||||
::
|
||||
:: if no url specified, all under path are marked as read
|
||||
::
|
||||
++ seen-submission
|
||||
|= [=path murl=(unit url)]
|
||||
^- (quip card _state)
|
||||
=/ =links (~(gut by by-group) path *links)
|
||||
:: new: urls we want to, but haven't yet, marked as seen
|
||||
^- internal-graph:gra
|
||||
=/ dis (~(get by comments) url.sub)
|
||||
?~ dis
|
||||
[%empty ~]
|
||||
:- %graph
|
||||
^- graph:gra
|
||||
%+ gas:orm ~
|
||||
%+ turn comments.u.dis
|
||||
|= [=ship =time udon=@t]
|
||||
^- [atom node:gra]
|
||||
:- time
|
||||
:_ `internal-graph:gra`[%empty ~]
|
||||
=/ contents ~[text+udon]
|
||||
:* author=ship
|
||||
index=~[time.sub time]
|
||||
time-sent=time
|
||||
contents
|
||||
hash=``@ux`(sham `parent-hash ship time contents)
|
||||
signatures=~
|
||||
==
|
||||
::
|
||||
=/ new=(set url)
|
||||
%. seen.links
|
||||
%~ dif in
|
||||
^- (set url)
|
||||
?^ murl (sy ~[u.murl])
|
||||
%- ~(gas in *(set url))
|
||||
%+ turn submissions.links
|
||||
|=(submission url)
|
||||
?: =(~ new) [~ state]
|
||||
=. seen.links (~(uni in seen.links) new)
|
||||
:_ state(by-group (~(put by by-group) path links))
|
||||
[%give %fact ~[/seen] %link-update !>([%observation path new])]~
|
||||
:: +hear-submission: record page someone else saved
|
||||
::
|
||||
++ hear-submission
|
||||
|= [=path =submission]
|
||||
^- (quip card _state)
|
||||
?< =(~ path)
|
||||
:: add link to group submissions
|
||||
++ on-bad-path
|
||||
|= [=path =links]
|
||||
^- (list card)
|
||||
~| discarding-malformed-links+[path links]
|
||||
~
|
||||
::
|
||||
=/ =links (~(gut by by-group) path *links)
|
||||
=^ added submissions.links
|
||||
?: ?=(^ (find ~[submission] submissions.links))
|
||||
[| submissions.links]
|
||||
:- &
|
||||
(submissions:merge:store submissions.links ~[submission])
|
||||
=. by-group (~(put by by-group) path links)
|
||||
:: add submission to global sites
|
||||
++ add-graph
|
||||
|= [=resource =graph:gra]
|
||||
^- card
|
||||
%- poke-graph-store
|
||||
[%0 now.bowl %add-graph resource graph `%graph-validator-link]
|
||||
::
|
||||
=/ =site (site-from-url:store url.submission)
|
||||
=. by-site (~(add ja by-site) site [path submission])
|
||||
:: send updates to subscribers
|
||||
++ archive-graph
|
||||
|= =resource
|
||||
^- card
|
||||
%- poke-graph-store
|
||||
[%0 now.bowl %archive-graph resource]
|
||||
::
|
||||
:_ state
|
||||
?. added ~
|
||||
:_ ~
|
||||
:+ %give %fact
|
||||
:+ :~ /submissions
|
||||
[%submissions path]
|
||||
==
|
||||
%link-update
|
||||
!>([%submissions path [submission]~])
|
||||
:: +read-comment: record a comment someone else made
|
||||
::
|
||||
++ read-comment
|
||||
|= [=path =url =comment]
|
||||
^- (quip card _state)
|
||||
:: add comment to url's discussion
|
||||
::
|
||||
=/ urls (~(gut by discussions) path *(map ^url discussion))
|
||||
=/ =discussion (~(gut by urls) url *discussion)
|
||||
=^ added comments.discussion
|
||||
?: ?=(^ (find ~[comment] comments.discussion))
|
||||
[| comments.discussion]
|
||||
:- &
|
||||
(comments:merge:store comments.discussion ~[comment])
|
||||
=. urls (~(put by urls) url discussion)
|
||||
=. discussions (~(put by discussions) path urls)
|
||||
:: send updates to subscribers
|
||||
::
|
||||
:_ state
|
||||
?. added ~
|
||||
:_ ~
|
||||
:+ %give %fact
|
||||
:+ :~ /discussions
|
||||
[%discussions '' path]
|
||||
[%discussions (build-discussion-path:store url)]
|
||||
[%discussions (build-discussion-path:store path url)]
|
||||
==
|
||||
%link-update
|
||||
!>([%discussions path url [comment]~])
|
||||
::
|
||||
:: reading
|
||||
::
|
||||
++ get-local-pages
|
||||
|= =path
|
||||
^- (map ^path pages)
|
||||
?~ path
|
||||
:: all paths
|
||||
::
|
||||
%- ~(run by by-group)
|
||||
|=(links ours)
|
||||
:: specific path
|
||||
::
|
||||
%+ ~(put by *(map ^path pages)) path
|
||||
ours:(~(gut by by-group) path *links)
|
||||
::
|
||||
++ get-submissions
|
||||
|= =path
|
||||
^- (map ^path submissions)
|
||||
?~ path
|
||||
:: all paths
|
||||
::
|
||||
%- ~(run by by-group)
|
||||
|=(links submissions)
|
||||
:: specific path
|
||||
::
|
||||
%+ ~(put by *(map ^path submissions)) path
|
||||
submissions:(~(gut by by-group) path *links)
|
||||
::
|
||||
++ get-all-unseen
|
||||
^- (jug path url)
|
||||
%- ~(rut by by-group)
|
||||
|= [=path *]
|
||||
(get-unseen path)
|
||||
::
|
||||
++ get-unseen
|
||||
|= =path
|
||||
^- (set url)
|
||||
=/ =links
|
||||
(~(gut by by-group) path *links)
|
||||
%- ~(gas in *(set url))
|
||||
%+ murn submissions.links
|
||||
|= submission
|
||||
?: (~(has in seen.links) url) ~
|
||||
(some url)
|
||||
::
|
||||
++ is-seen
|
||||
|= =path
|
||||
^- ?
|
||||
=/ [=^path =url]
|
||||
(break-discussion-path:store path)
|
||||
%. url
|
||||
%~ has in
|
||||
seen:(~(gut by by-group) path *links)
|
||||
::
|
||||
::
|
||||
++ get-annotations
|
||||
|= =path
|
||||
^- (per-path-url notes)
|
||||
=/ args=[=^path =url]
|
||||
(break-discussion-path:store path)
|
||||
|^ ?~ path
|
||||
:: all paths
|
||||
::
|
||||
(~(run by discussions) get-ours)
|
||||
:: specific path
|
||||
::
|
||||
%+ ~(put by *(per-path-url notes)) path.args
|
||||
%- get-ours
|
||||
%+ ~(gut by discussions) path.args
|
||||
*(map url discussion)
|
||||
::
|
||||
++ get-ours
|
||||
|= m=(map url discussion)
|
||||
^- (map url notes)
|
||||
?: =(~ url.args)
|
||||
:: all urls
|
||||
::
|
||||
%- ~(run by m)
|
||||
|=(discussion ours)
|
||||
:: specific url
|
||||
::
|
||||
%+ ~(put by *(map url notes)) url.args
|
||||
ours:(~(gut by m) url.args *discussion)
|
||||
++ poke-graph-store
|
||||
|= =update:gra
|
||||
^- card
|
||||
:* %pass /migrate-link %agent [our.bowl %graph-store]
|
||||
%poke %graph-update !>(update)
|
||||
==
|
||||
--
|
||||
::
|
||||
++ get-discussions
|
||||
|= =path
|
||||
^- (per-path-url comments)
|
||||
=/ args=[=^path =url]
|
||||
(break-discussion-path:store path)
|
||||
|^ ?~ path
|
||||
:: all paths
|
||||
::
|
||||
(~(run by discussions) get-comments)
|
||||
:: specific path
|
||||
::
|
||||
%+ ~(put by *(per-path-url comments)) path.args
|
||||
%- get-comments
|
||||
%+ ~(gut by discussions) path.args
|
||||
*(map url discussion)
|
||||
::
|
||||
++ get-comments
|
||||
|= m=(map url discussion)
|
||||
^- (map url comments)
|
||||
?: =(~ url.args)
|
||||
:: all urls
|
||||
::
|
||||
%- ~(run by m)
|
||||
|=(discussion comments)
|
||||
:: specific url
|
||||
::
|
||||
%+ ~(put by *(map url comments)) url.args
|
||||
comments:(~(gut by m) url.args *discussion)
|
||||
--
|
||||
++ on-poke on-poke:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-watch on-watch:def
|
||||
++ on-leave on-leave:def
|
||||
++ on-agent on-agent:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
|
@ -1,626 +1,39 @@
|
||||
:: link-view: frontend endpoints
|
||||
::
|
||||
:: endpoints, mapping onto link-store's paths. p is for page as in pagination.
|
||||
:: only the /0/submissions endpoint provides updates.
|
||||
:: as with link-store, urls are expected to use +wood encoding.
|
||||
::
|
||||
:: /json/0/submissions initial + updates for all
|
||||
:: /json/[p]/submissions/[collection] page for one collection
|
||||
:: /json/[p]/discussions/[wood-url]/[collection] page for url in collection
|
||||
:: /json/[n]/submission/[wood-url]/[collection] nth matching submission
|
||||
:: /json/seen mark-as-read updates
|
||||
::
|
||||
/- *link, view=link-view
|
||||
/- *invite-store, group-store
|
||||
/- listen-hook=link-listen-hook
|
||||
/- group-hook, permission-hook, permission-group-hook
|
||||
/- metadata-hook, contact-view
|
||||
/- pull-hook, *group
|
||||
/+ store=link-store, metadata, *server, default-agent, verb, dbug, grpl=group
|
||||
/+ group-store, resource
|
||||
:: link-view: no longer in use
|
||||
/+ default-agent, verb, dbug
|
||||
~% %link-view-top ..is ~
|
||||
::
|
||||
::
|
||||
|%
|
||||
+$ versioned-state
|
||||
$% state-0
|
||||
state-1
|
||||
==
|
||||
+$ state-0
|
||||
$: %0
|
||||
~
|
||||
==
|
||||
::
|
||||
+$ state-1
|
||||
$: %1
|
||||
~
|
||||
$% [%0 ~]
|
||||
[%1 ~]
|
||||
[%2 ~]
|
||||
==
|
||||
::
|
||||
+$ card card:agent:gall
|
||||
--
|
||||
::
|
||||
=| state-1
|
||||
=| [%2 ~]
|
||||
=* state -
|
||||
::
|
||||
%+ verb |
|
||||
%- agent:dbug
|
||||
^- agent:gall
|
||||
=<
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
do ~(. +> bowl)
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
++ on-init
|
||||
^- (quip card _this)
|
||||
:_ this
|
||||
:~ [%pass /submissions %agent [our.bowl %link-store] %watch /submissions]
|
||||
[%pass /discussions %agent [our.bowl %link-store] %watch /discussions]
|
||||
[%pass /seen %agent [our.bowl %link-store] %watch /seen]
|
||||
::
|
||||
=+ [%invite-action !>([%create /link])]
|
||||
[%pass /invitatory/create %agent [our.bowl %invite-store] %poke -]
|
||||
::
|
||||
=+ /invitatory/link
|
||||
[%pass - %agent [our.bowl %invite-store] %watch -]
|
||||
:* %pass /srv %agent [our.bowl %file-server]
|
||||
%poke %file-server-action
|
||||
!>([%serve-dir /'~link' /app/landscape %.n %.y])
|
||||
==
|
||||
==
|
||||
::
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= old-vase=vase
|
||||
^- (quip card _this)
|
||||
=/ old !<(versioned-state old-vase)
|
||||
?- -.old
|
||||
%1 [~ this]
|
||||
%0
|
||||
:_ this(state [%1 ~])
|
||||
:- [%pass /connect %arvo %e %disconnect [~ /'~link']]
|
||||
:~ :* %pass /srv %agent [our.bowl %file-server]
|
||||
%poke %file-server-action
|
||||
!>([%serve-dir /'~link' /app/landscape %.n %.y])
|
||||
== ==
|
||||
==
|
||||
::
|
||||
++ on-poke
|
||||
|= [=mark =vase]
|
||||
^- (quip card _this)
|
||||
?> (team:title our.bowl src.bowl)
|
||||
:_ this
|
||||
?+ mark (on-poke:def mark vase)
|
||||
%link-action
|
||||
[(handle-action:do !<(action:store vase)) ~]
|
||||
::
|
||||
%link-view-action
|
||||
(handle-view-action:do !<(action:view vase))
|
||||
==
|
||||
::
|
||||
++ on-watch
|
||||
|= =path
|
||||
^- (quip card _this)
|
||||
?: ?=([%json %seen ~] path)
|
||||
[~ this]
|
||||
?: ?=([%tile ~] path)
|
||||
:_ this
|
||||
~[give-tile-data:do]
|
||||
?. ?=([%json @ @ *] path)
|
||||
(on-watch:def path)
|
||||
=/ p=@ud (slav %ud i.t.path)
|
||||
?+ t.t.path (on-watch:def path)
|
||||
[%submissions ~]
|
||||
:_ this
|
||||
(give-initial-submissions:do p ~)
|
||||
::
|
||||
[%submissions ^]
|
||||
:_ this
|
||||
(give-initial-submissions:do p t.t.t.path)
|
||||
::
|
||||
[%submission @ ^]
|
||||
:_ this
|
||||
(give-specific-submission:do p (break-discussion-path:store t.t.t.path))
|
||||
::
|
||||
[%discussions @ ^]
|
||||
:_ this
|
||||
(give-initial-discussions:do p (break-discussion-path:store t.t.t.path))
|
||||
==
|
||||
::
|
||||
++ on-agent
|
||||
|= [=wire =sign:agent:gall]
|
||||
^- (quip card _this)
|
||||
?+ -.sign (on-agent:def wire sign)
|
||||
%poke-ack
|
||||
?. ?=([%join-group @ @ @ @ ~] wire)
|
||||
(on-agent:def wire sign)
|
||||
?^ p.sign
|
||||
(on-agent:def wire sign)
|
||||
=/ rid=resource
|
||||
(de-path:resource t.t.wire)
|
||||
=/ host=ship
|
||||
(slav %p i.t.wire)
|
||||
:_ this
|
||||
(joined-group:do host rid)
|
||||
::
|
||||
%kick
|
||||
:_ this
|
||||
=/ app=term
|
||||
?: ?=([%invites *] wire)
|
||||
%invite-store
|
||||
%link-store
|
||||
[%pass wire %agent [our.bowl app] %watch wire]~
|
||||
::
|
||||
%fact
|
||||
=* mark p.cage.sign
|
||||
=* vase q.cage.sign
|
||||
?+ mark (on-agent:def wire sign)
|
||||
%invite-update [(handle-invite-update:do !<(invite-update vase)) this]
|
||||
%link-initial [~ this]
|
||||
::
|
||||
%link-update
|
||||
:_ this
|
||||
:- (send-update:do !<(update:store vase))
|
||||
?: =(/discussions wire) ~
|
||||
~[give-tile-data:do]
|
||||
==
|
||||
==
|
||||
::
|
||||
++ on-arvo
|
||||
|= [=wire =sign-arvo]
|
||||
^- (quip card _this)
|
||||
?. ?=([%e %bound *] sign-arvo)
|
||||
(on-arvo:def wire sign-arvo)
|
||||
~? !accepted.sign-arvo
|
||||
[dap.bowl "bind rejected!" binding.sign-arvo]
|
||||
[~ this]
|
||||
::
|
||||
++ on-peek on-peek:def
|
||||
++ on-leave on-leave:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
::
|
||||
~% %link-view-logic ..card ~
|
||||
|_ =bowl:gall
|
||||
+* md ~(. metadata bowl)
|
||||
grp ~(. grpl bowl)
|
||||
+* this .
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
++ page-size 25
|
||||
++ get-paginated
|
||||
|* [page=(unit @ud) list=(list)]
|
||||
^- [total=@ud pages=@ud page=_list]
|
||||
=/ l=@ud (lent list)
|
||||
:+ l
|
||||
%+ add (div l page-size)
|
||||
(min 1 (mod l page-size))
|
||||
?~ page list
|
||||
%+ swag
|
||||
[(mul u.page page-size) page-size]
|
||||
list
|
||||
++ on-init [~ this]
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= old-vase=vase
|
||||
^- (quip card _this)
|
||||
:_ this(state [%2 ~])
|
||||
[%pass /connect %arvo %e %disconnect [~ /'~link']]~
|
||||
::
|
||||
++ page-to-json
|
||||
=, enjs:format
|
||||
|* $: page-number=@ud
|
||||
[total-items=@ud total-pages=@ud page=(list)]
|
||||
item-to-json=$-(* json)
|
||||
==
|
||||
^- json
|
||||
%- pairs
|
||||
:~ 'totalItems'^(numb total-items)
|
||||
'totalPages'^(numb total-pages)
|
||||
'pageNumber'^(numb page-number)
|
||||
'page'^a+(turn page item-to-json)
|
||||
==
|
||||
++ do-poke
|
||||
|= [app=term =mark =vase]
|
||||
^- card
|
||||
[%pass /create/[app]/[mark] %agent [our.bowl app] %poke mark vase]
|
||||
::
|
||||
++ joined-group
|
||||
|= [host=ship rid=resource]
|
||||
^- (list card)
|
||||
=/ =path
|
||||
(en-path:resource rid)
|
||||
:~
|
||||
:: sync the group
|
||||
::
|
||||
%^ do-poke %group-pull-hook
|
||||
%pull-hook-action
|
||||
!> ^- action:pull-hook
|
||||
[%add host rid]
|
||||
::
|
||||
:: sync the metadata
|
||||
::
|
||||
%^ do-poke %metadata-hook
|
||||
%metadata-hook-action
|
||||
!> ^- metadata-hook-action:metadata-hook
|
||||
[%add-synced host path]
|
||||
::
|
||||
:: sync the collection
|
||||
::
|
||||
%^ do-poke %link-listen-hook
|
||||
%link-listen-action
|
||||
!> ^- action:listen-hook
|
||||
[%watch ~[name.rid]]
|
||||
==
|
||||
::
|
||||
++ handle-invite-update
|
||||
|= upd=invite-update
|
||||
^- (list card)
|
||||
?. ?=(%accepted -.upd) ~
|
||||
?. =(/link path.upd) ~
|
||||
=/ rid=resource
|
||||
(de-path:resource path.invite.upd)
|
||||
:~ :: add self
|
||||
:* %pass
|
||||
[%join-group (scot %p ship.invite.upd) path.invite.upd]
|
||||
%agent [entity.rid %group-push-hook]
|
||||
%poke %group-update
|
||||
!> ^- action:group-store
|
||||
[%add-members rid (sy our.bowl ~)]
|
||||
== ==
|
||||
::
|
||||
++ handle-action
|
||||
|= =action:store
|
||||
^- card
|
||||
[%pass /action %agent [our.bowl %link-store] %poke %link-action !>(action)]
|
||||
::
|
||||
++ handle-view-action
|
||||
|= act=action:view
|
||||
^- (list card)
|
||||
?- -.act
|
||||
%create (handle-create +.act)
|
||||
%delete (handle-delete +.act)
|
||||
%invite (handle-invite +.act)
|
||||
==
|
||||
::
|
||||
++ handle-create
|
||||
|= [=path title=@t description=@t members=create-members:view real-group=?]
|
||||
^- (list card)
|
||||
=/ group-path=^path
|
||||
?- -.members
|
||||
%group path.members
|
||||
::
|
||||
%ships
|
||||
[%ship (scot %p our.bowl) path]
|
||||
==
|
||||
=; group-setup=(list card)
|
||||
%+ weld group-setup
|
||||
:~ :: add collection to metadata-store
|
||||
::
|
||||
%^ do-poke %metadata-hook
|
||||
%metadata-action
|
||||
!> ^- metadata-action:md
|
||||
:^ %add group-path
|
||||
[%link path]
|
||||
%* . *metadata:md
|
||||
title title
|
||||
description description
|
||||
date-created now.bowl
|
||||
creator our.bowl
|
||||
==
|
||||
::
|
||||
:: expose the metadata
|
||||
::
|
||||
%^ do-poke %metadata-hook
|
||||
%metadata-hook-action
|
||||
!> ^- metadata-hook-action:metadata-hook
|
||||
[%add-owned group-path]
|
||||
::
|
||||
:: watch the collection ourselves
|
||||
::
|
||||
%^ do-poke %link-listen-hook
|
||||
%link-listen-action
|
||||
!> ^- action:listen-hook
|
||||
[%watch path]
|
||||
==
|
||||
?: ?=(%group -.members) ~
|
||||
:: if the group is "real", make contact-view do the heavy lifting
|
||||
=/ rid=resource
|
||||
(de-path:resource group-path)
|
||||
?: real-group
|
||||
:- %^ do-poke %contact-view
|
||||
%contact-view-action
|
||||
!> ^- contact-view-action:contact-view
|
||||
[%groupify rid title description]
|
||||
%+ turn ~(tap in ships.members)
|
||||
|= =ship
|
||||
^- card
|
||||
%^ do-poke %invite-hook
|
||||
%invite-action
|
||||
!> ^- invite-action
|
||||
:^ %invite /link
|
||||
(sham group-path eny.bowl)
|
||||
:* our.bowl
|
||||
%group-hook
|
||||
group-path
|
||||
ship
|
||||
title
|
||||
==
|
||||
:: for "unmanaged" groups, do it ourselves
|
||||
::
|
||||
=/ =policy
|
||||
[%invite ships.members]
|
||||
:* :: create the new group
|
||||
::
|
||||
%^ do-poke %group-store
|
||||
%group-action
|
||||
!> ^- action:group-store
|
||||
[%add-group rid policy %.y]
|
||||
::
|
||||
:: send invites
|
||||
::
|
||||
%+ turn ~(tap in ships.members)
|
||||
|= =ship
|
||||
^- card
|
||||
%^ do-poke %invite-hook
|
||||
%invite-action
|
||||
!> ^- invite-action
|
||||
:^ %invite /link
|
||||
(sham group-path eny.bowl)
|
||||
:* our.bowl
|
||||
%group-hook
|
||||
group-path
|
||||
ship
|
||||
title
|
||||
==
|
||||
==
|
||||
::
|
||||
++ handle-delete
|
||||
|= =path
|
||||
^- (list card)
|
||||
=/ groups=(list ^path)
|
||||
(groups-from-resource:md [%link path])
|
||||
%- zing
|
||||
%+ turn groups
|
||||
|= =group=^path
|
||||
=/ rid=resource
|
||||
(de-path:resource group-path)
|
||||
%+ snoc
|
||||
^- (list card)
|
||||
:: if it's a real group, we can't/shouldn't unsync it. this leaves us with
|
||||
:: no way to stop propagation of collection deletion.
|
||||
::
|
||||
?. ?=([%'~' ^] group-path) ~
|
||||
:: if it's an unmanaged group, we just stop syncing the group & metadata,
|
||||
:: and clean up the group (after un-hooking it, to not push deletion).
|
||||
::
|
||||
:~ %^ do-poke %group-hook
|
||||
%group-hook-action
|
||||
!> ^- action:group-hook
|
||||
[%remove rid]
|
||||
::
|
||||
%^ do-poke %metadata-hook
|
||||
%metadata-hook-action
|
||||
!> ^- metadata-hook-action:metadata-hook
|
||||
[%remove group-path]
|
||||
::
|
||||
%^ do-poke %group-store
|
||||
%group-action
|
||||
!> ^- action:group-store
|
||||
[%remove-group rid ~]
|
||||
==
|
||||
:: remove collection from metadata-store
|
||||
::
|
||||
%^ do-poke %metadata-store
|
||||
%metadata-action
|
||||
!> ^- metadata-action:md
|
||||
[%remove group-path [%link path]]
|
||||
::
|
||||
++ handle-invite
|
||||
|= [=path ships=(set ship)]
|
||||
^- (list card)
|
||||
%- zing
|
||||
%+ turn (groups-from-resource:md %link path)
|
||||
|= =group=^path
|
||||
^- (list card)
|
||||
=/ rid=resource
|
||||
(de-path:resource group-path)
|
||||
=/ =group
|
||||
(need (scry-group:grp rid))
|
||||
%- zing
|
||||
:~
|
||||
?. ?=(%invite -.policy.group)
|
||||
~
|
||||
:~ %^ do-poke %group-store
|
||||
%group-action
|
||||
!> ^- action:group-store
|
||||
[%change-policy rid %invite %add-invites ships]
|
||||
==
|
||||
::
|
||||
%+ turn ~(tap in ships)
|
||||
|= =ship
|
||||
^- card
|
||||
%^ do-poke %invite-hook
|
||||
%invite-action
|
||||
!> ^- invite-action
|
||||
:^ %invite /link
|
||||
(sham group-path eny.bowl)
|
||||
:* our.bowl
|
||||
%group-pull-hook
|
||||
group-path
|
||||
ship
|
||||
(rsh 3 1 (spat path))
|
||||
==
|
||||
==
|
||||
:: +give-tile-data: total unread count as json object
|
||||
::
|
||||
::NOTE the full recalc of totals here probably isn't the end of the world.
|
||||
:: but in case it is, well, here it is.
|
||||
::
|
||||
++ give-tile-data
|
||||
^- card
|
||||
=; =json
|
||||
[%give %fact ~[/tile] %json !>(json)]
|
||||
%+ frond:enjs:format 'unseen'
|
||||
%- numb:enjs:format
|
||||
%- %~ rep in
|
||||
(scry-for (jug path url) /unseen)
|
||||
|= [[=path unseen=(set url)] total=@ud]
|
||||
%+ add total
|
||||
~(wyt in unseen)
|
||||
::
|
||||
:: +give-initial-submissions: page of submissions on path
|
||||
::
|
||||
:: for the / path, give page for every path
|
||||
::
|
||||
:: result is in the shape of: {
|
||||
:: "/some/path": {
|
||||
:: totalItems: 1,
|
||||
:: totalPages: 1,
|
||||
:: pageNumber: 0,
|
||||
:: page: [
|
||||
:: { commentCount: 1, ...restOfTheSubmission }
|
||||
:: ]
|
||||
:: },
|
||||
:: "/maybe/more": { etc }
|
||||
:: }
|
||||
::
|
||||
++ give-initial-submissions
|
||||
~/ %link-view-initial-submissions
|
||||
|= [p=@ud =requested=path]
|
||||
^- (list card)
|
||||
:_ :: only keep the base case alive (for updates), kick all others
|
||||
::
|
||||
?: &(=(0 p) ?=(~ requested-path)) ~
|
||||
[%give %kick ~ ~]~
|
||||
=; =json
|
||||
[%give %fact ~ %json !>(json)]
|
||||
%+ frond:enjs:format 'link-update'
|
||||
%+ frond:enjs:format 'initial-submissions'
|
||||
%- pairs:enjs:format
|
||||
%+ turn
|
||||
%~ tap by
|
||||
%+ scry-for (map path submissions)
|
||||
[%submissions requested-path]
|
||||
|= [=path =submissions]
|
||||
^- [@t json]
|
||||
:- (spat path)
|
||||
=; =json
|
||||
:: add unseen count
|
||||
::
|
||||
?> ?=(%o -.json)
|
||||
:- %o
|
||||
%+ ~(put by p.json) 'unseenCount'
|
||||
%- numb:enjs:format
|
||||
%~ wyt in
|
||||
%+ scry-for (set url)
|
||||
[%unseen path]
|
||||
?: &(=(0 p) ?=(~ requested-path))
|
||||
:: for a broad-scope initial result, only give total counts
|
||||
::
|
||||
=, enjs:format
|
||||
%- pairs
|
||||
=+ l=(lent submissions)
|
||||
:~ 'totalItems'^(numb l)
|
||||
'totalPages'^(numb (div l page-size))
|
||||
==
|
||||
%^ page-to-json p
|
||||
%+ get-paginated `p
|
||||
submissions
|
||||
|= =submission
|
||||
^- json
|
||||
=/ =json (submission:enjs:store submission)
|
||||
?> ?=([%o *] json)
|
||||
:: add in seen status
|
||||
::
|
||||
=. p.json
|
||||
%+ ~(put by p.json) 'seen'
|
||||
:- %b
|
||||
%+ scry-for ?
|
||||
[%seen (build-discussion-path:store path url.submission)]
|
||||
:: add in comment count
|
||||
::
|
||||
=; comment-count=@ud
|
||||
:- %o
|
||||
%+ ~(put by p.json) 'commentCount'
|
||||
(numb:enjs:format comment-count)
|
||||
%- lent
|
||||
~| [path url.submission]
|
||||
^- comments
|
||||
=- (~(got by (~(got by -) path)) url.submission)
|
||||
%+ scry-for (per-path-url comments)
|
||||
:- %discussions
|
||||
(build-discussion-path:store path url.submission)
|
||||
::
|
||||
++ give-specific-submission
|
||||
|= [n=@ud =path =url]
|
||||
:_ [%give %kick ~ ~]~
|
||||
=; =json
|
||||
[%give %fact ~ %json !>(json)]
|
||||
%+ frond:enjs:format 'link-update'
|
||||
%+ frond:enjs:format 'submission'
|
||||
^- json
|
||||
=; sub=(unit submission)
|
||||
?~ sub ~
|
||||
(submission:enjs:store u.sub)
|
||||
=/ =submissions
|
||||
=- (~(got by -) path)
|
||||
%+ scry-for (map ^path submissions)
|
||||
[%submissions path]
|
||||
|-
|
||||
?~ submissions ~
|
||||
=* sub i.submissions
|
||||
?. =(url.sub url)
|
||||
$(submissions t.submissions)
|
||||
?: =(0 n) `sub
|
||||
$(n (dec n), submissions t.submissions)
|
||||
::
|
||||
++ give-initial-discussions
|
||||
|= [p=@ud =path =url]
|
||||
^- (list card)
|
||||
:_ ?: =(0 p) ~
|
||||
[%give %kick ~ ~]~
|
||||
=; =json
|
||||
[%give %fact ~ %json !>(json)]
|
||||
%+ frond:enjs:format 'link-update'
|
||||
%+ frond:enjs:format 'initial-discussions'
|
||||
%^ page-to-json p
|
||||
%+ get-paginated `p
|
||||
=- (~(got by (~(got by -) path)) url)
|
||||
%+ scry-for (per-path-url comments)
|
||||
[%discussions (build-discussion-path:store path url)]
|
||||
comment:enjs:store
|
||||
::
|
||||
++ send-update
|
||||
|= =update:store
|
||||
^- card
|
||||
?+ -.update ~|([dap.bowl %unexpected-update -.update] !!)
|
||||
%submissions
|
||||
%+ give-json
|
||||
%+ frond:enjs:format 'link-update'
|
||||
(update:enjs:store update)
|
||||
:~ /json/0/submissions
|
||||
(weld /json/0/submissions path.update)
|
||||
==
|
||||
::
|
||||
%discussions
|
||||
%+ give-json
|
||||
%+ frond:enjs:format 'link-update'
|
||||
(update:enjs:store update)
|
||||
:_ ~
|
||||
%+ weld /json/0/discussions
|
||||
(build-discussion-path:store [path url]:update)
|
||||
::
|
||||
%observation
|
||||
%+ give-json
|
||||
%+ frond:enjs:format 'link-update'
|
||||
(update:enjs:store update)
|
||||
~[/json/seen]
|
||||
==
|
||||
::
|
||||
++ give-json
|
||||
|= [=json paths=(list path)]
|
||||
^- card
|
||||
[%give %fact paths %json !>(json)]
|
||||
::
|
||||
++ scry-for
|
||||
|* [=mold =path]
|
||||
.^ mold
|
||||
%gx
|
||||
(scot %p our.bowl)
|
||||
%link-store
|
||||
(scot %da now.bowl)
|
||||
(snoc `^path`path %noun)
|
||||
==
|
||||
++ on-poke on-poke:def
|
||||
++ on-watch on-watch:def
|
||||
++ on-agent on-agent:def
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-leave on-leave:def
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
|
@ -1,4 +1,6 @@
|
||||
:: metadata-hook: allow syncing foreign metadata
|
||||
:: metadata-hook [landscape]:
|
||||
::
|
||||
:: allow syncing foreign metadata
|
||||
::
|
||||
:: watch paths:
|
||||
:: /group/%group-path all updates related to this group
|
||||
@ -37,7 +39,7 @@
|
||||
[[%pass /updates %agent [our.bowl %metadata-store] %watch /updates]~ this]
|
||||
::
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
++ on-load
|
||||
|= =vase
|
||||
=/ old
|
||||
!<(versioned-state vase)
|
||||
|
@ -1,4 +1,6 @@
|
||||
:: metadata-store: data store for application metadata and mappings
|
||||
:: metadata-store [landscape]:
|
||||
::
|
||||
:: data store for application metadata and mappings
|
||||
:: between groups and resources within applications
|
||||
::
|
||||
:: group-paths are expected to be an existing group path
|
||||
|
@ -1,4 +1,6 @@
|
||||
:: permission-group-hook: groups into permissions
|
||||
:: permission-group-hook [landscape]:
|
||||
::
|
||||
:: groups into permissions
|
||||
::
|
||||
:: mirror the ships in specified groups to specified permission paths
|
||||
::
|
||||
|
@ -1,4 +1,6 @@
|
||||
:: permission-hook: mirror remote permissions
|
||||
:: permission-hook [landscape]:
|
||||
::
|
||||
:: mirror remote permissions
|
||||
::
|
||||
:: allows mirroring permissions between local and foreign ships.
|
||||
:: local permission path are exposed according to the permssion paths
|
||||
|
@ -1,4 +1,6 @@
|
||||
:: permission-store: track black- and whitelists of ships
|
||||
:: permission-store [landscape]:
|
||||
::
|
||||
:: track black- and whitelists of ships
|
||||
::
|
||||
/- *permission-store
|
||||
/+ default-agent, verb, dbug
|
||||
|
@ -1,4 +1,6 @@
|
||||
:: pool-group-hook: maintain groups based on invite pool
|
||||
:: pool-group-hook [landscape]:
|
||||
::
|
||||
:: maintain groups based on invite pool
|
||||
::
|
||||
:: looks at our invite tree, adds our siblings to group at +group-path
|
||||
::
|
||||
|
@ -1,3 +1,7 @@
|
||||
:: publish [landscape]
|
||||
::
|
||||
:: stores notebooks in clay, subscribes and allow subscriptions to notebooks
|
||||
::
|
||||
/- *publish
|
||||
/- *group
|
||||
/- group-hook
|
||||
|
@ -1,3 +1,7 @@
|
||||
:: s3-store [landscape]:
|
||||
::
|
||||
:: stores s3 keys for uploading and sharing images and objects
|
||||
::
|
||||
/- *s3
|
||||
/+ s3-json, default-agent, verb, dbug
|
||||
~% %s3-top ..is ~
|
||||
|
@ -1,5 +1,6 @@
|
||||
::
|
||||
:: Soto: A Dojo relay for Urbit's Landscape interface
|
||||
:: soto [landscape]: A Dojo relay for Urbit's Landscape interface
|
||||
::
|
||||
:: Relays sole-effects to subscribers and forwards sole-action pokes
|
||||
::
|
||||
/- sole
|
||||
|
@ -1,5 +1,5 @@
|
||||
/- spider
|
||||
/+ libstrand=strand, default-agent, verb
|
||||
/+ libstrand=strand, default-agent, verb, server
|
||||
=, strand=strand:libstrand
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
@ -17,15 +17,25 @@
|
||||
$: starting=(map yarn [=trying =vase])
|
||||
running=trie
|
||||
tid=(map tid yarn)
|
||||
serving=(map tid [@ta =mark])
|
||||
==
|
||||
::
|
||||
+$ clean-slate-any
|
||||
$^ clean-slate-ket
|
||||
$% clean-slate-sig
|
||||
clean-slate-1
|
||||
clean-slate
|
||||
==
|
||||
::
|
||||
+$ clean-slate
|
||||
$: %2
|
||||
starting=(map yarn [=trying =vase])
|
||||
running=(list yarn)
|
||||
tid=(map tid yarn)
|
||||
serving=(map tid [@ta =mark])
|
||||
==
|
||||
::
|
||||
+$ clean-slate-1
|
||||
$: %1
|
||||
starting=(map yarn [=trying =vase])
|
||||
running=(list yarn)
|
||||
@ -133,7 +143,10 @@
|
||||
sc ~(. spider-core bowl)
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
::
|
||||
++ on-init on-init:def
|
||||
++ on-init
|
||||
^- (quip card _this)
|
||||
:_ this
|
||||
~[bind-eyre:sc]
|
||||
++ on-save clean-state:sc
|
||||
++ on-load
|
||||
|^
|
||||
@ -141,7 +154,9 @@
|
||||
=+ !<(any=clean-slate-any old-state)
|
||||
=? any ?=(^ -.any) (old-to-1 any)
|
||||
=? any ?=(~ -.any) (old-to-1 any)
|
||||
?> ?=(%1 -.any)
|
||||
=^ upgrade-cards any
|
||||
(old-to-2 any)
|
||||
?> ?=(%2 -.any)
|
||||
::
|
||||
=. tid.state tid.any
|
||||
=/ yarns=(list yarn)
|
||||
@ -154,12 +169,26 @@
|
||||
(handle-stop-thread:sc (yarn-to-tid i.yarns) |)
|
||||
=^ cards-2 this
|
||||
$(yarns t.yarns)
|
||||
[(weld cards-1 cards-2) this]
|
||||
[:(weld upgrade-cards cards-1 cards-2) this]
|
||||
::
|
||||
++ old-to-1
|
||||
|= old=clean-slate-ket
|
||||
^- clean-slate
|
||||
^- clean-slate-1
|
||||
1+old(starting (~(run by starting.old) |=([* v=vase] none+v)))
|
||||
::
|
||||
++ old-to-2
|
||||
|= old=clean-slate-any
|
||||
^- (quip card clean-slate)
|
||||
?> ?=(?(%1 %2) -.old)
|
||||
?: ?=(%2 -.old)
|
||||
`old
|
||||
:- ~[bind-eyre:sc]
|
||||
:* %2
|
||||
starting.old
|
||||
running.old
|
||||
tid.old
|
||||
~
|
||||
==
|
||||
--
|
||||
::
|
||||
++ on-poke
|
||||
@ -172,6 +201,9 @@
|
||||
%spider-input (on-poke-input:sc !<(input vase))
|
||||
%spider-start (handle-start-thread:sc !<(start-args vase))
|
||||
%spider-stop (handle-stop-thread:sc !<([tid ?] vase))
|
||||
::
|
||||
%handle-http-request
|
||||
(handle-http-request:sc !<([@ta =inbound-request:eyre] vase))
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
@ -182,6 +214,7 @@
|
||||
?+ path (on-watch:def path)
|
||||
[%thread @ *] (on-watch:sc t.path)
|
||||
[%thread-result @ ~] (on-watch-result:sc i.t.path)
|
||||
[%http-response *] `state
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
@ -216,6 +249,7 @@
|
||||
?+ wire (on-arvo:def wire sign-arvo)
|
||||
[%thread @ *] (handle-sign:sc i.t.wire t.t.wire sign-arvo)
|
||||
[%build @ ~] (handle-build:sc i.t.wire sign-arvo)
|
||||
[%bind ~] `state
|
||||
==
|
||||
[cards this]
|
||||
:: On unexpected failure, kill all outstanding strands
|
||||
@ -228,6 +262,41 @@
|
||||
--
|
||||
::
|
||||
|_ =bowl:gall
|
||||
::
|
||||
++ bind-eyre
|
||||
^- card
|
||||
[%pass /bind %arvo %e %connect [~ /spider] %spider]
|
||||
::
|
||||
++ handle-http-request
|
||||
|= [eyre-id=@ta =inbound-request:eyre]
|
||||
^- (quip card _state)
|
||||
?> authenticated.inbound-request
|
||||
=/ url
|
||||
(parse-request-line:server url.request.inbound-request)
|
||||
?> ?=([%spider @t @t @t ~] site.url)
|
||||
=* input-mark i.t.site.url
|
||||
=* thread i.t.t.site.url
|
||||
=* output-mark i.t.t.t.site.url
|
||||
=/ =tid
|
||||
(scot %uv (sham eny.bowl))
|
||||
=. serving.state
|
||||
(~(put by serving.state) tid [eyre-id output-mark])
|
||||
=+ .^
|
||||
=tube:clay
|
||||
%cc
|
||||
/(scot %p our.bowl)/[q.byk.bowl]/(scot %da now.bowl)/json/[input-mark]
|
||||
==
|
||||
?> ?=(^ body.request.inbound-request)
|
||||
=/ body=json
|
||||
(need (de-json:html q.u.body.request.inbound-request))
|
||||
=/ input=vase
|
||||
(tube !>(body))
|
||||
=/ =start-args
|
||||
[~ `tid thread input]
|
||||
=^ cards state
|
||||
(handle-start-thread start-args)
|
||||
[cards state]
|
||||
::
|
||||
++ on-poke-input
|
||||
|= input
|
||||
=/ yarn (~(got by tid.state) tid)
|
||||
@ -394,6 +463,25 @@
|
||||
:~ [%give %fact ~[/thread-result/[tid]] %thread-fail !>([term tang])]
|
||||
[%give %kick ~[/thread-result/[tid]] ~]
|
||||
==
|
||||
++ thread-http-fail
|
||||
|= [=tid =term =tang]
|
||||
^- (quip card ^state)
|
||||
=- (fall - `state)
|
||||
%+ bind
|
||||
(~(get by serving.state) tid)
|
||||
|= [eyre-id=@ta output=mark]
|
||||
:_ state(serving (~(del by serving.state) tid))
|
||||
%+ give-simple-payload:app:server eyre-id
|
||||
^- simple-payload:http
|
||||
:_ ~ :_ ~
|
||||
?. ?=(http-error:spider term)
|
||||
((slog tang) 500)
|
||||
?- term
|
||||
%bad-request 400
|
||||
%forbidden 403
|
||||
%nonexistent 404
|
||||
%offline 504
|
||||
==
|
||||
::
|
||||
++ thread-fail
|
||||
|= [=yarn =term =tang]
|
||||
@ -402,7 +490,24 @@
|
||||
=/ =tid (yarn-to-tid yarn)
|
||||
=/ fail-cards (thread-say-fail tid term tang)
|
||||
=^ cards state (thread-clean yarn)
|
||||
[(weld fail-cards cards) state]
|
||||
=^ http-cards state (thread-http-fail tid term tang)
|
||||
[:(weld fail-cards cards http-cards) state]
|
||||
::
|
||||
++ thread-http-response
|
||||
|= [=tid =vase]
|
||||
^- (quip card ^state)
|
||||
=- (fall - `state)
|
||||
%+ bind
|
||||
(~(get by serving.state) tid)
|
||||
|= [eyre-id=@ta output=mark]
|
||||
=+ .^
|
||||
=tube:clay
|
||||
%cc
|
||||
/(scot %p our.bowl)/[q.byk.bowl]/(scot %da now.bowl)/[output]/json
|
||||
==
|
||||
:_ state(serving (~(del by serving.state) tid))
|
||||
%+ give-simple-payload:app:server eyre-id
|
||||
(json-response:gen:server !<(json (tube vase)))
|
||||
::
|
||||
++ thread-done
|
||||
|= [=yarn =vase]
|
||||
@ -413,8 +518,10 @@
|
||||
:~ [%give %fact ~[/thread-result/[tid]] %thread-done vase]
|
||||
[%give %kick ~[/thread-result/[tid]] ~]
|
||||
==
|
||||
=^ http-cards state
|
||||
(thread-http-response tid vase)
|
||||
=^ cards state (thread-clean yarn)
|
||||
[(weld done-cards cards) state]
|
||||
[:(weld done-cards cards http-cards) state]
|
||||
::
|
||||
++ thread-clean
|
||||
|= =yarn
|
||||
@ -474,5 +581,5 @@
|
||||
::
|
||||
++ clean-state
|
||||
!> ^- clean-slate
|
||||
1+state(running (turn (tap-yarn running.state) head))
|
||||
2+state(running (turn (tap-yarn running.state) head))
|
||||
--
|
||||
|
@ -1,3 +1,7 @@
|
||||
:: weather [landscape]:
|
||||
::
|
||||
:: holds latlong, gets weather data from API, passes it on to subscribers
|
||||
::
|
||||
/+ *server, default-agent, verb, dbug
|
||||
=, format
|
||||
::
|
||||
|
61
pkg/arvo/lib/graph-view.hoon
Normal file
61
pkg/arvo/lib/graph-view.hoon
Normal file
@ -0,0 +1,61 @@
|
||||
/- sur=graph-view
|
||||
/+ resource, group-store
|
||||
^?
|
||||
=< [sur .]
|
||||
=, sur
|
||||
|%
|
||||
++ dejs
|
||||
=, dejs:format
|
||||
|%
|
||||
++ action
|
||||
|^
|
||||
^- $-(json ^action)
|
||||
%- of
|
||||
:~ create+create
|
||||
delete+delete
|
||||
join+join
|
||||
leave+leave
|
||||
groupify+groupify
|
||||
::invite+invite
|
||||
==
|
||||
::
|
||||
++ create
|
||||
%- ou
|
||||
:~ resource+(un dejs:resource)
|
||||
title+(un so)
|
||||
description+(un so)
|
||||
mark+(uf ~ (mu so))
|
||||
associated+(un associated)
|
||||
==
|
||||
::
|
||||
++ leave
|
||||
%- ot
|
||||
:~ resource+dejs:resource
|
||||
==
|
||||
::
|
||||
++ delete
|
||||
%- ot
|
||||
:~ resource+dejs:resource
|
||||
==
|
||||
::
|
||||
++ join
|
||||
%- ot
|
||||
:~ resource+dejs:resource
|
||||
ship+(su ;~(pfix sig fed:ag))
|
||||
==
|
||||
::
|
||||
++ groupify
|
||||
%- ou
|
||||
:~ resource+(un dejs:resource)
|
||||
to+(uf ~ (mu dejs:resource))
|
||||
==
|
||||
++ invite !!
|
||||
::
|
||||
++ associated
|
||||
%- of
|
||||
:~ group+dejs:resource
|
||||
policy+policy:dejs:group-store
|
||||
==
|
||||
--
|
||||
--
|
||||
--
|
@ -13,12 +13,24 @@
|
||||
::
|
||||
++ get-graph
|
||||
|= res=resource
|
||||
^- marked-graph:store
|
||||
%+ scry-for marked-graph:store
|
||||
^- update:store
|
||||
%+ scry-for update:store
|
||||
/graph/(scot %p entity.res)/[name.res]
|
||||
::
|
||||
++ peek-log
|
||||
++ get-update-log
|
||||
|= rid=resource
|
||||
^- update-log:store
|
||||
%+ scry-for update-log:store
|
||||
/update-log/(scot %p entity.rid)/[name.rid]
|
||||
::
|
||||
++ peek-update-log
|
||||
|= res=resource
|
||||
^- (unit time)
|
||||
(scry-for (unit time) /peek-update-log/(scot %p entity.res)/[name.res])
|
||||
::
|
||||
++ get-update-log-subset
|
||||
|= [res=resource start=@da]
|
||||
^- update-log:store
|
||||
%+ scry-for update-log:store
|
||||
/update-log-subset/(scot %p entity.res)/[name.res]/(scot %da start)/'~'
|
||||
--
|
||||
|
@ -105,6 +105,8 @@
|
||||
%file-server
|
||||
%glob
|
||||
%graph-store
|
||||
%graph-pull-hook
|
||||
%graph-push-hook
|
||||
==
|
||||
::
|
||||
++ deft-fish :: default connects
|
||||
@ -207,7 +209,7 @@
|
||||
==
|
||||
::
|
||||
++ on-load
|
||||
|= [hood-version=?(%1 %2 %3 %4 %5 %6 %7 %8 %9) old=any-state]
|
||||
|= [hood-version=?(%1 %2 %3 %4 %5 %6 %7 %8 %9 %10) old=any-state]
|
||||
=< se-abet =< se-view
|
||||
=. sat old
|
||||
=. dev (~(gut by bin) ost *source)
|
||||
@ -236,6 +238,9 @@
|
||||
(se-born | %home %group-pull-hook)
|
||||
=? ..on-load (lte hood-version %9)
|
||||
(se-born | %home %graph-store)
|
||||
=? ..on-load (lte hood-version %10)
|
||||
=> (se-born | %home %graph-push-hook)
|
||||
(se-born | %home %graph-pull-hook)
|
||||
..on-load
|
||||
::
|
||||
++ reap-phat :: ack connect
|
||||
|
@ -1,3 +1,23 @@
|
||||
:: lib/pull-hook: helper for creating a push hook
|
||||
::
|
||||
:: lib/pull-hook is a helper for automatically pulling data from a
|
||||
:: corresponding push-hook to a store.
|
||||
::
|
||||
:: ## Interfacing notes:
|
||||
::
|
||||
:: The inner door may interact with the library by producing cards.
|
||||
:: Do not pass any cards on a wire beginning with /helper as these
|
||||
:: wires are reserved by this library. Any watches/pokes/peeks not
|
||||
:: listed below will be routed to the inner door.
|
||||
::
|
||||
:: ## Subscription paths
|
||||
::
|
||||
:: /tracking: The set of resources we are pulling
|
||||
::
|
||||
:: ## Pokes
|
||||
::
|
||||
:: %pull-hook-action: Add/remove a resource from pulling.
|
||||
::
|
||||
/- *pull-hook
|
||||
/+ default-agent, resource
|
||||
::
|
||||
@ -5,12 +25,24 @@
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
::
|
||||
:: $config: configuration for the pull hook
|
||||
::
|
||||
:: .store-name: name of the store to send subscription updates to.
|
||||
:: .update-mark: mark that updates will be tagged with
|
||||
:: .push-hook-name: name of the corresponding push-hook
|
||||
::
|
||||
+$ config
|
||||
$: store-name=term
|
||||
update=mold
|
||||
update-mark=term
|
||||
push-hook-name=term
|
||||
==
|
||||
::
|
||||
:: $state-0: state for the pull hook
|
||||
::
|
||||
:: .tracking: a map of resources we are pulling, and the ships that
|
||||
:: we are pulling them from.
|
||||
:: .inner-state: state given to internal door
|
||||
::
|
||||
+$ state-0
|
||||
$: %0
|
||||
@ -37,7 +69,29 @@
|
||||
|* config
|
||||
$_ ^|
|
||||
|_ bowl:gall
|
||||
:: +on-pull-nack: handle failed pull subscription
|
||||
::
|
||||
:: This arm is called when a pull subscription fails. lib/pull-hook
|
||||
:: will automatically delete the resource from .tracking by the
|
||||
:: time this arm is called.
|
||||
::
|
||||
++ on-pull-nack
|
||||
|~ [resource tang]
|
||||
*[(list card) _^|(..on-init)]
|
||||
:: +on-pull-kick: produce any additional resubscribe path
|
||||
::
|
||||
:: If non-null, the produced path is appended to the original
|
||||
:: subscription path. This should be used to encode extra
|
||||
:: information onto the path in order to reduce the payload of a
|
||||
:: kick and resubscribe.
|
||||
::
|
||||
:: If null, a resubscribe is not attempted
|
||||
::
|
||||
++ on-pull-kick
|
||||
|~ resource
|
||||
*(unit path)
|
||||
::
|
||||
:: from agent:gall
|
||||
++ on-init
|
||||
*[(list card) _^|(..on-init)]
|
||||
::
|
||||
@ -75,26 +129,6 @@
|
||||
++ on-fail
|
||||
|~ [term tang]
|
||||
*[(list card) _^|(..on-init)]
|
||||
:: +on-pull-nack: handle failed pull subscription
|
||||
::
|
||||
:: This arm is called when a pull subscription fails.
|
||||
::
|
||||
++ on-pull-nack
|
||||
|~ [resource tang]
|
||||
*[(list card) _^|(..on-init)]
|
||||
:: +on-pull-kick: produce any additional resubscribe path
|
||||
::
|
||||
:: If non-null, the produced path is appended to the original
|
||||
:: subscription path. This should be used to encode extra
|
||||
:: information onto the path in order to reduce the payload of a
|
||||
:: kick and resubscribe.
|
||||
::
|
||||
:: If null, a resubscribe is not attempted
|
||||
::
|
||||
++ on-pull-kick
|
||||
|~ resource
|
||||
*(unit path)
|
||||
:: ::
|
||||
--
|
||||
++ agent
|
||||
|* =config
|
||||
@ -209,7 +243,10 @@
|
||||
=^ cards pull-hook
|
||||
(on-fail:og term tang)
|
||||
[cards this]
|
||||
++ on-peek on-peek:def
|
||||
++ on-peek
|
||||
|= =path
|
||||
^- (unit (unit cage))
|
||||
(on-peek:og path)
|
||||
--
|
||||
|_ =bowl:gall
|
||||
+* og ~(. pull-hook bowl)
|
||||
@ -225,6 +262,7 @@
|
||||
++ add
|
||||
|= [=ship =resource]
|
||||
~| resource
|
||||
?< |(=(our.bowl ship) =(our.bowl entity.resource))
|
||||
?: (~(has by tracking) resource)
|
||||
[~ state]
|
||||
=. tracking
|
||||
|
@ -1,8 +1,41 @@
|
||||
:: lib/push-hook: helper for creating a push hook
|
||||
::
|
||||
:: lib/push-hook is a helper for automatically pushing data from a
|
||||
:: local store to the corresponding pull-hook on remote ships. It also
|
||||
:: proxies remote pokes to the store.
|
||||
::
|
||||
:: ## Interfacing notes:
|
||||
::
|
||||
:: The inner door may interact with the library by producing cards.
|
||||
:: Do not pass any cards on a wire beginning with /helper as these
|
||||
:: wires are reserved by this library. Any watches/pokes/peeks not
|
||||
:: listed below will be routed to the inner door.
|
||||
::
|
||||
:: ## Subscription paths
|
||||
::
|
||||
:: /resource/[resource]: Receive initial state and updates to
|
||||
:: .resource. .resource should be encoded with en-path:resource from
|
||||
:: /lib/resource. Facts on this path will be of mark
|
||||
:: update-mark.config
|
||||
::
|
||||
:: ## Pokes
|
||||
::
|
||||
:: %push-hook-action: Add/remove a resource from pushing.
|
||||
:: [update-mark.config]: A poke to proxy to the local store
|
||||
::
|
||||
/- *push-hook
|
||||
/+ default-agent, resource
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
::
|
||||
:: $config: configuration for the push hook
|
||||
::
|
||||
:: .store-name: name of the store to proxy pokes and
|
||||
:: subscriptions to
|
||||
:: .store-path: subscription path to receive updates on
|
||||
:: .update-mark: mark that updates will be tagged with
|
||||
:: .pull-hook-name: name of the corresponding pull-hook
|
||||
::
|
||||
+$ config
|
||||
$: store-name=term
|
||||
store-path=path
|
||||
@ -10,6 +43,12 @@
|
||||
update-mark=term
|
||||
pull-hook-name=term
|
||||
==
|
||||
::
|
||||
:: $state-0: state for the push hook
|
||||
::
|
||||
:: .sharing: resources that the push hook is proxying
|
||||
:: .inner-state: state given to internal door
|
||||
::
|
||||
+$ state-0
|
||||
$: %0
|
||||
sharing=(set resource)
|
||||
@ -21,6 +60,48 @@
|
||||
$_ ^|
|
||||
|_ bowl:gall
|
||||
::
|
||||
:: +resource-for-update: get affected resource from an update
|
||||
::
|
||||
:: Given a vase of the update, the mark of which is
|
||||
:: update-mark.config, produce the affected resource, if any.
|
||||
::
|
||||
++ resource-for-update
|
||||
|~ vase
|
||||
*(unit resource)
|
||||
::
|
||||
:: +take-update: handle update from store
|
||||
::
|
||||
:: Given an update from the store, do other things after proxying
|
||||
:: the update
|
||||
::
|
||||
++ take-update
|
||||
|~ vase
|
||||
*[(list card) _^|(..on-init)]
|
||||
:: +should-proxy-update: should forward update to store
|
||||
::
|
||||
:: If %.y is produced, then the update is forwarded to the local
|
||||
:: store. If %.n is produced then the update is not forwarded and
|
||||
:: the poke fails.
|
||||
::
|
||||
++ should-proxy-update
|
||||
|~ vase
|
||||
*?
|
||||
:: +initial-watch: produce initial state for a subscription
|
||||
::
|
||||
:: .resource is the resource being subscribed to.
|
||||
:: .path is any additional information in the subscription wire.
|
||||
:: This would typically be used to encode state that the subscriber
|
||||
:: already has. For example, a chat client might encode
|
||||
:: the number of messages that it already has, or the date it last
|
||||
:: received an update.
|
||||
::
|
||||
:: If +initial-watch crashes, the subscription fails.
|
||||
::
|
||||
++ initial-watch
|
||||
|~ [path resource]
|
||||
*vase
|
||||
:: from agent:gall
|
||||
::
|
||||
++ on-init
|
||||
*[(list card) _^|(..on-init)]
|
||||
::
|
||||
@ -58,35 +139,6 @@
|
||||
++ on-fail
|
||||
|~ [term tang]
|
||||
*[(list card) _^|(..on-init)]
|
||||
:: +resource-for-update: get affected resource from an update
|
||||
++ resource-for-update
|
||||
|~ vase
|
||||
*(unit resource)
|
||||
::
|
||||
:: +on-update: handle update from store
|
||||
::
|
||||
:: Do extra stuff on store update
|
||||
++ take-update
|
||||
|~ vase
|
||||
*[(list card) _^|(..on-init)]
|
||||
:: +should-proxy-update: should forward update to store
|
||||
::
|
||||
:: If %.y is produced, then the update is forwarded to the local
|
||||
:: store. If %.n is produced then the update is not forwarded and
|
||||
:: the poke fails.
|
||||
::
|
||||
++ should-proxy-update
|
||||
|~ vase
|
||||
*?
|
||||
:: +initial-watch: produce initial state for a subscription
|
||||
::
|
||||
:: .resource is the resource being subscribed to.
|
||||
:: .path is any additional information in the subscription wire
|
||||
::
|
||||
++ initial-watch
|
||||
|~ [path resource]
|
||||
*vase
|
||||
::
|
||||
--
|
||||
++ agent
|
||||
|* =config
|
||||
|
@ -241,6 +241,16 @@
|
||||
;< our=@p bind:m get-our
|
||||
(watch wire [our term] path)
|
||||
::
|
||||
++ scry
|
||||
|* [=mold =path]
|
||||
=/ m (strand ,mold)
|
||||
^- form:m
|
||||
?> ?=(^ path)
|
||||
?> ?=(^ t.path)
|
||||
;< =bowl:spider bind:m get-bowl
|
||||
%- pure:m
|
||||
.^(mold i.path (scot %p our.bowl) i.t.path (scot %da now.bowl) t.t.path)
|
||||
::
|
||||
++ leave
|
||||
|= [=wire =dock]
|
||||
=/ m (strand ,~)
|
||||
@ -285,6 +295,20 @@
|
||||
[%pass /wait/(scot %da until) %arvo %b %wait until]
|
||||
(send-raw-card card)
|
||||
::
|
||||
++ map-err
|
||||
|* computation-result=mold
|
||||
=/ m (strand ,computation-result)
|
||||
|= [f=$-([term tang] [term tang]) computation=form:m]
|
||||
^- form:m
|
||||
|= tin=strand-input:strand
|
||||
=* loop $
|
||||
=/ c-res (computation tin)
|
||||
?: ?=(%cont -.next.c-res)
|
||||
c-res(self.next ..loop(computation self.next.c-res))
|
||||
?. ?=(%fail -.next.c-res)
|
||||
c-res
|
||||
c-res(err.next (f err.next.c-res))
|
||||
::
|
||||
++ set-timeout
|
||||
|* computation-result=mold
|
||||
=/ m (strand ,computation-result)
|
||||
@ -478,6 +502,17 @@
|
||||
`[%skip ~]
|
||||
`[%done +>.sign-arvo.u.in.tin]
|
||||
==
|
||||
:: +check-online: require that peer respond before timeout
|
||||
::
|
||||
++ check-online
|
||||
|= [who=ship lag=@dr]
|
||||
=/ m (strand ,~)
|
||||
^- form:m
|
||||
%+ (map-err ,~) |=(* [%offline *tang])
|
||||
%+ (set-timeout ,~) lag
|
||||
;< ~ bind:m
|
||||
(poke [who %hood] %helm-hi !>(~))
|
||||
(pure:m ~)
|
||||
::
|
||||
:: Queue on skip, try next on fail %ignore
|
||||
::
|
||||
|
@ -1,7 +1,9 @@
|
||||
/+ *graph-store
|
||||
|_ upd=update
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun upd
|
||||
++ json (update:enjs upd)
|
||||
--
|
||||
::
|
||||
|
27
pkg/arvo/mar/graph/validator/link.hoon
Normal file
27
pkg/arvo/mar/graph/validator/link.hoon
Normal file
@ -0,0 +1,27 @@
|
||||
/- *post
|
||||
|_ i=indexed-post
|
||||
++ grow
|
||||
|%
|
||||
++ noun i
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun
|
||||
|= p=*
|
||||
=/ ip ;;(indexed-post p)
|
||||
?+ index.p.ip ~|(index+index.p.ip !!)
|
||||
:: top-level link post; title and url
|
||||
::
|
||||
[@ ~]
|
||||
?> ?=([[%text @] [%url @] ~] contents.p.ip)
|
||||
ip
|
||||
::
|
||||
:: comment on link post; comment text
|
||||
::
|
||||
[@ @ ~]
|
||||
?> ?=([[%text @] ~] contents.p.ip)
|
||||
ip
|
||||
==
|
||||
--
|
||||
++ grad %noun
|
||||
--
|
13
pkg/arvo/mar/graph/view-action.hoon
Normal file
13
pkg/arvo/mar/graph/view-action.hoon
Normal file
@ -0,0 +1,13 @@
|
||||
/+ *graph-view
|
||||
|_ act=action
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun act
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun action
|
||||
++ json action:dejs
|
||||
--
|
||||
--
|
45
pkg/arvo/sur/graph-view.hoon
Normal file
45
pkg/arvo/sur/graph-view.hoon
Normal file
@ -0,0 +1,45 @@
|
||||
/- *group, store=graph-store
|
||||
/+ resource
|
||||
^?
|
||||
|%
|
||||
:: $associated: A group to associate, or a policy if it is unmanaged
|
||||
::
|
||||
+$ associated
|
||||
$% [%group rid=resource]
|
||||
[%policy =policy]
|
||||
==
|
||||
::
|
||||
:: $error: An error from a graph-view poke
|
||||
::
|
||||
:: %offline: Ship is offline
|
||||
:: %bad-perms: Not permitted
|
||||
:: %unknown: Anything not described above
|
||||
::
|
||||
+$ error
|
||||
?(%offline %bad-perms %unknown)
|
||||
:: $action: A semantic action on graphs
|
||||
::
|
||||
:: %create: Create a graph and associated metadata
|
||||
:: %delete: Delete a graph
|
||||
:: %join: Join a graph
|
||||
:: %invite: Invite users to a graph
|
||||
:: %groupify: Make graph into managed group
|
||||
::
|
||||
+$ action
|
||||
$%
|
||||
$: %create
|
||||
rid=resource
|
||||
title=@t
|
||||
description=@t
|
||||
mark=(unit mark)
|
||||
=associated
|
||||
==
|
||||
[%delete rid=resource]
|
||||
[%leave rid=resource]
|
||||
[%join rid=resource =ship]
|
||||
::[%invite rid=resource ships=(set ship)]
|
||||
[%groupify rid=resource to=(unit resource)]
|
||||
[%forward rid=resource =update:store]
|
||||
==
|
||||
--
|
||||
|
@ -5,4 +5,10 @@
|
||||
+$ input [=tid =cage]
|
||||
+$ tid tid:strand
|
||||
+$ bowl bowl:strand
|
||||
+$ http-error
|
||||
$? %bad-request :: 400
|
||||
%forbidden :: 403
|
||||
%nonexistent :: 404
|
||||
%offline :: 504
|
||||
==
|
||||
--
|
||||
|
60
pkg/arvo/ted/graph/create.hoon
Normal file
60
pkg/arvo/ted/graph/create.hoon
Normal file
@ -0,0 +1,60 @@
|
||||
/- spider, graph=graph-store, *metadata-store, *group, group-store
|
||||
/+ strandio, resource, graph-view
|
||||
=>
|
||||
|%
|
||||
++ strand strand:spider
|
||||
++ poke poke:strandio
|
||||
++ poke-our poke-our:strandio
|
||||
::
|
||||
++ handle-group
|
||||
|= [rid=resource =associated:graph-view]
|
||||
=/ m (strand ,resource)
|
||||
?: ?=(%group -.associated)
|
||||
(pure:m rid.associated)
|
||||
=/ =action:group-store
|
||||
[%add-group rid policy.associated %&]
|
||||
;< ~ bind:m (poke-our %group-store %group-action !>(action))
|
||||
;< ~ bind:m
|
||||
(poke-our %group-push-hook %push-hook-action !>([%add rid]))
|
||||
(pure:m rid)
|
||||
--
|
||||
::
|
||||
=, strand=strand:spider
|
||||
^- thread:spider
|
||||
|= arg=vase
|
||||
=/ m (strand ,vase)
|
||||
^- form:m
|
||||
=+ !<(=action:graph-view arg)
|
||||
?> ?=(%create -.action)
|
||||
;< =bowl:spider bind:m get-bowl:strandio
|
||||
:: Add graph to graph-store
|
||||
::
|
||||
?. =(our.bowl entity.rid.action)
|
||||
(strand-fail:strandio %bad-request ~)
|
||||
=/ =update:graph
|
||||
[%0 now.bowl %add-graph rid.action *graph:graph mark.action]
|
||||
;< ~ bind:m
|
||||
(poke-our %graph-store graph-update+!>(update))
|
||||
;< ~ bind:m
|
||||
(poke-our %graph-push-hook %push-hook-action !>([%add rid.action]))
|
||||
:: Add group, if graph is unmanaged
|
||||
::
|
||||
;< group=resource bind:m
|
||||
(handle-group rid.action associated.action)
|
||||
=/ group-path=path
|
||||
(en-path:resource group)
|
||||
:: Setup metadata
|
||||
::
|
||||
=/ =metadata
|
||||
%* . *metadata
|
||||
title title.action
|
||||
description description.action
|
||||
date-created now.bowl
|
||||
creator our.bowl
|
||||
==
|
||||
=/ act=metadata-action
|
||||
[%add group-path graph+(en-path:resource rid.action) metadata]
|
||||
;< ~ bind:m (poke-our %metadata-hook %metadata-action !>(act))
|
||||
;< ~ bind:m
|
||||
(poke-our %metadata-hook %metadata-hook-action !>([%add-owned group-path]))
|
||||
(pure:m !>(~))
|
70
pkg/arvo/ted/graph/delete.hoon
Normal file
70
pkg/arvo/ted/graph/delete.hoon
Normal file
@ -0,0 +1,70 @@
|
||||
/- spider, graph-view, graph=graph-store, *metadata-store, *group
|
||||
/+ strandio, resource
|
||||
=>
|
||||
|%
|
||||
++ strand strand:spider
|
||||
++ poke poke:strandio
|
||||
++ poke-our poke-our:strandio
|
||||
::
|
||||
++ scry-metadata
|
||||
|= rid=resource
|
||||
=/ m (strand ,(unit resource))
|
||||
;< paxs=(unit (set path)) bind:m
|
||||
%+ scry:strandio ,(unit (set path))
|
||||
;: weld
|
||||
/gx/metadata-store/resource/publish
|
||||
(en-path:resource rid)
|
||||
/noun
|
||||
==
|
||||
?~ paxs (pure:m ~)
|
||||
?~ u.paxs (pure:m ~)
|
||||
(pure:m `(de-path:resource n.u.paxs))
|
||||
::
|
||||
++ scry-group
|
||||
|= rid=resource
|
||||
=/ m (strand ,group)
|
||||
;< ugroup=(unit group) bind:m
|
||||
%+ scry:strandio ,(unit group)
|
||||
;: weld
|
||||
/gx/group-store/groups
|
||||
(en-path:resource rid)
|
||||
/noun
|
||||
==
|
||||
(pure:m (need ugroup))
|
||||
::
|
||||
++ delete-graph
|
||||
|= rid=resource
|
||||
=/ m (strand ,~)
|
||||
^- form:m
|
||||
;< ~ bind:m
|
||||
(poke-our %graph-push-hook %push-hook-action !>([%remove rid]))
|
||||
;< =bowl:spider bind:m get-bowl:strandio
|
||||
;< ~ bind:m
|
||||
(poke-our %graph-store %graph-update !>([%0 now.bowl %archive-graph rid]))
|
||||
(pure:m ~)
|
||||
--
|
||||
::
|
||||
^- thread:spider
|
||||
|= arg=vase
|
||||
=/ m (strand ,vase)
|
||||
^- form:m
|
||||
=+ !<(=action:graph-view arg)
|
||||
?> ?=(%delete -.action)
|
||||
;< =bowl:spider bind:m get-bowl:strandio
|
||||
?. =(our.bowl entity.rid.action)
|
||||
(strand-fail:strandio %bad-request ~)
|
||||
;< ugroup-rid=(unit resource) bind:m
|
||||
(scry-metadata rid.action)
|
||||
?~ ugroup-rid !!
|
||||
;< =group bind:m
|
||||
(scry-group u.ugroup-rid)
|
||||
?. hidden.group
|
||||
;< ~ bind:m
|
||||
(delete-graph rid.action)
|
||||
(pure:m !>(~))
|
||||
;< ~ bind:m
|
||||
(poke-our %group-push-hook %push-hook-action !>([%remove rid.action]))
|
||||
;< ~ bind:m
|
||||
(poke-our %group-store %group-action !>([%remove-group rid.action]))
|
||||
;< ~ bind:m (delete-graph rid.action)
|
||||
(pure:m !>(~))
|
20
pkg/arvo/ted/graph/forward.hoon
Normal file
20
pkg/arvo/ted/graph/forward.hoon
Normal file
@ -0,0 +1,20 @@
|
||||
/- spider, graph-view, graph=graph-store, *metadata-store, *group
|
||||
/+ strandio, resource
|
||||
=>
|
||||
|%
|
||||
++ strand strand:spider
|
||||
++ poke poke:strandio
|
||||
--
|
||||
::
|
||||
^- thread:spider
|
||||
|= arg=vase
|
||||
=/ m (strand ,vase)
|
||||
^- form:m
|
||||
=+ !<(=action:graph-view arg)
|
||||
?> ?=(%forward -.action)
|
||||
;< ~ bind:m
|
||||
%+ (map-err:strandio ,~) |=(* [%forbidden ~])
|
||||
%+ poke
|
||||
[entity.rid.action %graph-push-hook]
|
||||
[%graph-update !>(update.action)]
|
||||
(pure:m !>(~))
|
74
pkg/arvo/ted/graph/groupify.hoon
Normal file
74
pkg/arvo/ted/graph/groupify.hoon
Normal file
@ -0,0 +1,74 @@
|
||||
/- spider, graph-view, graph=graph-store, *metadata-store, *group, *metadata-store
|
||||
/+ strandio, resource
|
||||
=>
|
||||
|%
|
||||
++ strand strand:spider
|
||||
++ poke poke:strandio
|
||||
++ poke-our poke-our:strandio
|
||||
::
|
||||
++ check-live
|
||||
|= who=ship
|
||||
=/ m (strand ,~)
|
||||
^- form:m
|
||||
%+ (set-timeout:strandio ,~) ~s20
|
||||
;< ~ bind:m
|
||||
(poke [who %hood] %helm-hi !>(~))
|
||||
(pure:m ~)
|
||||
::
|
||||
++ scry-group
|
||||
|= rid=resource
|
||||
=/ m (strand ,group)
|
||||
^- form:m
|
||||
;< ugroup=(unit group) bind:m
|
||||
%+ scry:strandio (unit group)
|
||||
%+ weld /gx/group-store/groups
|
||||
(snoc (en-path:resource rid) %noun)
|
||||
?> ?=(^ ugroup)
|
||||
(pure:m u.ugroup)
|
||||
::
|
||||
++ scry-metadatum
|
||||
|= rid=resource
|
||||
=/ m (strand ,metadata)
|
||||
^- form:m
|
||||
=/ enc-path=@t
|
||||
(scot %t (spat (en-path:resource rid)))
|
||||
;< umeta=(unit metadata) bind:m
|
||||
%+ scry:strandio (unit metadata)
|
||||
%+ weld /gx/metadata-store/metadata
|
||||
/[enc-path]/graph/[enc-path]/noun
|
||||
?> ?=(^ umeta)
|
||||
(pure:m u.umeta)
|
||||
--
|
||||
::
|
||||
^- thread:spider
|
||||
|= arg=vase
|
||||
=/ m (strand ,vase)
|
||||
^- form:m
|
||||
=+ !<(=action:graph-view arg)
|
||||
?> ?=(%groupify -.action)
|
||||
;< =group bind:m (scry-group rid.action)
|
||||
?. hidden.group
|
||||
(strand-fail:strandio %bad-request ~)
|
||||
;< =metadata bind:m
|
||||
(scry-metadatum rid.action)
|
||||
?~ to.action
|
||||
;< ~ bind:m
|
||||
%+ poke-our %contact-view
|
||||
contact-view-action+!>([%groupify rid.action title.metadata description.metadata])
|
||||
(pure:m !>(~))
|
||||
;< new=^group bind:m (scry-group u.to.action)
|
||||
?< hidden.new
|
||||
=/ new-path
|
||||
(en-path:resource u.to.action)
|
||||
=/ app-path
|
||||
(en-path:resource rid.action)
|
||||
=/ add-md=metadata-action
|
||||
[%add new-path graph+app-path metadata]
|
||||
;< ~ bind:m
|
||||
(poke-our %metadata-store metadata-action+!>(add-md))
|
||||
;< ~ bind:m
|
||||
%+ poke-our %metadata-store
|
||||
metadata-action+!>([%remove app-path graph+app-path])
|
||||
;< ~ bind:m
|
||||
(poke-our %group-store %group-update !>([%remove-group rid.action]))
|
||||
(pure:m !>(~))
|
61
pkg/arvo/ted/graph/join.hoon
Normal file
61
pkg/arvo/ted/graph/join.hoon
Normal file
@ -0,0 +1,61 @@
|
||||
/- spider, graph-view, graph=graph-store, *metadata-store, *group
|
||||
/+ strandio, resource
|
||||
=>
|
||||
|%
|
||||
++ strand strand:spider
|
||||
++ fail strand-fail:strand
|
||||
++ poke poke:strandio
|
||||
++ poke-our poke-our:strandio
|
||||
::
|
||||
++ scry-metadata
|
||||
|= rid=resource
|
||||
=/ m (strand ,(unit resource))
|
||||
^- form:m
|
||||
;< pax=(unit (set path)) bind:m
|
||||
%+ scry:strandio ,(unit (set path))
|
||||
;: weld
|
||||
/gx/metadata-store/resource/graph
|
||||
(en-path:resource rid)
|
||||
/noun
|
||||
==
|
||||
%- pure:m
|
||||
?~ pax ~
|
||||
?~ u.pax ~
|
||||
`(de-path:resource n.u.pax)
|
||||
--
|
||||
::
|
||||
^- thread:spider
|
||||
|= arg=vase
|
||||
=/ m (strand ,vase)
|
||||
^- form:m
|
||||
=+ !<(=action:graph-view arg)
|
||||
?> ?=(%join -.action)
|
||||
;< =bowl:spider bind:m get-bowl:strandio
|
||||
?: =(our.bowl entity.rid.action)
|
||||
(fail %bad-request ~)
|
||||
;< group=(unit resource) bind:m (scry-metadata rid.action)
|
||||
?^ group
|
||||
:: We have group, graph is managed
|
||||
;< ~ bind:m
|
||||
%+ poke-our %graph-pull-hook
|
||||
pull-hook-action+!>([%add ship.action rid.action])
|
||||
(pure:m !>(~))
|
||||
:: Else, add group then join
|
||||
;< ~ bind:m
|
||||
%+ (map-err:strandio ,~) |=(* [%forbidden ~])
|
||||
%+ poke
|
||||
[ship.action %group-push-hook]
|
||||
group-update+!>([%add-members rid.action (sy our.bowl ~)])
|
||||
::
|
||||
;< ~ bind:m
|
||||
%+ poke-our %group-pull-hook
|
||||
pull-hook-action+!>([%add ship.action rid.action])
|
||||
::
|
||||
;< ~ bind:m
|
||||
%+ poke-our %metadata-hook
|
||||
metadata-hook-action+!>([%add-synced ship.action rid.action])
|
||||
::
|
||||
;< ~ bind:m
|
||||
%+ poke-our %graph-pull-hook
|
||||
pull-hook-action+!>([%add ship.action rid.action])
|
||||
(pure:m !>(~))
|
67
pkg/arvo/ted/graph/leave.hoon
Normal file
67
pkg/arvo/ted/graph/leave.hoon
Normal file
@ -0,0 +1,67 @@
|
||||
/- spider, graph-view, graph=graph-store, *metadata-store, *group
|
||||
/+ strandio, resource
|
||||
=>
|
||||
|%
|
||||
++ strand strand:spider
|
||||
++ poke poke:strandio
|
||||
++ poke-our poke-our:strandio
|
||||
::
|
||||
++ scry-metadata
|
||||
|= rid=resource
|
||||
=/ m (strand ,resource)
|
||||
^- form:m
|
||||
;< pax=(unit (set path)) bind:m
|
||||
%+ scry:strandio ,(unit (set path))
|
||||
;: weld
|
||||
/gx/metadata-store/resource/graph
|
||||
(en-path:resource rid)
|
||||
/noun
|
||||
==
|
||||
?> ?=(^ pax)
|
||||
?> ?=(^ u.pax)
|
||||
(pure:m (de-path:resource n.u.pax))
|
||||
::
|
||||
++ scry-group
|
||||
|= rid=resource
|
||||
=/ m (strand ,group)
|
||||
^- form:m
|
||||
;< ugroup=(unit group) bind:m
|
||||
%+ scry:strandio ,(unit group)
|
||||
;: weld
|
||||
/gx/group-store/resource/graph
|
||||
(en-path:resource rid)
|
||||
/noun
|
||||
==
|
||||
(pure:m (need ugroup))
|
||||
::
|
||||
++ delete-graph
|
||||
|= rid=resource
|
||||
=/ m (strand ,~)
|
||||
^- form:m
|
||||
;< ~ bind:m
|
||||
(poke-our %graph-pull-hook %pull-hook-action !>([%remove rid]))
|
||||
;< ~ bind:m
|
||||
(poke-our %graph-store %graph-update !>([%archive-graph rid]))
|
||||
(pure:m ~)
|
||||
--
|
||||
::
|
||||
^- thread:spider
|
||||
|= arg=vase
|
||||
=/ m (strand ,vase)
|
||||
^- form:m
|
||||
=+ !<([=action:graph-view ~] arg)
|
||||
?> ?=(%leave -.action)
|
||||
;< =bowl:spider bind:m get-bowl:strandio
|
||||
?: =(our.bowl entity.rid.action)
|
||||
(strand-fail:strandio %bad-request ~)
|
||||
;< group-rid=resource bind:m (scry-metadata rid.action)
|
||||
;< g=group bind:m (scry-group group-rid)
|
||||
?. hidden.g
|
||||
;< ~ bind:m (delete-graph rid.action)
|
||||
(pure:m !>(~))
|
||||
;< ~ bind:m
|
||||
(poke-our %group-push-hook %pull-hook-action !>([%remove rid.action]))
|
||||
;< ~ bind:m
|
||||
(poke-our %group-store %group-action !>([%remove-group rid.action]))
|
||||
;< ~ bind:m (delete-graph rid.action)
|
||||
(pure:m !>(~))
|
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"
|
||||
}
|
||||
},
|
||||
"memoize-one": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz",
|
||||
"integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA=="
|
||||
},
|
||||
"memory-fs": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
|
||||
@ -6807,6 +6802,11 @@
|
||||
"tslib": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
|
||||
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
|
||||
},
|
||||
"node-forge": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz",
|
||||
@ -7058,6 +7058,14 @@
|
||||
"integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
|
||||
"dev": true
|
||||
},
|
||||
"oembed-parser": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/oembed-parser/-/oembed-parser-1.4.1.tgz",
|
||||
"integrity": "sha512-1KqnfrXF3TiAQhJ9+vv3dEtMhPSVSOT9D9XPqLjEtaQg5liPc3LQ65YjgKHo7Z/YY/kmZ1PDb5gMcOxxCPPdBA==",
|
||||
"requires": {
|
||||
"node-fetch": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"omit-deep": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/omit-deep/-/omit-deep-0.3.0.tgz",
|
||||
@ -7937,6 +7945,14 @@
|
||||
"xtend": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"react-oembed-container": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-oembed-container/-/react-oembed-container-1.0.0.tgz",
|
||||
"integrity": "sha512-YppvCDgxZkn6qgwAIpxRtmMtxaMpau8yQhm8nzmH7yHpDapmHxzakXvQke5qPfmdYyYW4CsKDfVfGoX14NvQkw==",
|
||||
"requires": {
|
||||
"prop-types": "^15.6.0"
|
||||
}
|
||||
},
|
||||
"react-router": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",
|
||||
@ -7973,13 +7989,13 @@
|
||||
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.0.tgz",
|
||||
"integrity": "sha512-IgmcegOSi5SNX+2Snh1vqmF0Vg/CbkycU9XZbOHJlZ6kMzTmi3yc254oB1WCkgA7OQtIAoLmcSFuHTc/tlcqXg=="
|
||||
},
|
||||
"react-window": {
|
||||
"version": "1.8.5",
|
||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.5.tgz",
|
||||
"integrity": "sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q==",
|
||||
"react-virtuoso": {
|
||||
"version": "0.20.0",
|
||||
"resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-0.20.0.tgz",
|
||||
"integrity": "sha512-h+U6t/+m91AzfUe6bBfaacdLLJl1y8v7CfcXwPgQ/Dic+vNlgQmi6cIKTq18zuF+kI8Q7QN0ojIeqPHWbU8TZA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"memoize-one": ">=3.1.1 <6"
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"tslib": "^1.11.1"
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
@ -8260,6 +8276,11 @@
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||
"dev": true
|
||||
},
|
||||
"resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
|
||||
|
@ -20,6 +20,7 @@
|
||||
"moment": "^2.20.1",
|
||||
"mousetrap": "^1.6.5",
|
||||
"mousetrap-global-bind": "^1.1.0",
|
||||
"oembed-parser": "^1.4.1",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.5.2",
|
||||
"react-codemirror2": "^6.0.1",
|
||||
@ -29,8 +30,9 @@
|
||||
"react-dom": "^16.8.6",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-markdown": "^4.3.1",
|
||||
"react-oembed-container": "^1.0.0",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"react-window": "^1.8.5",
|
||||
"react-virtuoso": "^0.20.0",
|
||||
"remark-disable-tokenizers": "^1.0.24",
|
||||
"style-loader": "^1.2.1",
|
||||
"styled-components": "^5.1.0",
|
||||
|
@ -57,4 +57,16 @@ export default class BaseApi<S extends object = {}> {
|
||||
scry<T>(app: string, path: Path): Promise<T> {
|
||||
return fetch(`/~/scry/${app}${path}.json`).then(r => r.json() as Promise<T>);
|
||||
}
|
||||
|
||||
|
||||
async spider<T>(inputMark: string, outputMark: string, threadName: string, body: any): Promise<T> {
|
||||
|
||||
const res = await fetch(`/spider/${inputMark}/${threadName}/${outputMark}.json`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ export default class ChatApi extends BaseApi<StoreState> {
|
||||
* Fetch backlog
|
||||
*/
|
||||
fetchMessages(start: number, end: number, path: Path) {
|
||||
fetch(`/chat-view/paginate/${start}/${end}${path}`)
|
||||
return fetch(`/chat-view/paginate/${start}/${end}${path}`)
|
||||
.then(response => response.json())
|
||||
.then((json) => {
|
||||
this.store.handleEvent({
|
||||
|
@ -1,7 +1,9 @@
|
||||
import BaseApi from './base';
|
||||
import { StoreState } from '../store/type';
|
||||
import { Patp, Path, PatpNoSig } from '~/types/noun';
|
||||
|
||||
import _ from 'lodash';
|
||||
import {makeResource, resourceFromPath} from '../lib/group';
|
||||
import {GroupPolicy, Enc, Post} from '~/types';
|
||||
|
||||
export const createPost = (contents: Object[], parentIndex: string = '') => {
|
||||
return {
|
||||
@ -20,6 +22,68 @@ export default class GraphApi extends BaseApi<StoreState> {
|
||||
return this.action('graph-store', 'graph-update', action)
|
||||
}
|
||||
|
||||
private viewAction(threadName: string, action: any) {
|
||||
return this.spider('graph-view-action', 'json', threadName, action);
|
||||
}
|
||||
|
||||
createManagedGraph(name: string, title: string, description: string, group: Path) {
|
||||
const associated = { group: resourceFromPath(group) };
|
||||
|
||||
const resource = makeResource(`~${window.ship}`, name);
|
||||
return this.viewAction('graph-create', {
|
||||
"create": {
|
||||
resource,
|
||||
title,
|
||||
description,
|
||||
associated
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createUnmanagedGraph(name: string, title: string, description: string, policy: Enc<GroupPolicy>) {
|
||||
|
||||
const resource = makeResource(`~${window.ship}`, name);
|
||||
return this.viewAction('graph-create', {
|
||||
"create": {
|
||||
resource,
|
||||
title,
|
||||
description,
|
||||
associated: { policy }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
joinGraph(ship: Patp, name: string) {
|
||||
const resource = makeResource(ship, name);
|
||||
return this.viewAction('graph-join', {
|
||||
join: {
|
||||
resource,
|
||||
ship,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteGraph(name: string) {
|
||||
const resource = makeResource(`~${window.ship}`, name);
|
||||
return this.viewAction('graph-delete', {
|
||||
"delete": {
|
||||
resource
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
groupifyGraph(ship: Patp, name: string, toPath?: string) {
|
||||
const resource = makeResource(ship, name);
|
||||
const to = toPath && resourceFromPath(toPath);
|
||||
|
||||
return this.viewAction('graph-groupify', {
|
||||
groupify: {
|
||||
resource,
|
||||
to
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addGraph(ship: Patp, name: string, graph: any, mark: any) {
|
||||
this.storeAction({
|
||||
'add-graph': {
|
||||
@ -38,8 +102,9 @@ export default class GraphApi extends BaseApi<StoreState> {
|
||||
});
|
||||
}
|
||||
|
||||
addPost(ship: Patp, name: string, post: Object) {
|
||||
addPost(ship: Patp, name: string, post: Post) {
|
||||
let nodes = {};
|
||||
const resource = { ship, name };
|
||||
nodes[post.index] = {
|
||||
post,
|
||||
children: { empty: null }
|
||||
@ -47,7 +112,7 @@ export default class GraphApi extends BaseApi<StoreState> {
|
||||
|
||||
return this.storeAction({
|
||||
'add-nodes': {
|
||||
resource: { ship, name },
|
||||
resource,
|
||||
nodes
|
||||
}
|
||||
});
|
||||
@ -63,7 +128,7 @@ export default class GraphApi extends BaseApi<StoreState> {
|
||||
}
|
||||
|
||||
removeNodes(ship: Patp, name: string, indices: string[]) {
|
||||
this.storeAction({
|
||||
return this.storeAction({
|
||||
'remove-nodes': {
|
||||
resource: { ship, name },
|
||||
indices
|
||||
@ -107,7 +172,7 @@ export default class GraphApi extends BaseApi<StoreState> {
|
||||
});
|
||||
}
|
||||
|
||||
getGraphSubset(ship: string, resource: string, start: string, end: start) {
|
||||
getGraphSubset(ship: string, resource: string, start: string, end: string) {
|
||||
this.scry<any>(
|
||||
'graph-store',
|
||||
`/graph-subset/${ship}/${resource}/${end}/${start}`
|
||||
|
@ -1,6 +1,6 @@
|
||||
import BaseApi from "./base";
|
||||
import { StoreState } from "../store/type";
|
||||
import { BackgroundConfig } from "../types/local-update";
|
||||
import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from "../types/local-update";
|
||||
|
||||
export default class LocalApi extends BaseApi<StoreState> {
|
||||
getBaseHash() {
|
||||
@ -69,6 +69,16 @@ export default class LocalApi extends BaseApi<StoreState> {
|
||||
});
|
||||
}
|
||||
|
||||
setRemoteContentPolicy(policy: LocalUpdateRemoteContentPolicy) {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
remoteContentPolicy: policy
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dehydrate() {
|
||||
this.store.dehydrate();
|
||||
}
|
||||
|
@ -82,6 +82,17 @@ export default class PublishApi extends BaseApi {
|
||||
return this.action('publish', 'publish-action', act);
|
||||
}
|
||||
|
||||
groupify(bookId: string, group: Path | null) {
|
||||
return this.publishAction({
|
||||
groupify: {
|
||||
book: bookId,
|
||||
target: group,
|
||||
inclusive: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
newBook(bookId: string, title: string, description: string, group?: Path) {
|
||||
const groupInfo = group ? { 'group-path': group,
|
||||
invitees: [],
|
||||
|
@ -13,3 +13,8 @@ export function resourceFromPath(path: Path): Resource {
|
||||
const [, , ship, name] = path.split('/');
|
||||
return { ship, name }
|
||||
}
|
||||
|
||||
export function makeResource(ship: string, name:string) {
|
||||
return { ship, name };
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,12 @@
|
||||
import defaultApps from './default-apps';
|
||||
import { cite } from '~/logic/lib/util';
|
||||
|
||||
const indexes = new Map([
|
||||
['commands', []],
|
||||
['subscriptions', []],
|
||||
['groups', []],
|
||||
['apps', []]
|
||||
['apps', []],
|
||||
['other', []]
|
||||
]);
|
||||
|
||||
// result schematic
|
||||
@ -41,8 +43,6 @@ const commandIndex = function () {
|
||||
}
|
||||
});
|
||||
|
||||
commands.push(result('Profile', '/~profile', 'profile', null));
|
||||
|
||||
return commands;
|
||||
};
|
||||
|
||||
@ -54,6 +54,9 @@ const appIndex = function (apps) {
|
||||
.filter((e) => {
|
||||
return apps[e]?.type?.basic;
|
||||
})
|
||||
.sort((a,b) => {
|
||||
return a.localeCompare(b);
|
||||
})
|
||||
.map((e) => {
|
||||
const obj = result(
|
||||
apps[e].type.basic.title,
|
||||
@ -70,6 +73,14 @@ const appIndex = function (apps) {
|
||||
return applications;
|
||||
};
|
||||
|
||||
const otherIndex = function() {
|
||||
const other = [];
|
||||
other.push(result('Profile and Settings', '/~profile/identity', 'profile', null));
|
||||
other.push(result('Log Out', '/~/logout', 'logout', null));
|
||||
|
||||
return other;
|
||||
};
|
||||
|
||||
export default function index(associations, apps) {
|
||||
// all metadata from all apps is indexed
|
||||
// into subscriptions and groups
|
||||
@ -99,7 +110,7 @@ export default function index(associations, apps) {
|
||||
title,
|
||||
`/~${app}${each['app-path']}`,
|
||||
app.charAt(0).toUpperCase() + app.slice(1),
|
||||
shipStart.slice(0, shipStart.indexOf('/'))
|
||||
cite(shipStart.slice(0, shipStart.indexOf('/')))
|
||||
);
|
||||
groups.push(obj);
|
||||
} else {
|
||||
@ -107,7 +118,7 @@ export default function index(associations, apps) {
|
||||
title,
|
||||
`/~${each['app-name']}/join${each['app-path']}`,
|
||||
app.charAt(0).toUpperCase() + app.slice(1),
|
||||
shipStart.slice(0, shipStart.indexOf('/'))
|
||||
(associations?.contacts?.[each['group-path']]?.metadata?.title || null)
|
||||
);
|
||||
subscriptions.push(obj);
|
||||
}
|
||||
@ -118,6 +129,7 @@ export default function index(associations, apps) {
|
||||
indexes.set('subscriptions', subscriptions);
|
||||
indexes.set('groups', groups);
|
||||
indexes.set('apps', appIndex(apps));
|
||||
indexes.set('other', otherIndex());
|
||||
|
||||
return indexes;
|
||||
};
|
||||
|
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';
|
||||
|
||||
|
||||
export function useWaitForProps<P>(props: P, timeout: number) {
|
||||
export function useWaitForProps<P>(props: P, timeout: number = 0) {
|
||||
const [resolve, setResolve] = useState<() => void>(() => () => {});
|
||||
const [ready, setReady] = useState<(p: P) => boolean | undefined>();
|
||||
|
||||
@ -24,9 +24,11 @@ export function useWaitForProps<P>(props: P, timeout: number) {
|
||||
setReady(() => r);
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
setResolve(() => resolve);
|
||||
setTimeout(() => {
|
||||
reject(new Error("Timed out"));
|
||||
}, timeout);
|
||||
if(timeout > 0) {
|
||||
setTimeout(() => {
|
||||
reject(new Error("Timed out"));
|
||||
}, timeout);
|
||||
}
|
||||
});
|
||||
},
|
||||
[setResolve, setReady, timeout]
|
||||
|
@ -1,8 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
import { StoreState } from '../../../store/type';
|
||||
import { StoreState } from '~/logic/store/type';
|
||||
import { Cage } from '~/types/cage';
|
||||
import { ChatUpdate } from '~/types/chat-update';
|
||||
import { ChatHookUpdate } from '~/types/chat-hook-update';
|
||||
import { Envelope } from "~/types/chat-update";
|
||||
|
||||
type ChatState = Pick<StoreState, 'chatInitialized' | 'chatSynced' | 'inbox' | 'pendingMessages'>;
|
||||
|
||||
@ -49,8 +50,11 @@ export default class ChatReducer<S extends ChatState> {
|
||||
messages(json: ChatUpdate, state: S) {
|
||||
const data = _.get(json, 'messages', false);
|
||||
if (data) {
|
||||
state.inbox[data.path].envelopes =
|
||||
state.inbox[data.path].envelopes.concat(data.envelopes);
|
||||
state.inbox[data.path].envelopes = _.unionBy(
|
||||
state.inbox[data.path].envelopes,
|
||||
data.envelopes,
|
||||
(envelope: Envelope) => envelope.uid
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,10 +140,21 @@ const addNodes = (json, state) => {
|
||||
};
|
||||
|
||||
const removeNodes = (json, state) => {
|
||||
const _remove = (graph, index) => {
|
||||
if (index.length === 1) {
|
||||
graph.delete(index[0]);
|
||||
} else {
|
||||
const child = graph.get(index[0]);
|
||||
_remove(child.children, index.slice(1));
|
||||
graph.set(index[0], child);
|
||||
}
|
||||
};
|
||||
const data = _.get(json, 'remove-nodes', false);
|
||||
if (data) {
|
||||
console.log(data);
|
||||
if (!(data.resource in state.graphs)) { return; }
|
||||
const { ship, name } = data.resource;
|
||||
const res = `${ship}/${name}`;
|
||||
if (!(res in state.graphs)) { return; }
|
||||
|
||||
data.indices.forEach((index) => {
|
||||
console.log(index);
|
||||
@ -151,13 +162,7 @@ const removeNodes = (json, state) => {
|
||||
let indexArr = index.split('/').slice(1).map((ind) => {
|
||||
return parseInt(ind, 10);
|
||||
});
|
||||
|
||||
if (indexArr.length === 1) {
|
||||
state.graphs[data.resource].delete(indexArr[0]);
|
||||
} else {
|
||||
// TODO: recursive
|
||||
}
|
||||
|
||||
_remove(state.graphs[res], indexArr);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -78,6 +78,7 @@ export default class GroupReducer<S extends GroupState> {
|
||||
this.addGroup(data, state);
|
||||
this.removeGroup(data, state);
|
||||
this.changePolicy(data, state);
|
||||
this.expose(data, state);
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,6 +188,15 @@ export default class GroupReducer<S extends GroupState> {
|
||||
}
|
||||
}
|
||||
|
||||
expose(json: GroupUpdate, state: S) {
|
||||
if( 'expose' in json && state) {
|
||||
const { resource } = json.expose;
|
||||
const resourcePath = resourceAsPath(resource);
|
||||
state.groups[resourcePath].hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private inviteChangePolicy(diff: InvitePolicyDiff, policy: InvitePolicy) {
|
||||
if ('addInvites' in diff) {
|
||||
const { addInvites } = diff;
|
||||
|
@ -3,7 +3,7 @@ import { StoreState } from '~/store/type';
|
||||
import { Cage } from '~/types/cage';
|
||||
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> {
|
||||
rehydrate(state: S) {
|
||||
@ -18,7 +18,7 @@ export default class LocalReducer<S extends LocalState> {
|
||||
}
|
||||
|
||||
dehydrate(state: S) {
|
||||
const json = _.pick(state, ['hideNicknames' , 'hideAvatars' , 'background']);
|
||||
const json = _.pick(state, ['hideNicknames' , 'hideAvatars' , 'background', 'remoteContentPolicy']);
|
||||
localStorage.setItem('localReducer', JSON.stringify(json));
|
||||
}
|
||||
reduce(json: Cage, state: S) {
|
||||
@ -31,6 +31,7 @@ export default class LocalReducer<S extends LocalState> {
|
||||
this.hideAvatars(data, state)
|
||||
this.hideNicknames(data, state)
|
||||
this.omniboxShown(data, state);
|
||||
this.remoteContentPolicy(data, state);
|
||||
}
|
||||
}
|
||||
baseHash(obj: LocalUpdate, state: S) {
|
||||
@ -70,6 +71,12 @@ export default class LocalReducer<S extends LocalState> {
|
||||
}
|
||||
}
|
||||
|
||||
remoteContentPolicy(obj: LocalUpdate, state: S) {
|
||||
if('remoteContentPolicy' in obj) {
|
||||
state.remoteContentPolicy = obj.remoteContentPolicy;
|
||||
}
|
||||
}
|
||||
|
||||
hideAvatars(obj: LocalUpdate, state: S) {
|
||||
if('hideAvatars' in obj) {
|
||||
state.hideAvatars = obj.hideAvatars;
|
||||
|
@ -6,6 +6,7 @@ import InviteReducer from '../reducers/invite-update';
|
||||
import LinkReducer from '../reducers/link-update';
|
||||
import ListenReducer from '../reducers/listen-update';
|
||||
import LocalReducer from '../reducers/local';
|
||||
import S3Reducer from '../reducers/s3-update';
|
||||
|
||||
import BaseStore from './base';
|
||||
|
||||
@ -21,6 +22,7 @@ export default class LinksStore extends BaseStore {
|
||||
this.localReducer = new LocalReducer();
|
||||
this.linkReducer = new LinkReducer();
|
||||
this.listenReducer = new ListenReducer();
|
||||
this.s3Reducer = new S3Reducer();
|
||||
}
|
||||
|
||||
initialState() {
|
||||
@ -37,6 +39,7 @@ export default class LinksStore extends BaseStore {
|
||||
comments: {},
|
||||
seen: {},
|
||||
permissions: {},
|
||||
s3: {},
|
||||
sidebarShown: true
|
||||
};
|
||||
}
|
||||
@ -50,6 +53,7 @@ export default class LinksStore extends BaseStore {
|
||||
this.localReducer.reduce(data, this.state);
|
||||
this.linkReducer.reduce(data, this.state);
|
||||
this.listenReducer.reduce(data, this.state);
|
||||
this.s3Reducer.reduce(data, this.state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,6 +54,12 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
suspendedFocus: null,
|
||||
baseHash: null,
|
||||
background: undefined,
|
||||
remoteContentPolicy: {
|
||||
imageShown: true,
|
||||
audioShown: true,
|
||||
videoShown: true,
|
||||
oembedShown: true,
|
||||
},
|
||||
hideAvatars: false,
|
||||
hideNicknames: false,
|
||||
invites: {},
|
||||
|
@ -11,7 +11,8 @@ import { Permissions } from '~/types/permission-update';
|
||||
import { LaunchState, WeatherState } from '~/types/launch-update';
|
||||
import { LinkComments, LinkCollections, LinkSeen } from '~/types/link-update';
|
||||
import { ConnectionStatus } from '~/types/connection';
|
||||
import { BackgroundConfig } from '~/types/local-update';
|
||||
import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from '~/types/local-update';
|
||||
import {Graphs} from '~/types/graph-update';
|
||||
|
||||
export interface StoreState {
|
||||
// local state
|
||||
@ -22,6 +23,7 @@ export interface StoreState {
|
||||
connection: ConnectionStatus;
|
||||
baseHash: string | null;
|
||||
background: BackgroundConfig;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
// invite state
|
||||
@ -35,7 +37,7 @@ export interface StoreState {
|
||||
groupKeys: Set<Path>;
|
||||
permissions: Permissions;
|
||||
s3: S3State;
|
||||
graphs: Object;
|
||||
graphs: Graphs;
|
||||
graphKeys: Set<String>;
|
||||
|
||||
|
||||
|
@ -73,6 +73,10 @@ export interface Envelope {
|
||||
letter: Letter;
|
||||
}
|
||||
|
||||
export type IMessage = Envelope & {
|
||||
pending?: boolean
|
||||
};
|
||||
|
||||
interface LetterText {
|
||||
text: string;
|
||||
}
|
||||
|
30
pkg/interface/src/types/graph-update.ts
Normal file
30
pkg/interface/src/types/graph-update.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import {Patp} from "./noun";
|
||||
|
||||
|
||||
export interface TextContent { text: string; };
|
||||
export interface UrlContent { url: string; }
|
||||
export interface CodeContent { expresssion: string; output: string; };
|
||||
export interface ReferenceContent { uid: string; }
|
||||
export type Content = TextContent | UrlContent | CodeContent | ReferenceContent;
|
||||
|
||||
export interface Post {
|
||||
author: Patp;
|
||||
contents: Content[];
|
||||
hash?: string;
|
||||
index: string;
|
||||
pending?: boolean;
|
||||
signatures: string[];
|
||||
'time-sent': number;
|
||||
}
|
||||
|
||||
|
||||
export interface GraphNode {
|
||||
children: Graph;
|
||||
post: Post;
|
||||
}
|
||||
|
||||
export type Graph = Map<number, GraphNode>;
|
||||
|
||||
export type Graphs = { [rid: string]: Graph };
|
||||
|
||||
|
@ -5,6 +5,7 @@ export * from './connection';
|
||||
export * from './contact-update';
|
||||
export * from './global';
|
||||
export * from './group-update';
|
||||
export * from './graph-update';
|
||||
export * from './invite-update';
|
||||
export * from './launch-update';
|
||||
export * from './link-listen-update';
|
||||
|
@ -1,12 +1,3 @@
|
||||
export type LocalUpdate =
|
||||
LocalUpdateSidebarToggle
|
||||
| LocalUpdateSetDark
|
||||
| LocalUpdateBaseHash
|
||||
| LocalUpdateBackgroundConfig
|
||||
| LocalUpdateHideAvatars
|
||||
| LocalUpdateHideNicknames
|
||||
| LocalUpdateSetOmniboxShown;
|
||||
|
||||
interface LocalUpdateSidebarToggle {
|
||||
sidebarToggle: boolean;
|
||||
}
|
||||
@ -31,7 +22,16 @@ interface LocalUpdateHideNicknames {
|
||||
hideNicknames: boolean;
|
||||
}
|
||||
|
||||
export type BackgroundConfig = BackgroundConfigUrl | BackgroundConfigColor | undefined;
|
||||
interface LocalUpdateSetOmniboxShown {
|
||||
omniboxShown: boolean;
|
||||
}
|
||||
|
||||
export interface LocalUpdateRemoteContentPolicy {
|
||||
imageShown: boolean;
|
||||
audioShown: boolean;
|
||||
videoShown: boolean;
|
||||
oembedShown: boolean;
|
||||
}
|
||||
|
||||
interface BackgroundConfigUrl {
|
||||
type: 'url';
|
||||
@ -43,6 +43,14 @@ interface BackgroundConfigColor {
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface LocalUpdateSetOmniboxShown {
|
||||
omniboxShown: boolean;
|
||||
}
|
||||
export type BackgroundConfig = BackgroundConfigUrl | BackgroundConfigColor | undefined;
|
||||
|
||||
export type LocalUpdate =
|
||||
LocalUpdateSidebarToggle
|
||||
| LocalUpdateSetDark
|
||||
| LocalUpdateBaseHash
|
||||
| LocalUpdateBackgroundConfig
|
||||
| LocalUpdateHideAvatars
|
||||
| LocalUpdateHideNicknames
|
||||
| LocalUpdateSetOmniboxShown
|
||||
| LocalUpdateRemoteContentPolicy;
|
@ -1,7 +1,7 @@
|
||||
import { hot } from 'react-hot-loader/root';
|
||||
import 'react-hot-loader';
|
||||
import * as React from 'react';
|
||||
import { BrowserRouter as Router, Route, withRouter, Switch } from 'react-router-dom';
|
||||
import { BrowserRouter as Router, withRouter } from 'react-router-dom';
|
||||
import styled, { ThemeProvider, createGlobalStyle } from 'styled-components';
|
||||
import { sigil as sigiljs, stringRenderer } from 'urbit-sigil-js';
|
||||
import Helmet from 'react-helmet';
|
||||
@ -16,8 +16,7 @@ import dark from './themes/old-dark';
|
||||
|
||||
import { Content } from './components/Content';
|
||||
import StatusBar from './components/StatusBar';
|
||||
import Omnibox from './components/Omnibox';
|
||||
import ErrorComponent from './components/Error';
|
||||
import Omnibox from './components/leap/Omnibox';
|
||||
|
||||
import GlobalStore from '~/logic/store/store';
|
||||
import GlobalSubscription from '~/logic/subscription/global';
|
||||
@ -36,7 +35,7 @@ const Root = styled.div`
|
||||
background-size: cover;
|
||||
` : p.background?.type === 'color' ? `
|
||||
background-color: ${p.background.color};
|
||||
` : ``
|
||||
` : ''
|
||||
}
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
@ -135,7 +134,8 @@ class App extends React.Component {
|
||||
ship={this.ship}
|
||||
api={this.api}
|
||||
subscription={this.subscription}
|
||||
{...state} />
|
||||
{...state}
|
||||
/>
|
||||
</Router>
|
||||
</Root>
|
||||
</ThemeProvider>
|
||||
@ -143,6 +143,5 @@ class App extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default process.env.NODE_ENV === 'production' ? App : hot(App);
|
||||
|
||||
|
@ -7,7 +7,6 @@ import './css/custom.css';
|
||||
import { Skeleton } from './components/skeleton';
|
||||
import { Sidebar } from './components/sidebar';
|
||||
import { ChatScreen } from './components/chat';
|
||||
import { MemberScreen } from './components/member';
|
||||
import { SettingsScreen } from './components/settings';
|
||||
import { NewScreen } from './components/new';
|
||||
import { JoinScreen } from './components/join';
|
||||
@ -89,7 +88,8 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
|
||||
pendingMessages,
|
||||
groups,
|
||||
hideAvatars,
|
||||
hideNicknames
|
||||
hideNicknames,
|
||||
remoteContentPolicy
|
||||
} = props;
|
||||
|
||||
const renderChannelSidebar = (props, station?) => (
|
||||
@ -194,6 +194,11 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
|
||||
render={(props) => {
|
||||
let station = `/${props.match.params.ship}/${props.match.params.station}`;
|
||||
|
||||
// ensure we know joined chats
|
||||
if(!chatInitialized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
associations={associations}
|
||||
@ -226,56 +231,57 @@ export default class ChatApp extends React.Component<ChatAppProps, {}> {
|
||||
envelopes: []
|
||||
};
|
||||
|
||||
let roomContacts = {};
|
||||
const associatedGroup =
|
||||
station in associations['chat'] &&
|
||||
'group-path' in associations.chat[station]
|
||||
? associations.chat[station]['group-path']
|
||||
: '';
|
||||
let roomContacts = {};
|
||||
const associatedGroup =
|
||||
station in associations['chat'] &&
|
||||
'group-path' in associations.chat[station]
|
||||
? associations.chat[station]['group-path']
|
||||
: '';
|
||||
|
||||
if (associations.chat[station] && associatedGroup in contacts) {
|
||||
roomContacts = contacts[associatedGroup];
|
||||
}
|
||||
if (associations.chat[station] && associatedGroup in contacts) {
|
||||
roomContacts = contacts[associatedGroup];
|
||||
}
|
||||
|
||||
const association =
|
||||
station in associations['chat'] ? associations.chat[station] : {};
|
||||
const association =
|
||||
station in associations['chat'] ? associations.chat[station] : {};
|
||||
|
||||
const group = groups[association['group-path']] || groupBunts.group();
|
||||
const group = groups[association['group-path']] || groupBunts.group();
|
||||
|
||||
const popout = props.match.url.includes('/popout/');
|
||||
const popout = props.match.url.includes('/popout/');
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
associations={associations}
|
||||
invites={invites}
|
||||
sidebarHideOnMobile={true}
|
||||
return (
|
||||
<Skeleton
|
||||
associations={associations}
|
||||
invites={invites}
|
||||
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}
|
||||
sidebarShown={sidebarShown}
|
||||
sidebar={renderChannelSidebar(props, station)}
|
||||
>
|
||||
<ChatScreen
|
||||
chatSynced={chatSynced || {}}
|
||||
station={station}
|
||||
association={association}
|
||||
api={api}
|
||||
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>
|
||||
);
|
||||
}}
|
||||
chatInitialized={chatInitialized}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
{...props}
|
||||
/>
|
||||
</Skeleton>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
|
@ -8,13 +8,15 @@ import { ChatHeader } from './lib/chat-header';
|
||||
import { ChatInput } from "./lib/chat-input";
|
||||
import { deSig } from "~/logic/lib/util";
|
||||
import { ChatHookUpdate } from "~/types/chat-hook-update";
|
||||
import ChatApi from "~/logic/api/chat";
|
||||
import { Inbox, Envelope } from "~/types/chat-update";
|
||||
import { Contacts } from "~/types/contact-update";
|
||||
import { Path, Patp } from "~/types/noun";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { Association } from "~/types/metadata-update";
|
||||
import {Group} from "~/types/group-update";
|
||||
import { LocalUpdateRemoteContentPolicy } from "~/types";
|
||||
import { S3Upload, SubmitDragger } from '~/views/components/s3-upload';
|
||||
import { IUnControlledCodeMirror } from "react-codemirror2";
|
||||
|
||||
|
||||
type ChatScreenProps = RouteComponentProps<{
|
||||
@ -26,7 +28,7 @@ type ChatScreenProps = RouteComponentProps<{
|
||||
association: Association;
|
||||
api: GlobalApi;
|
||||
read: number;
|
||||
length: number;
|
||||
mailboxSize: number;
|
||||
inbox: Inbox;
|
||||
contacts: Contacts;
|
||||
group: Group;
|
||||
@ -38,13 +40,16 @@ type ChatScreenProps = RouteComponentProps<{
|
||||
envelopes: Envelope[];
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
};
|
||||
|
||||
interface ChatScreenState {
|
||||
messages: Map<string, string>;
|
||||
dragover: boolean;
|
||||
}
|
||||
|
||||
export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
|
||||
private chatInput: React.RefObject<ChatInput>;
|
||||
lastNumPending = 0;
|
||||
activityTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
@ -53,8 +58,11 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
|
||||
|
||||
this.state = {
|
||||
messages: new Map(),
|
||||
dragover: false,
|
||||
};
|
||||
|
||||
this.chatInput = React.createRef();
|
||||
|
||||
moment.updateLocale("en", {
|
||||
calendar: {
|
||||
sameDay: "[Today]",
|
||||
@ -67,6 +75,26 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
|
||||
});
|
||||
}
|
||||
|
||||
readyToUpload(): boolean {
|
||||
return Boolean(this.chatInput.current?.s3Uploader.current?.inputRef.current);
|
||||
}
|
||||
|
||||
onDragEnter() {
|
||||
if (!this.readyToUpload()) {
|
||||
return;
|
||||
}
|
||||
this.setState({ dragover: true });
|
||||
}
|
||||
|
||||
onDrop(event: DragEvent) {
|
||||
this.setState({ dragover: false });
|
||||
if (!event.dataTransfer || !event.dataTransfer.files.length) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.chatInput.current?.uploadFiles(event.dataTransfer.files);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
@ -97,42 +125,36 @@ export class ChatScreen extends Component<ChatScreenProps, ChatScreenState> {
|
||||
!(props.station in props.chatSynced) &&
|
||||
props.envelopes.length > 0;
|
||||
|
||||
const unreadCount = props.length - props.read;
|
||||
const unreadCount = props.mailboxSize - props.read;
|
||||
const unreadMsg = unreadCount > 0 && props.envelopes[unreadCount - 1];
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
key={props.station}
|
||||
className="h-100 w-100 overflow-hidden flex flex-column relative">
|
||||
<ChatHeader
|
||||
match={props.match}
|
||||
location={props.location}
|
||||
api={props.api}
|
||||
group={props.group}
|
||||
association={props.association}
|
||||
station={props.station}
|
||||
sidebarShown={props.sidebarShown}
|
||||
popout={props.popout} />
|
||||
className="h-100 w-100 overflow-hidden flex flex-column relative"
|
||||
onDragEnter={this.onDragEnter.bind(this)}
|
||||
onDragOver={event => {
|
||||
event.preventDefault();
|
||||
if (!this.state.dragover) {
|
||||
this.setState({ dragover: true });
|
||||
}
|
||||
}}
|
||||
onDragLeave={() => this.setState({ dragover: false })}
|
||||
onDrop={this.onDrop.bind(this)}
|
||||
>
|
||||
{this.state.dragover ? <SubmitDragger /> : null}
|
||||
<ChatHeader {...props} />
|
||||
<ChatWindow
|
||||
history={props.history}
|
||||
isChatMissing={isChatMissing}
|
||||
isChatLoading={isChatLoading}
|
||||
isChatUnsynced={isChatUnsynced}
|
||||
unreadCount={unreadCount}
|
||||
unreadMsg={unreadMsg}
|
||||
pendingMessages={pendingMessages}
|
||||
messages={props.envelopes}
|
||||
length={props.length}
|
||||
contacts={props.contacts}
|
||||
association={props.association}
|
||||
group={props.group}
|
||||
stationPendingMessages={pendingMessages}
|
||||
ship={props.match.params.ship}
|
||||
station={props.station}
|
||||
api={props.api}
|
||||
hideNicknames={props.hideNicknames}
|
||||
hideAvatars={props.hideAvatars}
|
||||
/>
|
||||
{...props} />
|
||||
<ChatInput
|
||||
ref={this.chatInput}
|
||||
api={props.api}
|
||||
numMsgs={lastMsgNum}
|
||||
station={props.station}
|
||||
|
@ -101,9 +101,14 @@ export default class ChatEditor extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const {
|
||||
inCodeMode,
|
||||
placeholder,
|
||||
message,
|
||||
...props
|
||||
} = this.props;
|
||||
|
||||
const codeTheme = props.inCodeMode ? ' code' : '';
|
||||
const codeTheme = inCodeMode ? ' code' : '';
|
||||
|
||||
const options = {
|
||||
mode: MARKDOWN_CONFIG,
|
||||
@ -112,10 +117,13 @@ export default class ChatEditor extends Component {
|
||||
lineWrapping: true,
|
||||
scrollbarStyle: 'native',
|
||||
cursorHeight: 0.85,
|
||||
placeholder: props.inCodeMode ? 'Code...' : props.placeholder,
|
||||
placeholder: inCodeMode ? 'Code...' : placeholder,
|
||||
extraKeys: {
|
||||
'Enter': () => {
|
||||
this.submit();
|
||||
},
|
||||
'Esc': () => {
|
||||
this.editor?.getInputField().blur();
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -124,11 +132,11 @@ export default class ChatEditor extends Component {
|
||||
<div
|
||||
className={
|
||||
'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)' }}>
|
||||
<CodeEditor
|
||||
value={props.message}
|
||||
value={message}
|
||||
options={options}
|
||||
onChange={(e, d, v) => this.messageChange(e, d, v)}
|
||||
editorDidMount={(editor) => {
|
||||
@ -137,6 +145,7 @@ export default class ChatEditor extends Component {
|
||||
editor.focus();
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,268 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import ChatEditor from './chat-editor';
|
||||
import { S3Upload } from './s3-upload'
|
||||
;
|
||||
import { uxToHex } from '~/logic/lib/util';
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
|
||||
|
||||
const URL_REGEX = new RegExp(String(/^((\w+:\/\/)[-a-zA-Z0-9:@;?&=\/%\+\.\*!'\(\),\$_\{\}\^~\[\]`#|]+)/.source));
|
||||
|
||||
export class ChatInput extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
inCodeMode: false,
|
||||
};
|
||||
|
||||
this.submit = this.submit.bind(this);
|
||||
this.toggleCode = this.toggleCode.bind(this);
|
||||
}
|
||||
|
||||
uploadSuccess(url) {
|
||||
const { props } = this;
|
||||
props.api.chat.message(
|
||||
props.station,
|
||||
`~${window.ship}`,
|
||||
Date.now(),
|
||||
{ url }
|
||||
);
|
||||
}
|
||||
|
||||
uploadError(error) {
|
||||
// no-op for now
|
||||
}
|
||||
|
||||
toggleCode() {
|
||||
this.setState({
|
||||
inCodeMode: !this.state.inCodeMode
|
||||
});
|
||||
}
|
||||
|
||||
getLetterType(letter) {
|
||||
if (letter.startsWith('/me ')) {
|
||||
letter = letter.slice(4);
|
||||
// remove insignificant leading whitespace.
|
||||
// aces might be relevant to style.
|
||||
while (letter[0] === '\n') {
|
||||
letter = letter.slice(1);
|
||||
}
|
||||
|
||||
return {
|
||||
me: letter
|
||||
};
|
||||
} else if (this.isUrl(letter)) {
|
||||
return {
|
||||
url: letter
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
text: letter
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
isUrl(string) {
|
||||
try {
|
||||
return URL_REGEX.test(string);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
submit(text) {
|
||||
const { props, state } = this;
|
||||
if (state.inCodeMode) {
|
||||
this.setState({
|
||||
inCodeMode: false
|
||||
}, () => {
|
||||
props.api.chat.message(
|
||||
props.station,
|
||||
`~${window.ship}`,
|
||||
Date.now(), {
|
||||
code: {
|
||||
expression: text,
|
||||
output: undefined
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let messages = [];
|
||||
let message = [];
|
||||
let isInCodeBlock = false;
|
||||
let endOfCodeBlock = false;
|
||||
text.split(/\r?\n/).forEach((line, index) => {
|
||||
if (index !== 0) {
|
||||
message.push('\n');
|
||||
}
|
||||
// A line of backticks enters and exits a codeblock
|
||||
if (line.startsWith('```')) {
|
||||
// But we need to check if we've ended a codeblock
|
||||
endOfCodeBlock = isInCodeBlock;
|
||||
isInCodeBlock = (!isInCodeBlock);
|
||||
} else {
|
||||
endOfCodeBlock = false;
|
||||
}
|
||||
|
||||
if (isInCodeBlock || endOfCodeBlock) {
|
||||
message.push(line);
|
||||
} else {
|
||||
line.split(/\s/).forEach((str) => {
|
||||
if (
|
||||
(str.startsWith('`') && str !== '`')
|
||||
|| (str === '`' && !isInCodeBlock)
|
||||
) {
|
||||
isInCodeBlock = true;
|
||||
} else if (
|
||||
(str.endsWith('`') && str !== '`')
|
||||
|| (str === '`' && isInCodeBlock)
|
||||
) {
|
||||
isInCodeBlock = false;
|
||||
}
|
||||
|
||||
if (this.isUrl(str) && !isInCodeBlock) {
|
||||
if (message.length > 0) {
|
||||
// If we're in the middle of a message, add it to the stack and reset
|
||||
messages.push(message);
|
||||
message = [];
|
||||
}
|
||||
messages.push([str]);
|
||||
message = [];
|
||||
} else {
|
||||
message.push(str);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (message.length) {
|
||||
// Add any remaining message
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
props.deleteMessage();
|
||||
|
||||
messages.forEach((message) => {
|
||||
if (message.length > 0) {
|
||||
message = this.getLetterType(message.join(' '));
|
||||
props.api.chat.message(
|
||||
props.station,
|
||||
`~${window.ship}`,
|
||||
Date.now(),
|
||||
message
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// perf testing:
|
||||
/*let closure = () => {
|
||||
let x = 0;
|
||||
for (var i = 0; i < 30; i++) {
|
||||
x++;
|
||||
props.api.chat.message(
|
||||
props.station,
|
||||
`~${window.ship}`,
|
||||
Date.now(),
|
||||
{
|
||||
text: `${x}`
|
||||
}
|
||||
);
|
||||
}
|
||||
setTimeout(closure, 1000);
|
||||
};
|
||||
this.closure = closure.bind(this);
|
||||
setTimeout(this.closure, 2000);*/
|
||||
}
|
||||
|
||||
uploadSuccess(url) {
|
||||
const { props } = this;
|
||||
props.api.chat.message(
|
||||
props.station,
|
||||
`~${window.ship}`,
|
||||
Date.now(),
|
||||
{ url }
|
||||
);
|
||||
}
|
||||
|
||||
uploadError(error) {
|
||||
// no-op for now
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
const color = props.ownerContact
|
||||
? uxToHex(props.ownerContact.color) : '000000';
|
||||
|
||||
const sigilClass = props.ownerContact
|
||||
? '' : 'mix-blend-diff';
|
||||
|
||||
const avatar = (
|
||||
props.ownerContact &&
|
||||
((props.ownerContact.avatar !== null) && !props.hideAvatars)
|
||||
)
|
||||
? <img src={props.ownerContact.avatar} height={24} width={24} className="dib" />
|
||||
: <Sigil
|
||||
ship={window.ship}
|
||||
size={24}
|
||||
color={`#${color}`}
|
||||
classes={sigilClass}
|
||||
/>;
|
||||
|
||||
return (
|
||||
<div className={
|
||||
"pa3 cf flex black white-d bt b--gray4 b--gray1-d bg-white " +
|
||||
"bg-gray0-d relative"
|
||||
}
|
||||
style={{ flexGrow: 1 }}>
|
||||
<div className="fl"
|
||||
style={{
|
||||
marginTop: 6,
|
||||
flexBasis: 24,
|
||||
height: 24
|
||||
}}>
|
||||
{avatar}
|
||||
</div>
|
||||
<ChatEditor
|
||||
inCodeMode={state.inCodeMode}
|
||||
submit={this.submit}
|
||||
onUnmount={props.onUnmount}
|
||||
message={props.message}
|
||||
placeholder='Message...' />
|
||||
<div className="ml2 mr2"
|
||||
style={{
|
||||
height: '16px',
|
||||
width: '16px',
|
||||
flexBasis: 16,
|
||||
marginTop: 10
|
||||
}}>
|
||||
<S3Upload
|
||||
configuration={props.s3.configuration}
|
||||
credentials={props.s3.credentials}
|
||||
uploadSuccess={this.uploadSuccess.bind(this)}
|
||||
uploadError={this.uploadError.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<div style={{
|
||||
height: '16px',
|
||||
width: '16px',
|
||||
flexBasis: 16,
|
||||
marginTop: 10
|
||||
}}>
|
||||
<img style={{
|
||||
filter: state.inCodeMode && 'invert(100%)',
|
||||
height: '14px',
|
||||
width: '14px',
|
||||
}}
|
||||
onClick={this.toggleCode}
|
||||
src="/~chat/img/CodeEval.png"
|
||||
className="contrast-10-d bg-white bg-none-d ba b--gray1-d br1" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
255
pkg/interface/src/views/apps/chat/components/lib/chat-input.tsx
Normal file
255
pkg/interface/src/views/apps/chat/components/lib/chat-input.tsx
Normal file
@ -0,0 +1,255 @@
|
||||
import React, { Component } from 'react';
|
||||
import ChatEditor from './chat-editor';
|
||||
import { S3Upload, SubmitDragger } from '~/views/components/s3-upload'
|
||||
;
|
||||
import { uxToHex } from '~/logic/lib/util';
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage';
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { Envelope } from '~/types/chat-update';
|
||||
import { Contacts, S3Configuration } from '~/types';
|
||||
|
||||
interface ChatInputProps {
|
||||
api: GlobalApi;
|
||||
numMsgs: number;
|
||||
station: any;
|
||||
owner: string;
|
||||
ownerContact: any;
|
||||
envelopes: Envelope[];
|
||||
contacts: Contacts;
|
||||
onUnmount(msg: string): void;
|
||||
s3: any;
|
||||
placeholder: string;
|
||||
message: string;
|
||||
deleteMessage(): void;
|
||||
hideAvatars: boolean;
|
||||
onPaste?(): void;
|
||||
}
|
||||
|
||||
interface ChatInputState {
|
||||
inCodeMode: boolean;
|
||||
submitFocus: boolean;
|
||||
uploadingPaste: boolean;
|
||||
}
|
||||
|
||||
|
||||
export class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
||||
public s3Uploader: React.RefObject<S3Upload>;
|
||||
private chatEditor: React.RefObject<ChatEditor>;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
inCodeMode: false,
|
||||
submitFocus: false,
|
||||
uploadingPaste: false,
|
||||
};
|
||||
|
||||
this.s3Uploader = React.createRef();
|
||||
this.chatEditor = React.createRef();
|
||||
|
||||
this.submit = this.submit.bind(this);
|
||||
this.toggleCode = this.toggleCode.bind(this);
|
||||
|
||||
}
|
||||
|
||||
toggleCode() {
|
||||
this.setState({
|
||||
inCodeMode: !this.state.inCodeMode
|
||||
});
|
||||
}
|
||||
|
||||
getLetterType(letter) {
|
||||
if (letter.startsWith('/me ')) {
|
||||
letter = letter.slice(4);
|
||||
// remove insignificant leading whitespace.
|
||||
// aces might be relevant to style.
|
||||
while (letter[0] === '\n') {
|
||||
letter = letter.slice(1);
|
||||
}
|
||||
|
||||
return {
|
||||
me: letter
|
||||
};
|
||||
} else if (isUrl(letter)) {
|
||||
return {
|
||||
url: letter
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
text: letter
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
submit(text) {
|
||||
const { props, state } = this;
|
||||
if (state.inCodeMode) {
|
||||
this.setState({
|
||||
inCodeMode: false
|
||||
}, () => {
|
||||
props.api.chat.message(
|
||||
props.station,
|
||||
`~${window.ship}`,
|
||||
Date.now(), {
|
||||
code: {
|
||||
expression: text,
|
||||
output: undefined
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const messages = tokenizeMessage(text);
|
||||
|
||||
props.deleteMessage();
|
||||
|
||||
messages.forEach((message) => {
|
||||
if (message.length > 0) {
|
||||
message = this.getLetterType(message.join(' '));
|
||||
props.api.chat.message(
|
||||
props.station,
|
||||
`~${window.ship}`,
|
||||
Date.now(),
|
||||
message
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
uploadSuccess(url) {
|
||||
const { props } = this;
|
||||
if (this.state.uploadingPaste) {
|
||||
this.chatEditor.current.editor.setValue(url);
|
||||
this.setState({ uploadingPaste: false });
|
||||
} else {
|
||||
props.api.chat.message(
|
||||
props.station,
|
||||
`~${window.ship}`,
|
||||
Date.now(),
|
||||
{ url }
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
uploadError(error) {
|
||||
// no-op for now
|
||||
}
|
||||
|
||||
readyToUpload(): boolean {
|
||||
return Boolean(this.s3Uploader.current?.inputRef.current);
|
||||
}
|
||||
|
||||
onPaste(codemirrorInstance, event: ClipboardEvent) {
|
||||
if (!event.clipboardData || !event.clipboardData.files.length) {
|
||||
return;
|
||||
}
|
||||
this.setState({ uploadingPaste: true });
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.uploadFiles(event.clipboardData.files);
|
||||
}
|
||||
|
||||
uploadFiles(files: FileList) {
|
||||
if (!this.readyToUpload()) {
|
||||
return;
|
||||
}
|
||||
this.s3Uploader.current.inputRef.current.files = files;
|
||||
const fire = document.createEvent("HTMLEvents");
|
||||
fire.initEvent("change", true, true);
|
||||
this.s3Uploader.current?.inputRef.current?.dispatchEvent(fire);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
|
||||
const color = props.ownerContact
|
||||
? uxToHex(props.ownerContact.color) : '000000';
|
||||
|
||||
const sigilClass = props.ownerContact
|
||||
? '' : 'mix-blend-diff';
|
||||
|
||||
const avatar = (
|
||||
props.ownerContact &&
|
||||
((props.ownerContact.avatar !== null) && !props.hideAvatars)
|
||||
)
|
||||
? <img src={props.ownerContact.avatar} height={24} width={24} className="dib" />
|
||||
: <Sigil
|
||||
ship={window.ship}
|
||||
size={24}
|
||||
color={`#${color}`}
|
||||
classes={sigilClass}
|
||||
/>;
|
||||
|
||||
return (
|
||||
<div className={
|
||||
"pa3 cf flex black white-d bt b--gray4 b--gray1-d bg-white " +
|
||||
"bg-gray0-d relative"
|
||||
}
|
||||
style={{ flexGrow: 1 }}
|
||||
>
|
||||
<div className="fl"
|
||||
style={{
|
||||
marginTop: 6,
|
||||
flexBasis: 24,
|
||||
height: 24
|
||||
}}>
|
||||
{avatar}
|
||||
</div>
|
||||
<ChatEditor
|
||||
ref={this.chatEditor}
|
||||
inCodeMode={state.inCodeMode}
|
||||
submit={this.submit}
|
||||
onUnmount={props.onUnmount}
|
||||
message={props.message}
|
||||
onPaste={this.onPaste.bind(this)}
|
||||
placeholder='Message...'
|
||||
/>
|
||||
<div className="ml2 mr2"
|
||||
style={{
|
||||
height: '16px',
|
||||
width: '16px',
|
||||
flexBasis: 16,
|
||||
marginTop: 10
|
||||
}}>
|
||||
<S3Upload
|
||||
ref={this.s3Uploader}
|
||||
configuration={props.s3.configuration}
|
||||
credentials={props.s3.credentials}
|
||||
uploadSuccess={this.uploadSuccess.bind(this)}
|
||||
uploadError={this.uploadError.bind(this)}
|
||||
accept="*"
|
||||
>
|
||||
<img
|
||||
className="invert-d"
|
||||
src="/~chat/img/ImageUpload.png"
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
</S3Upload>
|
||||
</div>
|
||||
<div style={{
|
||||
height: '16px',
|
||||
width: '16px',
|
||||
flexBasis: 16,
|
||||
marginTop: 10
|
||||
}}>
|
||||
<img style={{
|
||||
filter: state.inCodeMode ? 'invert(100%)' : '',
|
||||
height: '14px',
|
||||
width: '14px',
|
||||
}}
|
||||
onClick={this.toggleCode}
|
||||
src="/~chat/img/CodeEval.png"
|
||||
className="contrast-10-d bg-white bg-none-d ba b--gray1-d br1" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -2,87 +2,91 @@ import React, { PureComponent, Fragment } from "react";
|
||||
import moment from "moment";
|
||||
|
||||
import { Message } from "./message";
|
||||
import { Envelope } from "~/types/chat-update";
|
||||
import _ from "lodash";
|
||||
|
||||
type IMessage = Envelope & { pending?: boolean };
|
||||
|
||||
|
||||
export const ChatMessage = (props) => {
|
||||
const {
|
||||
msg,
|
||||
previousMsg,
|
||||
nextMsg,
|
||||
isLastUnread,
|
||||
group,
|
||||
association,
|
||||
contacts,
|
||||
unreadRef,
|
||||
hideAvatars,
|
||||
hideNicknames
|
||||
} = props;
|
||||
|
||||
// Render sigil if previous message is not by the same sender
|
||||
const aut = ["author"];
|
||||
const renderSigil =
|
||||
_.get(nextMsg, aut) !== _.get(msg, aut, msg.author);
|
||||
const paddingTop = renderSigil;
|
||||
const paddingBot =
|
||||
_.get(previousMsg, aut) !== _.get(msg, aut, msg.author);
|
||||
|
||||
const when = ["when"];
|
||||
const dayBreak =
|
||||
moment(_.get(nextMsg, when)).format("YYYY.MM.DD") !==
|
||||
moment(_.get(msg, when)).format("YYYY.MM.DD");
|
||||
|
||||
const messageElem = (
|
||||
<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>
|
||||
export class ChatMessage extends PureComponent {
|
||||
render() {
|
||||
const {
|
||||
msg,
|
||||
previousMsg,
|
||||
nextMsg,
|
||||
isFirstUnread,
|
||||
group,
|
||||
association,
|
||||
contacts,
|
||||
unreadRef,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
remoteContentPolicy,
|
||||
className = ''
|
||||
} = this.props;
|
||||
|
||||
// Render sigil if previous message is not by the same sender
|
||||
const aut = ["author"];
|
||||
const renderSigil =
|
||||
_.get(nextMsg, aut) !== _.get(msg, aut, msg.author);
|
||||
const paddingTop = renderSigil;
|
||||
const paddingBot =
|
||||
_.get(previousMsg, aut) !== _.get(msg, aut, msg.author);
|
||||
|
||||
const when = ["when"];
|
||||
const dayBreak =
|
||||
moment(_.get(nextMsg, when)).format("YYYY.MM.DD") !==
|
||||
moment(_.get(msg, when)).format("YYYY.MM.DD");
|
||||
|
||||
const messageElem = (
|
||||
<Message
|
||||
key={msg.uid}
|
||||
msg={msg}
|
||||
renderSigil={renderSigil}
|
||||
paddingTop={paddingTop}
|
||||
paddingBot={paddingBot}
|
||||
pending={Boolean(msg.pending)}
|
||||
group={group}
|
||||
contacts={contacts}
|
||||
association={association}
|
||||
hideNicknames={hideNicknames}
|
||||
hideAvatars={hideAvatars}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
} else if (dayBreak) {
|
||||
return (
|
||||
<Fragment key={msg.uid}>
|
||||
{messageElem}
|
||||
<div
|
||||
className="pv3 gray2 b--gray2 flex items-center justify-center f9 "
|
||||
>
|
||||
<p>{moment(_.get(msg, when)).calendar()}</p>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
return messageElem;
|
||||
|
||||
if (isFirstUnread) {
|
||||
return (
|
||||
<Fragment key={msg.uid}>
|
||||
{messageElem}
|
||||
<div ref={unreadRef}
|
||||
className="mv2 green2 flex items-center f9">
|
||||
<hr className="dn-s ma0 w2 b--green2 bt-0" />
|
||||
<p className="mh4">New messages below</p>
|
||||
<hr className="ma0 flex-grow-1 b--green2 bt-0" />
|
||||
{dayBreak && (
|
||||
<p className="gray2 mh4">
|
||||
{moment(_.get(msg, when)).calendar()}
|
||||
</p>
|
||||
)}
|
||||
<hr
|
||||
style={{ width: "calc(50% - 48px)" }}
|
||||
className="b--green2 ma0 bt-0"
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
} else if (dayBreak) {
|
||||
return (
|
||||
<Fragment key={msg.uid}>
|
||||
<div
|
||||
className="pv3 gray2 b--gray2 flex items-center justify-center f9 "
|
||||
>
|
||||
<p>{moment(_.get(msg, when)).calendar()}</p>
|
||||
</div>
|
||||
{messageElem}
|
||||
</Fragment>
|
||||
);
|
||||
} else {
|
||||
return messageElem;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
import React, { Component, Fragment } from "react";
|
||||
|
||||
import { scrollIsAtTop, scrollIsAtBottom } from "~/logic/lib/util";
|
||||
|
||||
// Restore chat position on FF when new messages come in
|
||||
const recalculateScrollTop = (lastScrollHeight, scrollContainer) => {
|
||||
if (!scrollContainer || !lastScrollHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newScrollTop = scrollContainer.scrollHeight - lastScrollHeight;
|
||||
if (scrollContainer.scrollTop !== 0 ||
|
||||
scrollContainer.scrollTop === newScrollTop) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollContainer.scrollTop = scrollContainer.scrollHeight - lastScrollHeight;
|
||||
};
|
||||
|
||||
|
||||
export class ChatScrollContainer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// only for FF
|
||||
this.state = {
|
||||
lastScrollHeight: null
|
||||
};
|
||||
|
||||
this.isTriggeredScroll = false;
|
||||
|
||||
this.isAtBottom = true;
|
||||
this.isAtTop = false;
|
||||
|
||||
this.containerDidScroll = this.containerDidScroll.bind(this);
|
||||
|
||||
this.containerRef = React.createRef();
|
||||
this.scrollRef = React.createRef();
|
||||
}
|
||||
|
||||
containerDidScroll(e) {
|
||||
const { props } = this;
|
||||
if (scrollIsAtTop(e.target)) {
|
||||
// Save scroll position for FF
|
||||
if (navigator.userAgent.includes("Firefox")) {
|
||||
this.setState({
|
||||
lastScrollHeight: e.target.scrollHeight,
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.isAtTop) {
|
||||
props.scrollIsAtTop();
|
||||
}
|
||||
|
||||
this.isTriggeredScroll = false;
|
||||
this.isAtBottom = false;
|
||||
this.isAtTop = true;
|
||||
} else if (scrollIsAtBottom(e.target) && !this.isTriggeredScroll) {
|
||||
if (!this.isAtBottom) {
|
||||
props.scrollIsAtBottom();
|
||||
}
|
||||
|
||||
this.isTriggeredScroll = false;
|
||||
this.isAtBottom = true;
|
||||
this.isAtTop = false;
|
||||
} else {
|
||||
this.isAtBottom = false;
|
||||
this.isAtTop = false;
|
||||
this.isTriggeredScroll = false;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
// Replace with just the "not Firefox" implementation
|
||||
// when Firefox #1042151 is patched.
|
||||
|
||||
if (navigator.userAgent.includes("Firefox")) {
|
||||
return this.firefoxScrollContainer();
|
||||
} else {
|
||||
return this.normalScrollContainer();
|
||||
}
|
||||
}
|
||||
|
||||
firefoxScrollContainer() {
|
||||
return (
|
||||
<div
|
||||
className="relative overflow-y-scroll h-100"
|
||||
onScroll={this.containerDidScroll}
|
||||
ref={this.containerRef}>
|
||||
<div
|
||||
className="bg-white bg-gray0-d pt3 pb2 flex flex-column-reverse"
|
||||
style={{ resize: "vertical" }}>
|
||||
<div ref={this.scrollRef}></div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
normalScrollContainer() {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"overflow-y-scroll bg-white bg-gray0-d pt3 pb2 flex " +
|
||||
"flex-column-reverse relative"
|
||||
}
|
||||
style={{ height: "100%", resize: "vertical" }}
|
||||
onScroll={this.containerDidScroll}>
|
||||
<div ref={this.scrollRef}></div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
this.isTriggeredScroll = true;
|
||||
if (this.scrollRef.current) {
|
||||
this.scrollRef.current.scrollIntoView(false);
|
||||
}
|
||||
|
||||
if (navigator.userAgent.includes("Firefox")) {
|
||||
recalculateScrollTop(
|
||||
this.state.lastScrollHeight,
|
||||
this.scrollContainer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
scrollToReference(ref) {
|
||||
this.isTriggeredScroll = true;
|
||||
if (this.scrollRef.current && ref.current) {
|
||||
ref.current.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
|
||||
if (navigator.userAgent.includes("Firefox")) {
|
||||
recalculateScrollTop(
|
||||
this.state.lastScrollHeight,
|
||||
this.scrollContainer
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,54 +1,140 @@
|
||||
import React, { Component, Fragment } from "react";
|
||||
import { Virtuoso as VirtualList, VirtuosoMethods } from 'react-virtuoso';
|
||||
|
||||
import { ChatMessage } from './chat-message';
|
||||
import { ChatScrollContainer } from "./chat-scroll-container";
|
||||
import { UnreadNotice } from "./unread-notice";
|
||||
import { ResubscribeElement } from "./resubscribe-element";
|
||||
import { BacklogElement } from "./backlog-element";
|
||||
import { Envelope, IMessage } from "~/types/chat-update";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { Patp, Path } from "~/types/noun";
|
||||
import { Contacts } from "~/types/contact-update";
|
||||
import { Association } from "~/types/metadata-update";
|
||||
import { Group } from "~/types/group-update";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import _ from "lodash";
|
||||
import { LocalUpdateRemoteContentPolicy } from "~/types";
|
||||
import { ListRange } from "react-virtuoso/dist/engines/scrollSeekEngine";
|
||||
|
||||
|
||||
const MAX_BACKLOG_SIZE = 1000;
|
||||
const DEFAULT_BACKLOG_SIZE = 200;
|
||||
const PAGE_SIZE = 50;
|
||||
const INITIAL_LOAD = 20;
|
||||
const DEFAULT_BACKLOG_SIZE = 200;
|
||||
const IDLE_THRESHOLD = 3;
|
||||
|
||||
const Placeholder = ({ height, index, className = '', style = {}, ...props }) => (
|
||||
<div className={`w-100 f7 pl3 pt4 pr3 cf flex lh-copy ${className}`} style={{ height, ...style }} {...props}>
|
||||
<div className="fl pr3 v-top bg-white bg-gray0-d">
|
||||
<span
|
||||
className="db bg-gray2 bg-white-d"
|
||||
style={{
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
borderRadius: "50%",
|
||||
visibility: (index % 5 == 0) ? "initial" : "hidden",
|
||||
}}
|
||||
></span>
|
||||
</div>
|
||||
<div className="fr clamp-message white-d" style={{ flexGrow: 1, marginTop: -8 }}>
|
||||
<div className="hide-child" style={{paddingTop: "6px", visibility: (index % 5 == 0) ? "initial" : "hidden" }}>
|
||||
<p className={`v-mid f9 gray2 dib mr3 c-default`}>
|
||||
<span className="mw5 db"><span className="bg-gray5 bg-gray1-d db w-100 h-100"></span></span>
|
||||
</p>
|
||||
<p className="v-mid mono f9 gray2 dib"><span className="bg-gray5 bg-gray1-d db w-100 h-100" style={{height: "1em", width: `${(index % 3 + 1) * 3}em`}}></span></p>
|
||||
<p className="v-mid mono f9 ml2 gray2 dib child dn-s"><span className="bg-gray5 bg-gray1-d db w-100 h-100"></span></p>
|
||||
</div>
|
||||
<span className="bg-gray5 bg-gray1-d db w-100 h-100 db" style={{height: `1em`, width: `${(index % 5) * 20}%`}}></span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
export class ChatWindow extends Component {
|
||||
type ChatWindowProps = RouteComponentProps<{
|
||||
ship: Patp;
|
||||
station: string;
|
||||
}> & {
|
||||
unreadCount: number;
|
||||
envelopes: Envelope[];
|
||||
isChatMissing: boolean;
|
||||
isChatLoading: boolean;
|
||||
isChatUnsynced: boolean;
|
||||
unreadMsg: Envelope | false;
|
||||
stationPendingMessages: IMessage[];
|
||||
mailboxSize: number;
|
||||
contacts: Contacts;
|
||||
association: Association;
|
||||
group: Group;
|
||||
ship: Patp;
|
||||
station: any;
|
||||
api: GlobalApi;
|
||||
hideNicknames: boolean;
|
||||
hideAvatars: boolean;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
}
|
||||
|
||||
interface ChatWindowState {
|
||||
fetchPending: boolean;
|
||||
idle: boolean;
|
||||
range: ListRange;
|
||||
initialized: boolean;
|
||||
}
|
||||
|
||||
export class ChatWindow extends Component<ChatWindowProps, ChatWindowState> {
|
||||
private unreadReference: React.RefObject<Component>;
|
||||
private virtualList: React.RefObject<VirtuosoMethods>;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
numPages: 1,
|
||||
};
|
||||
|
||||
this.hasAskedForMessages = false;
|
||||
this.state = {
|
||||
fetchPending: false,
|
||||
idle: (this.initialIndex() < props.mailboxSize - IDLE_THRESHOLD) ? true : false,
|
||||
range: { startIndex: 0, endIndex: 0},
|
||||
initialized: false
|
||||
};
|
||||
|
||||
this.dismissUnread = this.dismissUnread.bind(this);
|
||||
this.scrollIsAtBottom = this.scrollIsAtBottom.bind(this);
|
||||
this.scrollIsAtTop = this.scrollIsAtTop.bind(this);
|
||||
this.initialIndex = this.initialIndex.bind(this);
|
||||
this.scrollToUnread = this.scrollToUnread.bind(this);
|
||||
|
||||
this.scrollReference = React.createRef();
|
||||
this.unreadReference = React.createRef();
|
||||
this.virtualList = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.initialFetch();
|
||||
}
|
||||
|
||||
if (this.state.numPages === 1 && this.props.unreadCount < INITIAL_LOAD) {
|
||||
this.dismissUnread();
|
||||
this.scrollToBottom();
|
||||
}
|
||||
initialIndex() {
|
||||
const { mailboxSize, unreadCount } = this.props;
|
||||
return Math.min(Math.max(mailboxSize - 1 < INITIAL_LOAD
|
||||
? 0
|
||||
: unreadCount // otherwise if there are unread messages
|
||||
? mailboxSize - unreadCount - 1 // put the one right before at the top
|
||||
: mailboxSize - 1,
|
||||
0), mailboxSize);
|
||||
}
|
||||
|
||||
initialFetch() {
|
||||
const { props } = this;
|
||||
if (props.messages.length > 0) {
|
||||
const unreadUnloaded = props.unreadCount - props.messages.length;
|
||||
|
||||
if (unreadUnloaded <= MAX_BACKLOG_SIZE &&
|
||||
unreadUnloaded + INITIAL_LOAD > DEFAULT_BACKLOG_SIZE) {
|
||||
this.fetchBacklog(unreadUnloaded + INITIAL_LOAD);
|
||||
} else {
|
||||
this.fetchBacklog(DEFAULT_BACKLOG_SIZE);
|
||||
const { envelopes, mailboxSize, unreadCount } = this.props;
|
||||
if (envelopes.length > 0) {
|
||||
const start = Math.min(mailboxSize - unreadCount, mailboxSize - DEFAULT_BACKLOG_SIZE);
|
||||
this.fetchMessages(start, start + DEFAULT_BACKLOG_SIZE, true);
|
||||
const initialIndex = this.initialIndex();
|
||||
if (initialIndex < mailboxSize - IDLE_THRESHOLD) {
|
||||
this.setState({ idle: true });
|
||||
}
|
||||
if (unreadCount !== mailboxSize) {
|
||||
this.virtualList.current?.scrollToIndex({
|
||||
index: initialIndex,
|
||||
align: initialIndex <= 1 ? 'end' : 'start'
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.setState({ initialized: true });
|
||||
}, 500);
|
||||
} else {
|
||||
this.setState({ initialized: true });
|
||||
}
|
||||
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.initialFetch();
|
||||
@ -57,140 +143,164 @@ export class ChatWindow extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { props, state } = this;
|
||||
const { isChatMissing, history, envelopes, mailboxSize, unreadCount } = this.props;
|
||||
let { idle } = this.state;
|
||||
|
||||
if (props.isChatMissing) {
|
||||
props.history.push("/~chat");
|
||||
} else if (props.messages.length >= prevProps.messages.length + 10) {
|
||||
this.hasAskedForMessages = false;
|
||||
let numPages = props.unreadCount > 0 ?
|
||||
Math.ceil(props.unreadCount / PAGE_SIZE) : this.state.numPages;
|
||||
if (isChatMissing) {
|
||||
history.push("/~chat");
|
||||
} else if (envelopes.length !== prevProps.envelopes.length && this.state.fetchPending) {
|
||||
this.setState({ fetchPending: false });
|
||||
}
|
||||
|
||||
if (this.state.numPages === numPages) {
|
||||
if (props.unreadCount > 20) {
|
||||
this.scrollToUnread();
|
||||
if (this.state.range.endIndex !== prevState.range.endIndex) {
|
||||
if (this.state.range.endIndex < mailboxSize - IDLE_THRESHOLD) {
|
||||
if (!idle) {
|
||||
idle = true;
|
||||
}
|
||||
} else {
|
||||
this.setState({ numPages }, () => {
|
||||
if (props.unreadCount > 20) {
|
||||
this.scrollToUnread();
|
||||
}
|
||||
});
|
||||
} else if (idle && (unreadCount === 0 || this.state.range.endIndex === 0)) {
|
||||
idle = false;
|
||||
}
|
||||
} else if (
|
||||
state.numPages === 1 &&
|
||||
this.props.unreadCount < INITIAL_LOAD &&
|
||||
this.props.unreadCount > 0
|
||||
) {
|
||||
this.dismissUnread();
|
||||
this.scrollToBottom();
|
||||
this.setState({ idle });
|
||||
}
|
||||
}
|
||||
|
||||
scrollIsAtTop() {
|
||||
const { props, state } = this;
|
||||
this.setState({ numPages: state.numPages + 1 }, () => {
|
||||
if (state.numPages * PAGE_SIZE < props.length) {
|
||||
this.fetchBacklog(DEFAULT_BACKLOG_SIZE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
scrollIsAtBottom() {
|
||||
if (this.state.numPages !== 1) {
|
||||
this.setState({ numPages: 1 });
|
||||
this.dismissUnread();
|
||||
if (!idle && idle !== prevState.idle) {
|
||||
setTimeout(() => {
|
||||
this.virtualList.current?.scrollToIndex(mailboxSize);
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
if (this.scrollReference.current) {
|
||||
this.scrollReference.current.scrollToBottom();
|
||||
if (!idle && prevProps.unreadCount !== unreadCount) {
|
||||
this.virtualList.current?.scrollToIndex(mailboxSize);
|
||||
}
|
||||
if (this.state.numPages !== 1) {
|
||||
this.setState({ numPages: 1 });
|
||||
|
||||
if (!idle && envelopes.length !== prevProps.envelopes.length) {
|
||||
this.virtualList.current?.scrollToIndex(mailboxSize);
|
||||
}
|
||||
}
|
||||
|
||||
scrollToUnread() {
|
||||
if (this.scrollReference.current && this.unreadReference.current) {
|
||||
this.scrollReference.current.scrollToReference(this.unreadReference);
|
||||
}
|
||||
const { mailboxSize, unreadCount } = this.props;
|
||||
this.virtualList.current?.scrollToIndex({
|
||||
index: mailboxSize - unreadCount,
|
||||
align: 'center'
|
||||
});
|
||||
}
|
||||
|
||||
dismissUnread() {
|
||||
this.props.api.chat.read(this.props.station);
|
||||
}
|
||||
|
||||
fetchBacklog(size) {
|
||||
const { props } = this;
|
||||
fetchMessages(start, end, force = false) {
|
||||
start = Math.max(start, 0);
|
||||
end = Math.max(end, 0);
|
||||
const { api, mailboxSize, station } = this.props;
|
||||
|
||||
if (
|
||||
props.messages.length >= props.length ||
|
||||
this.hasAskedForMessages ||
|
||||
props.length <= 0
|
||||
(this.state.fetchPending ||
|
||||
mailboxSize <= 0)
|
||||
&& !force
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
api.chat
|
||||
.fetchMessages(Math.max(mailboxSize - end, 0), Math.min(mailboxSize - start, mailboxSize), station)
|
||||
.finally(() => {
|
||||
this.setState({ fetchPending: false });
|
||||
});
|
||||
|
||||
const start =
|
||||
props.length - props.messages[props.messages.length - 1].number;
|
||||
if (start > 0) {
|
||||
const end = start + size < props.length ? start + size : props.length;
|
||||
props.api.chat.fetchMessages(start + 1, end, props.station);
|
||||
this.hasAskedForMessages = true;
|
||||
}
|
||||
this.setState({ fetchPending: true });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props, state } = this;
|
||||
const sliceLength = Math.min(
|
||||
state.numPages * PAGE_SIZE,
|
||||
props.messages.length + props.pendingMessages.length
|
||||
);
|
||||
const messages =
|
||||
props.pendingMessages
|
||||
.concat(props.messages)
|
||||
.slice(0, sliceLength);
|
||||
const {
|
||||
envelopes,
|
||||
stationPendingMessages,
|
||||
unreadCount,
|
||||
unreadMsg,
|
||||
isChatLoading,
|
||||
isChatUnsynced,
|
||||
api,
|
||||
ship,
|
||||
station,
|
||||
association,
|
||||
group,
|
||||
contacts,
|
||||
mailboxSize,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
remoteContentPolicy,
|
||||
} = this.props;
|
||||
|
||||
const messages: Envelope[] = [];
|
||||
const debouncedFetch = _.debounce(this.fetchMessages, 500).bind(this);
|
||||
|
||||
envelopes
|
||||
.forEach((message) => {
|
||||
messages[message.number] = message;
|
||||
});
|
||||
|
||||
stationPendingMessages.sort((a, b) => a.when - b.when).forEach((message, index) => {
|
||||
messages[mailboxSize + index + 1] = message;
|
||||
});
|
||||
|
||||
return (
|
||||
<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>
|
||||
unreadCount={unreadCount}
|
||||
unreadMsg={this.state.idle ? unreadMsg : false}
|
||||
dismissUnread={this.dismissUnread}
|
||||
onClick={this.scrollToUnread}
|
||||
/>
|
||||
<BacklogElement isChatLoading={isChatLoading} />
|
||||
<ResubscribeElement {...{ api, host: ship, station, isChatUnsynced}} />
|
||||
{messages.length ? <VirtualList
|
||||
ref={this.virtualList}
|
||||
style={{height: '100%', width: '100%', visibility: this.state.initialized ? 'initial' : 'hidden'}}
|
||||
totalCount={mailboxSize + stationPendingMessages.length}
|
||||
followOutput={!this.state.idle}
|
||||
endReached={this.dismissUnread}
|
||||
scrollSeek={{
|
||||
enter: velocity => Math.abs(velocity) > 2000,
|
||||
exit: velocity => Math.abs(velocity) < 200,
|
||||
change: (_velocity, _range) => {},
|
||||
placeholder: this.state.initialized ? Placeholder : () => <div></div>
|
||||
}}
|
||||
startReached={() => debouncedFetch(0, DEFAULT_BACKLOG_SIZE)}
|
||||
overscan={DEFAULT_BACKLOG_SIZE}
|
||||
rangeChanged={(range) => {
|
||||
this.setState({ range });
|
||||
debouncedFetch(range.startIndex - (DEFAULT_BACKLOG_SIZE / 2), range.endIndex + (DEFAULT_BACKLOG_SIZE / 2));
|
||||
}}
|
||||
item={(i) => {
|
||||
const number = i + 1;
|
||||
const msg = messages[number];
|
||||
|
||||
if (!msg) {
|
||||
debouncedFetch(number - DEFAULT_BACKLOG_SIZE, number + DEFAULT_BACKLOG_SIZE);
|
||||
return <Placeholder index={number} height="0px" style={{overflow: 'hidden'}} />;
|
||||
}
|
||||
return <ChatMessage
|
||||
key={number}
|
||||
unreadRef={this.unreadReference}
|
||||
isFirstUnread={
|
||||
unreadCount
|
||||
&& mailboxSize - unreadCount === number
|
||||
&& this.state.idle
|
||||
}
|
||||
msg={msg}
|
||||
previousMsg={messages[number + 1]}
|
||||
nextMsg={messages[number - 1]}
|
||||
association={association}
|
||||
group={group}
|
||||
contacts={contacts}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
className={number === mailboxSize + stationPendingMessages.length ? 'pb3' : ''}
|
||||
/>
|
||||
}}
|
||||
/> : <div style={{height: '100%', width: '100%'}}></div>}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
@ -1,106 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Button } from '@tlon/indigo-react';
|
||||
|
||||
const IMAGE_REGEX = new RegExp(/(jpg|img|png|gif|tiff|jpeg|webp|webm|svg)$/i);
|
||||
|
||||
const YOUTUBE_REGEX =
|
||||
new RegExp(
|
||||
String(/(?:https?:\/\/(?:[a-z]+.)?)/.source) // protocol
|
||||
+ /(?:youtu\.?be(?:\.com)?\/)(?:embed\/)?/.source // short and long-links
|
||||
+ /(?:(?:(?:(?:watch\?)?(?:time_continue=(?:[0-9]+))?.+v=)?([a-zA-Z0-9_-]+))(?:\?t\=(?:[0-9a-zA-Z]+))?)/.source // id
|
||||
);
|
||||
|
||||
export default class UrlContent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
unfold: false,
|
||||
copied: false
|
||||
};
|
||||
this.unfoldEmbed = this.unfoldEmbed.bind(this);
|
||||
}
|
||||
|
||||
unfoldEmbed(id) {
|
||||
let unfoldState = this.state.unfold;
|
||||
unfoldState = !unfoldState;
|
||||
this.setState({ unfold: unfoldState });
|
||||
this.iframe.setAttribute('src', this.iframe.dataset.src);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { props } = this;
|
||||
const content = props.content;
|
||||
const imgMatch = IMAGE_REGEX.exec(props.content.url);
|
||||
const ytMatch = YOUTUBE_REGEX.exec(props.content.url);
|
||||
|
||||
let contents = content.url;
|
||||
if (imgMatch) {
|
||||
contents = (
|
||||
<img
|
||||
className="o-80-d"
|
||||
src={content.url}
|
||||
style={{
|
||||
maxWidth: '18rem'
|
||||
}}
|
||||
></img>
|
||||
);
|
||||
return (
|
||||
<a className='f7 lh-copy v-top word-break-all'
|
||||
href={content.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{contents}
|
||||
</a>
|
||||
);
|
||||
} else if (ytMatch) {
|
||||
contents = (
|
||||
<div className={'embed-container mb2 w-100 w-75-l w-50-xl ' +
|
||||
((this.state.unfold === true)
|
||||
? 'db' : 'dn')}
|
||||
>
|
||||
<iframe
|
||||
ref={(el) => {
|
||||
this.iframe = el;
|
||||
}}
|
||||
width="560"
|
||||
height="315"
|
||||
data-src={`https://www.youtube.com/embed/${ytMatch[1]}`}
|
||||
frameBorder="0" allow="picture-in-picture, fullscreen"
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<a href={content.url}
|
||||
className='f7 lh-copy v-top bb b--white-d word-break-all'
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{content.url}
|
||||
</a>
|
||||
<Button
|
||||
border={1}
|
||||
style={{ display: 'inline-flex', height: '1.66em' }} // Height is hacked to line-height until Button supports proper size
|
||||
ml={1}
|
||||
onClick={e => this.unfoldEmbed()}
|
||||
>
|
||||
{this.state.unfold ? 'collapse' : 'embed'}
|
||||
</Button>
|
||||
{contents}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<a className='f7 lh-copy v-top bb b--white-d b--black word-break-all'
|
||||
href={content.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{contents}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,51 +1,54 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
|
||||
export const DeleteButton = memo(({ isOwner, station, changeLoading, association, contacts, api }) => {
|
||||
const leaveButtonClasses = (!isOwner) ? 'pointer' : 'c-default';
|
||||
const deleteButtonClasses = (isOwner) ?
|
||||
'b--red2 red2 pointer bg-gray0-d' :
|
||||
'b--gray3 gray3 bg-gray0-d c-default';
|
||||
|
||||
export const DeleteButton = (props) => {
|
||||
const { isOwner, station, changeLoading, api } = props;
|
||||
const leaveButtonClasses = (!isOwner) ? 'pointer' : 'c-default';
|
||||
const deleteButtonClasses = (isOwner) ?
|
||||
'b--red2 red2 pointer bg-gray0-d' :
|
||||
'b--gray3 gray3 bg-gray0-d c-default';
|
||||
|
||||
const deleteChat = () => {
|
||||
changeLoading(
|
||||
true,
|
||||
true,
|
||||
isOwner ? 'Deleting chat...' : 'Leaving chat...',
|
||||
() => {
|
||||
api.chat.delete(station);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-100 cf">
|
||||
<div className={'w-100 fl mt3 ' + ((isOwner) ? 'o-30' : '')}>
|
||||
<p className="f8 mt3 lh-copy db">Leave Chat</p>
|
||||
<p className="f9 gray2 db mb4">
|
||||
Remove this chat from your chat list.{' '}
|
||||
You will need to request for access again.
|
||||
</p>
|
||||
<a onClick={(!isOwner) ? deleteChat : null}
|
||||
className={
|
||||
'dib f9 black gray4-d bg-gray0-d ba pa2 b--black b--gray1-d ' +
|
||||
leaveButtonClasses
|
||||
}>
|
||||
Leave this chat
|
||||
</a>
|
||||
</div>
|
||||
<div className={'w-100 fl mt3 ' + ((!isOwner) ? 'o-30' : '')}>
|
||||
<p className="f8 mt3 lh-copy db">Delete Chat</p>
|
||||
<p className="f9 gray2 db mb4">
|
||||
Permanently delete this chat.{' '}
|
||||
All current members will no longer see this chat.
|
||||
</p>
|
||||
<a onClick={(isOwner) ? deleteChat : null}
|
||||
className={'dib f9 ba pa2 ' + deleteButtonClasses}
|
||||
>Delete this chat</a>
|
||||
</div>
|
||||
</div>
|
||||
const deleteChat = () => {
|
||||
changeLoading(
|
||||
true,
|
||||
true,
|
||||
isOwner ? 'Deleting chat...' : 'Leaving chat...',
|
||||
() => {
|
||||
api.chat.delete(station);
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const groupPath = association['group-path'];
|
||||
const unmanagedVillage = !contacts[groupPath];
|
||||
|
||||
return (
|
||||
<div className="w-100 cf">
|
||||
<div className={'w-100 fl mt3 ' + ((isOwner) ? 'o-30' : '')}>
|
||||
<p className="f8 mt3 lh-copy db">Leave Chat</p>
|
||||
<p className="f9 gray2 db mb4">
|
||||
Remove this chat from your chat list.{' '}
|
||||
{unmanagedVillage
|
||||
? 'You will need to request for access again'
|
||||
: 'You will need to join again from the group page.'
|
||||
}
|
||||
</p>
|
||||
<a onClick={(!isOwner) ? deleteChat : null}
|
||||
className={
|
||||
'dib f9 black gray4-d bg-gray0-d ba pa2 b--black b--gray1-d ' +
|
||||
leaveButtonClasses
|
||||
}>
|
||||
Leave this chat
|
||||
</a>
|
||||
</div>
|
||||
<div className={'w-100 fl mt3 ' + ((!isOwner) ? 'o-30' : '')}>
|
||||
<p className="f8 mt3 lh-copy db">Delete Chat</p>
|
||||
<p className="f9 gray2 db mb4">
|
||||
Permanently delete this chat.{' '}
|
||||
All current members will no longer see this chat.
|
||||
</p>
|
||||
<a onClick={(isOwner) ? deleteChat : null}
|
||||
className={'dib f9 ba pa2 ' + deleteButtonClasses}
|
||||
>Delete this chat</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
@ -1,13 +1,13 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ChannelItem } from './channel-item';
|
||||
import { deSig } from "~/logic/lib/util";
|
||||
import { deSig, cite } from "~/logic/lib/util";
|
||||
|
||||
export class GroupItem extends Component {
|
||||
render() {
|
||||
const { props } = this;
|
||||
const association = props.association ? props.association : {};
|
||||
const DEFAULT_TITLE_REGEX = new RegExp(`(( <-> )?~${deSig(window.ship)}( <-> )?)`);
|
||||
const DEFAULT_TITLE_REGEX = new RegExp(`(( <-> )?~(?:${window.ship}|${deSig(cite(window.ship))})( <-> )?)`);
|
||||
|
||||
let title = association['app-path'] ? association['app-path'] : 'Direct Messages';
|
||||
if (association.metadata && association.metadata.title) {
|
||||
|
@ -2,7 +2,7 @@ import React, { Component } from 'react';
|
||||
|
||||
import TextContent from './content/text';
|
||||
import CodeContent from './content/code';
|
||||
import UrlContent from './content/url';
|
||||
import RemoteContent from '~/views/components/RemoteContent';
|
||||
|
||||
|
||||
export default class MessageContent extends Component {
|
||||
@ -15,7 +15,18 @@ export default class MessageContent extends Component {
|
||||
if ('code' in content) {
|
||||
return <CodeContent content={content} />;
|
||||
} else if ('url' in content) {
|
||||
return <UrlContent content={content} />;
|
||||
return (
|
||||
<RemoteContent
|
||||
url={content.url}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
imageProps={{style: {
|
||||
maxWidth: '18rem'
|
||||
}}}
|
||||
videoProps={{style: {
|
||||
maxWidth: '18rem'
|
||||
}}}
|
||||
/>
|
||||
);
|
||||
} else if ('me' in content) {
|
||||
return (
|
||||
<p className='f7 i lh-copy v-top'>
|
||||
|
@ -1,37 +1,41 @@
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { OverlaySigil } from './overlay-sigil';
|
||||
import MessageContent from './message-content';
|
||||
import { uxToHex, cite, writeText } from '~/logic/lib/util';
|
||||
import moment from 'moment';
|
||||
|
||||
|
||||
export const Message = (props) => {
|
||||
const pending = props.msg.pending ? ' o-40' : '';
|
||||
const {
|
||||
msg,
|
||||
renderSigil,
|
||||
remoteContentPolicy,
|
||||
className = ''
|
||||
} = props;
|
||||
const pending = msg.pending ? ' o-40' : '';
|
||||
const containerClass =
|
||||
props.renderSigil ?
|
||||
`w-100 f7 pl3 pt4 pr3 cf flex lh-copy ` + pending :
|
||||
'w-100 pr3 cf hide-child flex' + pending;
|
||||
renderSigil
|
||||
? `w-100 f7 pl3 pt4 pr3 cf flex lh-copy ${pending} ${className}`
|
||||
: `w-100 pr3 cf hide-child flex ${pending} ${className}`;
|
||||
|
||||
const timestamp =
|
||||
moment.unix(props.msg.when / 1000).format(
|
||||
props.renderSigil ? 'hh:mm a' : 'hh:mm'
|
||||
moment.unix(msg.when / 1000).format(
|
||||
renderSigil ? 'hh:mm a' : 'hh:mm'
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<div className={containerClass}
|
||||
style={{
|
||||
minHeight: 'min-content'
|
||||
}}>
|
||||
{
|
||||
props.renderSigil ? (
|
||||
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} />
|
||||
<MessageContent letter={msg.letter} remoteContentPolicy={remoteContentPolicy}/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -41,66 +45,80 @@ export const Message = (props) => {
|
||||
};
|
||||
|
||||
const renderWithSigil = (props, timestamp) => {
|
||||
const paddingTop = props.paddingTop ? { 'paddingTop': '6px' } : '';
|
||||
const datestamp =
|
||||
'~' + moment.unix(props.msg.when / 1000).format('YYYY.M.D');
|
||||
|
||||
const contact = props.msg.author in props.contacts
|
||||
? props.contacts[props.msg.author] : false;
|
||||
const showNickname = !props.hideNicknames && contact?.nickname;
|
||||
let name = `~${props.msg.author}`;
|
||||
let color = '#000000';
|
||||
let sigilClass = 'mix-blend-diff';
|
||||
if (contact) {
|
||||
name = showNickname
|
||||
? contact.nickname
|
||||
: `~${props.msg.author}`;
|
||||
color = `#${uxToHex(contact.color)}`;
|
||||
sigilClass = '';
|
||||
}
|
||||
const paddingTop = props.paddingTop ? { 'paddingTop': '6px' } : '';
|
||||
const datestamp =
|
||||
'~' + moment.unix(props.msg.when / 1000).format('YYYY.M.D');
|
||||
|
||||
if (`~${props.msg.author}` === name) {
|
||||
name = cite(props.msg.author);
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
const contact = props.msg.author in props.contacts
|
||||
? props.contacts[props.msg.author] : false;
|
||||
const showNickname = !props.hideNicknames && contact?.nickname;
|
||||
let name = `~${props.msg.author}`;
|
||||
let color = '#000000';
|
||||
let sigilClass = 'mix-blend-diff';
|
||||
if (contact) {
|
||||
name = showNickname
|
||||
? contact.nickname
|
||||
: `~${props.msg.author}`;
|
||||
color = `#${uxToHex(contact.color)}`;
|
||||
sigilClass = '';
|
||||
}
|
||||
|
||||
if (`~${props.msg.author}` === name) {
|
||||
name = cite(props.msg.author);
|
||||
}
|
||||
|
||||
let nameSpan = null;
|
||||
|
||||
const copyNotice = (saveName) => {
|
||||
if (nameSpan !== null) {
|
||||
nameSpan.innerText = 'Copied';
|
||||
setTimeout(() => {
|
||||
nameSpan.innerText = saveName;
|
||||
}, 800);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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')
|
||||
}
|
||||
ref={(e) => nameSpan = e}
|
||||
onClick={() => {
|
||||
writeText(`~${props.msg.author}`);
|
||||
copyNotice(name);
|
||||
}}
|
||||
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} remoteContentPolicy={props.remoteContentPolicy} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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';
|
||||
|
||||
export const UnreadNotice = (props) => {
|
||||
const { unreadCount, unreadMsg, dismissUnread } = props;
|
||||
const { unreadCount, unreadMsg, dismissUnread, onClick } = props;
|
||||
|
||||
if (!unreadMsg || (unreadCount === 0)) {
|
||||
return null;
|
||||
@ -22,7 +22,7 @@ export const UnreadNotice = (props) => {
|
||||
"ba b--green2 green2 bg-white bg-gray0-d flex items-center " +
|
||||
"pa2 f9 justify-between br1"
|
||||
}>
|
||||
<p className="lh-copy db">
|
||||
<p className="lh-copy db pointer" onClick={onClick}>
|
||||
{unreadCount} new messages since{' '}
|
||||
{datestamp && (
|
||||
<>
|
||||
|
@ -85,6 +85,8 @@ export class SettingsScreen extends Component {
|
||||
isOwner={isOwner}
|
||||
changeLoading={this.changeLoading}
|
||||
station={station}
|
||||
association={association}
|
||||
contacts={contacts}
|
||||
api={api} />
|
||||
<MetadataSettings
|
||||
isOwner={isOwner}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user