Merge branch 'master' into next/landscape

This commit is contained in:
Hunter Miller 2022-01-05 16:59:32 -06:00
commit 318cb9f00e
111 changed files with 113106 additions and 88301 deletions

View File

@ -39,7 +39,6 @@ on:
- 'pkg/docker-image/**'
- 'pkg/ent/**'
- 'pkg/ge-additions/**'
- 'pkg/hs/**'
- 'pkg/libaes_siv/**'
- 'pkg/urbit/**'
- 'bin/**'
@ -50,7 +49,6 @@ on:
- 'pkg/docker-image/**'
- 'pkg/ent/**'
- 'pkg/ge-additions/**'
- 'pkg/hs/**'
- 'pkg/libaes_siv/**'
- 'pkg/urbit/**'
- 'bin/**'
@ -74,12 +72,12 @@ jobs:
# for the docker build. We don't want in on Mac, where it isn't but
# it breaks the nix install. The two `if` clauses should be mutually
# exclusive
- uses: cachix/install-nix-action@v13
- uses: cachix/install-nix-action@v16
with:
extra_nix_config: |
system-features = nixos-test benchmark big-parallel kvm
if: ${{ matrix.os == 'ubuntu-latest' }}
- uses: cachix/install-nix-action@v13
- uses: cachix/install-nix-action@v16
if: ${{ matrix.os != 'ubuntu-latest' }}
- uses: cachix/cachix-action@v10
@ -95,28 +93,6 @@ jobs:
- if: ${{ matrix.os == 'ubuntu-latest' }}
run: nix-build -A docker-image
haskell:
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu-latest }
- { os: macos-latest }
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: cachix/install-nix-action@v13
- uses: cachix/cachix-action@v10
with:
name: ares
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
- run: nix-build -A hs.urbit-king.components.exes.urbit-king --arg enableStatic true
- run: nix-build -A hs-checks
- run: nix-build shell.nix
mingw:
runs-on: windows-latest
defaults:

View File

@ -16,7 +16,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: cachix/install-nix-action@v13
- uses: cachix/install-nix-action@v16
with:
extra_nix_config: |
system-features = nixos-test benchmark big-parallel kvm

View File

@ -17,7 +17,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: cachix/install-nix-action@v13
- uses: cachix/install-nix-action@v16
- uses: cachix/cachix-action@v10
with:
name: ${{ secrets.CACHIX_NAME }}

View File

@ -9,8 +9,12 @@ interface, see its [contribution guidelines][interface].
For information on Arvo's maintainers, see [pkg/arvo][main].
For more extensive information on Urbit development, such as how to set up an
environment and how to submit a grant, see the [developer documentation][dev].
[start]: https://urbit.org/using/install
[interface]: /pkg/interface/CONTRIBUTING.md
[dev]: https://urbit.org/docs/development
## Fake ships
@ -55,8 +59,9 @@ urbit -F zod -B "bin/solid.pill" -A "pkg/arvo"
The canonical source tree is the `master` branch of
[https://github.com/urbit/urbit][repo]. You should typically branch off of
`master` when commencing new work; similarly, when we pull in your
contribution, we'll do so by merging it to `master`.
`master` when commencing new work. Most pull requests should be merging into
one of the `next/*` branches, depending on what part of the system the pull
request is targeting.
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
@ -294,5 +299,5 @@ Questions or other communications about contributing to Urbit can go to
[repo]: https://github.com/urbit/urbit
[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
[hoon]: https://urbit.org/docs/hoon/reference/style
[main]: https://github.com/urbit/urbit/tree/master/pkg/arvo#maintainers

View File

@ -29,7 +29,7 @@ released.
### Release branches
Release branches are code that is ready to release. All release branch names
should start with `release/`.
should start with `next/`.
All code must be reviewed before being pushed to a release branch. Thus,
feature branches should be PR'd against a release branch, not master.
@ -41,57 +41,55 @@ be released together -- unless one of the underlying commits is separately put
on a release branch.
Here's a worked example. The rule is to make however many branches are useful,
and no more. This example is not prescriptive, the developers making the
and no more. This example is not prescriptive; the developers making the
changes may add, remove, or rename branches in this flow at will.
Suppose you (plural, the dev community at large) complete some work in a
userspace app, and you put it in `release/next-userspace`. Separately, you make
a small JS change. If you PR it to `release/next-userspace`, then it will only
be released at the same time as the app changes. Maybe this is fine, or maybe
you want this change to go out quickly, and the change in
`release/next-userspace` is relatively risky, so you don't want to push it out
on Friday afternoon. In this case, put the change in another release branch,
say `release/next-js`. Now either can be released independently.
userspace app, and you put it in `next/landscape`. Separately, you make a small
JS change. If you PR it to `next/landscape`, then it will only be released at
the same time as the app changes. Maybe this is fine, or maybe you want this
change to go out quickly, and the change in `next/landscape` is relatively
risky, so you don't want to push it out on Friday afternoon. In this case, put
the change in another release branch, say `next/js`. Now either can be released
independently.
Suppose you do further work that you want to PR to `release/next-userspace`, but
it depends on your fixes in `release/next-js`. Simply merge `release/next-js`
into either your feature branch or `release/next-userspace` and PR your finished
work to `release/next-userspace`. Now there is a one-way coupling:
`release/next-userspace` contains `release/next-js`, so releasing it will
implicitly release `release/next-js`. However, you can still release
`release/next-js` independently.
Suppose you do further work that you want to PR to `next/landscape`, but it
depends on your fixes in `next/js`. Simply merge `next/js` into either your
feature branch or `next/landscape` and PR your finished work to
`next/landscape`. Now there is a one-way coupling: `next/landscape` contains
`next/js`, so releasing it will implicitly release `next/js`. However, you can
still release `next/js` independently.
This scheme extends to other branches, like `release/next-kernel` or
`release/os1.1` or `release/ford-fusion`. Some branches may be long-lived and
represent simply the "next" release of something, while others will have a
definite lifetime that corresponds to development of a particular feature or
numbered release.
This scheme extends to other branches, like `next/base` or `next/os1.1` or
`next/ford-fusion`. Some branches may be long-lived and represent simply the
"next" release of something, while others will have a definite lifetime that
corresponds to development of a particular feature or numbered release.
Since they are "done", release branches should be considered "public", in the
sense that others may depend on them at will. Thus, never rebase a release
branch.
When cutting a new release, you can filter branches with `git branch --list
'release/*'` or by typing "release/" in the branch filter on Github. This will
give you the list of branches which have passed review and may be merged to
master and released. When choosing which branches to release, make sure you
understand the risks of releasing them immediately. If merging these produces
nontrivial conflicts, consider asking the developers on those branches to merge
between themselves. In many cases a developer can do this directly, but if it's
'next/*'` or by typing "next/" in the branch filter on Github. This will give
you the list of branches which have passed review and may be merged to master
and released. When choosing which branches to release, make sure you understand
the risks of releasing them immediately. If merging these produces nontrivial
conflicts, consider asking the developers on those branches to merge between
themselves. In many cases a developer can do this directly, but if it's
sufficiently nontrivial, this may be a reviewed PR of one release branch into
another.
### Non-OTAable release branches
#### Standard release branches
In some cases, work is completed which cannot be OTA'd as written. For example,
the code may lack state adapters, or it may not properly handle outstanding
subscriptions. It could also be code which is planned to be released only upon
a breach (network-wide or rolling).
While you can always create non-standard release branches to stage for a
particular release, most changes should go through the following:
In this case, the code may be PR'd to a `na-release/` branch. All rules are the
same as for release branches, except that the code does not need to apply
cleanly to an existing ship. If you later write state adapter or otherwise make
it OTAable, then you may PR it to a release branch.
- next/base -- changes to the %base desk in pkg/arvo
- next/garden -- changes to the %garden desk
- next/landscape -- changes to the %landscape desk
- next/bitcoin -- changes to the %bitcoin desk
- next/webterm -- changes to the %webterm desk
- next/vere -- changes to the runtime
### Other cases
@ -163,9 +161,10 @@ If you're making a Vere release, just play it safe and update all the pills.
For an Urbit OS release, after all the merge commits, make a release with the
commit message "release: urbit-os-v1.0.xx". This commit should have up-to-date
artifacts from pkg/interface and a new solid pill. If neither the pill nor the
JS need to be updated (e.g if the pill was already updated in the previous merge
commit), consider making the release commit with --allow-empty.
artifacts from pkg/interface and a new version number in the desk.docket-0 of
any desk which changed. If neither the pill nor the JS need to be updated (e.g
if the pill was already updated in the previous merge commit), consider making
the release commit with --allow-empty.
If anything in `pkg/interface` has changed, ensure it has been built and
deployed properly. You'll want to do this before making a pill, since you want
@ -191,21 +190,23 @@ What you should do here depends on the type of release being made.
First, for Urbit OS releases:
If it's a very trivial hotfix that you know isn't going to break
anything, tag it as `urbit-os-vx.y.z`. Here 'x' refers to the product version
(e.g. OS1, OS2..), 'y' to the continuity era in that version, and 'z' to an
OTA patch counter. So for a hotfix version, you'll just want to increment 'z'.
If it's a very trivial hotfix that you know isn't going to break anything, tag
it as `urbit-os-vx.y`. Here 'x' is the major version and 'y' is an OTA patch
counter. Change `urbit-os` to e.g. `landscape` or another desk if that's what you're
releasing. If you're releasing changes to more than one desk, add a separate
tag for each desk (but only make one announcment email/post, with all of the
desks listed).
Use an annotated tag, i.e.
```
git tag -a urbit-os-vx.y.z
git tag -a urbit-os-vx.y
```
The tag format should look something like this:
```
urbit-os-vx.y.z
urbit-os-vx.y
This release will be pushed to the network as an over-the-air update.
@ -236,17 +237,17 @@ If the commit descriptions are too poor to easily do this, then again, yell at
your fellow contributors to make them better in the future.
If it's *not* a trivial hotfix, you should probably make any number of release
candidate tags (e.g. `urbit-os-vx.y.z.rc1`, `urbit-os-vx.y.z.rc2`, ..), test
candidate tags (e.g. `urbit-os-vx.y.rc1`, `urbit-os-vx.y.rc2`, ..), test
them, and after you confirm one of them is good, tag the release as
`urbit-os-vx.y.z`.
`urbit-os-vx.y`.
For Vere releases:
Tag the release as `urbit-vx.y.z`. The tag format should look something like
Tag the release as `urbit-vx.y`. The tag format should look something like
this:
```
urbit-vx.y.z
urbit-vx.y
Note that this Vere release will by default boot fresh ships using an Urbit OS
va.b.c pill.
@ -254,10 +255,10 @@ va.b.c pill.
Release binaries:
(linux64)
https://bootstrap.urbit.org/urbit-vx.y.z-linux64.tgz
https://bootstrap.urbit.org/urbit-vx.y-linux64.tgz
(macOS)
https://bootstrap.urbit.org/urbit-vx.y.z-darwin.tgz
https://bootstrap.urbit.org/urbit-vx.y-darwin.tgz
Release notes:
@ -295,10 +296,10 @@ and stars to the rest of the network.
For consistency, I create a release tarball and then rsync the files in.
```
$ wget https://github.com/urbit/urbit/archive/urbit-os-vx.y.z.tar.gz
$ tar xzf urbit-os-vx.y.z.tar.gz
$ wget https://github.com/urbit/urbit/archive/urbit-os-vx.y.tar.gz
$ tar xzf urbit-os-vx.y.tar.gz
$ herb zod -p hood -d "+hood/mount /=home="
$ rsync -zr --delete urbit-urbit-os-vx.y.z/pkg/arvo/ zod/home
$ rsync -zr --delete urbit-urbit-os-vx.y/pkg/arvo/ zod/home
$ herb zod -p hood -d "+hood/commit %home"
$ herb zod -p hood -d "+hood/merge %kids our %home"
```
@ -306,16 +307,11 @@ $ herb zod -p hood -d "+hood/merge %kids our %home"
For Vere updates, this means simply shutting down each desired ship, installing
the new binary, and restarting the pier with it.
#### Continuous deployment
A subset of release branches are deployed continuously to the network. Thus far
this only includes `release/next-userspace`, which deploys livenet-compatible
changes to select QA ships. Any push to master will automatically
merge master into `release/next-userspace` to keep the streams at parity.
### Announce the update
Post an announcement to urbit-dev. The tag annotation, basically, is fine here
-- I usually add the %base hash (for Urbit OS releases) and the release binary
-- I usually add the %cz hash (for Urbit OS releases) and the release binary
URLs (for Vere releases). Check the urbit-dev archives for examples of these
announcements.
Post the same announcement to the group feed of Urbit Community.

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6f48518fe49584a6532a20018f4ac4eae3817b25d85d60536a99643eb5d65b2b
size 22872573
oid sha256:e660fba934c5b80eeda64037a1f28c71eff4b2ea0bd28809b91432ca3d5ef08a
size 23052691

View File

@ -14,10 +14,6 @@
$ nix-build -A urbit --argstr crossSystem x86_64-unknown-linux-musl \
--arg enableStatic true
Static urbit-king binary:
$ nix-build -A hs.urbit-king.components.exes.urbit-king --arg enableStatic true
Static release tarball:
$ nix-build -A tarball --arg enableStatic true
@ -28,15 +24,6 @@
$ nix-build -A brass.build
$ nix-build -A solid.build
Run the king-haskell checks (.tests are _build_ the test code, .checks _runs_):
$ nix-build -A hs.urbit-king.checks.urbit-king-tests
Build a specific Haskell package from ./pkg/hs:
$ nix-build -A hs.urbit-noun.components.library
$ nix-build -A hs.urbit-atom.components.benchmarks.urbit-atom-bench
$ nix-build -A hs.urbit-atom.components.tests.urbit-atom-tests
*/
# The build system where packages will be _built_.
@ -52,7 +39,7 @@
# Overlays to apply to the last package set in cross compilation.
, crossOverlays ? [ ]
# Whether to use pkgs.pkgsStatic.* to obtain statically linked package
# dependencies - ie. when building fully-static libraries or executables.
# dependencies - ie. when building fully-static libraries or executables.
, enableStatic ? false }:
let
@ -76,7 +63,7 @@ let
# Enrich the global package set with our local functions and packages.
# Cross vs static build dependencies can be selectively overridden for
# inputs like python and haskell-nix
# inputs like python etc.
callPackage =
pkgsNative.lib.callPackageWith (pkgsStatic // libLocal // pkgsLocal);
@ -113,22 +100,12 @@ let
urcrypt = callPackage ./nix/pkgs/urcrypt { inherit enableStatic; };
docker-image = callPackage ./nix/pkgs/docker-image { };
hs = callPackage ./nix/pkgs/hs {
inherit enableStatic;
inherit (pkgsCross) haskell-nix;
};
};
# Additional top-level packages and attributes exposed for convenience.
pkgsExtra = with pkgsLocal; rec {
# Expose packages with local customisations (like patches) for dev access.
inherit (pkgsCross) libsigsegv;
# Collect haskell check (aka "run the tests") attributes so we can run every
# test for our local haskell packages, similar to the urbit-tests attribute.
hs-checks = (pkgsNative.recurseIntoAttrs
(libLocal.collectHaskellComponents pkgsLocal.hs)).checks;
inherit (pkgsStatic) libsigsegv lmdb;
urbit-debug = urbit.override { enableDebug = true; };
urbit-tests = libLocal.testFakeShip {
@ -145,14 +122,12 @@ let
# Create a .tgz of the primary binaries.
tarball = let
name = "urbit-v${urbit.version}-${urbit.system}";
urbit-king = hs.urbit-king.components.exes.urbit-king;
in libLocal.makeReleaseTarball {
inherit name;
contents = {
"${name}/urbit" = "${urbit}/bin/urbit";
"${name}/urbit-worker" = "${urbit}/bin/urbit-worker";
"${name}/urbit-king" = "${urbit-king}/bin/urbit-king";
};
};

View File

@ -13,42 +13,19 @@
let
sourcesFinal = import ./sources.nix { inherit pkgs; } // sources;
finalSources = import ./sources.nix { } // sources;
haskellNix = import sourcesFinal."haskell.nix" {
sourcesOverride = {
hackage = sourcesFinal."hackage.nix";
stackage = sourcesFinal."stackage.nix";
};
};
pkgs = import finalSources.nixpkgs {
inherit system config crossSystem crossOverlays;
configFinal = haskellNix.config // config;
overlaysFinal = haskellNix.overlays ++ [
(_final: prev: {
# Add top-level .sources attribute for other overlays to access sources.
sources = sourcesFinal;
# Additional non-convential package/exe mappings for shellFor.tools.
haskell-nix = prev.haskell-nix // {
toolPackageName = prev.haskell-nix.toolPackageName // {
shellcheck = "ShellCheck";
};
};
})
# General unguarded (native) overrides for nixpkgs.
(import ./overlays/native.nix)
# Specific overrides guarded by the host platform.
(import ./overlays/musl.nix)
] ++ overlays;
pkgs = import sourcesFinal.nixpkgs {
inherit system crossSystem crossOverlays;
config = configFinal;
overlays = overlaysFinal;
overlays = [
# Make prev.sources available to subsequent overlays.
(_final: _prev: { sources = finalSources; })
# General unguarded (native) overrides for nixpkgs.
(import ./overlays/native.nix)
# Specific overrides guarded by the host platform.
(import ./overlays/musl.nix)
];
};
in pkgs // {

View File

@ -1,6 +1,6 @@
# Functions that are expected run on the native (non-cross) system.
{ lib, recurseIntoAttrs, haskell-nix, callPackage }:
{ callPackage }:
rec {
bootFakeShip = callPackage ./boot-fake-ship.nix { };
@ -10,28 +10,4 @@ rec {
fetchGitHubLFS = callPackage ./fetch-github-lfs.nix { };
makeReleaseTarball = callPackage ./make-release-tarball.nix { };
collectHaskellComponents = project:
let
# These functions pull out from the Haskell project either all the
# components of a particular type, or all the checks.
pkgs = haskell-nix.haskellLib.selectProjectPackages project;
collectChecks = _:
recurseIntoAttrs (builtins.mapAttrs (_: p: p.checks) pkgs);
collectComponents = type:
haskell-nix.haskellLib.collectComponents' type pkgs;
# Recompute the Haskell package set sliced by component type
in builtins.mapAttrs (type: f: f type) {
# These names must match the subcomponent: components.<name>.<...>
"library" = collectComponents;
"tests" = collectComponents;
"benchmarks" = collectComponents;
"exes" = collectComponents;
"checks" = collectChecks;
};
}

View File

@ -91,7 +91,7 @@ let
"''${curl[@]}" -s --output "$out" "$href"
'';
impureEnvVars = stdenvNoCC.lib.fetchers.proxyImpureEnvVars;
impureEnvVars = lib.fetchers.proxyImpureEnvVars;
SSL_CERT_FILE = "${cacert}/etc/ssl/certs/ca-bundle.crt";

View File

@ -23,6 +23,4 @@ in prev.lib.optionalAttrs isMusl {
rhash = overrideStdenv prev.rhash;
numactl = overrideStdenv prev.numactl;
lmdb = overrideStdenv prev.lmdb;
}

View File

@ -9,11 +9,7 @@ in {
version = final.sources.h2o.rev;
src = final.sources.h2o;
outputs = [ "out" "dev" "lib" ];
});
secp256k1 = prev.secp256k1.overrideAttrs (_attrs: {
version = final.sources.secp256k1.rev;
src = final.sources.secp256k1;
meta.platforms = prev.lib.platforms.linux ++ prev.lib.platforms.darwin;
});
libsigsegv = prev.libsigsegv.overrideAttrs (attrs: {
@ -23,7 +19,7 @@ in {
];
});
curlMinimal = prev.curl.override {
curlUrbit = prev.curlMinimal.override {
http2Support = false;
scpSupport = false;
gssSupport = false;

View File

@ -16,7 +16,7 @@ let
in {
gmp = enableStatic prev.gmp;
curlMinimal = enableStatic prev.curlMinimal;
curlUrbit = enableStatic prev.curlUrbit;
libuv = enableStatic prev.libuv;
@ -26,12 +26,8 @@ in {
lmdb = prev.lmdb.overrideAttrs (old:
configureFlags old // {
# Why remove the so version? It's easier than preventing it from being
# built with lmdb's custom Makefiles, and it can't exist in the output
# because otherwise the linker will preferentially choose the .so over
# the .a.
postInstall = ''
rm $out/lib/liblmdb.so
postPatch = ''
sed '/^ILIBS\t/s/liblmdb\$(SOEXT)//' -i Makefile
'';
});
}

View File

@ -18,7 +18,7 @@ stdenvNoCC.mkDerivation {
phases = [ "installPhase" "fixupPhase" ];
installPhase = ''
mkdir -p $out/bin
mkdir -p $out/bin
cp $src $out/bin/herb
chmod +x $out/bin/herb
'';

View File

@ -1,90 +0,0 @@
{ lib, stdenv, darwin, haskell-nix, lmdb, gmp, zlib, libffi, brass
, enableStatic ? stdenv.hostPlatform.isStatic }:
haskell-nix.stackProject {
compiler-nix-name = "ghc884";
index-state = "2020-09-24T00:00:00Z";
# This is incredibly difficult to get right, almost everything goes wrong.
# See: https://github.com/input-output-hk/haskell.nix/issues/496
src = haskell-nix.haskellLib.cleanSourceWith {
# Otherwise this depends on the name in the parent directory, which
# reduces caching, and is particularly bad on Hercules.
# See: https://github.com/hercules-ci/support/issues/40
name = "urbit-hs";
src = ../../../pkg/hs;
};
modules = [{
# This corresponds to the set of packages (boot libs) that ship with GHC.
# We declare them yere to ensure any dependency gets them from GHC itself
# rather than trying to re-install them into the package database.
nonReinstallablePkgs = [
"Cabal"
"Win32"
"array"
"base"
"binary"
"bytestring"
"containers"
"deepseq"
"directory"
"filepath"
"ghc"
"ghc-boot"
"ghc-boot-th"
"ghc-compact"
"ghc-heap"
"ghc-prim"
"ghci"
"ghcjs-prim"
"ghcjs-th"
"haskeline"
"hpc"
"integer-gmp"
"integer-simple"
"mtl"
"parsec"
"pretty"
"process"
"rts"
"stm"
"template-haskell"
"terminfo"
"text"
"time"
"transformers"
"unix"
"xhtml"
];
# Override various project-local flags and build configuration.
packages = {
urbit-king.components.exes.urbit-king = {
enableStatic = enableStatic;
enableShared = !enableStatic;
configureFlags = lib.optionals enableStatic [
"--ghc-option=-optl=-L${lmdb}/lib"
"--ghc-option=-optl=-L${gmp}/lib"
"--ghc-option=-optl=-L${libffi}/lib"
"--ghc-option=-optl=-L${zlib}/lib"
] ++ lib.optionals (enableStatic && stdenv.isDarwin)
[ "--ghc-option=-optl=-L${darwin.libiconv}/lib" ];
postInstall = lib.optionalString (enableStatic && stdenv.isDarwin) ''
find "$out/bin" -type f -exec \
install_name_tool -change \
${stdenv.cc.libc}/lib/libSystem.B.dylib \
/usr/lib/libSystem.B.dylib {} \;
'';
};
urbit-king.components.tests.urbit-king-tests.testFlags =
[ "--brass-pill=${brass.lfs}" ];
lmdb.components.library.libs = lib.mkForce [ lmdb ];
};
}];
}

View File

@ -1,6 +1,6 @@
{ lib, stdenv, coreutils, pkgconfig # build/env
, cacert, ca-bundle, ivory # codegen
, curlMinimal, ent, gmp, h2o, libsigsegv, libuv, lmdb # libs
, curlUrbit, ent, gmp, h2o, libsigsegv, libuv, lmdb # libs
, murmur3, openssl, softfloat3, urcrypt, zlib #
, enableStatic ? stdenv.hostPlatform.isStatic # opts
, enableDebug ? false
@ -25,7 +25,7 @@ in stdenv.mkDerivation {
buildInputs = [
cacert
ca-bundle
curlMinimal
curlUrbit
ent
gmp
h2o

View File

@ -31,6 +31,24 @@
"url": "https://github.com/LMDB/lmdb/archive/48a7fed59a8aae623deff415dda27097198ca0c1.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"secp256k1": {
"branch": "master",
"description": "Optimized C library for ECDSA signatures and secret/public key operations on curve secp256k1.",
"homepage": null,
"owner": "bitcoin-core",
"pmnsh": {
"include": "include",
"lib": ".libs",
"make": "libsecp256k1.la",
"prepare": "./autogen.sh && ./configure --disable-shared --enable-benchmark=no --enable-exhaustive-tests=no --enable-experimental --enable-module-ecdh --enable-module-recovery --enable-module-schnorrsig --enable-tests=yes CFLAGS=-DSECP256K1_API="
},
"repo": "secp256k1",
"rev": "7973576f6e3ab27d036a09397152b124d747f4ae",
"sha256": "0vjk55dv0mkph4k6bqgkykmxn05ngzvhc4rzjnvn33xzi8dzlvah",
"type": "tarball",
"url": "https://github.com/bitcoin-core/secp256k1/archive/7973576f6e3ab27d036a09397152b124d747f4ae.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"uv": {
"branch": "v1.x",
"description": "Cross-platform asynchronous I/O",

View File

@ -3,17 +3,17 @@
"branch": "master",
"description": "H2O - the optimized HTTP/1, HTTP/2, HTTP/3 server",
"homepage": "https://h2o.examp1e.net",
"pmnsh": {
"include": "include",
"prepare": "cmake .",
"make": "libh2o",
"compat": {
"mingw": {
"prepare": "cmake -G\"MSYS Makefiles\" -DCMAKE_INSTALL_PREFIX=. ."
}
}
},
"owner": "h2o",
"pmnsh": {
"compat": {
"mingw": {
"prepare": "cmake -G\"MSYS Makefiles\" -DCMAKE_INSTALL_PREFIX=. ."
}
},
"include": "include",
"make": "libh2o",
"prepare": "cmake ."
},
"repo": "h2o",
"rev": "v2.2.6",
"sha256": "0qni676wqvxx0sl0pw9j0ph7zf2krrzqc1zwj73mgpdnsr8rsib7",
@ -21,47 +21,23 @@
"url": "https://github.com/h2o/h2o/archive/v2.2.6.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"hackage.nix": {
"branch": "master",
"description": "Automatically generated Nix expressions for Hackage",
"homepage": "",
"owner": "input-output-hk",
"repo": "hackage.nix",
"rev": "ed4d2759c9e6ca8133a4170f99fabdd76f30f51a",
"sha256": "1n5fk8zsxnbca96zk4ikh74iz3lzh35m302q65zk1rx3nmy4027d",
"type": "tarball",
"url": "https://github.com/input-output-hk/hackage.nix/archive/ed4d2759c9e6ca8133a4170f99fabdd76f30f51a.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"haskell.nix": {
"branch": "master",
"description": "Alternative Haskell Infrastructure for Nixpkgs",
"homepage": "https://input-output-hk.github.io/haskell.nix",
"owner": "input-output-hk",
"repo": "haskell.nix",
"rev": "bbb34dcdf7b90d478002f91713531f418ddf1b53",
"sha256": "1qq397j8vnlp5npk8r675fzjfimg74fcvrkxcdgx7vj48315bh2w",
"type": "tarball",
"url": "https://github.com/input-output-hk/haskell.nix/archive/bbb34dcdf7b90d478002f91713531f418ddf1b53.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"libaes_siv": {
"branch": "master",
"description": null,
"homepage": null,
"owner": "dfoxfranke",
"pmnsh": {
"compat": {
"m1brew": {
"prepare": "cmake .",
"make": "install CFLAGS=$(pkg-config --cflags openssl)"
"make": "install CFLAGS=$(pkg-config --cflags openssl)",
"prepare": "cmake ."
},
"mingw": {
"prepare": "cmake -G\"MSYS Makefiles\" -DDISABLE_DOCS:BOOL=ON .",
"make": "aes_siv_static"
"make": "aes_siv_static",
"prepare": "cmake -G\"MSYS Makefiles\" -DDISABLE_DOCS:BOOL=ON ."
}
}
},
"owner":"dfoxfranke",
"repo": "libaes_siv",
"rev": "9681279cfaa6e6399bb7ca3afbbc27fc2e19df4b",
"sha256": "1g4wy0m5wpqx7z6nillppkh5zki9fkx9rdw149qcxh7mc5vlszzi",
@ -73,10 +49,10 @@
"branch": "master",
"description": null,
"homepage": null,
"owner": "urbit",
"pmnsh": {
"make": "static"
},
"owner": "urbit",
"repo": "murmur3",
"rev": "71a75d57ca4e7ca0f7fc2fd84abd93595b0624ca",
"sha256": "0k7jq2nb4ad9ajkr6wc4w2yy2f2hkwm3nkbj2pklqgwsg6flxzwg",
@ -97,23 +73,23 @@
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"nixpkgs": {
"branch": "master",
"branch": "nixos-21.11",
"description": "Nix Packages collection",
"homepage": null,
"owner": "nixos",
"homepage": "",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "166ab9d237409c4b74b1f8ca31476ead35e8fe53",
"sha256": "13i43kvbkdl3dh8b986j6mxbn355mqjhcxrd8cni8zfx1z0wrscr",
"rev": "573095944e7c1d58d30fc679c81af63668b54056",
"sha256": "07s5cwhskqvy82b4rld9b14ljc0013pig23i3jx3l3f957rk95pg",
"type": "tarball",
"url": "https://github.com/nixos/nixpkgs/archive/166ab9d237409c4b74b1f8ca31476ead35e8fe53.tar.gz",
"url": "https://github.com/NixOS/nixpkgs/archive/573095944e7c1d58d30fc679c81af63668b54056.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"softfloat3": {
"branch": "master",
"description": null,
"homepage": null,
"owner": "urbit",
"pmnsh": {
"include": "source/include",
"compat": {
"m1brew": {
"lib": "build/template-FAST_INT64",
@ -123,44 +99,14 @@
"lib": "build/Win64-MinGW-w64",
"make": "-C build/Win64-MinGW-w64 libsoftfloat3.a"
}
}
},
"include": "source/include"
},
"owner": "urbit",
"repo": "berkeley-softfloat-3",
"rev": "ec4c7e31b32e07aad80e52f65ff46ac6d6aad986",
"sha256": "1lz4bazbf7lns1xh8aam19c814a4n4czq5xsq5rmi9sgqw910339",
"type": "tarball",
"url": "https://github.com/urbit/berkeley-softfloat-3/archive/ec4c7e31b32e07aad80e52f65ff46ac6d6aad986.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"secp256k1": {
"branch": "master",
"description": "Optimized C library for ECDSA signatures and secret/public key operations on curve secp256k1.",
"homepage": null,
"pmnsh": {
"include": "include",
"lib": ".libs",
"prepare": "./autogen.sh && ./configure --disable-shared --enable-module-recovery CFLAGS=-DSECP256K1_API=",
"make": "libsecp256k1.la"
},
"owner": "bitcoin-core",
"repo": "secp256k1",
"rev": "26de4dfeb1f1436dae1fcf17f57bdaa43540f940",
"sha256": "03i3nv8d3ci7q9y98q11rrp3rvwdqc0hc0ss0pr6xckybvizsmbb",
"type": "tarball",
"url": "https://github.com/bitcoin-core/secp256k1/archive/26de4dfeb1f1436dae1fcf17f57bdaa43540f940.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"stackage.nix": {
"branch": "master",
"description": "Automatically generated Nix expressions of Stackage snapshots",
"homepage": "",
"owner": "input-output-hk",
"repo": "stackage.nix",
"rev": "08312f475f4f5f3b6578e7a78dc501de6fea8792",
"sha256": "15j1l6616kfv7351jxwgb9kj6y8227fcm87nxwabmbn1q6a8q2kf",
"type": "tarball",
"url": "https://github.com/input-output-hk/stackage.nix/archive/08312f475f4f5f3b6578e7a78dc501de6fea8792.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
}
}

View File

@ -6,149 +6,169 @@ let
# The fetchers. fetch_<type> fetches specs of type <type>.
#
fetch_file = pkgs: spec:
if spec.builtin or true then
builtins_fetchurl { inherit (spec) url sha256; }
else
pkgs.fetchurl { inherit (spec) url sha256; };
fetch_file = pkgs: name: spec:
let
name' = sanitizeName name + "-src";
in
if spec.builtin or true then
builtins_fetchurl { inherit (spec) url sha256; name = name'; }
else
pkgs.fetchurl { inherit (spec) url sha256; name = name'; };
fetch_tarball = pkgs: name: spec:
let
ok = str: !builtins.isNull (builtins.match "[a-zA-Z0-9+-._?=]" str);
# sanitize the name, though nix will still fail if name starts with period
name' = stringAsChars (x: if !ok x then "-" else x) "${name}-src";
in if spec.builtin or true then
builtins_fetchTarball {
name = name';
inherit (spec) url sha256;
}
else
pkgs.fetchzip {
name = name';
inherit (spec) url sha256;
};
name' = sanitizeName name + "-src";
in
if spec.builtin or true then
builtins_fetchTarball { name = name'; inherit (spec) url sha256; }
else
pkgs.fetchzip { name = name'; inherit (spec) url sha256; };
fetch_git = spec:
builtins.fetchGit {
url = spec.repo;
inherit (spec) rev ref;
};
fetch_git = name: spec:
let
ref =
if spec ? ref then spec.ref else
if spec ? branch then "refs/heads/${spec.branch}" else
if spec ? tag then "refs/tags/${spec.tag}" else
abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!";
in
builtins.fetchGit { url = spec.repo; inherit (spec) rev; inherit ref; };
fetch_local = spec: spec.path;
fetch_builtin-tarball = name:
throw ''
[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`.
$ niv modify ${name} -a type=tarball -a builtin=true'';
fetch_builtin-tarball = name: throw
''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`.
$ niv modify ${name} -a type=tarball -a builtin=true'';
fetch_builtin-url = name:
throw ''
[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`.
$ niv modify ${name} -a type=file -a builtin=true'';
fetch_builtin-url = name: throw
''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`.
$ niv modify ${name} -a type=file -a builtin=true'';
#
# Various helpers
#
# https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695
sanitizeName = name:
(
concatMapStrings (s: if builtins.isList s then "-" else s)
(
builtins.split "[^[:alnum:]+._?=-]+"
((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name)
)
);
# The set of packages used when specs are fetched using non-builtins.
mkPkgs = sources:
mkPkgs = sources: system:
let
sourcesNixpkgs =
import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; })
{ };
import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; };
hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath;
hasThisAsNixpkgsPath = <nixpkgs> == ./.;
in if builtins.hasAttr "nixpkgs" sources then
sourcesNixpkgs
else if hasNixpkgsPath && !hasThisAsNixpkgsPath then
import <nixpkgs> { }
else
abort ''
Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or
add a package called "nixpkgs" to your sources.json.
'';
in
if builtins.hasAttr "nixpkgs" sources
then sourcesNixpkgs
else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then
import <nixpkgs> {}
else
abort
''
Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or
add a package called "nixpkgs" to your sources.json.
'';
# The actual fetching function.
fetch = pkgs: name: spec:
if !builtins.hasAttr "type" spec then
if ! builtins.hasAttr "type" spec then
abort "ERROR: niv spec ${name} does not have a 'type' attribute"
else if spec.type == "file" then
fetch_file pkgs spec
else if spec.type == "tarball" then
fetch_tarball pkgs name spec
else if spec.type == "git" then
fetch_git spec
else if spec.type == "local" then
fetch_local spec
else if spec.type == "builtin-tarball" then
fetch_builtin-tarball name
else if spec.type == "builtin-url" then
fetch_builtin-url name
else if spec.type == "file" then fetch_file pkgs name spec
else if spec.type == "tarball" then fetch_tarball pkgs name spec
else if spec.type == "git" then fetch_git name spec
else if spec.type == "local" then fetch_local spec
else if spec.type == "builtin-tarball" then fetch_builtin-tarball name
else if spec.type == "builtin-url" then fetch_builtin-url name
else
abort
"ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}";
abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}";
# If the environment variable NIV_OVERRIDE_${name} is set, then use
# the path directly as opposed to the fetched source.
replace = name: drv:
let
saneName = stringAsChars (c: if isNull (builtins.match "[a-zA-Z0-9]" c) then "_" else c) name;
ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}";
in
if ersatz == "" then drv else
# this turns the string into an actual Nix path (for both absolute and
# relative paths)
if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}";
# Ports of functions for older nix versions
# a Nix version of mapAttrs if the built-in doesn't exist
mapAttrs = builtins.mapAttrs or (f: set:
with builtins;
listToAttrs (map (attr: {
name = attr;
value = f attr set.${attr};
}) (attrNames set)));
mapAttrs = builtins.mapAttrs or (
f: set: with builtins;
listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))
);
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295
range = first: last:
if first > last then
[ ]
else
builtins.genList (n: first + n) (last - first + 1);
range = first: last: if first > last then [] else builtins.genList (n: first + n) (last - first + 1);
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257
stringToCharacters = s:
map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1));
stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1));
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269
stringAsChars = f: s: concatStrings (map f (stringToCharacters s));
concatMapStrings = f: list: concatStrings (map f list);
concatStrings = builtins.concatStringsSep "";
# https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331
optionalAttrs = cond: as: if cond then as else {};
# fetchTarball version that is compatible between all the versions of Nix
builtins_fetchTarball = { url, name, sha256 }@attrs:
let inherit (builtins) lessThan nixVersion fetchTarball;
in if lessThan nixVersion "1.12" then
fetchTarball { inherit name url; }
else
fetchTarball attrs;
builtins_fetchTarball = { url, name ? null, sha256 }@attrs:
let
inherit (builtins) lessThan nixVersion fetchTarball;
in
if lessThan nixVersion "1.12" then
fetchTarball ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; }))
else
fetchTarball attrs;
# fetchurl version that is compatible between all the versions of Nix
builtins_fetchurl = { url, sha256 }@attrs:
let inherit (builtins) lessThan nixVersion fetchurl;
in if lessThan nixVersion "1.12" then
fetchurl { inherit url; }
else
fetchurl attrs;
builtins_fetchurl = { url, name ? null, sha256 }@attrs:
let
inherit (builtins) lessThan nixVersion fetchurl;
in
if lessThan nixVersion "1.12" then
fetchurl ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; }))
else
fetchurl attrs;
# Create the final "sources" from the config
mkSources = config:
mapAttrs (name: spec:
if builtins.hasAttr "outPath" spec then
abort
"The values in sources.json should not have an 'outPath' attribute"
else
spec // { outPath = fetch config.pkgs name spec; }) config.sources;
mapAttrs (
name: spec:
if builtins.hasAttr "outPath" spec
then abort
"The values in sources.json should not have an 'outPath' attribute"
else
spec // { outPath = replace name (fetch config.pkgs name spec); }
) config.sources;
# The "config" used by the fetchers
mkConfig = { sourcesFile ? ./sources.json
, sources ? builtins.fromJSON (builtins.readFile sourcesFile)
, pkgs ? mkPkgs sources }: rec {
mkConfig =
{ sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null
, sources ? if isNull sourcesFile then {} else builtins.fromJSON (builtins.readFile sourcesFile)
, system ? builtins.currentSystem
, pkgs ? mkPkgs sources system
}: rec {
# The sources, i.e. the attribute set of spec name to spec
inherit sources;
# The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers
inherit pkgs;
};
in mkSources (mkConfig { }) // {
__functor = _: settings: mkSources (mkConfig settings);
}
in
mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); }

View File

@ -32,7 +32,8 @@
++ on-watch
|= =path
^- (quip card:agent:gall _this)
?> ?=([%session @ ~] path)
?> =(our src):bowl
?> ?=([%session @ %view ~] path)
:_ this
:: scry prompt and cursor position out of dill for initial response
::
@ -57,12 +58,13 @@
:_ this
%+ turn p.sign-arvo
|= =blit:dill
[%give %fact [%session %$ ~]~ %blit !>(blit)]
[%give %fact [%session %$ %view ~]~ %blit !>(blit)]
==
::
++ on-poke
|= [=mark =vase]
^- (quip card:agent:gall _this)
?> =(our src):bowl
?. ?=(%belt mark)
~| [%unexpected-mark mark]
!!

View File

@ -1,466 +0,0 @@
:: azimuth: constants and utilities
::
/+ ethereum
::
=> => [azimuth-types ethereum-types .]
|%
+$ complete-ship
$: state=point
history=(list diff-point) ::TODO maybe block/event nr? :: newest first
keys=(map life pass)
==
::
++ fleet (map @p complete-ship)
::
++ eth-type
|%
++ point
:~ [%bytes-n 32] :: encryptionKey
[%bytes-n 32] :: authenticationKey
%bool :: hasSponsor
%bool :: active
%bool :: escapeRequested
%uint :: sponsor
%uint :: escapeRequestedTo
%uint :: cryptoSuiteVersion
%uint :: keyRevisionNumber
%uint :: continuityNumber
==
++ deed
:~ %address :: owner
%address :: managementProxy
%address :: spawnProxy
%address :: votingProxy
%address :: transferProxy
==
--
::
++ eth-noun
|%
+$ point
$: encryption-key=octs
authentication-key=octs
has-sponsor=?
active=?
escape-requested=?
sponsor=@ud
escape-to=@ud
crypto-suite=@ud
key-revision=@ud
continuity-number=@ud
==
+$ deed
$: owner=address
management-proxy=address
spawn-proxy=address
voting-proxy=address
transfer-proxy=address
==
--
::
++ function
|%
++ azimuth
$% [%points who=@p]
[%rights who=@p]
[%get-spawned who=@p]
[%dns-domains ind=@ud]
==
--
::
:: # diffs
::
++ update
$% [%full ships=(map ship point) dns=dnses heard=events]
[%difs dis=(list (pair event-id diff-azimuth))]
==
::
:: # constants
::
:: contract addresses
++ contracts mainnet-contracts
++ mainnet-contracts
|%
:: azimuth: data contract
::
++ azimuth
0x223c.067f.8cf2.8ae1.73ee.5caf.ea60.ca44.c335.fecb
::
++ ecliptic
0xa5b6.109a.d2d3.5191.b3bc.32c0.0e45.26be.56fe.321f
::
++ linear-star-release
0x86cd.9cd0.992f.0423.1751.e376.1de4.5cec.ea5d.1801
::
++ conditional-star-release
0x8c24.1098.c3d3.498f.e126.1421.633f.d579.86d7.4aea
::
++ delegated-sending
0xf790.8ab1.f1e3.52f8.3c5e.bc75.051c.0565.aeae.a5fb
::
++ naive
0xeb70.029c.fb3c.53c7.78ea.f68c.d28d.e725.390a.1fe9
::
:: launch: block number of azimuth deploy
::
++ launch 6.784.800
::
:: public: block number of azimuth becoming independent
::
++ public 7.033.765
::
++ chain-id 1
--
::
:: Testnet contract addresses
::
++ ropsten-contracts
|%
++ azimuth
0x308a.b6a6.024c.f198.b57e.008d.0ac9.ad02.1988.6579
::
++ ecliptic
0x8b9f.86a2.8921.d9c7.05b3.113a.755f.b979.e1bd.1bce
::
++ linear-star-release
0x1f8e.dd03.1ee4.1474.0aed.b39b.84fb.8f2f.66ca.422f
::
++ conditional-star-release
0x0
::
++ delegated-sending
0x3e8c.a510.354b.c2fd.bbd6.1502.52d9.3105.c9c2.7bbe
::
++ naive
0xe7cf.4b83.06d3.11ba.ca15.585f.e3f0.7cd0.441c.21d1
::
++ launch 4.601.630
++ public launch
++ chain-id 3
--
::
:: Local contract addresses
::
:: These addresses are only reproducible if you use the deploy
:: script in bridge
::
++ local-contracts
|%
++ ecliptic
0x56db.68f2.9203.ff44.a803.faa2.404a.44ec.bb7a.7480
++ azimuth
0x863d.9c2e.5c4c.1335.96cf.ac29.d552.55f0.d0f8.6381
++ delegated-sending
0xb71c.0b6c.ee1b.cae5.6dfe.95cd.9d3e.41dd.d7ea.fc43
++ linear-star-release
0x3c3.dc12.be65.8158.d1d7.f9e6.6e08.ec40.99c5.68e4
++ conditional-star-release
0x35eb.3b10.2d9c.1b69.ac14.69c1.b1fe.1799.850c.d3eb
++ naive
0x6bb8.8a9b.bd82.be7a.997f.eb01.929c.6ec7.8988.fe12
++ launch 0
++ public 0
++ chain-id 1.337
--
::
:: ++ azimuth 0x863d.9c2e.5c4c.1335.96cf.ac29.d552.55f0.d0f8.6381 :: local bridge
:: hashes of ship event signatures
++ azimuth-events
|%
::
:: OwnerChanged(uint32,address)
++ owner-changed
0x16d0.f539.d49c.6cad.822b.767a.9445.bfb1.
cf7e.a6f2.a6c2.b120.a7ea.4cc7.660d.8fda
::
:: Activated(uint32)
++ activated
0xe74c.0380.9d07.69e1.b1f7.06cc.8414.258c.
d1f3.b6fe.020c.d15d.0165.c210.ba50.3a0f
::
:: Spawned(uint32,uint32)
++ spawned
0xb2d3.a6e7.a339.f5c8.ff96.265e.2f03.a010.
a854.1070.f374.4a24.7090.9644.1508.1546
::
:: EscapeRequested(uint32,uint32)
++ escape-requested
0xb4d4.850b.8f21.8218.141c.5665.cba3.79e5.
3e9b.b015.b51e.8d93.4be7.0210.aead.874a
::
:: EscapeCanceled(uint32,uint32)
++ escape-canceled
0xd653.bb0e.0bb7.ce83.93e6.24d9.8fbf.17cd.
a590.2c83.28ed.0cd0.9988.f368.90d9.932a
::
:: EscapeAccepted(uint32,uint32)
++ escape-accepted
0x7e44.7c9b.1bda.4b17.4b07.96e1.00bf.7f34.
ebf3.6dbb.7fe6.6549.0b1b.fce6.246a.9da5
::
:: LostSponsor(uint32,uint32)
++ lost-sponsor
0xd770.4f9a.2519.3dbd.0b0c.b4a8.09fe.ffff.
a7f1.9d1a.ae88.17a7.1346.c194.4482.10d5
::
:: ChangedKeys(uint32,bytes32,bytes32,uint32,uint32)
++ changed-keys
0xaa10.e7a0.117d.4323.f1d9.9d63.0ec1.69be.
bb3a.988e.8957.70e3.5198.7e01.ff54.23d5
::
:: BrokeContinuity(uint32,uint32)
++ broke-continuity
0x2929.4799.f1c2.1a37.ef83.8e15.f79d.d91b.
cee2.df99.d63c.d1c1.8ac9.68b1.2951.4e6e
::
:: ChangedSpawnProxy(uint32,address)
++ changed-spawn-proxy
0x9027.36af.7b3c.efe1.0d9e.840a.ed0d.687e.
35c8.4095.122b.2505.1a20.ead8.866f.006d
::
:: ChangedTransferProxy(uint32,address)
++ changed-transfer-proxy
0xcfe3.69b7.197e.7f0c.f067.93ae.2472.a9b1.
3583.fecb.ed2f.78df.a14d.1f10.796b.847c
::
:: ChangedManagementProxy(uint32,address)
++ changed-management-proxy
0xab9c.9327.cffd.2acc.168f.afed.be06.139f.
5f55.cb84.c761.df05.e051.1c25.1e2e.e9bf
::
:: ChangedVotingProxy(uint32,address)
++ changed-voting-proxy
0xcbd6.269e.c714.57f2.c7b1.a227.74f2.46f6.
c5a2.eae3.795e.d730.0db5.1768.0c61.c805
::
:: ChangedDns(string,string,string)
++ changed-dns
0xfafd.04ad.e1da.ae2e.1fdb.0fc1.cc6a.899f.
d424.063e.d5c9.2120.e67e.0730.53b9.4898
--
--
::
:: logic
::
|%
++ pass-from-eth
|= [enc=octs aut=octs sut=@ud]
^- pass
%^ cat 3 'b'
?. &(=(1 sut) =(p.enc 32) =(p.aut 32))
(cat 8 0 0)
(cat 8 q.aut q.enc)
::
++ point-from-eth
|= [who=@p point:eth-noun deed:eth-noun]
^- point
::
:: ownership
::
:+ :* owner
management-proxy
voting-proxy
transfer-proxy
==
::
:: network state
::
?. active ~
:- ~
:* key-revision
::
(pass-from-eth encryption-key authentication-key crypto-suite)
::
continuity-number
::
[has-sponsor `@p`sponsor]
::
?. escape-requested ~
``@p`escape-to
==
::
:: spawn state
::
?. ?=(?(%czar %king) (clan:title who)) ~
:- ~
:* spawn-proxy
~ ::TODO call getSpawned to fill this
==
::
++ event-log-to-point-diff
=, azimuth-events
=, abi:ethereum
|= log=event-log:rpc:ethereum
^- (unit (pair ship diff-point))
~? ?=(~ mined.log) %processing-unmined-event
::
?: =(i.topics.log owner-changed)
=/ [who=@ wer=address]
(decode-topics t.topics.log ~[%uint %address])
`[who %owner wer]
::
?: =(i.topics.log activated)
=/ who=@
(decode-topics t.topics.log ~[%uint])
`[who %activated who]
::
?: =(i.topics.log spawned)
=/ [pre=@ who=@]
(decode-topics t.topics.log ~[%uint %uint])
`[pre %spawned who]
::
?: =(i.topics.log escape-requested)
=/ [who=@ wer=@]
(decode-topics t.topics.log ~[%uint %uint])
`[who %escape `wer]
::
?: =(i.topics.log escape-canceled)
=/ who=@ (decode-topics t.topics.log ~[%uint])
`[who %escape ~]
::
?: =(i.topics.log escape-accepted)
=/ [who=@ wer=@]
(decode-topics t.topics.log ~[%uint %uint])
`[who %sponsor & wer]
::
?: =(i.topics.log lost-sponsor)
=/ [who=@ pos=@]
(decode-topics t.topics.log ~[%uint %uint])
`[who %sponsor | pos]
::
?: =(i.topics.log changed-keys)
=/ who=@ (decode-topics t.topics.log ~[%uint])
=/ [enc=octs aut=octs sut=@ud rev=@ud]
%+ decode-results data.log
~[[%bytes-n 32] [%bytes-n 32] %uint %uint]
`[who %keys rev (pass-from-eth enc aut sut)]
::
?: =(i.topics.log broke-continuity)
=/ who=@ (decode-topics t.topics.log ~[%uint])
=/ num=@ (decode-results data.log ~[%uint])
`[who %continuity num]
::
?: =(i.topics.log changed-management-proxy)
=/ [who=@ sox=address]
(decode-topics t.topics.log ~[%uint %address])
`[who %management-proxy sox]
::
?: =(i.topics.log changed-voting-proxy)
=/ [who=@ tox=address]
(decode-topics t.topics.log ~[%uint %address])
`[who %voting-proxy tox]
::
?: =(i.topics.log changed-spawn-proxy)
=/ [who=@ sox=address]
(decode-topics t.topics.log ~[%uint %address])
`[who %spawn-proxy sox]
::
?: =(i.topics.log changed-transfer-proxy)
=/ [who=@ tox=address]
(decode-topics t.topics.log ~[%uint %address])
`[who %transfer-proxy tox]
::
:: warn about unimplemented events, but ignore
:: the ones we know are harmless.
~? ?! .= i.topics.log
:: OwnershipTransferred(address,address)
0x8be0.079c.5316.5914.1344.cd1f.d0a4.f284.
1949.7f97.22a3.daaf.e3b4.186f.6b64.57e0
[%unimplemented-event i.topics.log]
~
::
++ apply-point-diff
|= [pot=point dif=diff-point]
^- point
?- -.dif
%full new.dif
::
%activated
%_ pot
net `[0 0 0 &^(^sein:title who.dif) ~]
kid ?. ?=(?(%czar %king) (clan:title who.dif)) ~
`[0x0 ~]
==
::
:: ownership
::
%owner pot(owner.own new.dif)
%transfer-proxy pot(transfer-proxy.own new.dif)
%management-proxy pot(management-proxy.own new.dif)
%voting-proxy pot(voting-proxy.own new.dif)
::
:: networking
::
?(%keys %continuity %sponsor %escape)
?> ?=(^ net.pot)
?- -.dif
%keys
pot(life.u.net life.dif, pass.u.net pass.dif)
::
%sponsor
%= pot
sponsor.u.net new.dif
escape.u.net ?:(has.new.dif ~ escape.u.net.pot)
==
::
%continuity pot(continuity-number.u.net new.dif)
%escape pot(escape.u.net new.dif)
==
::
:: spawning
::
?(%spawned %spawn-proxy)
?> ?=(^ kid.pot)
?- -.dif
%spawned
=- pot(spawned.u.kid -)
(~(put in spawned.u.kid.pot) who.dif)
::
%spawn-proxy pot(spawn-proxy.u.kid new.dif)
==
==
::
++ parse-id
|= id=@t
^- azimuth:function
|^
~| id
%+ rash id
;~ pose
(function %points 'points' shipname)
(function %get-spawned 'getSpawned' shipname)
(function %dns-domains 'dnsDomains' dem:ag)
==
::
++ function
|* [tag=@tas fun=@t rul=rule]
;~(plug (cold tag (jest fun)) (ifix [pal par] rul))
::
++ shipname
;~(pfix sig fed:ag)
--
::
++ function-to-call
|%
++ azimuth
|= cal=azimuth:function
^- [id=@t dat=call-data:rpc:ethereum]
?- -.cal
%points
:- (crip "points({(scow %p who.cal)})")
['points(uint32)' ~[uint+`@`who.cal]]
::
%rights
:- (crip "rights({(scow %p who.cal)})")
['rights(uint32)' ~[uint+`@`who.cal]]
::
%get-spawned
:- (crip "getSpawned({(scow %p who.cal)})")
['getSpawned(uint32)' ~[uint+`@`who.cal]]
::
%dns-domains
:- (crip "dnsDomains({(scow %ud ind.cal)})")
['dnsDomains(uint256)' ~[uint+ind.cal]]
==
--
--

1
pkg/arvo/lib/azimuth.hoon Symbolic link
View File

@ -0,0 +1 @@
../../base-dev/lib/azimuth.hoon

View File

@ -1,18 +0,0 @@
::
:::: /hoon/atom/mar
::
/? 310
::
:::: A minimal atom mark
::
=, mimes:html
|_ ato=@
++ grab |%
++ noun @
++ mime |=([* p=octs] q.p)
--
++ grow |%
++ mime [/application/x-urb-unknown (as-octs ato)]
--
++ grad %mime
--

1
pkg/arvo/mar/atom.hoon Symbolic link
View File

@ -0,0 +1 @@
../../base-dev/mar/atom.hoon

View File

@ -87,7 +87,7 @@
0x223c.067f.8cf2.8ae1.73ee.5caf.ea60.ca44.c335.fecb
::
++ ecliptic
0x6ac0.7b7c.4601.b5ce.11de.8dfe.6335.b871.c7c4.dd4d
0xa5b6.109a.d2d3.5191.b3bc.32c0.0e45.26be.56fe.321f
::
++ linear-star-release
0x86cd.9cd0.992f.0423.1751.e376.1de4.5cec.ea5d.1801
@ -98,6 +98,9 @@
++ delegated-sending
0xf790.8ab1.f1e3.52f8.3c5e.bc75.051c.0565.aeae.a5fb
::
++ naive
0xeb70.029c.fb3c.53c7.78ea.f68c.d28d.e725.390a.1fe9
::
:: launch: block number of azimuth deploy
::
++ launch 6.784.800
@ -105,6 +108,8 @@
:: public: block number of azimuth becoming independent
::
++ public 7.033.765
::
++ chain-id 1
--
::
:: Testnet contract addresses
@ -126,8 +131,12 @@
++ delegated-sending
0x3e8c.a510.354b.c2fd.bbd6.1502.52d9.3105.c9c2.7bbe
::
++ naive
0xe7cf.4b83.06d3.11ba.ca15.585f.e3f0.7cd0.441c.21d1
::
++ launch 4.601.630
++ public launch
++ chain-id 3
--
::
:: Local contract addresses
@ -137,9 +146,9 @@
::
++ local-contracts
|%
++ ecliptic
++ ecliptic
0x56db.68f2.9203.ff44.a803.faa2.404a.44ec.bb7a.7480
++ azimuth
++ azimuth
0x863d.9c2e.5c4c.1335.96cf.ac29.d552.55f0.d0f8.6381
++ delegated-sending
0xb71c.0b6c.ee1b.cae5.6dfe.95cd.9d3e.41dd.d7ea.fc43
@ -147,8 +156,11 @@
0x3c3.dc12.be65.8158.d1d7.f9e6.6e08.ec40.99c5.68e4
++ conditional-star-release
0x35eb.3b10.2d9c.1b69.ac14.69c1.b1fe.1799.850c.d3eb
++ naive
0x6bb8.8a9b.bd82.be7a.997f.eb01.929c.6ec7.8988.fe12
++ launch 0
++ public 0
++ chain-id 1.337
--
::
:: ++ azimuth 0x863d.9c2e.5c4c.1335.96cf.ac29.d552.55f0.d0f8.6381 :: local bridge

View File

@ -0,0 +1,18 @@
::
:::: /hoon/atom/mar
::
/? 310
::
:::: A minimal atom mark
::
=, mimes:html
|_ ato=@
++ grab |%
++ noun @
++ mime |=([* p=octs] q.p)
--
++ grow |%
++ mime [/application/x-urb-unknown (as-octs ato)]
--
++ grad %mime
--

View File

@ -1 +0,0 @@
[%zuse 420]

View File

@ -0,0 +1,233 @@
/- sur=hark-store
^?
=, sur
=< [. sur]
|%
++ enjs
=, enjs:format
|%
++ update
|= upd=^update
^- json
%+ frond -.upd
?+ -.upd a+~
%added (notification +.upd)
%add-note (add-note +.upd)
%timebox (timebox +.upd)
%more (more +.upd)
%read-each (read-each +.upd)
%read-count (place +.upd)
%unread-each (read-each +.upd)
%unread-count (unread-count +.upd)
%saw-place (saw-place +.upd)
%all-stats (all-stats +.upd)
%del-place (place +.upd)
::%read-note (index +.upd)
::%note-read (note-read +.upd)
%archived (archived +.upd)
==
::
++ add-note
|= [bi=^bin bo=^body]
%- pairs
:~ bin+(bin bi)
body+(body bo)
==
::
++ saw-place
|= [p=^place t=(unit ^time)]
%- pairs
:~ place+(place p)
time+?~(t ~ (time u.t))
==
::
++ archived
|= [t=^time l=^lid n=^notification]
%- pairs
:~ lid+(lid l)
time+s+(scot %ud t)
notification+(notification n)
==
::
++ note-read
|= *
(pairs ~)
::
++ all-stats
|= places=(map ^place ^stats)
^- json
:- %a
^- (list json)
%+ turn ~(tap by places)
|= [p=^place s=^stats]
%- pairs
:~ stats+(stats s)
place+(place p)
==
::
++ stats
|= s=^stats
^- json
%- pairs
:~ each+a+(turn ~(tap in each.s) (cork spat (lead %s)))
last+(time last.s)
count+(numb count.s)
==
++ more
|= upds=(list ^update)
^- json
a+(turn upds update)
::
++ place
|= =^place
%- pairs
:~ desk+s+desk.place
path+s+(spat path.place)
==
::
++ bin
|= =^bin
%- pairs
:~ place+(place place.bin)
path+s+(spat path.bin)
==
++ notification
|= ^notification
^- json
%- pairs
:~ time+(time date)
bin+(^bin bin)
body+(bodies body)
==
++ bodies
|= bs=(list ^body)
^- json
a+(turn bs body)
::
++ contents
|= cs=(list ^content)
^- json
a+(turn cs content)
::
++ content
|= c=^content
^- json
%+ frond -.c
?- -.c
%ship s+(scot %p ship.c)
%text s+cord.c
==
::
++ body
|= ^body
^- json
%- pairs
:~ title+(contents title)
content+(contents content)
time+(^time time)
link+s+(spat link)
==
::
++ binned-notification
|= [=^bin =^notification]
%- pairs
:~ bin+(^bin bin)
notification+(^notification notification)
==
++ lid
|= l=^lid
^- json
%+ frond -.l
?- -.l
?(%seen %unseen) ~
%archive s+(scot %ud time.l)
==
::
++ timebox
|= [li=^lid l=(list ^notification)]
^- json
%- pairs
:~ lid+(lid li)
notifications+a+(turn l notification)
==
::
++ read-each
|= [p=^place pax=^path]
%- pairs
:~ place+(place p)
path+(path pax)
==
::
++ unread-count
|= [p=^place inc=? count=@ud]
%- pairs
:~ place+(place p)
inc+b+inc
count+(numb count)
==
--
++ dejs
=, dejs:format
|%
:: TODO: fix +stab
::
++ pa
|= j=json
^- path
?> ?=(%s -.j)
?: =('/' p.j) /
(stab p.j)
::
++ place
%- ot
:~ desk+so
path+pa
==
::
++ bin
%- ot
:~ path+pa
place+place
==
::
++ read-each
%- ot
:~ place+place
path+pa
==
::
:: parse date as @ud
:: TODO: move to zuse
++ sd
|= jon=json
^- @da
?> ?=(%s -.jon)
`@da`(rash p.jon dem:ag)
::
++ lid
%- of
:~ archive+sd
unseen+ul
seen+ul
==
::
++ archive
%- ot
:~ lid+lid
bin+bin
==
::
++ action
^- $-(json ^action)
%- of
:~ archive-all+ul
archive+archive
opened+ul
read-count+place
read-each+read-each
read-note+bin
==
--
--

View File

@ -0,0 +1 @@
../../garden-dev/lib/hark-store.hoon

View File

@ -1,235 +0,0 @@
/- sur=hark-store
^?
=, sur
=< [. sur]
|%
++ enjs
=, enjs:format
|%
++ update
|= upd=^update
^- json
|^
%+ frond -.upd
?+ -.upd a+~
%added (notification +.upd)
%add-note (add-note +.upd)
%timebox (timebox +.upd)
%more (more +.upd)
%read-each (read-each +.upd)
%read-count (place +.upd)
%unread-each (read-each +.upd)
%unread-count (unread-count +.upd)
%saw-place (saw-place +.upd)
%all-stats (all-stats +.upd)
%del-place (place +.upd)
::%read-note (index +.upd)
::%note-read (note-read +.upd)
%archived (archived +.upd)
==
::
++ add-note
|= [bi=^bin bo=^body]
%- pairs
:~ bin+(bin bi)
body+(body bo)
==
::
++ saw-place
|= [p=^place t=(unit ^time)]
%- pairs
:~ place+(place p)
time+?~(t ~ (time u.t))
==
::
++ archived
|= [t=^time l=^lid n=^notification]
%- pairs
:~ lid+(lid l)
time+s+(scot %ud t)
notification+(notification n)
==
::
++ note-read
|= *
(pairs ~)
::
++ all-stats
|= places=(map ^place ^stats)
^- json
:- %a
^- (list json)
%+ turn ~(tap by places)
|= [p=^place s=^stats]
%- pairs
:~ stats+(stats s)
place+(place p)
==
::
++ stats
|= s=^stats
^- json
%- pairs
:~ each+a+(turn ~(tap in each.s) (cork spat (lead %s)))
last+(time last.s)
count+(numb count.s)
==
++ more
|= upds=(list ^update)
^- json
a+(turn upds update)
::
++ place
|= =^place
%- pairs
:~ desk+s+desk.place
path+s+(spat path.place)
==
::
++ bin
|= =^bin
%- pairs
:~ place+(place place.bin)
path+s+(spat path.bin)
==
++ notification
|= ^notification
^- json
%- pairs
:~ time+(time date)
bin+(^bin bin)
body+(bodies body)
==
++ bodies
|= bs=(list ^body)
^- json
a+(turn bs body)
::
++ contents
|= cs=(list ^content)
^- json
a+(turn cs content)
::
++ content
|= c=^content
^- json
%+ frond -.c
?- -.c
%ship s+(scot %p ship.c)
%text s+cord.c
==
::
++ body
|= ^body
^- json
%- pairs
:~ title+(contents title)
content+(contents content)
time+(^time time)
link+s+(spat link)
==
::
++ binned-notification
|= [=^bin =^notification]
%- pairs
:~ bin+(^bin bin)
notification+(^notification notification)
==
++ lid
|= l=^lid
^- json
%+ frond -.l
?- -.l
?(%seen %unseen) ~
%archive s+(scot %ud time.l)
==
::
++ timebox
|= [li=^lid l=(list ^notification)]
^- json
%- pairs
:~ lid+(lid li)
notifications+a+(turn l notification)
==
::
++ read-each
|= [p=^place pax=^path]
%- pairs
:~ place+(place p)
path+(path pax)
==
::
++ unread-count
|= [p=^place inc=? count=@ud]
%- pairs
:~ place+(place p)
inc+b+inc
count+(numb count)
==
--
--
++ dejs
=, dejs:format
|%
:: TODO: fix +stab
::
++ pa
|= j=json
^- path
?> ?=(%s -.j)
?: =('/' p.j) /
(stab p.j)
::
++ place
%- ot
:~ desk+so
path+pa
==
::
++ bin
%- ot
:~ path+pa
place+place
==
::
++ read-each
%- ot
:~ place+place
path+pa
==
::
:: parse date as @ud
:: TODO: move to zuse
++ sd
|= jon=json
^- @da
?> ?=(%s -.jon)
`@da`(rash p.jon dem:ag)
::
++ lid
%- of
:~ archive+sd
unseen+ul
seen+ul
==
::
++ archive
%- ot
:~ lid+lid
bin+bin
==
::
++ action
^- $-(json ^action)
%- of
:~ archive-all+ul
archive+archive
opened+ul
read-count+place
read-each+read-each
read-note+bin
==
--
--

View File

@ -1 +0,0 @@
import ../../shell.nix

View File

@ -1,6 +1,6 @@
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials', 'storybook-addon-designs'],
webpackFinal: (config) => {
config.module.rules.push({
test: /\.(j|t)sx?$/,

View File

@ -1,19 +1,20 @@
import React from 'react';
import dark from '@tlon/indigo-dark';
import light from '@tlon/indigo-light';
import { Reset } from '@tlon/indigo-react';
import { BrowserRouter } from 'react-router-dom';
import { ThemeProvider } from 'styled-components';
import useGraphState from '~/logic/state/graph';
import useMetadataState from '~/logic/state/metadata';
import useContactState from '~/logic/state/contact';
import '~/views/landscape/css/custom.css';
import '~/views/css/fonts.css';
import '~/views/apps/chat/css/custom.css';
import '~/views/css/indigo-static.css';
import React from "react";
import dark from "@tlon/indigo-dark";
import light from "@tlon/indigo-light";
import { Reset } from "@tlon/indigo-react";
import { BrowserRouter } from "react-router-dom";
import { ThemeProvider } from "styled-components";
import useGraphState from "~/logic/state/graph";
import useGroupState from "~/logic/state/group";
import useMetadataState from "~/logic/state/metadata";
import useContactState from "~/logic/state/contact";
import "~/views/landscape/css/custom.css";
import "~/views/css/fonts.css";
import "~/views/apps/chat/css/custom.css";
import "~/views/css/indigo-static.css";
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
@ -22,170 +23,197 @@ export const parameters = {
},
};
const groupPreview = {
group: "/ship/~bollug-worlus/urbit-index",
channels: {
"/ship/~darrux-landes/index-weekly": {
metadata: {
preview: false,
vip: "",
title: "Index Weekly",
description: "A weekly roundup of content from around the network",
creator: "~bollug-worlus",
picture: "",
hidden: false,
config: {
graph: "publish",
},
"date-created": "~2020.4.6..21.53.30..dc68",
color: "0x0",
},
"app-name": "graph",
resource: "/ship/~bollug-worlus/index-weekly",
group: "/ship/~bollug-worlus/urbit-index",
},
},
members: 1237,
"channel-count": 3,
metadata: {
preview: false,
vip: "",
title: "Urbit Index",
description: "A weekly roundup of content form around the network",
creator: "~bollug-worlus",
picture: "",
hidden: false,
config: {
group: null,
},
"date-created": "~2020.4.6..21.53.30..dc68",
color: "0x0",
},
};
const groupPending = (progress) => ({
hidden: false,
started: Date.now() - 3600,
ship: "~bollug-worlus",
progress,
shareContact: false,
autojoin: false,
app: "groups",
invite: [],
});
export const globalTypes = {
theme: {
name: 'Theme',
description: 'Global Theme for components',
defaultValue: 'light',
name: "Theme",
description: "Global Theme for components",
defaultValue: "light",
toolbar: {
icon: 'circlehollow',
items: ['light', 'dark'],
icon: "circlehollow",
items: ["light", "dark"],
},
},
};
export const decorators = [
(Story, context) => {
window.ship = 'sampel-palnet';
const theme = context.globals.theme === 'light' ? light : dark;
window.ship = "sampel-palnet";
const theme = context.globals.theme === "light" ? light : dark;
useContactState.setState({
contacts: {
'~ridlur-figbud': {
status: 'please like and subscribe',
'last-updated': 1616609090555,
"~ridlur-figbud": {
status: "please like and subscribe",
"last-updated": 1616609090555,
avatar: null,
cover: null,
bio: '',
nickname: 'Gav',
color: '0x26.3e0f',
bio: "",
nickname: "Gav",
color: "0x26.3e0f",
groups: [],
},
'~sampel-palnet': {
status: 'A test status',
'last-updated': 1616609090555,
"~sampel-palnet": {
status: "A test status",
"last-updated": 1616609090555,
avatar: null,
cover: null,
bio: '',
nickname: 'You',
color: '0x26.3e0f',
bio: "",
nickname: "You",
color: "0x26.3e0f",
groups: [],
},
},
});
useGroupState.setState({
pendingJoin: {
"/ship/~bollug-worlus/urbit-index-start": groupPending("start"),
"/ship/~bollug-worlus/urbit-index-metadata": groupPending("metadata"),
"/ship/~bollug-worlus/urbit-index-done": groupPending("done"),
"/ship/~bollug-worlus/urbit-index-error": groupPending("no-perms"),
},
});
useMetadataState.setState({
associations: {
groups: {
'/ship/~bitbet-bolbel/urbit-community': {
"/ship/~bitbet-bolbel/urbit-community": {
metadata: {
preview: false,
vip: '',
title: 'Urbit Community',
description: 'World hub, help desk, meet and greet, etc.',
creator: '~bitbet-bolbel',
vip: "",
title: "Urbit Community",
description: "World hub, help desk, meet and greet, etc.",
creator: "~bitbet-bolbel",
picture:
'https://fabled-faster.nyc3.digitaloceanspaces.com/fabled-faster/2021.4.02..21.52.41-UC.png',
"https://fabled-faster.nyc3.digitaloceanspaces.com/fabled-faster/2021.4.02..21.52.41-UC.png",
hidden: false,
config: {
group: {
'app-name': 'graph',
resource: '/ship/~bitbet-bolbel/urbit-community-5.963',
"app-name": "graph",
resource: "/ship/~bitbet-bolbel/urbit-community-5.963",
},
},
'date-created': '~2020.6.25..21.39.35..2fd2',
color: '0x8f.9c9d',
"date-created": "~2020.6.25..21.39.35..2fd2",
color: "0x8f.9c9d",
},
'app-name': 'groups',
resource: '/ship/~bitbet-bolbel/urbit-community',
group: '/ship/~bitbet-bolbel/urbit-community',
"app-name": "groups",
resource: "/ship/~bitbet-bolbel/urbit-community",
group: "/ship/~bitbet-bolbel/urbit-community",
},
},
graph: {
'/ship/~bitbet-bolbel/links': {
"/ship/~bitbet-bolbel/links": {
metadata: {
preview: false,
vip: '',
title: 'Link Collection',
description: '',
creator: '~darrux-landes',
picture: '',
vip: "",
title: "Link Collection",
description: "",
creator: "~darrux-landes",
picture: "",
hidden: false,
config: {
graph: 'link',
graph: "link",
},
'date-created': '~2020.4.6..21.53.30..dc68',
color: '0x0',
"date-created": "~2020.4.6..21.53.30..dc68",
color: "0x0",
},
'app-name': 'graph',
resource: '/ship/~bitbet-bolbel/links',
group: '/ship/~bitbet-bolbel/urbit-community',
"app-name": "graph",
resource: "/ship/~bitbet-bolbel/links",
group: "/ship/~bitbet-bolbel/urbit-community",
},
'/ship/~darrux-landes/development': {
"/ship/~darrux-landes/development": {
metadata: {
preview: false,
vip: '',
title: 'Development',
vip: "",
title: "Development",
description:
'Urbit Development Mailing List: https://groups.google.com/a/urbit.org/forum/#!forum/dev',
creator: '~darrux-landes',
picture: '',
"Urbit Development Mailing List: https://groups.google.com/a/urbit.org/forum/#!forum/dev",
creator: "~darrux-landes",
picture: "",
hidden: false,
config: {
graph: 'chat',
graph: "chat",
},
'date-created': '~2020.4.6..21.53.30..dc68',
color: '0x0',
"date-created": "~2020.4.6..21.53.30..dc68",
color: "0x0",
},
'app-name': 'graph',
resource: '/ship/~darrux-landes/development',
group: '/ship/~bitbet-bolbel/urbit-community',
"app-name": "graph",
resource: "/ship/~darrux-landes/development",
group: "/ship/~bitbet-bolbel/urbit-community",
},
},
},
previews: {
'/ship/~bollug-worlus/urbit-index': {
group: '/ship/~bollug-worlus/urbit-index',
channels: {
'/ship/~darrux-landes/index-weekly': {
metadata: {
preview: false,
vip: '',
title: 'Index Weekly',
description: '',
creator: '~bollug-worlus',
picture: '',
hidden: false,
config: {
graph: 'publish',
},
'date-created': '~2020.4.6..21.53.30..dc68',
color: '0x0',
},
'app-name': 'graph',
resource: '/ship/~bollug-worlus/index-weekly',
group: '/ship/~bollug-worlus/urbit-index',
},
},
members: 1237,
metadata: {
preview: false,
vip: '',
title: 'Urbit Index',
description: '',
creator: '~bollug-worlus',
picture: '',
hidden: false,
config: {
group: null,
},
'date-created': '~2020.4.6..21.53.30..dc68',
color: '0x0',
},
},
"/ship/~bollug-worlus/urbit-index": groupPreview,
"/ship/~bollug-worlus/urbit-index-start": groupPreview,
"/ship/~bollug-worlus/urbit-index-metadata": groupPreview,
"/ship/~bollug-worlus/urbit-index-done": groupPreview,
"/ship/~bollug-worlus/urbit-index-error": groupPreview,
},
});
useContactState.setState({
contacts: {
'~sampel-palnet': {
status: 'Just urbiting',
'last-updated': 1621511447583,
"~sampel-palnet": {
status: "Just urbiting",
"last-updated": 1621511447583,
avatar: null,
cover: null,
bio: 'An urbit user',
nickname: 'Sample Planet',
color: '0xee.5432',
bio: "An urbit user",
nickname: "Sample Planet",
color: "0xee.5432",
groups: [],
},
},
@ -193,27 +221,27 @@ export const decorators = [
useGraphState.setState({
looseNodes: {
'darrux-landes/development': {
'/170141184505059416342852185329797955584': {
"darrux-landes/development": {
"/170141184505059416342852185329797955584": {
post: {
index: '/170141184505059416342852185329797955584',
author: 'sipfyn-pidmex',
'time-sent': 1621275183241,
index: "/170141184505059416342852185329797955584",
author: "sipfyn-pidmex",
"time-sent": 1621275183241,
signatures: [
{
signature:
'0x3.9e41.4f04.3cac.786e.30c1.f4cc.8db3.9a78.0401.d16f.6301.94d0.a08a.0695.5008.02bf.0e07.a7a9.3d87.85f7.6334.e598.4ed3.5dee.58a7.cbd3.30e6.d65b.1fc9.ac62.162a.daf0.ff14.9cca.4a93.8177.0755.7b74.9d52.c0a6.b27f.9001',
"0x3.9e41.4f04.3cac.786e.30c1.f4cc.8db3.9a78.0401.d16f.6301.94d0.a08a.0695.5008.02bf.0e07.a7a9.3d87.85f7.6334.e598.4ed3.5dee.58a7.cbd3.30e6.d65b.1fc9.ac62.162a.daf0.ff14.9cca.4a93.8177.0755.7b74.9d52.c0a6.b27f.9001",
life: 2,
ship: 'sipfyn-pidmex',
ship: "sipfyn-pidmex",
},
],
contents: [
{
text:
'is there a way to get a bunt of a specific instantance of a tagged union? i.e. if you have `$%([%a =atom] [%b =cell])`, can you get a bunt of specifically subtype `%a`?',
"is there a way to get a bunt of a specific instantance of a tagged union? i.e. if you have `$%([%a =atom] [%b =cell])`, can you get a bunt of specifically subtype `%a`?",
},
],
hash: '0xe790.53c1.0f2b.1e1b.8c30.7d33.236c.e69e',
hash: "0xe790.53c1.0f2b.1e1b.8c30.7d33.236c.e69e",
},
children: {
root: {},

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@
"@react-spring/web": "^9.1.1",
"@tlon/indigo-dark": "^1.0.6",
"@tlon/indigo-light": "^1.0.7",
"@tlon/indigo-react": "^1.2.23",
"@tlon/indigo-react": "^1.2.27",
"@tlon/sigil-js": "^1.4.3",
"@urbit/api": "^2.1.0",
"@urbit/http-api": "^2.1.0",

View File

@ -1,7 +1,7 @@
import Urbit from '@urbit/http-api';
const api = new Urbit('', '', (window as any).desk);
api.ship = window.ship;
// api.verbose = true;
api.verbose = true;
// @ts-ignore TODO window typings
window.api = api;

View File

@ -1,5 +1,7 @@
import useMetadataState from '../state/metadata';
import ob from 'urbit-ob';
import useInviteState from '../state/invite';
import {resourceAsPath} from '../../../../npm/api/dist';
function getGroupResourceRedirect(key: string) {
const association = useMetadataState.getState().associations.graph[`/ship/${key}`];
@ -67,7 +69,9 @@ function getGraphRedirect(link: string) {
function getInviteRedirect(link: string) {
const [,,app,uid] = link.split('/');
return `/invites/${app}/${uid}`;
const invite = useInviteState.getState().invites[app][uid];
if(!invite) { return ''; }
return { search: `?join-kind=${app}&join-path=${encodeURIComponent(resourceAsPath(invite.resource))}` };
}
function getDmRedirect(link: string) {

View File

@ -56,7 +56,7 @@ const commandIndex = function (currentGroup, groups, associations) {
if (canAdd) {
commands.push(result('Channel: Create', `/~landscape${workspace}/new`, 'Groups', null));
}
commands.push(result('Groups: Join', '/~landscape/join', 'Groups', null));
commands.push(result('Groups: Join', '?join-kind=group', 'Groups', null));
return commands;
};

View File

@ -19,7 +19,7 @@ export const isUrl = (str) => {
const raceRegexes = (str) => {
let link = str.match(URL_REGEX);
while(link?.[1]?.endsWith('(')) {
while(link?.[1]?.endsWith('(') || link?.[1].endsWith('[')) {
const resumePos = link[1].length + link[2].length;
const resume = str.slice(resumePos);
link = resume.match(URL_REGEX);

View File

@ -8,6 +8,7 @@ type InviteState = State & BaseState<State>;
const initial = (json: InviteUpdate, state: InviteState): InviteState => {
const data = _.get(json, 'initial', false);
if (data) {
state.loaded = true;
state.invites = data;
}
return state;

View File

@ -1,4 +1,4 @@
import { Association, Group, hideGroup, JoinRequests } from '@urbit/api';
import { Association, Group, JoinRequests, abortJoin } from '@urbit/api';
import { useCallback } from 'react';
import { reduce } from '../reducers/group-update';
import _ from 'lodash';
@ -15,7 +15,8 @@ export interface GroupState {
[group: string]: Group;
};
pendingJoin: JoinRequests;
hidePending: (group: string) => Promise<void>;
abortJoin: (group: string) => Promise<void>;
doneJoin: (group: string) => Promise<void>;
}
// @ts-ignore investigate zustand types
@ -24,12 +25,21 @@ const useGroupState = createState<GroupState>(
(set, get) => ({
groups: {},
pendingJoin: {},
hidePending: async (group) => {
abortJoin: async (group) => {
get().set((draft) => {
delete draft.pendingJoin[group];
});
await api.poke(hideGroup(group));
}
await api.poke(abortJoin(group));
},
doneJoin: async (group) => {
get().set((draft) => {
delete draft.pendingJoin[group];
});
await api.poke({ app: 'group-view', mark: 'group-view-action', json: {
done: group
}});
},
}),
['groups'],
[

View File

@ -1,4 +1,4 @@
import { Invites } from '@urbit/api';
import { deSig, Invite, Invites } from '@urbit/api';
import { reduce } from '../reducers/invite-update';
import _ from 'lodash';
import {
@ -9,14 +9,16 @@ import {
export interface InviteState {
invites: Invites;
loaded: boolean;
}
const useInviteState = createState<InviteState>(
'Invite',
{
invites: {}
invites: {},
loaded: false
},
['invites'],
['invites', 'loaded'],
[
(set, get) =>
createSubscription('invite-store', '/all', (e) => {
@ -29,3 +31,18 @@ const useInviteState = createState<InviteState>(
);
export default useInviteState;
interface InviteWithUid extends Invite {
uid: string;
}
export function useInviteForResource(app: string, ship: string, name: string) {
const { invites } = useInviteState();
const matches = Object.entries(invites?.[app] || {})
.reduce((acc, [uid, invite]) => {
const isMatch = (invite.resource.ship === deSig(ship)
&& invite.resource.name === name)
return isMatch ? [{ uid, ...invite}, ...acc] : acc;
}, [] as InviteWithUid[])
return matches?.[0];
}

View File

@ -0,0 +1,22 @@
import React from "react";
import { Story, Meta } from "@storybook/react";
import { Box } from "@tlon/indigo-react";
import { JoinPrompt, JoinPromptProps } from "~/views/landscape/components/Join/Join";
export default {
title: "Join/Prompt",
component: JoinPrompt,
} as Meta;
const Template: Story<JoinPromptProps> = (args) => (
<Box backgroundColor="white" p="2" width="fit-content">
<JoinPrompt {...args} />
</Box>
);
export const Prompt = Template.bind({});
Prompt.args = {
kind: 'groups',
}

View File

@ -0,0 +1,80 @@
import React from "react";
import { Story, Meta } from "@storybook/react";
import { Box } from "@tlon/indigo-react";
import { Join, JoinProps } from "~/views/landscape/components/Join/Join";
import { withDesign } from "storybook-addon-designs";
export default {
title: "Join/Form",
component: Join,
decorators: [withDesign],
} as Meta;
const Template: Story<JoinProps> = (args) => (
<Box backgroundColor="white" p="2" width="fit-content">
<Join {...args} />
</Box>
);
export const Prompt = Template.bind({});
Prompt.args = {
desc: {
kind: "groups",
group: "/ship/~bitbet-bolbel/urbit-community",
},
};
export const WithPreview = Template.bind({});
WithPreview.args = {
desc: {
kind: "groups",
group: "/ship/~bollug-worlus/urbit-index",
},
};
WithPreview.parameters = {
design: {
type: "figma",
url:
"https://www.figma.com/file/VxNYyFRnj8ZnqWG54VbVRM/Landscape-Baikal?node-id=1795%3A27718",
},
};
export const ProgressStart = Template.bind({});
ProgressStart.args = {
desc: {
kind: "groups",
group: "/ship/~bollug-worlus/urbit-index-start",
},
};
export const ProgressMetadata = Template.bind({});
ProgressMetadata.args = {
desc: {
kind: "groups",
group: "/ship/~bollug-worlus/urbit-index-metadata",
},
};
export const Finished = Template.bind({});
Finished.args = {
desc: {
kind: "groups",
group: "/ship/~bollug-worlus/urbit-index-done",
},
};
export const Error = Template.bind({});
Error.args = {
desc: {
kind: "groups",
group: "/ship/~bollug-worlus/urbit-index-error",
},
};

View File

@ -27,6 +27,17 @@ import './css/indigo-static.css';
import { Content } from './landscape/components/Content';
import './landscape/css/custom.css';
import { bootstrapApi } from '~/logic/api/bootstrap';
import { uxToHex } from '@urbit/api/dist';
function ensureValidHex(color) {
if (!color)
return '#000000';
const isUx = color.startsWith('0x');
const parsedColor = isUx ? uxToHex(color) : color;
return parsedColor.startsWith('#') ? parsedColor : `#${parsedColor}`;
}
const Root = withState(styled.div`
font-family: ${p => p.theme.fonts.sans};
@ -38,7 +49,7 @@ const Root = withState(styled.div`
background-image: url('${p.display.background}');
background-size: cover;
` : p.display.backgroundType === 'color' ? `
background-color: ${p.display.background};
background-color: ${ensureValidHex(p.display.background)};
` : `background-color: ${p.theme.colors.white};`
}
display: flex;

View File

@ -1,19 +1,18 @@
/* eslint-disable max-lines-per-function */
import { Box, Icon, Row, Text } from '@tlon/indigo-react';
import React, { ReactElement } from 'react';
import { Helmet } from 'react-helmet';
import { Route } from 'react-router-dom';
import styled from 'styled-components';
import useHarkState from '~/logic/state/hark';
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
import { JoinGroup } from '~/views/landscape/components/JoinGroup';
import { NewGroup } from '~/views/landscape/components/NewGroup';
import Groups from './components/Groups';
import ModalButton from './components/ModalButton';
import Tiles from './components/tiles';
import Tile from './components/tiles/tile';
import { Invite } from './components/Invite';
import './css/custom.css';
import { Box, Icon, Row, Text, Button } from "@tlon/indigo-react";
import React, { ReactElement } from "react";
import { Helmet } from "react-helmet";
import { Route, useHistory } from "react-router-dom";
import styled from "styled-components";
import useHarkState from "~/logic/state/hark";
import useSettingsState, { selectCalmState } from "~/logic/state/settings";
import Groups from "./components/Groups";
import { NewGroup } from "~/views/landscape/components/NewGroup";
import ModalButton from "./components/ModalButton";
import Tiles from "./components/tiles";
import Tile from "./components/tiles/tile";
import "./css/custom.css";
import { Join, JoinRoute } from "~/views/landscape/components/Join/Join";
const ScrollbarLessBox = styled(Box)`
scrollbar-width: none !important;
@ -28,73 +27,83 @@ interface LaunchAppProps {
}
export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
const notificationsCount = useHarkState(state => state.notificationsCount);
const notificationsCount = useHarkState((state) => state.notificationsCount);
const calmState = useSettingsState(selectCalmState);
const { hideUtilities, hideGroups } = calmState;
const history = useHistory();
return (
<>
<Helmet defer={false}>
<title>{ notificationsCount ? `(${String(notificationsCount) }) `: '' }Groups</title>
<title>
{notificationsCount ? `(${String(notificationsCount)}) ` : ""}Groups
</title>
</Helmet>
<Route path="/invites/:app/:uid">
<Invite />
<Route path="/join/:ship/:name">
<JoinRoute modal />
</Route>
<ScrollbarLessBox height='100%' overflowY='scroll' display="flex" flexDirection="column">
<ScrollbarLessBox
height="100%"
overflowY="scroll"
display="flex"
flexDirection="column"
>
<Box
mx={2}
display='grid'
gridTemplateColumns='repeat(auto-fill, minmax(128px, 1fr))'
display="grid"
gridTemplateColumns="repeat(auto-fill, minmax(128px, 1fr))"
gridGap={3}
p={2}
pt={0}
>
{!hideUtilities && <>
<Tile
bg="white"
color="scales.black20"
to="/~landscape/home"
p={0}
>
<Box
p={2}
height='100%'
width='100%'
bg='scales.black20'
border={1}
borderColor="lightGray"
>
<Row alignItems='center'>
<Icon
color="black"
icon="Home"
/>
<Text ml={2} mt='1px' color="black">My Channels</Text>
</Row>
</Box>
</Tile>
<Tiles />
<ModalButton
icon="Plus"
bg="washedGray"
color="black"
text="New Group"
style={{ gridColumnStart: 1 }}
>
<NewGroup />
</ModalButton>
<ModalButton
icon="BootNode"
bg="washedGray"
color="black"
text="Join Group"
>
{dismiss => <JoinGroup dismiss={dismiss} />}
</ModalButton>
</>}
{!hideGroups &&
(<Groups />)
}
{!hideUtilities && (
<>
<Tile
bg="white"
color="scales.black20"
to="/~landscape/home"
p={0}
>
<Box
p={2}
height="100%"
width="100%"
bg="scales.black20"
border={1}
borderColor="lightGray"
>
<Row alignItems="center">
<Icon color="black" icon="Home" />
<Text ml={2} mt="1px" color="black">
My Channels
</Text>
</Row>
</Box>
</Tile>
<Tiles />
<ModalButton
icon="Plus"
bg="white"
color="black"
text="New Group"
style={{ gridColumnStart: 1 }}
>
<NewGroup />
</ModalButton>
<Button
border={0}
p={0}
borderRadius={2}
onClick={() => history.push({ search: "?join-kind=group" })}
>
<Row backgroundColor="white" gapX="2" p={2} height="100%" width="100%" alignItems="center">
<Icon icon="BootNode" />
<Text fontWeight="medium" whiteSpace="nowrap">Join Group</Text>
</Row>
</Button>
</>
)}
{!hideGroups && <Groups />}
</Box>
</ScrollbarLessBox>
</>

View File

@ -1,16 +1,27 @@
import { Box, Col, Text } from '@tlon/indigo-react';
import { Association, Associations, Unreads } from '@urbit/api';
import f from 'lodash/fp';
import React from 'react';
import { getNotificationCount } from '~/logic/lib/hark';
import { alphabeticalOrder } from '~/logic/lib/util';
import useGroupState from '~/logic/state/group';
import useHarkState, { selHarkGraph } from '~/logic/state/hark';
import useMetadataState from '~/logic/state/metadata';
import { Box, Col, Text } from "@tlon/indigo-react";
import {
Association,
Associations,
resourceAsPath,
resourceFromPath,
Unreads,
} from "@urbit/api";
import f from "lodash/fp";
import _ from "lodash";
import React from "react";
import { useHistory } from "react-router-dom";
import { getNotificationCount } from "~/logic/lib/hark";
import { alphabeticalOrder } from "~/logic/lib/util";
import useGroupState from "~/logic/state/group";
import useHarkState, { selHarkGraph } from "~/logic/state/hark";
import useInviteState from "~/logic/state/invite";
import useMetadataState, { usePreview } from "~/logic/state/metadata";
import useSettingsState, {
selectCalmState
} from '~/logic/state/settings';
import Tile from '../components/tiles/tile';
selectCalmState,
SettingsState,
} from "~/logic/state/settings";
import Tile from "../components/tiles/tile";
import { useQuery } from "~/logic/lib/useQuery";
const sortGroupsAlph = (a: Association, b: Association) =>
alphabeticalOrder(a.metadata.title, b.metadata.title);
@ -25,7 +36,7 @@ const getGraphUnreads = (associations: Associations) => {
return (path: string) =>
f.flow(
f.pickBy((a: Association) => a.group === path),
f.map('resource'),
f.map("resource"),
f.map(selUnread),
f.reduce(f.add, 0)
)(associations.graph);
@ -37,18 +48,18 @@ const getGraphNotifications = (
) => (path: string) =>
f.flow(
f.pickBy((a: Association) => a.group === path),
f.map('resource'),
f.map(rid => getNotificationCount(unreads, rid)),
f.map("resource"),
f.map((rid) => getNotificationCount(unreads, rid)),
f.reduce(f.add, 0)
)(associations.graph);
export default function Groups(props: Parameters<typeof Box>[0]) {
const unreads = useHarkState(state => state.unreads);
const groupState = useGroupState(state => state.groups);
const associations = useMetadataState(state => state.associations);
const unreads = useHarkState((state) => state.unreads);
const groupState = useGroupState((state) => state.groups);
const associations = useMetadataState((state) => state.associations);
const groups = Object.values(associations?.groups || {})
.filter(e => e?.group in groupState)
.filter((e) => e?.group in groupState)
.sort(sortGroupsAlph);
const graphUnreads = getGraphUnreads(associations || ({} as Associations));
const graphNotifications = getGraphNotifications(
@ -56,6 +67,22 @@ export default function Groups(props: Parameters<typeof Box>[0]) {
unreads
);
const joining = useGroupState((s) =>
_.omit(
_.pickBy(s.pendingJoin || {}, req => req.app === 'groups' && req.progress != 'abort'),
groups.map((g) => g.group)
)
);
const invites = useInviteState(
(s) =>
Object.values(s.invites?.["groups"] || {}).map((inv) =>
resourceAsPath(inv.resource)
) || []
);
const pending = _.union(invites, Object.keys(joining)).filter(group => {
return !(group in (groupState?.groups || {})) && !(group in (associations.groups || {}))
});
return (
<>
{groups.map((group, index) => {
@ -73,10 +100,60 @@ export default function Groups(props: Parameters<typeof Box>[0]) {
/>
);
})}
{pending.map((group, idx) => (
<PendingGroup
key={group}
path={group}
first={idx === 0 && groups.length === 0}
/>
))}
</>
);
}
interface PendingGroupProps {
path: string;
first?: boolean;
}
function PendingGroup(props: PendingGroupProps) {
const { path, first } = props;
const history = useHistory();
const { preview, error } = usePreview(path);
const title = preview?.metadata?.title || path;
const { toQuery } = useQuery();
const onClick = () => {
const { ship, name } = resourceFromPath(path);
history.push(toQuery({ "join-kind": "groups", "join-path": path }));
};
const joining = useGroupState((s) => s.pendingJoin[path]?.progress);
return (
<Tile gridColumnStart={first ? 1 : undefined}>
<Col
onClick={onClick}
width="100%"
height="100%"
justifyContent="space-between"
>
<Box>
<Text gray>{title}</Text>
</Box>
<Box>
{!joining ? (
<Text color="blue">Invited</Text>
) : joining !== "done" ? (
<Text gray>Joining...</Text>
) : (
<Text color="blue">Recently joined</Text>
)}
</Box>
</Col>
</Tile>
);
}
interface GroupProps {
path: string;
title: string;
@ -87,6 +164,7 @@ interface GroupProps {
function Group(props: GroupProps) {
const { path, title, unreads, updates, first = false } = props;
const { hideUnreads } = useSettingsState(selectCalmState);
const request = useGroupState((s) => s.pendingJoin[path]);
return (
<Tile
position="relative"
@ -97,9 +175,10 @@ function Group(props: GroupProps) {
<Text>{title}</Text>
{!hideUnreads && (
<Col>
{!!request ? <Text color="blue">New group</Text> : null}
{updates > 0 && (
<Text mt={1} color="blue">
{updates} update{updates !== 1 && 's'}{' '}
{updates} update{updates !== 1 && "s"}{" "}
</Text>
)}
{unreads > 0 && <Text color="lightGray">{unreads}</Text>}

View File

@ -108,7 +108,6 @@ function useInviteAccept(resource: string, app?: string, uid?: string) {
return false;
}
await airlock.poke(join(ship, name));
await waiter((p) => {
return (
(resource in p.groups &&

View File

@ -1,5 +1,5 @@
import { Box, Row, SegmentedProgressBar, Text } from '@tlon/indigo-react';
import { joinError, joinProgress, JoinRequest, hideGroup } from '@urbit/api';
import { joinError, joinProgress, JoinRequest } from '@urbit/api';
import React, { useCallback } from 'react';
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
import airlock from '~/logic/api';
@ -24,10 +24,8 @@ export function JoiningStatus(props: JoiningStatusProps) {
const desc = description?.[current] || '';
const isError = joinError.indexOf(status.progress as any) !== -1;
const onHide = useCallback(
async () => {
await airlock.poke(hideGroup(resource));
},
[resource]
async () => { },
[]
);
return (
<Row

View File

@ -115,7 +115,12 @@ function GraphPermalink(
})();
}, [pending, graph, index]);
const showTransclusion = Boolean(association && node && transcluded < 1);
const permalink = getPermalinkForGraph(group, graph, index);
const permalink = (() => {
const link = `/perma${getPermalinkForGraph(group, graph, index).slice(16)}`;
return (!association && !loading)
? { search: `?join-kind=group&join-path=${encodeURIComponent(group)}&redir=${encodeURIComponent(link)}` }
: link
})();
const [nodeGroupHost, nodeGroupName] = association?.group.split('/').slice(-2) ?? ['Unknown', 'Unknown'];
const [nodeChannelHost, nodeChannelName] = association?.resource
@ -140,7 +145,7 @@ function GraphPermalink(
return (
<Col
as={Link}
to={`/perma${permalink.slice(16)}`}
to={permalink}
width="100%"
bg="white"
maxWidth={full ? null : '500px'}

View File

@ -7,7 +7,6 @@ import {
import { Form } from 'formik';
import React, { useCallback } from 'react';
import * as Yup from 'yup';
import { uxToHex } from '~/logic/lib/util';
import useSettingsState, { SettingsState } from '~/logic/state/settings';
import { FormikOnBlur } from '~/views/components/FormikOnBlur';
import { BackButton } from './BackButton';
@ -54,18 +53,28 @@ export default function DisplayForm() {
const onSubmit = useCallback(async (values) => {
const { putEntry } = useSettingsState.getState();
putEntry('display', 'backgroundType', values.bgType);
putEntry(
'display',
'background',
values.bgType === 'color'
? `#${uxToHex(values.bgColor || '0x0')}`
: values.bgType === 'url'
? values.bgUrl || ''
: false
);
putEntry('display', 'theme', values.theme);
}, []);
const { bgType, bgColor, bgUrl, theme } = initialValues;
if (bgType !== values.bgType) {
putEntry('display', 'backgroundType', values.bgType);
}
if (bgColor !== values.bgColor || bgUrl !== values.bgUrl) {
putEntry(
'display',
'background',
values.bgType === 'color'
? values.bgColor
: values.bgType === 'url'
? values.bgUrl || ''
: ''
);
}
if (theme !== values.theme) {
putEntry('display', 'theme', values.theme);
}
}, [initialValues]);
return (
<FormikOnBlur

View File

@ -21,6 +21,7 @@ export interface AuthorProps {
lineHeight?: string | number;
isRelativeTime?: boolean;
dontShowTime?: boolean;
gray?: boolean;
}
// eslint-disable-next-line max-lines-per-function
@ -35,6 +36,7 @@ function Author(props: AuthorProps & PropFunc<typeof Box>): ReactElement {
isRelativeTime,
dontShowTime,
lineHeight = 'tall',
gray = false,
...rest
} = props;
@ -88,7 +90,7 @@ function Author(props: AuthorProps & PropFunc<typeof Box>): ReactElement {
<Box display='flex' alignItems='baseline'>
<Text
ml={showImage ? 2 : 0}
color='black'
color={gray ? 'gray': 'black'}
fontSize='1'
cursor='pointer'
lineHeight={lineHeight}

View File

@ -1,15 +1,14 @@
import _ from 'lodash';
import {
Box, Col,
ErrorLabel, Label,
Row,
StatelessTextInput as Input
} from '@tlon/indigo-react';
import { useField } from 'formik';
import React, { FormEvent, useState, useEffect } from 'react';
import { hexToUx } from '~/logic/lib/util';
import { uxToHex } from '@urbit/api';
import React, { useState, useEffect, ChangeEvent, useMemo } from 'react';
import { uxToHex, hexToUx } from '@urbit/api';
import styled from 'styled-components';
export type ColorInputProps = Parameters<typeof Col>[0] & {
id: string;
@ -20,37 +19,57 @@ export type ColorInputProps = Parameters<typeof Col>[0] & {
const COLOR_REGEX = /^(\d|[a-f]|[A-F]){6}$/;
function padHex(hex: string) {
if(hex.length === 0) {
return '000000';
}
const repeat = 6 / hex.length;
if(Math.floor(repeat) === repeat) {
return hex.repeat(repeat);
}
if(hex.length < 6) {
return hex.slice(0,3).repeat(2);
}
return hex.slice(0,6);
function isValidHex(color: string): boolean {
return COLOR_REGEX.test(color);
}
function parseIncomingColor(value: string): string {
if (!value)
return '';
const isUx = value.startsWith('0x');
return isUx ? uxToHex(value) : value.replace('#', '');
}
const ClickInput = styled(Input)`
cursor: pointer;
`;
export function ColorInput(props: ColorInputProps) {
const { id, placeholder, label, caption, disabled, ...rest } = props;
const [{ value, onBlur }, meta, { setValue, setTouched }] = useField(id);
const [field, setField] = useState(uxToHex(value));
const [{ value }, meta, { setValue, setTouched }] = useField(id);
const [field, setField] = useState(parseIncomingColor(value));
useEffect(() => {
const newValue = hexToUx(padHex(field));
const update = (value: string) => {
const normalizedValue = value.trim().replace(/[^a-f\d]/gi, '').slice(0,6);
setField(normalizedValue);
};
const onText = (e: ChangeEvent<HTMLInputElement>) => update(e.target.value);
const pickerChange = useMemo(() => _.debounce(update, 300), []);
const updateField = useMemo(() => _.debounce((field: string) => {
const newValue = hexToUx(field);
setValue(newValue);
setTouched(true);
}, 100), []);
useEffect(() => {
if (isValidHex(field)) {
updateField(field);
}
}, [field]);
const onChange = (e: FormEvent<HTMLInputElement>) => {
const { value: newValue } = e.target as HTMLInputElement;
setField(newValue.slice(1));
};
const hex = uxToHex(value);
const isValid = COLOR_REGEX.test(hex);
useEffect(() => {
const parsedColor = parseIncomingColor(value);
if (parsedColor !== field) {
update(parsedColor);
}
}, [value]);
const isValid = isValidHex(field);
return (
<Box display='flex' flexDirection='column' {...rest}>
@ -60,13 +79,13 @@ export function ColorInput(props: ColorInputProps) {
{caption}
</Label>
) : null}
<Row mt={2} alignItems='flex-end'>
<Row mt={2} alignItems='flex-end' maxWidth="120px">
<Input
id={id}
borderTopRightRadius={0}
borderBottomRightRadius={0}
onBlur={onBlur}
onChange={onChange}
onBlur={onText}
onChange={onText}
value={field}
disabled={disabled || false}
borderRight={0}
@ -76,21 +95,21 @@ export function ColorInput(props: ColorInputProps) {
borderBottomRightRadius={1}
borderTopRightRadius={1}
border={1}
borderLeft={0}
borderColor='lightGray'
width='32px'
alignSelf='stretch'
bg={isValid ? `#${hex}` : 'transparent'}
bg={isValid ? `#${field}` : 'transparent'}
>
<Input
<ClickInput
width='100%'
height='100%'
alignSelf='stretch'
disabled={disabled || false}
type='color'
value={`#${isValid ? field : uxToHex(value)}`}
opacity={0}
overflow='hidden'
onChange={onChange}
onChange={(e: ChangeEvent<HTMLInputElement>) => pickerChange(e.target.value)}
/>
</Box>
</Row>

View File

@ -1,10 +1,8 @@
import { Box, Col, Icon, Row, Text } from '@tlon/indigo-react';
import React, { ReactElement, useCallback } from 'react';
import { Link } from 'react-router-dom';
import { useModal } from '~/logic/lib/useModal';
import useMetadataState, { usePreview } from '~/logic/state/metadata';
import { PropFunc } from '~/types';
import { JoinGroup } from '../landscape/components/JoinGroup';
import { MetadataIcon } from '../landscape/components/MetadataIcon';
type GroupLinkProps = {
@ -22,61 +20,44 @@ export function GroupLink({
useCallback(s => resource in s.associations.groups, [resource])
);
const { modal, showModal } = useModal({
modal: <JoinGroup autojoin={name} />
});
const { preview } = usePreview(resource);
const { preview } = usePreview(resource);
return (
<>
{modal}
<Row
{...rest}
as={Link}
to={joined ? `/~landscape/ship/${name}` : `/perma/group/${name}`}
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
e.stopPropagation();
if (e.metaKey || e.ctrlKey) {
return;
}
e.preventDefault();
showModal();
}}
flexShrink={1}
alignItems="center"
width="100%"
maxWidth="500px"
py={2}
pr={2}
cursor='pointer'
backgroundColor='white'
borderColor={borderColor}
opacity={preview ? '1' : '0.6'}
>
<MetadataIcon height={6} width={6} metadata={preview ? preview.metadata : { color: '0x0' , picture: '' }} />
<Col>
<Text ml={2} fontWeight="medium" mono={!preview}>
{preview ? preview.metadata.title : name}
</Text>
<Box pt='1' ml='2' display='flex' alignItems='center'>
{preview ?
<>
<Box display='flex' alignItems='center'>
<Icon icon='Users' color='gray' mr='1' />
<Text fontSize='0'color='gray' >
{preview.members}
{' '}
{preview.members > 1 ? 'peers' : 'peer'}
</Text>
</Box>
</>
: <Text fontSize='0'>Fetching member count</Text>}
</Box>
</Col>
</Row>
</>
<Row
{...rest}
as={Link}
to={joined ? `/~landscape/ship/${name}` : { search: `?join-kind=groups&join-path=/ship/${name}`}}
flexShrink={1}
alignItems="center"
width="100%"
maxWidth="500px"
py={2}
pr={2}
cursor='pointer'
backgroundColor='white'
borderColor={borderColor}
opacity={preview ? '1' : '0.6'}
>
<MetadataIcon height={6} width={6} metadata={preview ? preview.metadata : { color: '0x0' , picture: '' }} />
<Col>
<Text ml={2} fontWeight="medium" mono={!preview}>
{preview ? preview.metadata.title : name}
</Text>
<Box pt='1' ml='2' display='flex' alignItems='center'>
{preview ?
<>
<Box display='flex' alignItems='center'>
<Icon icon='Users' color='gray' mr='1' />
<Text fontSize='0'color='gray' >
{preview.members}
{' '}
{preview.members > 1 ? 'peers' : 'peer'}
</Text>
</Box>
</>
: <Text fontSize='0'>Fetching member count</Text>}
</Box>
</Col>
</Row>
);
}

View File

@ -3,9 +3,7 @@ import { Box, Icon, LoadingSpinner, Row, Text } from '@tlon/indigo-react';
import {
accept,
decline,
hideGroup,
Invite,
join,
joinProgress,
joinResult,
JoinRequest,
@ -170,7 +168,6 @@ export function useInviteAccept(resource: string, app?: string, uid?: string) {
return false;
}
await airlock.poke(join(ship, name));
await airlock.poke(accept(app, uid));
await waiter((p) => {
return (
@ -218,9 +215,6 @@ function InviteActions(props: {
await airlock.poke(decline(app, uid));
}, [app, uid]);
const hideJoin = useCallback(async () => {
await airlock.poke(hideGroup(resource));
}, [resource]);
if (status) {
return (
@ -228,7 +222,7 @@ function InviteActions(props: {
<StatelessAsyncButton
height={4}
backgroundColor="white"
onClick={hideJoin}
onClick={async () => {}}
>
{[...joinResult].includes(status?.progress as any)
? 'Dismiss'
@ -289,7 +283,6 @@ export function GroupInvite(props: GroupInviteProps): ReactElement {
if (status?.progress === 'done') {
const redir = inviteUrl(app !== 'groups', resource, graphAssoc?.metadata);
if (redir) {
airlock.poke(hideGroup(resource));
history.push(redir);
}
}

View File

@ -18,7 +18,6 @@ import { Dropdown } from './Dropdown';
import { ProfileStatus } from './ProfileStatus';
import ReconnectButton from './ReconnectButton';
import { StatusBarItem } from './StatusBarItem';
import { StatusBarJoins } from './StatusBarJoins';
import useHarkState from '~/logic/state/hark';
const localSel = selectLocalState(['toggleOmnibox']);
@ -83,7 +82,6 @@ const StatusBar = (props) => {
</Box>
)}
</StatusBarItem>
<StatusBarJoins />
<ReconnectButton />
</Row>
<Row justifyContent='flex-end'>

View File

@ -1,119 +0,0 @@
import { LoadingSpinner, Button } from '@tlon/indigo-react';
import React from 'react';
import { Box, Row, Col, Text } from '@tlon/indigo-react';
import { PropFunc } from '~/types';
import _ from 'lodash';
import { StatusBarItem } from './StatusBarItem';
import useGroupState from '~/logic/state/group';
import { JoinRequest, joinProgress } from '@urbit/api';
import { usePreview } from '~/logic/state/metadata';
import { Dropdown } from './Dropdown';
import { MetadataIcon } from '../landscape/components/MetadataIcon';
function Elbow(
props: { size?: number; color?: string } & PropFunc<typeof Box>
) {
const { size = 12, color = 'lightGray', ...rest } = props;
return (
<Box
{...rest}
overflow="hidden"
width={size}
height={size}
position="relative"
>
<Box
border="2px solid"
borderRadius={3}
borderColor={color}
position="absolute"
left="0px"
bottom="0px"
width={size * 2}
height={size * 2}
/>
</Box>
);
}
export function StatusBarJoins() {
const pendingJoin = useGroupState(s => s.pendingJoin);
if (
Object.keys(_.omitBy(pendingJoin, j => j.hidden || j.progress === 'done'))
.length === 0
) {
return null;
}
return (
<Dropdown
dropWidth="325px"
options={
<Col
left="0px"
top="120%"
position="absolute"
zIndex={10}
alignItems="flex-start"
p="2"
gapY="3"
border="1"
borderColor="lightGray"
borderRadius="1"
backgroundColor="white"
>
{Object.keys(pendingJoin).map(g => (
<JoinStatus key={g} group={g} join={pendingJoin[g]} />
))}
</Col>
}
alignX="left"
alignY="bottom"
>
<StatusBarItem mr="2" width="32px" flexShrink={0} border={0}>
<LoadingSpinner foreground="black" />
</StatusBarItem>
</Dropdown>
);
}
const description: string[] = [
'Contacting host...',
'Retrieving data...',
'Finished join',
'Unable to join, you do not have the correct permissions',
'Internal error, please file an issue'
];
export function JoinStatus({
group,
join
}: {
group: string;
join: JoinRequest;
}) {
const { preview } = usePreview(group);
const current = join && joinProgress.indexOf(join.progress);
const desc = _.isNumber(current) && description[current];
const onHide = () => {
useGroupState.getState().hidePending(group);
};
return (
<Row alignItems="center" gapX="3">
<Col gapY="2">
<Row alignItems="center" gapX="2">
{preview ? (
<MetadataIcon height={4} width={4} metadata={preview.metadata} />
) : null}
<Text>{preview?.metadata.title || group.slice(6)}</Text>
</Row>
<Row ml="2" alignItems="center" gapX="2">
<Elbow />
<Text>{desc}</Text>
</Row>
</Col>
<Button onClick={onHide}>Hide</Button>
</Row>
);
}

View File

@ -167,8 +167,15 @@ export function Omnibox(props: OmniboxProps): ReactElement {
if(shift && app === 'profile') {
// TODO: hacky, fix
link = link.replace('~profile', '~landscape/messages/dm');
}
if(link.startsWith('?')) {
history.push({
search: link
});
} else {
history.push(link);
}
history.push(link);
} else {
window.location.href = link;
}

View File

@ -47,7 +47,7 @@ export function ChannelDetails(props: ChannelDetailsProps) {
<Formik initialValues={initialValues} onSubmit={onSubmit}>
<Form style={{ display: 'contents' }}>
<FormGroupChild id="details" />
<Col mx={4} mb={4} flexShrink={0} gapY={4}>
<Col mx={4} mb={4} flexShrink={0} gapY={4} maxWidth="512px">
<Col mb={3}>
<Text id="details" fontSize={2} fontWeight="bold">
Channel Details

View File

@ -15,6 +15,8 @@ import { useShortcut } from '~/logic/state/settings';
import Landscape from '~/views/landscape/index';
import GraphApp from '../../apps/graph/App';
import { getNotificationRedirect } from '~/logic/lib/notificationRedirects';
import {JoinRoute} from './Join/Join';
import useInviteState from '~/logic/state/invite';
export const Container = styled(Box)`
flex-grow: 1;
@ -27,16 +29,20 @@ export const Content = (props) => {
const history = useHistory();
const location = useLocation();
const mdLoaded = useMetadataState(s => s.loaded);
const inviteLoaded = useInviteState(s => s.loaded);
useEffect(() => {
const query = new URLSearchParams(location.search);
if(mdLoaded && query.has('grid-note')) {
if(!(mdLoaded && inviteLoaded)) {
return;
}
if(query.has('grid-note')) {
history.push(getNotificationRedirect(query.get('grid-note')!));
} else if(mdLoaded && query.has('grid-link')) {
} else if(query.has('grid-link')) {
const link = decodeURIComponent(query.get('grid-link')!);
history.push(`/perma${link}`);
}
}, [location.search, mdLoaded]);
}, [location.search, mdLoaded, inviteLoaded]);
useShortcut('navForward', useCallback((e) => {
e.preventDefault();
@ -68,11 +74,11 @@ export const Content = (props) => {
return (
<Container>
<JoinRoute />
<Switch>
<Route
exact
path={['/', '/invites/:app/:uid']}
render={p => (
path="/" render={p => (
<LaunchApp
location={p.location}
match={p.match}

View File

@ -1,9 +1,9 @@
import { Col, Row, Text, Icon } from '@tlon/indigo-react';
import { Metadata } from '@urbit/api';
import React, { ReactElement, ReactNode } from 'react';
import { PropFunc, IconRef } from '~/types';
import { MetadataIcon } from './MetadataIcon';
import { useCopy } from '~/logic/lib/useCopy';
import { Col, Row, Text, Icon } from "@tlon/indigo-react";
import { Metadata } from "@urbit/api";
import React, { ReactElement, ReactNode } from "react";
import { PropFunc, IconRef } from "~/types";
import { MetadataIcon } from "./MetadataIcon";
import { useCopy } from "~/logic/lib/useCopy";
interface GroupSummaryProps {
metadata: Metadata;
memberCount: number;
@ -28,11 +28,12 @@ export function GroupSummary(
} = props;
const { doCopy, copyDisplay } = useCopy(
`web+urbitgraph://group${resource?.slice(5)}`,
'Copy',
'Checkmark'
"Copy",
"Checkmark"
);
return (
<Col {...rest} gapY={4} maxWidth={['100%', '288px']}>
<Col gapY={4} maxWidth={["100%", "288px"]} {...rest}>
<Row gapX={2} width="100%">
<MetadataIcon
width="40px"
@ -53,9 +54,9 @@ export function GroupSummary(
{props?.AllowCopy && (
<Icon
color="gray"
icon={props?.locked ? 'Locked' : (copyDisplay as IconRef)}
icon={props?.locked ? "Locked" : (copyDisplay as IconRef)}
onClick={!props?.locked ? doCopy : null}
cursor={props?.locked ? 'default' : 'pointer'}
cursor={props?.locked ? "default" : "pointer"}
/>
)}
</Row>
@ -69,8 +70,8 @@ export function GroupSummary(
</Row>
</Col>
</Row>
<Row width="100%">
{metadata.description && (
{metadata.description.length > 0 && (
<Row width="100%">
<Text
gray
width="100%"
@ -80,8 +81,8 @@ export function GroupSummary(
>
{metadata.description}
</Text>
)}
</Row>
</Row>
)}
{children}
</Col>
);

View File

@ -2,6 +2,7 @@ import { readGroup } from '@urbit/api';
import _ from 'lodash';
import React, { useCallback, useEffect } from 'react';
import Helmet from 'react-helmet';
import { Box } from '@tlon/indigo-react';
import {
Route,
RouteComponentProps, Switch
@ -25,7 +26,7 @@ import { NewChannel } from './NewChannel';
import { PopoverRoutes } from './PopoverRoutes';
import { Resource } from './Resource';
import { Skeleton } from './Skeleton';
import airlock from '~/logic/api';
import {Join, JoinRoute} from './Join/Join';
interface GroupsPaneProps {
baseUrl: string;
@ -59,6 +60,13 @@ export function GroupsPane(props: GroupsPaneProps) {
if (workspace.type !== 'group') {
return;
}
const { pendingJoin, doneJoin } = useGroupState.getState();
const group = getGroupFromWorkspace(workspace)!;
if(group in pendingJoin) {
doneJoin(group);
}
return () => {
setRecentGroups(gs => _.uniq([workspace.group, ...gs]));
};
@ -175,7 +183,31 @@ export function GroupsPane(props: GroupsPaneProps) {
</>
);
}}
/>
/>
<Route
path={relativePath('/pending/:ship/:name')}
render={(routeProps) => {
const { ship, name } = routeProps.match.params as Record<string, string>;
const desc = {
group: `/ship/${ship}/${name}`,
kind: 'graph' as const
};
return (<Skeleton
mobileHide
recentGroups={recentGroups}
{...props}
baseUrl={baseUrl}
>
<Box width="100%">
<Join desc={desc} />
</Box>
</Skeleton>
)
}}
>
</Route>
<Route
path={relativePath('/new')}
render={(routeProps) => {

View File

@ -0,0 +1,377 @@
import {
Col,
Row,
Text,
Box,
Button,
ManagedTextInputField,
ManagedCheckboxField,
ContinuousProgressBar,
} from "@tlon/indigo-react";
import { Formik, Form } from "formik";
import React, { useEffect } from "react";
import { useHistory, useLocation, useParams } from "react-router-dom";
import useGroupState from "~/logic/state/group";
import useInviteState, { useInviteForResource } from "~/logic/state/invite";
import useMetadataState, { usePreview } from "~/logic/state/metadata";
import { decline, Invite } from "@urbit/api";
import { join, JoinRequest } from "@urbit/api/groups";
import airlock from "~/logic/api";
import { joinError, joinResult, joinLoad, JoinProgress } from "@urbit/api";
import { useQuery } from "~/logic/lib/useQuery";
import { JoinKind, JoinDesc, JoinSkeleton } from "./Skeleton";
interface InviteWithUid extends Invite {
uid: string;
}
interface FormSchema {
autojoin: boolean;
shareContact: boolean;
}
const initialValues = {
autojoin: false,
shareContact: false,
};
function JoinForm(props: {
desc: JoinDesc;
dismiss: () => void;
invite?: InviteWithUid;
}) {
const { desc, dismiss, invite } = props;
const onSubmit = (values: FormSchema) => {
const [, , ship, name] = desc.group.split("/");
airlock.poke(
join(ship, name, desc.kind, values.autojoin, values.shareContact)
);
};
const onDecline = () => {
airlock.poke(decline(desc.kind, invite.uid));
dismiss();
};
const isGroups = desc.kind === "groups";
return (
<Formik initialValues={initialValues} onSubmit={onSubmit}>
<Form>
<Col p="4" gapY="4">
{isGroups ? (
<ManagedCheckboxField id="autojoin" label="Join all channels" />
) : null}
<ManagedCheckboxField id="shareContact" label="Share identity" />
<Row justifyContent="space-between" width="100%">
<Button onClick={dismiss}>Dismiss</Button>
<Row gapX="2">
{!invite ? null : (
<Button onClick={onDecline} destructive type="button">
Decline
</Button>
)}
<Button primary type="submit">
{!invite ? "Join Group" : "Accept"}
</Button>
</Row>
</Row>
</Col>
</Form>
</Formik>
);
}
const REQUEST: JoinDesc = {
group: "/ship/~bitbet-bolbel/urbit-community",
kind: "groups",
};
export function JoinInitial(props: {
invite?: InviteWithUid;
desc: JoinDesc;
modal: boolean;
dismiss: () => void;
}) {
const { desc, dismiss, modal, invite } = props;
const title = (() => {
const name = desc.kind === "graph" ? "Group Chat" : "Group";
if (invite) {
return `You've been invited to a ${name}`;
} else {
return `You're joining a ${name}`;
}
})();
return (
<JoinSkeleton modal={modal} desc={desc} title={title}>
<JoinForm invite={invite} dismiss={dismiss} desc={desc} />
</JoinSkeleton>
);
}
function JoinLoading(props: {
desc: JoinDesc;
modal: boolean;
request: JoinRequest;
dismiss: () => void;
finished: string;
}) {
const { desc, request, dismiss, modal, finished } = props;
const history = useHistory();
useEffect(() => {
if (desc.kind === "graph" && request.progress === "done") {
history.push(finished);
}
}, [request]);
const name = desc.kind === "graph" ? "Group Chat" : "Group";
const title = `Joining ${name}, please wait`;
const onCancel = () => {
useGroupState.getState().abortJoin(desc.group);
dismiss();
};
return (
<JoinSkeleton modal={modal} desc={desc} title={title}>
<Col maxWidth="512px" p="4" gapY="4">
{joinLoad.indexOf(request.progress as any) !== -1 ? (
<JoinProgressIndicator progress={request.progress} />
) : null}
<Box>
<Text>
If join seems to take a while, the host of the {name} may be
offline, or the connection between you both may be unstable.
</Text>
</Box>
<Row gapX="2">
<Button onClick={dismiss}>Dismiss</Button>
<Button destructive onClick={onCancel}>
Cancel Join
</Button>
</Row>
</Col>
</JoinSkeleton>
);
}
function JoinError(props: {
desc: JoinDesc;
request: JoinRequest;
modal: boolean;
}) {
const { desc, request, modal } = props;
const { preview } = usePreview(desc.group);
const group = preview?.metadata?.title ?? desc.group;
const title = `Joining ${group} failed`;
const explanation =
request.progress === "no-perms"
? "You do not have the correct permissions"
: "An unexpected error occurred";
return (
<JoinSkeleton modal={modal} title={title} desc={desc}>
<Col p="4" gapY="4">
<Text fontWeight="medium">{explanation}</Text>
<Row>
<Button>Dismiss</Button>
</Row>
</Col>
</JoinSkeleton>
);
}
export interface JoinProps {
desc: JoinDesc;
redir?: string;
modal?: boolean;
dismiss?: () => void;
}
export function Join(props: JoinProps) {
const { desc, modal, dismiss, redir } = props;
const { group, kind } = desc;
const [, , ship, name] = group.split("/");
const graph = kind === "graph";
const finishedPath = !!redir
? redir
: graph
? `/~landscape/messages/resource/chat/${ship}/${name}`
: `/~landscape/ship/${ship}/${name}`;
const history = useHistory();
const joinRequest = useGroupState((s) => s.pendingJoin[group]);
const invite = useInviteForResource(kind, ship, name);
const isDone = joinRequest && joinRequest.progress === "done";
const isErrored =
joinRequest && joinError.includes(joinRequest.progress as any);
const isLoading =
joinRequest && joinLoad.includes(joinRequest.progress as any);
useEffect(() => {
if (isDone && desc.kind == "graph") {
history.push(finishedPath);
}
}, [isDone, desc]);
return isDone ? (
<JoinDone
dismiss={dismiss}
modal={modal}
desc={desc}
finished={finishedPath}
/>
) : isLoading ? (
<JoinLoading
modal={modal}
dismiss={dismiss}
desc={desc}
request={joinRequest}
finished={finishedPath}
/>
) : isErrored ? (
<JoinError modal={modal} desc={desc} request={joinRequest} />
) : (
<JoinInitial modal={modal} dismiss={dismiss} desc={desc} invite={invite} />
);
}
interface PromptFormProps {
kind: string;
}
interface PromptFormSchema {
link: string;
}
export interface JoinPromptProps {
kind: string;
dismiss?: () => void;
}
export function JoinPrompt(props: JoinPromptProps) {
const { kind, dismiss } = props;
const { query, appendQuery } = useQuery();
const history = useHistory();
const initialValues = {
link: "",
};
const onSubmit = async ({ link }: PromptFormSchema) => {
const path = `/ship/${link}`;
history.push({
search: appendQuery({ "join-path": path }),
});
};
return (
<JoinSkeleton modal body={<Text>a</Text>} title="Join a Group">
<Formik initialValues={initialValues} onSubmit={onSubmit}>
<Form>
<Col p="4" gapY="4">
<ManagedTextInputField
label="Invite Link"
id="link"
caption="Enter either a web+urbitgraph:// link or an identifier in the form ~sampel-palnet/group"
/>
<Row gapX="2">
{!!dismiss ? (
<Button type="button" onClick={dismiss}>
Dismiss
</Button>
) : null}
<Button type="submit" primary>
Join
</Button>
</Row>
</Col>
</Form>
</Formik>
</JoinSkeleton>
);
}
function JoinProgressIndicator(props: { progress: JoinProgress }) {
const { progress } = props;
const percentage =
progress === "done" ? 100 : (joinLoad.indexOf(progress as any) + 1) * 25;
const description = (() => {
switch (progress) {
case "start":
return "Connecting to host";
case "added":
return "Retrieving members";
case "metadata":
return "Retrieving channels";
case "done":
return "Finished";
default:
return "";
}
})();
return (
<Col gapY="2">
<Text color="lightGray">{description}</Text>
<ContinuousProgressBar percentage={percentage} />
</Col>
);
}
export interface JoinDoneProps {
desc: JoinDesc;
modal: boolean;
finished: string;
dismiss: () => void;
}
export function JoinDone(props: JoinDoneProps) {
const { desc, modal, finished, dismiss } = props;
const { preview, error } = usePreview(desc.group);
const name = desc.kind === "groups" ? "Group" : "Group Chat";
const title = `Joined ${name} successfully`;
const history = useHistory();
const onView = () => {
history.push(finished);
};
return (
<JoinSkeleton title={title} modal={modal} desc={desc}>
<Col p="4" gapY="4">
<JoinProgressIndicator progress="done" />
<Row gapX="2">
<Button onClick={dismiss}>Dismiss</Button>
<Button onClick={onView} primary>
View Group
</Button>
</Row>
</Col>
</JoinSkeleton>
);
}
export function JoinRoute(props: { graph?: boolean; modal?: boolean }) {
const { modal = false, graph = false } = props;
const { query } = useQuery();
const history = useHistory();
const { pathname } = useLocation();
const kind = query.get("join-kind");
const path = query.get("join-path");
const redir = query.get("redir");
if (!kind) {
return null;
}
const desc: JoinDesc = path
? {
group: path,
kind: graph ? "graph" : "groups",
}
: undefined;
const dismiss = () => {
history.push(pathname);
};
return desc ? (
<Join desc={desc} modal dismiss={dismiss} redir={redir} />
) : (
<JoinPrompt kind={kind} dismiss={dismiss} />
);
}

View File

@ -0,0 +1,139 @@
import React, { useEffect, useState } from "react";
import {
Col,
Row,
Text,
Box,
Button,
ManagedTextInputField,
ManagedCheckboxField,
ContinuousProgressBar,
} from "@tlon/indigo-react";
import { ModalOverlay } from "~/views/components/ModalOverlay";
import Author from "~/views/components/Author";
import { GroupSummary } from "../GroupSummary";
import { resourceFromPath } from "~/logic/lib/group";
import useMetadataState, { usePreview } from "~/logic/state/metadata";
import useInviteState, { useInviteForResource } from "~/logic/state/invite";
import {useHistory} from "react-router-dom";
const SUMMARY_HEIGHT = "96px";
export type JoinKind = "graph" | "groups";
export interface JoinDesc {
group: string;
kind: JoinKind;
}
interface JoinSkeletonProps {
title: string;
desc?: JoinDesc;
modal: boolean;
children: JSX.Element;
onJoin?: () => void;
body?: JSX.Element;
}
export function JoinSkeleton(props: JoinSkeletonProps) {
const { title, body, children, onJoin, desc, modal } = props;
const history = useHistory();
const dismiss = () => {
history.push({ search: '' });
};
const inner = (
<Col
width={modal ? ["90vw", "384px"] : undefined}
borderRadius="2"
backgroundColor="white"
>
<Col
gapY="4"
p="4"
borderRadius="2"
backgroundColor="washedGray"
justifyContent="space-between"
flexGrow={1}
>
<Box maxWidth="512px">
<Text fontWeight="medium" fontSize="2">
{title}
</Text>
</Box>
{!!desc ? <JoinBody desc={desc} /> : null}
</Col>
{children}
</Col>
);
return modal ? (
<ModalOverlay dismiss={dismiss}>{inner}</ModalOverlay>
) : (
inner
);
}
export function JoinBody(props: { desc: JoinDesc }) {
const { desc } = props;
const { group, kind } = desc || {};
const { preview, error } = usePreview(group);
const { ship, name } = resourceFromPath(group);
const invite = useInviteForResource(kind, ship, name);
return (
<>
{!desc ? "Prompt invite link" : null}
{preview ? (
<GroupSummary
memberCount={preview.members}
channelCount={preview["channel-count"]}
metadata={preview.metadata}
height={SUMMARY_HEIGHT}
width="100%"
maxWidth="100%"
overflow="hidden"
/>
) : (
<FallbackSummary path={group} />
)}
{invite ? (
<Col gapY="2">
<Box>
<Text>
<Text mono>{invite.ship}</Text> <Text gray>invited you</Text>
</Text>
</Box>
{invite.text?.length > 0 ? (
<Box>
<Text>"{invite.text}"</Text>
</Box>
) : null}
</Col>
) : null}
</>
);
}
function FallbackSummary(props: { path: string }) {
const { path } = props;
const [, , ship, name] = path.split("/");
return (
<Row
height={SUMMARY_HEIGHT}
width="100%"
overflow="hidden"
alignItems="center"
gapX="0"
>
<Author gray fullNotIcon size={40} showImage ship={ship} dontShowTime />
<Text mono whiteSpace="nowrap" overflow="hidden" textOverflow="ellipsis">
/{name}
</Text>
</Row>
);
}

View File

@ -1,233 +0,0 @@
import {
Box, Col,
Icon,
ManagedTextInputField as Input, Row,
Text,
Button
} from '@tlon/indigo-react';
import { join, MetadataUpdatePreview } from '@urbit/api';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import _ from 'lodash';
import React, { ReactElement, useCallback, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import urbitOb from 'urbit-ob';
import * as Yup from 'yup';
import { useQuery } from '~/logic/lib/useQuery';
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
import { getModuleIcon } from '~/logic/lib/util';
import useGroupState from '~/logic/state/group';
import useMetadataState from '~/logic/state/metadata';
import { AsyncButton } from '~/views/components/AsyncButton';
import { FormError } from '~/views/components/FormError';
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
import { GroupSummary } from './GroupSummary';
import airlock from '~/logic/api';
const formSchema = Yup.object({
group: Yup.string()
.required('Must provide group to join')
.test('is-valid', 'Invalid group', (group: string | null | undefined) => {
if (!group) {
return false;
}
const [patp, name] = group.split('/');
return urbitOb.isValidPatp(patp) && name.length > 0;
})
});
interface FormSchema {
group: string;
}
interface JoinGroupProps {
autojoin?: string;
dismiss?: () => void;
}
function Autojoin(props: { autojoin: string | null }) {
const { submitForm } = useFormikContext();
useEffect(() => {
if (props.autojoin) {
submitForm();
}
}, []);
return null;
}
export function JoinGroup(props: JoinGroupProps): ReactElement {
const { autojoin, dismiss } = props;
const { associations, getPreview } = useMetadataState();
const [timedOut, setTimedOut] = useState(false);
const groups = useGroupState(state => state.groups);
const history = useHistory();
const initialValues: FormSchema = {
group: autojoin || ''
};
const [preview, setPreview] = useState<
MetadataUpdatePreview | string | null
>(null);
const waiter = useWaitForProps({ associations, groups }, _.isString(preview) ? 1 : 30000);
const { query } = useQuery();
const onConfirm = useCallback(async (group: string) => {
const [,,ship,name] = group.split('/');
if (group in groups) {
return history.push(`/~landscape${group}`);
}
await airlock.poke(join(ship, name));
try {
await waiter((p) => {
return group in p.groups &&
(group in (p.associations?.graph ?? {})
|| group in (p.associations?.groups ?? {}));
});
if(query.has('redir')) {
const redir = query.get('redir')!;
history.push(redir);
}
if(groups?.[group]?.hidden) {
const { metadata } = associations.graph[group];
if (metadata?.config && 'graph' in metadata.config) {
history.push(`/~landscape/home/resource/${metadata.config.graph}${group}`);
}
return;
} else {
history.push(`/~landscape${group}`);
}
} catch (e) {
setTimedOut(true);
console.error(e);
}
}, [waiter, history, associations, groups]);
const onSubmit = useCallback(
async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
const [ship, name] = values.group.split('/');
const path = `/ship/${ship}/${name}`;
if (path in groups) {
return history.push(`/~landscape${path}`);
}
// skip if it's unmanaged
try {
const prev = await getPreview(path);
actions.setStatus({ success: null });
setPreview(prev);
} catch (e) {
if (e === 'no-permissions') {
actions.setStatus({
error:
'Unable to join group, you do not have the correct permissions'
});
} else if (e === 'offline') {
setPreview(path);
} else {
actions.setStatus({ error: 'Unknown error' });
}
}
},
[waiter, history, onConfirm]
);
return (
<Col p={3}>
<Box mb={3}>
<Text fontSize={2} fontWeight="bold">
Join a Group
</Text>
</Box>
{ timedOut ? (
<Col width="100%" gapY={4}>
<Text>The host is not responding. You will receive a notification when the join requests succeeds
</Text>
<Button primary onClick={dismiss}>
Dismiss
</Button>
</Col>
) : _.isString(preview) ? (
<Col width="100%" gapY={4}>
<Text>The host appears to be offline. Join anyway?</Text>
<StatelessAsyncButton
primary
name="join"
onClick={() => onConfirm(preview)}
>
Join anyway
</StatelessAsyncButton>
</Col>
) : preview ? (
<>
<GroupSummary
metadata={preview.metadata}
memberCount={preview?.members}
channelCount={preview?.['channel-count']}
>
{ Object.keys(preview.channels).length > 0 && (
<Col
gapY={2}
p={2}
borderRadius={2}
border={1}
borderColor="washedGray"
bg="washedBlue"
maxHeight="300px"
overflowY="auto"
>
<Text gray fontSize={1}>
Channels
</Text>
<Box width="100%" flexShrink={0}>
{Object.values(preview.channels).map(({ metadata }: any, i) => (
<Row key={i} width="100%">
<Icon
mr={2}
color="blue"
icon={getModuleIcon(metadata?.config?.graph) as any}
/>
<Text color="blue">{metadata.title} </Text>
</Row>
))}
</Box>
</Col>
)}
</GroupSummary>
<StatelessAsyncButton
marginTop={3}
primary
name="join"
onClick={() => onConfirm(preview.group)}
>
Join {preview.metadata.title}
</StatelessAsyncButton>
</>
) : (
<Col width="100%" gapY={4}>
<Formik
validationSchema={formSchema}
initialValues={initialValues}
onSubmit={onSubmit}
>
<Form style={{ display: 'contents' }}>
<Autojoin autojoin={autojoin ?? null} />
<Input
id="group"
label="Group"
caption="What group are you joining?"
placeholder="~sampel-palnet/test-group"
/>
<AsyncButton mt={4}>Join Group</AsyncButton>
<FormError mt={4} />
</Form>
</Formik>
</Col>
)}
</Col>
);
}

View File

@ -13,11 +13,12 @@ import Dot from '~/views/components/Dot';
import { useHarkDm, useHarkStat } from '~/logic/state/hark';
import useSettingsState from '~/logic/state/settings';
import useGraphState from '~/logic/state/graph';
import {usePreview} from '~/logic/state/metadata';
function useAssociationStatus(resource: string) {
const [, , ship, name] = resource.split('/');
const [, , ship, name] = resource.split("/");
const graphKey = `${deSig(ship)}/${name}`;
const isSubscribed = useGraphState(s => s.graphKeys.has(graphKey));
const isSubscribed = useGraphState((s) => s.graphKeys.has(graphKey));
const stats = useHarkStat(`/graph/~${graphKey}`);
const { count, each } = stats;
const hasNotifications = false;
@ -43,6 +44,7 @@ function SidebarItemBase(props: {
title: string | ReactNode;
mono?: boolean;
pending?: boolean;
onClick?: () => void;
}) {
const {
title,
@ -53,22 +55,24 @@ function SidebarItemBase(props: {
hasUnread,
isSynced = false,
mono = false,
pending = false
pending = false,
onClick
} = props;
const color = isSynced
? hasUnread || hasNotification
? 'black'
: 'gray'
: 'lightGray';
? "black"
: "gray"
: "lightGray";
const fontWeight = hasUnread || hasNotification ? '500' : 'normal';
const fontWeight = hasUnread || hasNotification ? "500" : "normal";
return (
<HoverBoxLink
// ref={anchorRef}
to={to}
bg={pending ? 'lightBlue' : 'white'}
bgActive={pending ? 'washedBlue' : 'washedGray'}
onClick={onClick}
bg={pending ? "lightBlue" : "white"}
bgActive={pending ? "washedBlue" : "washedGray"}
width="100%"
display="flex"
justifyContent="space-between"
@ -108,7 +112,7 @@ function SidebarItemBase(props: {
mono={mono}
color={color}
fontWeight={fontWeight}
style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}
style={{ textOverflow: "ellipsis", whiteSpace: "pre" }}
>
{title}
</Text>
@ -118,156 +122,201 @@ function SidebarItemBase(props: {
);
}
export const SidebarDmItem = React.memo((props: {
ship: string;
selected?: boolean;
workspace: Workspace;
pending?: boolean;
}) => {
const { ship, selected = false, pending = false } = props;
const contact = useContact(ship);
const { hideAvatars, hideNicknames } = useSettingsState(s => s.calm);
const title =
!hideNicknames && contact?.nickname
? contact?.nickname
: cite(ship) ?? ship;
const { count, each } = useHarkDm(ship);
const unreads = count + each.length;
const img =
contact?.avatar && !hideAvatars ? (
<BaseImage
referrerPolicy="no-referrer"
src={contact.avatar}
width="16px"
height="16px"
borderRadius={2}
/>
) : (
<Sigil
ship={ship}
color={`#${uxToHex(contact?.color || '0x0')}`}
icon
padding={2}
size={16}
/>
);
return (
<SidebarItemBase
selected={selected}
hasNotification={false}
hasUnread={(unreads as number) > 0}
to={`/~landscape/messages/dm/${ship}`}
title={title}
mono={hideAvatars || !contact?.nickname}
isSynced
pending={pending}
>
{img}
</SidebarItemBase>
);
});
// eslint-disable-next-line max-lines-per-function
export const SidebarAssociationItem = React.memo((props: {
hideUnjoined: boolean;
association: Association;
export const SidebarPendingItem = (props: {
path: string;
selected: boolean;
workspace: Workspace;
}) => {
const { association, selected } = props;
const title = getItemTitle(association) || '';
const appName = association?.['app-name'];
let mod: string = appName;
if (association?.metadata?.config && 'graph' in association.metadata.config) {
mod = association.metadata.config.graph ;
}
const rid = association?.resource;
const groupPath = association?.group;
const group = useGroupState(state => state.groups[groupPath]);
const { hideNicknames } = useSettingsState(s => s.calm);
const contacts = useContactState(s => s.contacts);
const isUnmanaged = group?.hidden || false;
const DM = isUnmanaged && props.workspace?.type === 'messages';
const itemStatus = useAssociationStatus(rid);
const hasNotification = itemStatus === 'notification';
const hasUnread = itemStatus === 'unread';
const isSynced = itemStatus !== 'unsubscribed';
let baseUrl = `/~landscape${groupPath}`;
if (DM) {
baseUrl = '/~landscape/messages';
} else if (isUnmanaged) {
baseUrl = '/~landscape/home';
}
const to = isSynced
? `${baseUrl}/resource/${mod}${rid}`
: `${baseUrl}/join/${mod}${rid}`;
if (props.hideUnjoined && !isSynced) {
return null;
}
const participantNames = (str: string) => {
const color = isSynced
? hasUnread || hasNotification
? 'black'
: 'gray'
: 'lightGray';
if (_.includes(str, ',') && _.startsWith(str, '~')) {
const names = _.split(str, ', ');
return names.map((name, idx) => {
if (urbitOb.isValidPatp(name)) {
if (contacts[name]?.nickname && !hideNicknames)
return (
<Text key={name} bold={hasUnread} color={color}>
{contacts[name]?.nickname}
{idx + 1 != names.length ? ', ' : null}
</Text>
);
return (
<Text key={name} mono bold={hasUnread} color={color}>
{name}
<Text color={color}>{idx + 1 != names.length ? ', ' : null}</Text>
</Text>
);
} else {
return name;
}
});
} else {
return str;
}
};
const { path, selected } = props;
const { preview, error } = usePreview(path);
const color = `#${uxToHex(preview?.metadata?.color || "0x0")}`;
const title = preview?.metadata?.title || path;
const to = `/~landscape/messages/pending/${path.slice(6)}`;
return (
<SidebarItemBase
to={to}
title={title}
selected={selected}
hasUnread={hasUnread}
isSynced={isSynced}
title={
DM && !urbitOb.isValidPatp(title) ? participantNames(title) : title
}
hasNotification={hasNotification}
hasNotification={false}
hasUnread={false}
pending
>
{DM ? (
<Box
flexShrink={0}
height={16}
width={16}
borderRadius={2}
backgroundColor={
`#${uxToHex(props?.association?.metadata?.color)}` || '#000000'
}
/>
) : (
<Icon
display="block"
color={isSynced ? 'black' : 'lightGray'}
icon={getModuleIcon(mod as any)}
/>
)}
<Box
flexShrink={0}
height={16}
width={16}
borderRadius={2}
backgroundColor={color}
/>
</SidebarItemBase>
);
});
}
export const SidebarDmItem = React.memo(
(props: {
ship: string;
selected?: boolean;
workspace: Workspace;
pending?: boolean;
}) => {
const { ship, selected = false, pending = false } = props;
const contact = useContact(ship);
const { hideAvatars, hideNicknames } = useSettingsState((s) => s.calm);
const title =
!hideNicknames && contact?.nickname
? contact?.nickname
: cite(ship) ?? ship;
const { count, each } = useHarkDm(ship);
const unreads = count + each.length;
const img =
contact?.avatar && !hideAvatars ? (
<BaseImage
referrerPolicy="no-referrer"
src={contact.avatar}
width="16px"
height="16px"
borderRadius={2}
/>
) : (
<Sigil
ship={ship}
color={`#${uxToHex(contact?.color || "0x0")}`}
icon
padding={2}
size={16}
/>
);
return (
<SidebarItemBase
selected={selected}
hasNotification={false}
hasUnread={(unreads as number) > 0}
to={`/~landscape/messages/dm/${ship}`}
title={title}
mono={hideAvatars || !contact?.nickname}
isSynced
pending={pending}
>
{img}
</SidebarItemBase>
);
}
);
// eslint-disable-next-line max-lines-per-function
export const SidebarAssociationItem = React.memo(
(props: {
hideUnjoined: boolean;
association: Association;
selected: boolean;
workspace: Workspace;
}) => {
const { association, selected } = props;
const title = association ? getItemTitle(association) || "" : "";
const appName = association?.["app-name"];
let mod: string = appName;
if (
association?.metadata?.config &&
"graph" in association.metadata.config
) {
mod = association.metadata.config.graph;
}
const pending = useGroupState(s => association.group in s.pendingJoin);
console.log(pending);
const rid = association?.resource;
const { hideNicknames } = useSettingsState((s) => s.calm);
const contacts = useContactState((s) => s.contacts);
const group = useGroupState(s => association ? s.groups[association.group] : undefined);
const isUnmanaged = group?.hidden || false;
const DM = isUnmanaged && props.workspace?.type === "messages";
const itemStatus = useAssociationStatus(rid);
const hasNotification = itemStatus === "notification";
const hasUnread = itemStatus === "unread";
const isSynced = itemStatus !== "unsubscribed";
let baseUrl = `/~landscape${association.group}`;
if (DM) {
baseUrl = "/~landscape/messages";
} else if (isUnmanaged) {
baseUrl = "/~landscape/home";
}
const to = isSynced
? `${baseUrl}/resource/${mod}${rid}`
: `${baseUrl}/join/${mod}${rid}`;
const onClick = pending ? () => {
useGroupState.getState().doneJoin(rid);
} : undefined;
if (props.hideUnjoined && !isSynced) {
return null;
}
const participantNames = (str: string) => {
const color = isSynced
? hasUnread || hasNotification
? "black"
: "gray"
: "lightGray";
if (_.includes(str, ",") && _.startsWith(str, "~")) {
const names = _.split(str, ", ");
return names.map((name, idx) => {
if (urbitOb.isValidPatp(name)) {
if (contacts[name]?.nickname && !hideNicknames)
return (
<Text key={name} bold={hasUnread} color={color}>
{contacts[name]?.nickname}
{idx + 1 != names.length ? ", " : null}
</Text>
);
return (
<Text key={name} mono bold={hasUnread} color={color}>
{name}
<Text color={color}>
{idx + 1 != names.length ? ", " : null}
</Text>
</Text>
);
} else {
return name;
}
});
} else {
return str;
}
};
return (
<SidebarItemBase
to={to}
selected={selected}
hasUnread={hasUnread}
isSynced={isSynced}
title={
DM && !urbitOb.isValidPatp(title) ? participantNames(title) : title
}
hasNotification={hasNotification}
pending={pending}
onClick={onClick}
>
{DM ? (
<Box
flexShrink={0}
height={16}
width={16}
borderRadius={2}
backgroundColor={
`#${uxToHex(props?.association?.metadata?.color)}` || "#000000"
}
/>
) : (
<Icon
display="block"
color={isSynced ? "black" : "lightGray"}
icon={getModuleIcon(mod as any)}
/>
)}
</SidebarItemBase>
);
}
);

View File

@ -3,7 +3,7 @@ import { Associations, Graph, Unreads } from '@urbit/api';
import { patp, patp2dec } from 'urbit-ob';
import _ from 'lodash';
import { SidebarAssociationItem, SidebarDmItem } from './SidebarItem';
import { SidebarAssociationItem, SidebarDmItem, SidebarPendingItem } from './SidebarItem';
import useGraphState, { useInbox } from '~/logic/state/graph';
import useHarkState from '~/logic/state/hark';
import { alphabeticalOrder, getResourcePath, modulo } from '~/logic/lib/util';
@ -12,8 +12,10 @@ import { Workspace } from '~/types/workspace';
import useMetadataState from '~/logic/state/metadata';
import { useHistory } from 'react-router';
import { useShortcut } from '~/logic/state/settings';
import useGroupState from '~/logic/state/group';
import useInviteState from '~/logic/state/invite';
function sidebarSort(unreads: Unreads, pending: Set<string>): Record<SidebarSort, (a: string, b: string) => number> {
function sidebarSort(unreads: Unreads, pending: string[]): Record<SidebarSort, (a: string, b: string) => number> {
const { associations } = useMetadataState.getState();
const alphabetical = (a: string, b: string) => {
const aAssoc = associations[a];
@ -25,8 +27,8 @@ function sidebarSort(unreads: Unreads, pending: Set<string>): Record<SidebarSort
};
const lastUpdated = (a: string, b: string) => {
const aPend = pending.has(a.slice(1));
const bPend = pending.has(b.slice(1));
const aPend = pending.includes(a);
const bPend = pending.includes(b);
if(aPend && !bPend) {
return -1;
}
@ -50,7 +52,7 @@ function sidebarSort(unreads: Unreads, pending: Set<string>): Record<SidebarSort
};
}
function getItems(associations: Associations, workspace: Workspace, inbox: Graph, pending: Set<string>) {
function getItems(associations: Associations, workspace: Workspace, inbox: Graph, pending: string[]) {
const filtered = Object.keys(associations.graph).filter((a) => {
const assoc = associations.graph[a];
if(!('graph' in assoc.metadata.config)) {
@ -84,9 +86,9 @@ function getItems(associations: Associations, workspace: Workspace, inbox: Graph
: inbox.keys().map(x => patp(x.toString()));
const pend = workspace.type !== 'messages'
? []
: Array.from(pending).map(s => `~${s}`);
: pending
return [...filtered, ..._.union(direct, pend)];
return _.union(direct, pend, filtered);
}
export function SidebarList(props: {
@ -98,9 +100,18 @@ export function SidebarList(props: {
}): ReactElement {
const { selected, config, workspace } = props;
const associations = useMetadataState(state => state.associations);
const groups = useGroupState(s => s.groups);
const inbox = useInbox();
const graphKeys = useGraphState(s => s.graphKeys);
const pending = useGraphState(s => s.pendingDms);
const pendingDms = useGraphState(s => [...s.pendingDms].map(s => `~${s}`));
const pendingGroupChats = useGroupState(s => _.pickBy(s.pendingJoin, (req, rid) => !(rid in groups) && req.app === 'graph'));
const inviteGroupChats = useInviteState(
s => Object.values(s.invites?.['graph'] || {})
.map(inv => {
return `/ship/~${inv.resource.ship}/${inv.resource.name}`
}).filter(group => !(group in groups))
);
const pending = [...pendingDms, ...Object.keys(pendingGroupChats), ...inviteGroupChats];
const unreads = useHarkState(s => s.unreads);
const ordered = getItems(associations, workspace, inbox, pending)
@ -118,10 +129,16 @@ export function SidebarList(props: {
if(newChannel.startsWith('~')) {
path = `/~landscape/messages/dm/${newChannel}`;
} else {
const { metadata, resource } = associations.graph[ordered[newIdx]];
const joined = graphKeys.has(resource.slice(7));
if ('graph' in metadata.config) {
path = getResourcePath(workspace, resource, joined, metadata.config.graph);
const association = associations.graph[ordered[newIdx]];
if(!association) {
path = `/~landscape/messages`
return;
} else {
const { metadata, resource } = association;
const joined = graphKeys.has(resource.slice(7));
if ('graph' in metadata.config) {
path = getResourcePath(workspace, resource, joined, metadata.config.graph);
}
}
}
history.push(path);
@ -140,7 +157,22 @@ export function SidebarList(props: {
return (
<>
{ordered.map((pathOrShip) => {
return pathOrShip.startsWith('/') ? (
return pathOrShip.startsWith('~') ? (
<SidebarDmItem
key={pathOrShip}
ship={pathOrShip}
workspace={workspace}
selected={pathOrShip === selected}
pending={pending.includes(pathOrShip)}
/>
) : pending.includes(pathOrShip) ? (
<SidebarPendingItem
key={pathOrShip}
path={pathOrShip}
selected={pathOrShip === selected}
/>
) : (
<SidebarAssociationItem
key={pathOrShip}
selected={pathOrShip === selected}
@ -148,16 +180,7 @@ export function SidebarList(props: {
hideUnjoined={config.hideUnjoined}
workspace={workspace}
/>
) : (
<SidebarDmItem
key={pathOrShip}
ship={pathOrShip}
workspace={workspace}
selected={pathOrShip === selected}
pending={pending.has(pathOrShip.slice(1))}
/>
);
) ;
})}
</>
);

View File

@ -7,7 +7,6 @@ import useHarkState from '~/logic/state/hark';
import { Workspace } from '~/types/workspace';
import { Body } from '../components/Body';
import { GroupsPane } from './components/GroupsPane';
import { JoinGroup } from './components/JoinGroup';
import { NewGroup } from './components/NewGroup';
import './css/custom.css';
import _ from 'lodash';
@ -75,22 +74,6 @@ export default function Landscape() {
</Box>
</Body>
</Route>
<Route path="/~landscape/join/:ship?/:name?"
render={(routeProps) => {
const { ship, name } = routeProps.match.params;
const autojoin = ship && name ? `${ship}/${name}` : undefined;
return (
<Body>
<Box maxWidth="300px">
<JoinGroup
autojoin={autojoin}
{...routeProps}
/>
</Box>
</Body>
);
}}
/>
</Switch>
</>
);

View File

@ -0,0 +1,8 @@
import Urbit from '@urbit/http-api';
const api = new Urbit('', '', (window as any).desk);
api.ship = window.ship;
// api.verbose = true;
// @ts-ignore TODO window typings
window.api = api;
export default api;

View File

@ -1,50 +0,0 @@
import _ from 'lodash';
export default class Api {
ship: any;
channel: any;
bindPaths: any[];
constructor(ship, channel) {
this.ship = ship;
this.channel = channel;
this.bindPaths = [];
}
bind(path, method, ship = this.ship, appl = 'herm', success, fail) {
this.bindPaths = _.uniq([...this.bindPaths, path]);
(window as any).subscriptionId = this.channel.subscribe(ship, appl, path,
(err) => {
fail(err);
},
(event) => {
success({
data: event,
from: {
ship,
path
}
});
},
(err) => {
fail(err);
});
}
belt(belt) {
return this.action('herm', 'belt', belt);
}
action(appl, mark, data) {
return new Promise((resolve, reject) => {
this.channel.poke(window.ship, appl, mark, data,
(json) => {
resolve(json);
},
(err) => {
reject(err);
});
});
}
}

View File

@ -1,94 +1,471 @@
import { Box, Col } from '@tlon/indigo-react';
import React, { Component } from 'react';
import dark from '@tlon/indigo-dark';
import light from '@tlon/indigo-light';
/* eslint-disable max-lines */
import React, {
useEffect,
useRef,
useCallback
} from 'react';
import useTermState from './state';
import { useDark } from './join';
import api from './api';
import { Terminal, ITerminalOptions, ITheme } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { saveAs } from 'file-saver';
import { Box, Col, Reset, _dark, _light } from '@tlon/indigo-react';
import 'xterm/css/xterm.css';
import {
Belt, Blit, Stye, Stub, Tint, Deco,
pokeTask, pokeBelt
} from '@urbit/api/term';
import bel from './lib/bel';
import { ThemeProvider } from 'styled-components';
import Api from './api';
import { History } from './components/history';
import { Input } from './components/input';
import './css/custom.css';
import Store from './store';
import Subscription from './subscription';
import Channel from './lib/channel';
class TermApp extends Component<any, any> {
store: Store;
api: any;
subscription: any;
constructor(props) {
super(props);
this.store = new Store();
this.store.setStateHandler(this.setState.bind(this));
type TermAppProps = {
ship: string;
}
this.state = this.store.state;
const makeTheme = (dark: boolean): ITheme => {
let fg, bg: string;
if (dark) {
fg = 'white';
bg = 'rgb(26,26,26)';
} else {
fg = 'black';
bg = 'white';
}
// TODO indigo colors.
// we can't pluck these from ThemeContext because they have transparency.
// technically xterm supports transparency, but it degrades performance.
return {
foreground: fg,
background: bg,
brightBlack: '#7f7f7f', // NOTE slogs
cursor: fg
};
};
const termConfig: ITerminalOptions = {
logLevel: 'warn',
//
convertEol: true,
//
rows: 24,
cols: 80,
scrollback: 10000,
//
fontFamily: '"Source Code Pro", "Roboto mono", "Courier New", monospace',
fontWeight: 400,
// NOTE theme colors configured dynamically
//
bellStyle: 'sound',
bellSound: bel,
//
// allows text selection by holding modifier (option, or shift)
macOptionClickForcesSelection: true
};
const csi = (cmd: string, ...args: number[]) => {
return '\x1b[' + args.join(';') + cmd;
};
const tint = (t: Tint) => {
switch (t) {
case null: return '9';
case 'k': return '0';
case 'r': return '1';
case 'g': return '2';
case 'y': return '3';
case 'b': return '4';
case 'm': return '5';
case 'c': return '6';
case 'w': return '7';
default: return `8;2;${t.r%256};${t.g%256};${t.b%256}`;
}
};
const stye = (s: Stye) => {
let out = '';
// text decorations
//
if (s.deco.length > 0) {
out += s.deco.reduce((decs: number[], deco: Deco) => {
/* eslint-disable max-statements-per-line */
switch (deco) {
case null: decs.push(0); return decs;
case 'br': decs.push(1); return decs;
case 'un': decs.push(4); return decs;
case 'bl': decs.push(5); return decs;
default: console.log('weird deco', deco); return decs;
}
}, []).join(';');
}
resetControllers() {
this.api = null;
this.subscription = null;
// background color
//
if (s.back !== null) {
if (out !== '') {
out += ';';
}
out += '4';
out += tint(s.back);
}
componentDidMount() {
this.resetControllers();
// eslint-disable-next-line new-cap
const channel = new Channel();
this.api = new Api(window.ship, channel);
this.store.api = this.api;
this.subscription = new Subscription(this.store, this.api, channel);
this.subscription.start();
// foreground color
//
if (s.fore !== null) {
if (out !== '') {
out += ';';
}
out += '3';
out += tint(s.fore);
}
componentWillUnmount() {
this.subscription.delete();
this.store.clear();
this.resetControllers();
if (out === '') {
return out;
}
return '\x1b[' + out + 'm';
};
const showBlit = (term: Terminal, blit: Blit) => {
let out = '';
if ('bel' in blit) {
out += '\x07';
} else if ('clr' in blit) {
term.clear();
out += csi('u');
} else if ('hop' in blit) {
if (typeof blit.hop === 'number') {
out += csi('H', term.rows, blit.hop + 1);
} else {
out += csi('H', term.rows - blit.hop.r, blit.hop.c + 1);
}
out += csi('s'); // save cursor position
} else if ('put' in blit) {
out += blit.put.join('');
out += csi('u');
} else if ('klr' in blit) {
//TODO remove for new backend
{
out += csi('H', term.rows, 1);
out += csi('K');
}
out += blit.klr.reduce((lin: string, p: Stub) => {
lin += stye(p.stye);
lin += p.text.join('');
lin += csi('m', 0);
return lin;
}, '');
out += csi('u');
} else if ('nel' in blit) {
out += '\n';
} else if ('sag' in blit || 'sav' in blit) {
const sav = ('sag' in blit) ? blit.sag : blit.sav;
const name = sav.path.split('/').slice(-2).join('.');
const buff = Buffer.from(sav.file, 'base64');
const blob = new Blob([buff], { type: 'application/octet-stream' });
saveAs(blob, name);
} else if ('url' in blit) {
window.open(blit.url);
} else if ('wyp' in blit) {
out += '\r' + csi('K');
out += csi('u');
//
//TODO remove for new backend
} else if ('lin' in blit) {
out += csi('H', term.rows, 1);
out += csi('K');
out += blit.lin.join('');
} else if ('mor' in blit) {
out += '\n';
} else {
console.log('weird blit', blit);
}
getTheme() {
const { props } = this;
return ((props.dark && props?.display?.theme == 'auto') ||
props?.display?.theme == 'dark'
) ? dark : light;
}
term.write(out);
};
render() {
const theme = this.getTheme();
return (
<ThemeProvider theme={theme}>
// NOTE should generally only be passed the default terminal session
const showSlog = (term: Terminal, slog: string) => {
// set scroll region to exclude the bottom line,
// scroll up one line,
// move cursor to start of the newly created whitespace,
// set text to grey,
// print the slog,
// restore color, scroll region, and cursor.
//
term.write(csi('r', 1, term.rows - 1)
+ csi('S', 1)
+ csi('H', term.rows - 1, 1)
+ csi('m', 90)
+ slog
+ csi('m', 0)
+ csi('r')
+ csi('u'));
};
const readInput = (term: Terminal, e: string): Belt[] => {
const belts: Belt[] = [];
let strap = '';
while (e.length > 0) {
let c = e.charCodeAt(0);
// text input
//
if (c >= 32 && c !== 127) {
strap += e[0];
e = e.slice(1);
continue;
} else if ('' !== strap) {
belts.push({ txt: strap.split('') });
strap = '';
}
// special keys/characters
//
if (0 === c) {
term.write('\x07'); // bel
} else if (8 === c || 127 === c) {
belts.push({ bac: null });
} else if (13 === c) {
belts.push({ ret: null });
} else if (c <= 26) {
let k = String.fromCharCode(96 + c);
//NOTE prevent remote shut-downs
if ('d' !== k) {
belts.push({ ctl: k });
//TODO for new backend
// belts.push({ mod: { mod: 'ctl', key: k } });
}
}
// escape sequences
//
if (27 === c) { // ESC
e = e.slice(1);
c = e.charCodeAt(0);
if (91 === c || 79 === c) { // [ or O
e = e.slice(1);
c = e.charCodeAt(0);
/* eslint-disable max-statements-per-line */
switch (c) {
case 65: belts.push({ aro: 'u' }); break;
case 66: belts.push({ aro: 'd' }); break;
case 67: belts.push({ aro: 'r' }); break;
case 68: belts.push({ aro: 'l' }); break;
//
case 77: {
const m = e.charCodeAt(1) - 31;
if (1 === m) {
const c = e.charCodeAt(2) - 32;
const r = e.charCodeAt(3) - 32;
//TODO re-enable for new backend
// belts.push({ hit: { r: term.rows - r, c: c - 1 } });
}
e = e.slice(3);
break;
}
//
default: term.write('\x07'); break; // bel
}
} else if (c >= 97 && c <= 122) { // a <= c <= z
belts.push({ mod: { mod: 'met', key: e[0] } });
} else if (c === 46) { // .
belts.push({ mod: { mod: 'met', key: '.' } });
} else if (c === 8 || c === 127) {
belts.push({ mod: { mod: 'met', key: { bac: null } } });
} else {
term.write('\x07'); break; // bel
}
}
e = e.slice(1);
}
if ('' !== strap) {
belts.push({ txt: strap.split('') });
strap = '';
}
return belts;
};
export default function TermApp(props: TermAppProps) {
const container = useRef<HTMLDivElement>(null);
// TODO allow switching of selected
const { sessions, selected, slogstream, set } = useTermState();
const session = sessions[selected];
const dark = useDark();
const setupSlog = useCallback(() => {
console.log('slog: setting up...');
let available = false;
const slog = new EventSource('/~_~/slog', { withCredentials: true });
slog.onopen = (e) => {
console.log('slog: opened stream');
available = true;
};
slog.onmessage = (e) => {
const session = useTermState.getState().sessions[''];
if (!session) {
console.log('default session mia!', 'slog:', slog);
return;
}
showSlog(session.term, e.data);
};
slog.onerror = (e) => {
console.error('slog: eventsource error:', e);
if (available) {
window.setTimeout(() => {
if (slog.readyState !== EventSource.CLOSED) {
return;
}
console.log('slog: reconnecting...');
setupSlog();
}, 10000);
}
};
set((state) => {
state.slogstream = slog;
});
}, [sessions]);
const onInput = useCallback((ses: string, e: string) => {
const term = useTermState.getState().sessions[ses].term;
const belts = readInput(term, e);
belts.map((b) => { // NOTE passing api.poke(pokeBelt makes `this` undefined!
//TODO pokeBelt(ses, b);
api.poke({
app: 'herm',
mark: 'belt',
json: b
});
});
}, [sessions]);
const onResize = useCallback(() => {
// TODO debounce, if it ever becomes a problem
session?.fit.fit();
}, [session]);
// on-init, open slogstream
//
useEffect(() => {
if (!slogstream) {
setupSlog();
}
window.addEventListener('resize', onResize);
return () => {
// TODO clean up subs?
window.removeEventListener('resize', onResize);
};
}, [onResize, setupSlog]);
// on dark mode change, change terminals' theme
//
useEffect(() => {
const theme = makeTheme(dark);
for (const ses in sessions) {
sessions[ses].term.setOption('theme', theme);
}
if (container.current) {
container.current.style.backgroundColor = theme.background || '';
}
}, [dark, sessions]);
// on selected change, maybe setup the term, or put it into the container
//
useEffect(() => {
let ses = session;
// initialize terminal
//
if (!ses) {
// set up terminal
//
const term = new Terminal(termConfig);
term.setOption('theme', makeTheme(dark));
const fit = new FitAddon();
term.loadAddon(fit);
// start mouse reporting
//
term.write(csi('?9h'));
// set up event handlers
//
term.onData(e => onInput(selected, e));
term.onBinary(e => onInput(selected, e));
term.onResize((e) => {
//TODO re-enable once new backend lands
// api.poke(pokeTask(selected, { blew: { w: e.cols, h: e.rows } }));
});
ses = { term, fit };
// open subscription
//
api.subscribe({ app: 'herm', path: '/session/'+selected+'/view',
event: (e) => {
const ses = useTermState.getState().sessions[selected];
if (!ses) {
console.log('on blit: no such session', selected, sessions, useTermState.getState().sessions);
return;
}
showBlit(ses.term, e);
},
quit: () => { // quit
// TODO show user a message
}
});
}
if (container.current && !container.current.contains(ses.term.element || null)) {
ses.term.open(container.current);
ses.fit.fit();
ses.term.focus();
}
set((state) => {
state.sessions[selected] = ses;
});
return () => {
// TODO unload term from container
// but term.dispose is too powerful? maybe just empty the container?
};
}, [set, session, container]);
return (
<>
<ThemeProvider theme={dark ? _dark : _light}>
<Reset />
<Box
width='100%'
height='100%'
p={['0','3']}
style={{ boxSizing: 'border-box' }}
bg='white'
fontFamily='mono'
overflow='hidden'
>
<Col
p={3}
backgroundColor='white'
width='100%'
height='100%'
minHeight={0}
minWidth={0}
color='lightGray'
borderRadius={2}
border={['0','1']}
cursor='text'
style={{ boxSizing: 'border-box' }}
minHeight='0'
px={['0','2']}
pb={['0','2']}
ref={container}
>
{/* @ts-ignore declare props in later pass */}
<History log={this.state.lines.slice(0, -1)} />
<Input
ship={this.props.ship}
cursor={this.state.cursor}
api={this.api}
store={this.store}
line={this.state.lines.slice(-1)[0]}
/>
</Col>
</Box>
</ThemeProvider>
);
}
</>
);
}
export default TermApp;

View File

@ -1,35 +0,0 @@
import { Box } from '@tlon/indigo-react';
import React, { Component } from 'react';
import Line from './line';
export class History extends Component {
constructor(props) {
super(props);
}
render() {
return (
<Box
height='100%'
minHeight={0}
minWidth={0}
display='flex'
flexDirection='column-reverse'
overflowY='scroll'
style={{ resize: 'none' }}
>
<Box
mt='auto'
>
{/* @ts-ignore declare props in later pass */}
{this.props.log.map((line, i) => {
// @ts-ignore react memo not passing props
return <Line key={i} line={line} />;
})}
</Box>
</Box>
);
}
}
export default History;

View File

@ -1,128 +0,0 @@
import { BaseInput, Box, Row } from '@tlon/indigo-react';
import React, { Component } from 'react';
export class Input extends Component<any, {}> {
inputRef: React.RefObject<unknown>;
constructor(props) {
super(props);
this.state = {};
this.keyPress = this.keyPress.bind(this);
this.paste = this.paste.bind(this);
this.click = this.click.bind(this);
this.inputRef = React.createRef();
}
componentDidUpdate() {
if (
document.activeElement == this.inputRef.current
) {
// @ts-ignore ref type issues
this.inputRef.current.focus();
// @ts-ignore ref type issues
this.inputRef.current.setSelectionRange(this.props.cursor, this.props.cursor);
}
}
keyPress(e) {
const key = e.key;
// let paste and leap events pass
if ((e.getModifierState('Control') || e.getModifierState('Meta'))
&& (e.key === 'v' || e.key === '/')) {
return;
}
let belt = null;
if (key === 'ArrowLeft')
belt = { aro: 'l' };
else if (key === 'ArrowRight')
belt = { aro: 'r' };
else if (key === 'ArrowUp')
belt = { aro: 'u' };
else if (key === 'ArrowDown')
belt = { aro: 'd' };
else if (key === 'Backspace')
belt = { bac: null };
else if (key === 'Delete')
belt = { del: null };
else if (key === 'Tab')
belt = { ctl: 'i' };
else if (key === 'Enter')
belt = { ret: null };
else if (key.length === 1)
belt = { txt: [key] };
else
belt = null;
if (belt && e.getModifierState('Control')) {
if (belt.txt !== undefined)
belt = { ctl: belt.txt[0] };
} else
if (belt &&
(e.getModifierState('Meta') || e.getModifierState('Alt'))) {
if (belt.bac !== undefined)
belt = { met: 'bac' };
}
if (belt !== null) {
this.props.api.belt(belt);
}
e.preventDefault();
}
paste(e) {
const clipboardData = e.clipboardData || (window as any).clipboardData;
const clipboardText = clipboardData.getData('Text');
this.props.api.belt({ txt: [...clipboardText] });
e.preventDefault();
}
click(e) {
// prevent desynced cursor movement
e.preventDefault();
e.target.setSelectionRange(this.props.cursor, this.props.cursor);
}
render() {
const line = this.props.line;
let prompt = 'connecting...';
if (line) {
if (line.lin) {
prompt = line.lin.join('');
} else if (line.klr) {
// TODO render prompt style
prompt = line.klr.reduce((l, p) => (l + p.text.join('')), '');
}
}
return (
<Row flexGrow={1} position='relative'>
<Box flexShrink={0} width='100%' color='black' fontSize={0}>
<BaseInput
autoFocus
autoCorrect="off"
autoCapitalize="off"
color='lightGray'
minHeight={0}
display='inline-block'
width='100%'
spellCheck="false"
tabindex={0}
wrap="off"
fontFamily="mono"
id="term"
cursor={this.props.cursor}
onKeyDown={this.keyPress}
onClick={this.click}
onPaste={this.paste}
// @ts-ignore indigo-react doesn't let us pass refs
ref={this.inputRef}
defaultValue="connecting..."
value={prompt}
/>
</Box>
</Row>
);
}
}
export default Input;

View File

@ -1,66 +0,0 @@
import { Text } from '@tlon/indigo-react';
import React from 'react';
// @ts-ignore line isn't in props?
export default React.memo(({ line }) => {
// line body to jsx
// NOTE lines are lists of characters that might span multiple codepoints
//
let text = '';
if (line.lin) {
text = line.lin.join('');
} else if (line.klr) {
text = line.klr.map((part, i) => {
const prop = part.stye.deco.reduce((prop, deco) => {
switch (deco) {
case null: return prop;
case 'br': return { bold: true, ...prop };
case 'bl': return { className: 'blink', ...prop };
case 'un': return { style: { textDecoration: 'underline' }, ...prop };
default: console.log('weird deco', deco); return prop;
}
}, {});
switch (part.stye.fore) {
case null: break;
case 'r': prop.color = 'red'; break;
case 'g': prop.color = 'green'; break;
case 'b': prop.color = 'blue'; break;
case 'c': prop.color = 'cyan'; break;
case 'm': prop.color = 'purple'; break;
case 'y': prop.color = 'yellow'; break;
case 'k': prop.color = 'black'; break;
case 'w': prop.color = 'white'; break;
default: prop.color = '#' + part.stye.fore;
}
switch (part.stye.back) {
case null: break;
case 'r': prop.backgroundColor = 'red'; break;
case 'g': prop.backgroundColor = 'green'; break;
case 'b': prop.backgroundColor = 'blue'; break;
case 'c': prop.backgroundColor = 'cyan'; break;
case 'm': prop.backgroundColor = 'purple'; break;
case 'y': prop.backgroundColor = 'yellow'; break;
case 'k': prop.backgroundColor = 'black'; break;
case 'w': prop.backgroundColor = 'white'; break;
default: prop.backgroundColor = '#' + part.stye.back;
}
if (Object.keys(prop).length === 0) {
return part.text;
} else {
return (<Text mono fontSize='inherit' key={i} {...prop}>
{part.text.join('')}
</Text>);
}
});
}
// render line
//
return (
<Text mono display='flex'
fontSize={0}
style={{ overflowWrap: 'break-word', whiteSpace: 'pre-wrap' }}
>
{text}
</Text>
);
});

View File

@ -1,23 +0,0 @@
body, #root {
height: 100vh;
margin: 0;
padding: 0;
}
input#term {
background-color: inherit;
color: inherit;
border: none;
}
.blink {
animation: 4s ease-in-out infinite opacity_blink;
}
@keyframes opacity_blink {
0% { opacity: 0; }
10% { opacity: 1; }
80% { opacity: 1; }
90% { opacity: 0; }
100% { opacity: 0; }
}

View File

@ -8,8 +8,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<link rel="apple-touch-icon" href="/~landscape/img/touch_icon.png">
<link rel="icon" type="image/png" href="/~landscape/img/Favicon.png">
<!--<link rel="apple-touch-icon" href="/~landscape/img/touch_icon.png">
<link rel="icon" type="image/png" href="/~landscape/img/Favicon.png">-->
<link rel="manifest"
href='data:application/manifest+json,{
"name": "Terminal",
@ -18,6 +18,16 @@
"display": "standalone",
"background_color": "%23FFFFFF",
"theme_color": "%23000000"}' />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap" rel="stylesheet">
<style>
body, #root {
height: 100vh;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="root"></div>

View File

@ -0,0 +1,24 @@
import { useEffect, useState } from 'react';
import { useTheme } from './settings';
import useTermState from './state';
export function useDark() {
const [osDark, setOsDark] = useState(false);
useEffect(() => {
const themeWatcher = window.matchMedia('(prefers-color-scheme: dark)');
const update = (e: MediaQueryListEvent) => {
setOsDark(e.matches);
};
setOsDark(themeWatcher.matches);
themeWatcher.addListener(update);
return () => {
themeWatcher.removeListener(update);
}
}, []);
const theme = useTermState(s => s.theme);
return theme === 'dark' || (osDark && theme === 'auto');
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,290 +0,0 @@
export default class Channel {
constructor() {
this.init();
this.deleteOnUnload();
// a way to handle channel errors
//
//
this.onChannelError = (err) => {
console.error('event source error: ', err);
};
this.onChannelOpen = (e) => {
console.log('open', e);
};
}
init() {
this.debounceInterval = 500;
// unique identifier: current time and random number
//
this.uid =
new Date().getTime().toString() +
"-" +
Math.random().toString(16).slice(-6);
this.requestId = 1;
// the currently connected EventSource
//
this.eventSource = null;
// the id of the last EventSource event we received
//
this.lastEventId = 0;
// this last event id acknowledgment sent to the server
//
this.lastAcknowledgedEventId = 0;
// a registry of requestId to successFunc/failureFunc
//
// These functions are registered during a +poke and are executed
// in the onServerEvent()/onServerError() callbacks. Only one of
// the functions will be called, and the outstanding poke will be
// removed after calling the success or failure function.
//
this.outstandingPokes = new Map();
// a registry of requestId to subscription functions.
//
// These functions are registered during a +subscribe and are
// executed in the onServerEvent()/onServerError() callbacks. The
// event function will be called whenever a new piece of data on this
// subscription is available, which may be 0, 1, or many times. The
// disconnect function may be called exactly once.
//
this.outstandingSubscriptions = new Map();
this.outstandingJSON = [];
this.debounceTimer = null;
}
resetDebounceTimer() {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
this.debounceTimer = setTimeout(() => {
this.sendJSONToChannel();
}, this.debounceInterval)
}
setOnChannelError(onError = (err) => {}) {
this.onChannelError = onError;
}
setOnChannelOpen(onOpen = (e) => {}) {
this.onChannelOpen = onOpen;
}
deleteOnUnload() {
window.addEventListener("beforeunload", (event) => {
this.delete();
});
}
clearQueue() {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
this.sendJSONToChannel();
}
// sends a poke to an app on an urbit ship
//
poke(ship, app, mark, json, successFunc, failureFunc) {
let id = this.nextId();
this.outstandingPokes.set(
id,
{
success: successFunc,
fail: failureFunc
}
);
const j = {
id,
action: "poke",
ship,
app,
mark,
json
};
this.sendJSONToChannel(j);
}
// subscribes to a path on an specific app and ship.
//
// Returns a subscription id, which is the same as the same internal id
// passed to your Urbit.
subscribe(
ship,
app,
path,
connectionErrFunc = () => {},
eventFunc = () => {},
quitFunc = () => {},
subAckFunc = () => {},
) {
let id = this.nextId();
this.outstandingSubscriptions.set(
id,
{
err: connectionErrFunc,
event: eventFunc,
quit: quitFunc,
subAck: subAckFunc
}
);
const json = {
id,
action: "subscribe",
ship,
app,
path
}
this.resetDebounceTimer();
this.outstandingJSON.push(json);
return id;
}
// quit the channel
//
delete() {
let id = this.nextId();
clearInterval(this.ackTimer);
navigator.sendBeacon(this.channelURL(), JSON.stringify([{
id,
action: "delete"
}]));
if (this.eventSource) {
this.eventSource.close();
}
}
// unsubscribe to a specific subscription
//
unsubscribe(subscription) {
let id = this.nextId();
this.sendJSONToChannel({
id,
action: "unsubscribe",
subscription
});
}
// sends a JSON command command to the server.
//
sendJSONToChannel(j) {
let req = new XMLHttpRequest();
req.open("PUT", this.channelURL());
req.setRequestHeader("Content-Type", "application/json");
if (this.lastEventId == this.lastAcknowledgedEventId) {
if (j) {
this.outstandingJSON.push(j);
}
if (this.outstandingJSON.length > 0) {
let x = JSON.stringify(this.outstandingJSON);
req.send(x);
}
} else {
// we add an acknowledgment to clear the server side queue
//
// The server side puts messages it sends us in a queue until we
// acknowledge that we received it.
//
let payload = [
...this.outstandingJSON,
{action: "ack", "event-id": this.lastEventId}
];
if (j) {
payload.push(j)
}
let x = JSON.stringify(payload);
req.send(x);
this.lastAcknowledgedEventId = this.lastEventId;
}
this.outstandingJSON = [];
this.connectIfDisconnected();
}
// connects to the EventSource if we are not currently connected
//
connectIfDisconnected() {
if (this.eventSource) {
return;
}
this.eventSource = new EventSource(this.channelURL(), {withCredentials:true});
this.eventSource.onmessage = e => {
this.lastEventId = parseInt(e.lastEventId, 10);
let obj = JSON.parse(e.data);
let pokeFuncs = this.outstandingPokes.get(obj.id);
let subFuncs = this.outstandingSubscriptions.get(obj.id);
if (obj.response == "poke" && !!pokeFuncs) {
let funcs = pokeFuncs;
if (obj.hasOwnProperty("ok")) {
funcs["success"]();
} else if (obj.hasOwnProperty("err")) {
funcs["fail"](obj.err);
} else {
console.error("Invalid poke response: ", obj);
}
this.outstandingPokes.delete(obj.id);
} else if (obj.response == "subscribe" ||
(obj.response == "poke" && !!subFuncs)) {
let funcs = subFuncs;
if (obj.hasOwnProperty("err")) {
funcs["err"](obj.err);
this.outstandingSubscriptions.delete(obj.id);
} else if (obj.hasOwnProperty("ok")) {
funcs["subAck"](obj);
}
} else if (obj.response == "diff") {
// ensure we ack before channel clogs
if((this.lastEventId - this.lastAcknowledgedEventId) > 30) {
this.clearQueue();
}
let funcs = subFuncs;
funcs["event"](obj.json);
} else if (obj.response == "quit") {
let funcs = subFuncs;
funcs["quit"](obj);
this.outstandingSubscriptions.delete(obj.id);
} else {
console.log("Unrecognized response: ", e);
}
}
this.eventSource.onopen = this.onChannelOpen;
this.eventSource.onerror = e => {
this.delete();
this.init();
this.onChannelError(e);
}
}
channelURL() {
return "/~/channel/" + this.uid;
}
nextId() {
return this.requestId++;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,63 +1,23 @@
{
"name": "interface",
"name": "webterm",
"version": "1.0.0",
"description": "",
"main": "index.js",
"private": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@reach/disclosure": "^0.10.5",
"@reach/menu-button": "^0.10.5",
"@reach/tabs": "^0.10.5",
"@react-spring/web": "^9.1.1",
"@tlon/indigo-dark": "^1.0.6",
"@tlon/indigo-light": "^1.0.7",
"@tlon/indigo-react": "^1.2.23",
"@tlon/sigil-js": "^1.4.3",
"@urbit/api": "^1.1.1",
"@urbit/http-api": "^1.2.1",
"any-ascii": "^0.1.7",
"aws-sdk": "^2.830.0",
"big-integer": "^1.6.48",
"classnames": "^2.2.6",
"codemirror": "^5.59.2",
"css-loader": "^3.6.0",
"file-saver": "^2.0.5",
"formik": "^2.1.5",
"immer": "^9.0.2",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"mousetrap": "^1.6.5",
"mousetrap-global-bind": "^1.1.0",
"normalize-wheel": "1.0.1",
"oembed-parser": "^1.4.5",
"prop-types": "^15.7.2",
"querystring": "^0.2.0",
"react": "^16.14.0",
"react-codemirror2": "^6.0.1",
"react-dom": "^16.14.0",
"react-helmet": "^6.1.0",
"react-markdown": "^4.3.1",
"react-oembed-container": "^1.0.0",
"react-router-dom": "^5.2.0",
"react-use-gesture": "^9.1.3",
"react-virtuoso": "^0.20.3",
"react-visibility-sensor": "^5.1.1",
"remark": "^12.0.0",
"remark-breaks": "^2.0.2",
"remark-disable-tokenizers": "1.1.0",
"stacktrace-js": "^2.0.2",
"style-loader": "^1.3.0",
"styled-components": "^5.1.1",
"styled-system": "^5.1.5",
"suncalc": "^1.8.0",
"unist-util-visit": "^3.0.0",
"urbit-ob": "^5.0.1",
"workbox-core": "^6.0.2",
"workbox-precaching": "^6.0.2",
"workbox-recipes": "^6.0.2",
"workbox-routing": "^6.0.2",
"yup": "^0.29.3",
"xterm": "^4.15.0",
"xterm-addon-fit": "^0.5.0",
"zustand": "^3.5.0"
},
"devDependencies": {
@ -69,17 +29,11 @@
"@babel/preset-env": "^7.12.11",
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@storybook/addon-actions": "^6.2.9",
"@storybook/addon-essentials": "^6.2.9",
"@storybook/addon-links": "^6.2.9",
"@storybook/react": "^6.2.9",
"@types/lodash": "^4.14.168",
"@types/react": "^16.14.2",
"@types/react-dom": "^16.9.10",
"@types/react-router-dom": "^5.1.7",
"@types/styled-components": "^5.1.7",
"@types/styled-system": "^5.1.10",
"@types/yup": "^0.29.11",
"@typescript-eslint/eslint-plugin": "^4.15.0",
"@typescript-eslint/parser": "^4.24.0",
"@urbit/eslint-config": "^1.0.0",
@ -87,9 +41,7 @@
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.3",
"babel-loader": "^8.2.2",
"babel-plugin-lodash": "^3.3.4",
"babel-plugin-root-import": "^6.6.0",
"chromatic": "^5.8.3",
"clean-webpack-plugin": "^3.0.0",
"cross-env": "^7.0.3",
"eslint": "^7.26.0",
@ -99,13 +51,7 @@
"husky": "^6.0.0",
"jest": "^26.6.3",
"lint-staged": "^11.0.0",
"loki": "^0.28.1",
"moment-locales-webpack-plugin": "^1.2.0",
"react-hot-loader": "^4.13.0",
"sass": "^1.32.5",
"sass-loader": "^8.0.2",
"storybook-addon-designs": "^6.0.0",
"ts-mdast": "^1.0.0",
"typescript": "^4.2.4",
"webpack": "^4.46.0",
"webpack-cli": "^3.3.12",
@ -121,9 +67,6 @@
"start": "webpack-dev-server --config config/webpack.dev.js",
"test": "tsc && jest",
"jest": "jest",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook",
"chromatic": "chromatic --exit-zero-on-changes",
"hook-lint": "eslint --cache --fix"
},
"author": "",

View File

@ -0,0 +1,26 @@
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import create from 'zustand';
import produce from 'immer';
type Session = { term: Terminal, fit: FitAddon };
type Sessions = { [id: string]: Session; }
export interface TermState {
sessions: Sessions,
selected: string,
slogstream: null | EventSource,
theme: 'auto' | 'light' | 'dark'
};
const useTermState = create<TermState>((set, get) => ({
sessions: {} as Sessions,
selected: '', // empty string is default session
slogstream: null,
theme: 'auto',
set: (f: (draft: TermState) => void) => {
set(produce(f));
}
} as TermState));
export default useTermState;

View File

@ -1,94 +0,0 @@
import { saveAs } from 'file-saver';
import bel from './lib/bel';
export default class Store {
state: any;
api: any;
setState: any;
constructor() {
this.state = this.initialState();
}
initialState() {
return {
lines: [''],
cursor: 0
};
}
clear() {
this.setState(this.initialState());
}
handleEvent(data) {
// process slogs
//
if (data.slog) {
this.state.lines.splice(this.state.lines.length-1, 0, { lin: [data.slog] });
this.setState({ lines: this.state.lines });
return;
}
// process blits
//
const blit = data.data;
switch (Object.keys(blit)[0]) {
case 'bel':
bel.play();
break;
case 'clr':
this.state.lines = this.state.lines.slice(-1);
this.setState({ lines: this.state.lines });
break;
case 'hop':
// since lines are lists of characters that might span multiple
// codepoints, we need to calculate the byte-wise cursor position
// to avoid incorrect cursor rendering.
//
const line = this.state.lines[this.state.lines.length - 1];
let hops;
if (line.lin) {
hops = line.lin.slice(0, blit.hop);
} else if (line.klr) {
hops = line.klr.reduce((h, p) => {
if (h.length >= blit.hop)
return h;
return [...h, ...p.text.slice(0, blit.hop - h.length)];
}, []);
}
this.setState({ cursor: hops.join('').length });
break;
case 'lin':
this.state.lines[this.state.lines.length - 1] = blit;
this.setState({ lines: this.state.lines });
break;
case 'klr':
this.state.lines[this.state.lines.length - 1] = blit;
this.setState({ lines: this.state.lines });
break;
case 'mor':
this.state.lines.push('');
this.setState({ lines: this.state.lines });
break;
case 'sag':
blit.sav = blit.sag;
break;
case 'sav':
const name = blit.sav.path.split('/').slice(-2).join('.');
const buff = new Buffer(blit.sav.file, 'base64');
const blob = new Blob([buff], { type: 'application/octet-stream' });
saveAs(blob, name);
break;
case 'url':
// TODO too invasive? just print as <a>?
window.open(blit.url);
break;
default: console.log('weird blit', blit);
}
}
setStateHandler(setState) {
this.setState = setState;
}
}

View File

@ -1,87 +0,0 @@
export default class Subscription {
store: any;
api: any;
channel: any;
firstRoundComplete: boolean;
constructor(store, api, channel) {
this.store = store;
this.api = api;
this.channel = channel;
this.channel.setOnChannelError(this.onChannelError.bind(this));
this.firstRoundComplete = false;
}
start() {
if (this.api.ship) {
this.firstRound();
} else {
console.error('~~~ ERROR: Must set api.ship before operation ~~~');
}
this.setupSlog();
}
setupSlog() {
let available = false;
const slog = new EventSource('/~_~/slog', { withCredentials: true });
slog.onopen = (e) => {
console.log('slog: opened stream');
available = true;
};
slog.onmessage = (e) => {
this.handleEvent({ slog: e.data });
};
slog.onerror = (e) => {
console.error('slog: eventsource error:', e);
if (available) {
window.setTimeout(() => {
if (slog.readyState !== EventSource.CLOSED)
return;
console.log('slog: reconnecting...');
this.setupSlog();
}, 10000);
}
};
}
delete() {
this.channel.delete();
}
onChannelError(err) {
console.error('event source error: ', err);
console.log('initiating new channel');
this.firstRoundComplete = false;
setTimeout(() => {
this.store.handleEvent({
data: { clear : true }
});
this.start();
}, 2000);
}
subscribe(path, app) {
this.api.bind(path, 'PUT', this.api.ship, app,
this.handleEvent.bind(this),
(err) => {
console.log(err);
this.subscribe(path, app);
},
() => {
this.subscribe(path, app);
});
}
firstRound() {
this.subscribe('/session/', 'herm');
}
handleEvent(diff) {
this.store.handleEvent(diff);
}
}

View File

@ -1,33 +1,34 @@
/- view-sur=group-view, group-store, *group, metadata=metadata-store, hark=hark-store
/- inv=invite-store
/+ default-agent, agentio, mdl=metadata,
resource, dbug, grpl=group, conl=contact, verb
|%
++ card card:agent:gall
::
+$ base-state-0
joining=(map rid=resource [=ship =progress:view])
::
+$ base-state-1
joining=(map rid=resource request:view)
::
+$ state-zero
[%0 base-state-0]
[%0 *]
::
+$ state-one
[%1 base-state-0]
[%1 *]
::
+$ state-two
[%2 base-state-1]
[%2 *]
::
+$ state-three
[%3 joining=(map rid=resource request:view)]
::
+$ versioned-state
$% state-zero
state-one
state-two
state-three
==
::
++ view view-sur
--
=| state-two
=| state-three
=* state -
::
%- agent:dbug
@ -48,29 +49,10 @@
|= =vase
=+ !<(old=versioned-state vase)
=| cards=(list card)
|^
?- -.old
%2 [cards this(state old)]
%1 $(-.old %2, +.old (base-state-to-1 +.old))
%0 $(-.old %1, cards :_(cards (poke-self:pass:io noun+!>(%cleanup))))
==
::
++ base-state-to-1
|= base-state-0
%- ~(gas by *(map resource request:view))
(turn ~(tap by joining) request-to-1)
::
++ request-to-1
|= [rid=resource =ship =progress:view]
^- [resource request:view]
:- rid
%* . *request:view
started now.bowl
hidden %.n
ship ship
progress progress
==
--
|-
?: ?=(%3 -.old)
[cards this(state old)]
$(old *state-three)
::
++ on-poke
|= [=mark =vase]
@ -84,8 +66,9 @@
=+ !<(=action:view vase)
=^ cards state
?+ -.action !!
%join jn-abet:(jn-start:join:gc +.action)
%hide (hide:gc +.action)
%join jn-abet:(jn-start:join:gc +.action)
%abort jn-abet:(jn-abort:join:gc +.action)
%done jn-abet:(jn-done:join:gc +.action)
==
[cards this]
::
@ -106,7 +89,7 @@
++ on-agent
|= [=wire =sign:agent:gall]
=^ cards state
?+ wire `state
?+ wire (on-agent:def:gc wire sign)
[%join %ship @ @ *]
=/ rid
(de-path:resource t.wire)
@ -115,7 +98,18 @@
==
[cards this]
::
++ on-arvo on-arvo:def
++ on-arvo
|= [=wire sign=sign-arvo]
=^ cards state
?+ wire (on-arvo:def:gc wire sign)
[%breach ~]
?> ?=([%jael %public-keys *] sign)
?. ?=(%breach -.public-keys-result.sign)
`state
(breach who.public-keys-result.sign)
==
[cards this]
::
++ on-leave on-leave:def
++ on-fail on-fail:def
--
@ -124,6 +118,7 @@
++ grp ~(. grpl bowl)
++ io ~(. agentio bowl)
++ con ~(. conl bowl)
++ def ~(. (default-agent state %|) bowl)
++ hide
|= rid=resource
^- (quip card _state)
@ -133,7 +128,28 @@
:_ state
(fact:io group-view-update+!>(`update:view`[%initial joining]) /all ~)^~
:- (fact:io group-view-update+!>([%hide rid]) /all ~)^~
state(joining (~(put by joining) rid request(hidden %.y)))
state(joining (~(put by joining) rid request))
::
++ is-tracking
|= her=ship
^- ?
%+ lien ~(tap in ~(key by joining))
|=([him=ship name=term] =(her him))
::
++ breach
|= who=ship
^- (quip card _state)
=/ requests=(list [rid=resource =request:view])
~(tap by joining)
=| cards=(list card)
|- ^- (quip card _state)
?~ requests
[cards state]
?. =(entity.rid.i.requests who)
$(requests t.requests)
=^ crds state
jn-abet:jn-breach:(jn-abed:join rid.i.requests)
[(welp cards crds) state]
::
++ has-joined
|= rid=resource
@ -170,12 +186,54 @@
(emit (fact:io cage /all tx+(en-path:resource rid) ~))
group-view-update+!>([%progress rid progress])
::
++ watch-md
(emit (watch-our:(jn-pass-io /md) %metadata-store /updates))
::
++ watch-groups
(emit (watch-our:(jn-pass-io /groups) %group-store /groups))
::
++ pass
|%
++ pull-action pull-hook-action+!>([%add ship rid])
::
++ watch-md (watch-our:(jn-pass-io /md) %metadata-store /updates)
++ watch-groups (watch-our:(jn-pass-io /groups) %group-store /groups)
++ watch-md-nacks (watch-our:(jn-pass-io /md-nacks) %metadata-pull-hook /nack)
++ watch-grp-nacks (watch-our:(jn-pass-io /grp-nacks) %group-pull-hook /nack)
::
++ add-us
%+ poke:(jn-pass-io /add)
[ship %group-push-hook]
group-update-0+!>([%add-members rid (silt our.bowl ~)])
::
++ del-us
%+ poke:pass:io [ship %group-push-hook]
group-update-0+!>([%remove-members rid (silt our.bowl ~)])
::
++ remove-pull-groups
(poke-our:pass:io %group-pull-hook pull-hook-action+!>([%remove rid]))
::
++ pull-groups
(poke-our:(jn-pass-io /poke) %group-pull-hook pull-action)
++ pull-md
(poke-our:(jn-pass-io /poke) %metadata-pull-hook pull-action)
++ pull-co
(poke-our:(jn-pass-io /poke) %contact-pull-hook pull-action)
::
++ allow-co
%+ poke-our:(jn-pass-io /poke) %contact-store
contact-update-0+!>([%allow %group rid])
::
++ share-co
%+ poke:(jn-pass-io /poke)
[entity.rid %contact-push-hook]
[%contact-share !>([%share our.bowl])]
::
++ pull-gra
|= gr=resource
(poke-our:(jn-pass-io /poke) %graph-pull-hook pull-hook-action+!>([%add entity .]:gr))
::
++ retry
(poke-self:pass:io group-view-action+!>([%join rid ship]))
++ watch-breach
(~(arvo pass:io /breach) %j %public-keys (silt ship ~))
++ leave-breach
(~(arvo pass:io /breach) %j %nuke (silt ship ~))
--
++ jn-pass-io
|= pax=path
~(. pass:io (welp join+(en-path:resource rid) pax))
@ -191,12 +249,14 @@
[(flop cards) state]
::
++ jn-start
|= [rid=resource =^ship]
|= [rid=resource =^ship =app:view share-co=? autojoin=?]
^+ jn-core
?> ?= $@(~ [~ %done])
?> ?= $@(~ [~ ?(%done %abort)])
(bind (~(get by joining) rid) |=(request:view progress))
=/ =request:view
[now.bowl ship %start app share-co autojoin (get-invites app rid)]
=. joining
(~(put by joining) rid [%.n now.bowl ship %start])
(~(put by joining) rid request)
=. jn-core
(jn-abed rid)
=. jn-core
@ -205,14 +265,80 @@
group-view-update+!>([%started rid (~(got by joining) rid)])
~[/all]
?< ~|("already joined {<rid>}" (has-joined rid))
=. jn-core
%- emit
%+ poke:(jn-pass-io /add)
[ship %group-push-hook]
group-update-0+!>([%add-members rid (silt our.bowl ~)])
=. jn-core (emit add-us:pass)
=. jn-core (tx-progress %start)
=> watch-md
watch-groups
=? jn-core !(is-tracking ship)
(emit watch-breach:pass)
=> (emit watch-md:pass)
=> (emit watch-groups:pass)
=> (emit watch-grp-nacks:pass)
=> (emit watch-md-nacks:pass)
(emit watch-breach:pass)
::
++ jn-breach
=/ =request:view (~(got by joining) rid)
?. ?=(%start progress.request)
:: no action required, subscriptions are sane across breaches
jn-core
(emit add-us:pass)
::
++ jn-abort
|= r=resource
^+ jn-core
=. jn-core (jn-abed r)
(cleanup:rollback %abort)
::
++ jn-done
|= r=resource
=. joining (~(del by joining) r)
jn-core
::
++ rollback
|^
=/ =request:view (~(got by joining) rid)
?+ progress.request ~|(cannot-rollback/progress.request !!)
%start start
%added added
%metadata metadata
==
++ start jn-core
++ added (emit del-us:pass)
++ metadata (emit:added remove-pull-groups:pass)
--
::
++ get-invites
|= [=app:view rid=resource]
^- (set uid:view)
=+ .^(invit=(unit invitatory:inv) %gx (scry:io %invite-store /invitatory/[app]/noun))
?~ invit ~
%- ~(gas in *(set uid:view))
%+ murn ~(tap by u.invit)
|= [=uid:view =invite:inv]
?. =(rid resource.invite) ~
`uid
::
++ cleanup
|= =progress:view
=. jn-core
(tx-progress progress)
=. jn-core
(emit (leave-our:(jn-pass-io /groups) %group-store))
=. jn-core
(emit (leave-our:(jn-pass-io /md) %metadata-store))
=. jn-core
(emit (leave-our:(jn-pass-io /md-nacks) %metadata-pull-hook))
=. jn-core
(emit (leave-our:(jn-pass-io /grp-nacks) %group-pull-hook))
=/ =request:view (~(got by joining) rid)
=. jn-core
%- emit-many
%+ turn ~(tap in invite.request)
|= =uid:view
%+ poke-our:pass:io %invite-store
=- invite-action+!>(-)
^- action:inv
[%accept `@tas`app.request uid]
jn-core
::
++ jn-agent
|= [=wire =sign:agent:gall]
@ -225,34 +351,16 @@
(cleanup %no-perms)
=. jn-core
(tx-progress %added)
%- emit
%+ poke-our:(jn-pass-io /pull-groups) %group-pull-hook
pull-hook-action+!>([%add ship rid])
::
%pull-groups
?> ?=(%poke-ack -.sign)
(ack +.sign)
(emit pull-groups:pass)
::
%groups
?+ -.sign !!
%fact (groups-fact +.sign)
%watch-ack (ack +.sign)
%kick watch-groups
%kick (emit watch-groups:pass)
==
::
%pull-md
?> ?=(%poke-ack -.sign)
(ack +.sign)
::
%pull-co
?> ?=(%poke-ack -.sign)
(ack +.sign)
::
%share-co
?> ?=(%poke-ack -.sign)
(ack +.sign)
::
%push-co
%poke
?> ?=(%poke-ack -.sign)
(ack +.sign)
::
@ -260,13 +368,38 @@
?+ -.sign !!
%fact (md-fact +.sign)
%watch-ack (ack +.sign)
%kick watch-md
%kick (emit watch-md:pass)
==
::
%pull-graphs
?> ?=(%poke-ack -.sign)
%- cleanup
?^(p.sign %strange %done)
::
%md-nacks
?+ -.sign !!
%watch-ack (ack +.sign)
%kick (emit watch-md-nacks:pass)
::
%fact
?. =(%resource p.cage.sign) jn-core
=+ !<(nack=resource q.cage.sign)
?. =(nack rid) jn-core
(cleanup %strange)
==
::
%grp-nacks
?+ -.sign !!
%watch-ack (ack +.sign)
%kick (emit watch-grp-nacks:pass)
::
%fact
?. =(%resource p.cage.sign) jn-core
=+ !<(nack=resource q.cage.sign)
?. =(nack rid) jn-core
(cleanup %strange)
==
==
::
++ groups-fact
@ -274,19 +407,15 @@
?. ?=(%group-update-0 p.cage) jn-core
=+ !<(=update:group-store q.cage)
?. ?=(%initial-group -.update) jn-core
=/ =request:view (~(got by joining) rid)
?. =(rid resource.update) jn-core
%- emit-many
=/ cag=^cage pull-hook-action+!>([%add [entity .]:rid])
%- zing
:~ [(poke-our:(jn-pass-io /pull-md) %metadata-pull-hook cag)]~
[(poke-our:(jn-pass-io /pull-co) %contact-pull-hook cag)]~
::
?. scry-is-public:con ~
:_ ~
%+ poke:(jn-pass-io /share-co)
[entity.rid %contact-push-hook]
[%contact-share !>([%share our.bowl])]
==
=. jn-core (emit pull-md:pass)
=. jn-core (emit pull-co:pass)
?. |(share-co.request scry-is-public:con)
jn-core
?: scry-is-public:con (emit share-co:pass)
=. jn-core (emit allow-co:pass)
(emit share-co:pass)
::
++ md-fact
|= [=mark =vase]
@ -294,32 +423,40 @@
=+ !<(=update:metadata vase)
?. ?=(%initial-group -.update) jn-core
?. =(group.update rid) jn-core
|^ ^+ jn-core
=/ =request:view (~(got by joining) rid)
=/ feed feed-rid
=. jn-core (cleanup %done)
?. hidden:(need (scry-group:grp rid))
=/ hidden hidden:(need (scry-group:grp rid))
=? jn-core ?&(!hidden ?=(^ feed))
%- emit
(pull-gra:pass (need feed))
=? jn-core |(hidden autojoin.request)
%- emit-many
(turn graphs pull-gra:pass)
jn-core
::
++ feed-rid
^- (unit resource)
=/ list-md=(list [=md-resource:metadata =association:metadata])
%+ skim ~(tap by associations.update)
|= [=md-resource:metadata =association:metadata]
=(app-name.md-resource %groups)
?> ?=(^ list-md)
?~ list-md ~
=* metadatum metadatum.association.i.list-md
?. ?& ?=(%group -.config.metadatum)
?=(^ feed.config.metadatum)
?=(^ u.feed.config.metadatum)
?=([~ ~ *] feed.config.metadatum)
==
jn-core
=* feed resource.u.u.feed.config.metadatum
%- emit
%+ poke-our:(jn-pass-io /pull-feed) %graph-pull-hook
pull-hook-action+!>([%add [entity .]:feed])
%- emit-many
%+ murn ~(tap by associations.update)
|= [=md-resource:metadata =association:metadata]
^- (unit card)
?. =(app-name.md-resource %graph) ~
=* rid resource.md-resource
:- ~
%+ poke-our:(jn-pass-io /pull-graph) %graph-pull-hook
pull-hook-action+!>([%add [entity .]:rid])
~
`resource.u.u.feed.config.metadatum
::
++ graphs
^- (list resource)
%+ murn ~(tap by associations.update)
|= [=md-resource:metadata =association:metadata]
?. =(app-name.md-resource %graph) ~
`resource.md-resource
--
::
++ ack
|= err=(unit tang)
@ -327,66 +464,6 @@
%- (slog u.err)
(cleanup %strange)
::
++ notify
%- emit
%+ poke-our:(jn-pass-io /hark) %hark-store
=- hark-action+!>(-)
^- action:hark
|^
[%add-note bin body]
++ bin
^- bin:hark
[/ [q.byk.bowl /join/(scot %p entity.rid)/[name.rid]]]
++ title
|= [name=@t rest=@t]
text/(rap 3 'Joining group: "' name '" ' rest ~)
++ body
^- body:hark
=/ =request:view (~(got by joining) rid)
?> ?=(final:view progress.request)
=/ name (rap 3 (scot %p entity.rid) '/' name.rid ~)
?- progress.request
::
%done
=/ =metadatum:metadata (need (peek-metadatum:met %groups rid))
:* ~[(title title.metadatum 'succeeded')]
~
now.bowl
/
/groups/(scot %p entity.rid)/[name.rid]
==
::
%strange
:* ~[(title name 'errored unexpectedly')]
~
now.bowl
/
/
==
::
%no-perms
:* ~[(title name 'failed, you are not permitted to join the group')]
~
now.bowl
/
/
==
==
--
::
++ cleanup
|= =progress:view
=. jn-core
(tx-progress progress)
=. jn-core
(emit (leave-our:(jn-pass-io /groups) %group-store))
=. jn-core
(emit (leave-our:(jn-pass-io /md) %metadata-store))
=/ =request:view (~(got by joining) rid)
=? jn-core (lte (sub now.bowl started.request) ~s30)
notify
=. joining (~(del by joining) rid)
jn-core
--
--
--

View File

@ -38,13 +38,13 @@
=/ act=action !<(action vase)
?+ -.act ~
%invites
?. (team:title [our src]:bowl) ~
?. =,(bowl =(our src)) ~
:: outgoing. we must be inviting other ships. send them each an invite
::
%+ turn ~(tap in recipients.invites.act)
|= recipient=ship
^- card
?< (team:title our.bowl recipient)
?< =,(bowl =(our recipient))
%+ invite-hook-poke recipient
:^ %invite term.act uid.act
^- invite
@ -56,10 +56,10 @@
==
::
%invite
?: (team:title [our src]:bowl)
?: =,(bowl =(our src))
:: outgoing. we must be inviting another ship. send them the invite.
::
?< (team:title our.bowl recipient.invite.act)
?< =(our.bowl recipient.invite.act)
[(invite-hook-poke recipient.invite.act act)]~
:: else incoming. ensure invitatory exists and invite is not a duplicate.
::

View File

@ -196,7 +196,7 @@
^- (unit (unit cage))
?+ path (on-peek:def path)
[%x %all ~]
``noun+!>(invites)
``invite-update+!>([%initial invites])
::
[%x %invitatory @ ~]
:^ ~ ~ %noun

View File

@ -296,19 +296,21 @@
++ on-watch
|= =path
?> (team:title [our src]:bowl)
?. ?=([%preview @ @ @ ~] path)
(on-watch:def path)
=/ rid=resource
(de-path:resource t.path)
=/ prev=(unit group-preview:metadata)
?^ (peek-metadatum:met %groups rid)
(some (get-preview:met rid))
(~(get by previews) rid)
?~ prev
:_ this(pending (~(put in pending) rid))
(get-preview rid)^~
:_ this
(fact-init:io metadata-hook-update+!>([%preview u.prev]))^~
?+ path (on-watch:def path)
::
[%preview @ @ @ ~]
=/ rid=resource
(de-path:resource t.path)
=/ prev=(unit group-preview:metadata)
?^ (peek-metadatum:met %groups rid)
(some (get-preview:met rid))
(~(get by previews) rid)
?~ prev
:_ this(pending (~(put in pending) rid))
(get-preview rid)^~
:_ this
(fact-init:io metadata-hook-update+!>([%preview u.prev]))^~
==
::
++ on-leave on-leave:def
++ on-peek on-peek:def

View File

@ -22,6 +22,10 @@
$: =provider-state
=client-state
==
+$ base-state-2
$: notifications=(map uid notification)
base-state-0
==
::
+$ state-0
[%0 base-state-0]
@ -29,14 +33,18 @@
+$ state-1
[%1 base-state-0]
::
+$ state-2
[%2 base-state-2]
::
+$ versioned-state
$% state-0
state-1
state-2
==
::
--
::
=| state-1
=| state-2
=* state -
::
%- agent:dbug
@ -53,7 +61,9 @@
::
++ on-init
:_ this
[(~(watch-our pass:io /hark) %hark-store /notes)]~
:~ (~(watch-our pass:io /hark/notes) %hark-store /notes)
(~(watch-our pass:io /hark/updates) %hark-store /updates)
==
::
++ on-save !>(state)
++ on-load
@ -63,17 +73,22 @@
=| cards=(list card)
|-
?- -.old
%1 [(flop cards) this]
::
%0
%_ $
-.old %1
::
cards
%+ welp cards
:~ (~(leave-our pass:io /hark) %hark-store)
(~(watch-our pass:io /hark) %hark-store /notes)
==
%2
=/ upd=wire /hark/updates
=/ not=wire /hark/notes
=/ =dock [our.bowl %hark-store]
=? cards !(~(has by wex.bowl) [upd dock]) :: rewatch updates
:_(cards [%pass upd %agent dock %watch /updates])
=? cards !(~(has by wex.bowl) [not dock]) :: rewatch notes
:_(cards [%pass not %agent dock %watch /notes])
=. notifications.old ~
[(flop cards) this(state old)]
::
?(%0 %1)
%_ $
-.old %2
+.old [~ +.old]
==
==
::
@ -203,7 +218,17 @@
^- (quip card _this)
`this
::
++ on-peek on-peek:def
++ on-peek
|= =path
^- (unit (unit cage))
=/ =(pole knot) path
?+ pole [~ ~]
::
[%x %note uid=@t ~]
=/ =uid (slav %ux uid.pole)
=/ note=notification (~(got by notifications) uid)
``hark-note+!>(note)
==
::
++ on-agent
|= [=wire =sign:agent:gall]
@ -212,22 +237,20 @@
::
:: subscription from client to their own hark-store
::
[%hark ~]
[%hark @ ~]
?+ -.sign (on-agent:def wire sign)
%fact
:_ this
?. ?=(%hark-update p.cage.sign)
~
`this
=+ !<(hark-update=update:hark-store q.cage.sign)
?~ not=(filter-notifications:do hark-update) ~
:: only send the last one, since hark accumulates notifcations
=/ =update [%notification u.not]
=/ card=(unit card) ~ ::(fact-all:io %notify-update !>(update))
(drop card)
=^ upds notifications
(filter-notifications:do hark-update)
:_ this
(murn upds |=(=update (fact-all:io %notify-update !>(update))))
::
%kick
:_ this
[%pass /hark %agent [our.bowl %hark-store] %watch /updates]~
[%pass wire %agent [our.bowl %hark-store] %watch t.wire]~
==
::
:: subscription from provider to client
@ -240,13 +263,10 @@
?> ?=(%notify-update p.cage.sign)
=+ !<(=update q.cage.sign)
:_ this
?- -.update
%notification
=/ entry=(unit provider-entry) (~(get by provider-state) service)
?~ entry
~
[(send-notification:do u.entry who notification.update)]~
==
=/ entry=(unit provider-entry) (~(get by provider-state) service)
?~ entry
~
[(send-notification:do u.entry who update)]~
::
%kick
:_ this
@ -289,11 +309,20 @@
=. clients.u.entry (~(put by clients.u.entry) who `sid)
this(provider-state (~(put by provider-state) service u.entry))
::
[%remove-binding *]
`this
[%remove-binding *] `this
::
[%send-notification *]
`this
?> ?=(%iris -.sign-arvo)
?> ?=(%http-response +<.sign-arvo)
=* res client-response.sign-arvo
?> ?=(%finished -.res)
%. `this
=* status status-code.response-header.res
?: =(200 status) same
%+ slog
leaf/"Error sending notfication, status: {(scow %ud status)}"
?~ full-file.res ~
~[leaf/(trip `@t`q.data.u.full-file.res)]
==
::
++ on-fail on-fail:def
@ -302,27 +331,69 @@
+* gra ~(. graphlib bowl)
::
++ filter-notifications
|= =update:hark-store
^- (unit notification)
?. ?=(%add-note -.update) ~
=* place place.bin.update
?. ?=(%landscape desk.place) ~
?. ?=([%graph *] path.place) ~
=/ link=path link.body.update
?. ?=([@ @ @ *] link) ~
?~ ship=(slaw %p i.t.link) ~
=* name i.t.t.link
=/ =resource:resource [u.ship name]
=/ =index:graph-store
(turn t.t.t.link (curr rash dim:ag))
`[resource index]
|= upd=update:hark-store
^- (quip update _notifications)
?+ -.upd `notifications
::
%more
=| upds=(list update)
|-
?~ more.upd [upds notifications]
=^ us notifications
(filter-notifications i.more.upd)
$(upds (welp upds us), more.upd t.more.upd)
::
%read-count
=/ uids ~(tap in (uids-for-place place.upd))
=| upds=(list update)
|-
?~ uids
[upds notifications]
%_ $
notifications (~(del by notifications) i.uids)
upds :_(upds [i.uids %dismiss])
uids t.uids
==
::
%add-note
=/ note=notification +.upd
?. (should-notify note) `notifications
=/ =uid (shas %notify-uid eny.bowl)
:_ (~(put by notifications) uid note)
[uid %notify]~
==
::
++ should-notify
|= note=notification
^- ?
?. ?=([%graph @ @ *] path.place.bin.note)
|
=/ s=(unit ship) (slaw %p i.t.path.place.bin.note)
?~ s |
=/ =resource:resource
[u.s i.t.t.path.place.bin.note]
?& ?=(%landscape desk.place.bin.note)
?| ?=([%graph-validator-dm *] link.body.note)
?& (group-is-hidden resource)
?=([%graph-validator-chat *] link.body.note)
==
== ==
::
++ uids-for-place
|= =place:hark
%- ~(gas in *(set uid))
%+ murn ~(tap by notifications)
|= [=uid =notification]
^- (unit ^uid)
?. =(place.bin.notification place) ~
`uid
::
++ group-is-hidden
|= =resource:resource
^- (unit ?)
^- ?
=/ grp=(unit group:group) (~(scry-group group bowl) resource)
?~ grp ~
`hidden.u.grp
?~ grp |
hidden.u.grp
::
++ is-whitelisted
|= [who=@p entry=provider-entry]
@ -357,6 +428,12 @@
++ post-form
|= [=wire url=@t auth=@t params=(list [@t @t])]
^- card
=/ esc=$-(@t @t)
|=(t=@t (crip (en-urlt:html (trip t))))
=. params
%+ turn params
|= [p=@t q=@t]
[(esc p) (esc q)]
=/ data
%+ roll
%+ sort params
@ -385,16 +462,12 @@
(rap 3 out '&' p '=' q ~)
::
++ send-notification
|= [entry=provider-entry who=@p =notification]
|= [entry=provider-entry who=@p =update]
^- card
=/ params=(list [@t @t])
:~ identity+(rsh [3 1] (scot %p who))
ship+(rsh [3 1] (scot %p entity.resource.notification))
graph+name.resource.notification
:- %node
%+ roll index.notification
|= [in=@ out=@t]
(rap 3 out '/' (scot %ud in) ~)
action+`@t`action.update
uid+(scot %ux uid.update)
==
%: post-form
/send-notification/(scot %uv (sham eny.bowl))

View File

@ -1,9 +1,10 @@
:~ title+'Groups'
info+'A suite of applications to communicate on Urbit'
color+0xee.5432
glob-http+['https://bootstrap.urbit.org/glob-0v2.fn2uu.2iu5q.ddmsu.01br7.8uaft.glob' 0v2.fn2uu.2iu5q.ddmsu.01br7.8uaft]
glob-http+['https://bootstrap.urbit.org/glob-0v3.m2nd4.9tg9d.vs9ls.9rj6u.7lqhg.glob' 0v3.m2nd4.9tg9d.vs9ls.9rj6u.7lqhg]
base+'landscape'
version+[1 0 2]
version+[1 0 4]
website+'https://tlon.io'
license+'MIT'
==

View File

@ -10,5 +10,5 @@
=/ who (scot %p ship)
::
.^ update:graph-store
/gx/[our]/graph-store/[wen]/archive/[who]/[graph]/graph-update
/gx/[our]/graph-store/[wen]/archive/[who]/[graph]/graph-update-3
==

View File

@ -13,9 +13,10 @@
:~ create+create
remove+remove
join+join
abort+dejs-path:resource
leave+leave
invite+invite
hide+dejs-path:resource
done+dejs-path:resource
==
::
++ create
@ -34,6 +35,9 @@
%- ot
:~ resource+dejs:resource
ship+(su ;~(pfix sig fed:ag))
app+(su (perk %groups %graph ~))
'shareContact'^bo
autojoin+bo
==
::
++ invite
@ -74,10 +78,13 @@
++ request
|= req=^request
%- pairs
:~ hidden+b+hidden.req
started+(time started.req)
:~ started+(time started.req)
ship+(ship ship.req)
progress+s+progress.req
'shareContact'^b+share-co.req
autojoin+b+autojoin.req
app+s+`@t`app.req
invite+a+(turn ~(tap in invite.req) (cork (cury scot %ux) (lead %s)))
==
::
++ initial

View File

@ -0,0 +1 @@
../../garden-dev/lib/hark-store.hoon

View File

@ -13,7 +13,8 @@
=/ members
~(wyt in (members:grp rid))
=/ =metadatum:store
(need (peek-metadatum %groups rid))
?^ met=(peek-metadatum %groups rid) u.met
(need (peek-metadatum %graph rid))
[rid channels members channel-count metadatum]
::
++ channels

View File

@ -319,12 +319,18 @@
|= =path
^- [(list card:agent:gall) agent:gall]
?> (team:title our.bowl src.bowl)
?. ?=([%tracking ~] path)
?+ path
:: forward by default
=^ cards pull-hook
(on-watch:og path)
[cards this]
:_ this
~[give-update]
::
[%nack ~] `this
::
[%tracking ~]
:_ this
~[give-update]
==
::
++ on-agent
|= [=wire =sign:agent:gall]
@ -455,7 +461,8 @@
|= tan=(unit tang)
?~ tan tr-core
?. versioned
(tr-ap-og:tr-cleanup |.((on-pull-nack:og rid u.tan)))
%- tr-ap-og:tr-cleanup:tr-give-nack
|.((on-pull-nack:og rid u.tan))
%- (slog leaf+"versioned nack for {<rid>} in {<dap.bowl>}" u.tan)
=/ pax
(kick-mule:virt rid |.((on-pull-kick:og rid)))
@ -569,6 +576,9 @@
:: +| %subscription: subscription cards
::
::
++ tr-give-nack
(tr-emit (fact:io resource+!>(rid) /nack ~))
::
++ tr-ver-wire
(make-wire /version)
::

View File

@ -0,0 +1,20 @@
/+ *hark-store
|%
+$ note [=bin =body]
--
|_ not=note
++ grad %noun
++ grow
|%
++ noun not
++ json
%- pairs:enjs:format
:~ bin+(bin:enjs bin.not)
body+(body:enjs body.not)
==
--
++ grab
|%
++ noun note
--
--

View File

@ -1,12 +1,27 @@
/- *resource, *group
^?
|%
+$ app ?(%graph %groups)
+$ uid @uvH
::
:: $request: State of a join request
::
:: .started: Time request first sent
:: .ship: Host of group
:: .progress: Progress of request
:: .share-co: Automatically share contact?
:: .autojoin: Automatically join graphs
:: .app: Whether we're joining a group or a graph
:: .invite: Associated invites
::
+$ request
$: hidden=?
started=time
$: started=time
=ship
=progress
=app
share-co=?
autojoin=?
invite=(set uid)
==
::
+$ action
@ -14,20 +29,38 @@
[%create name=term =policy title=@t description=@t]
[%remove =resource]
:: client side
[%join =resource =ship]
$: %join
=resource
=ship
=app
share-contact=?
autojoin=?
==
[%abort =resource]
[%leave =resource]
::
[%invite =resource ships=(set ship) description=@t]
:: pending ops
[%hide =resource]
[%done =resource]
==
:: $progress: state of a join request
::
:: %start: Waiting on add poke to succeed
:: %added: Waiting on groups
:: %metadata: Waiting on metadata
:: final: Join request succeeded/errors
+$ progress
?(%start %added final)
?(%start %added %metadata final)
::
:: $final: resolution of a join request
::
:: %no-perms: Failed, did not have permissions
:: %abort: Join request manually aborted
:: %strange: Failed unexpectedly
:: %done: Succeeded
::
+$ final
?(%no-perms %strange %done)
?(%no-perms %abort %strange %done)
::
+$ update
$% [%initial initial=(map resource request)]

View File

@ -1,4 +1,4 @@
/- resource, graph-store
/- hark=hark-store, resource, graph-store
|%
+$ provider-action
$% [%add service=term notify=@t binding=@t auth-token=@t =whitelist]
@ -12,10 +12,10 @@
[%remove-provider who=@p service=term]
==
::
+$ uid @uvH
::
+$ notification
$: =resource:resource
=index:graph-store
==
[=bin:hark =body:hark]
::
+$ whitelist
$: public=?
@ -24,7 +24,8 @@
groups=(set resource:resource)
==
::
+$ action ?(%notify %dismiss)
::
+$ update
$% [%notification =notification]
==
[=uid =action]
--

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