Merge branch 'next/vere' into jo/khan-c3

This commit is contained in:
Jōshin 2021-12-23 07:18:09 +00:00
commit 3cd1a697ef
No known key found for this signature in database
GPG Key ID: A8BE5A9A521639D0
161 changed files with 78305 additions and 39291 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

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

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

@ -2,7 +2,6 @@ resolver: lts-16.15
packages:
- natpmp-static
- proto
- racquire
- terminal-progress-bar
- urbit-atom

View File

@ -6,12 +6,10 @@
-}
module Urbit.King.CLI where
import ClassyPrelude hiding (log)
import Urbit.Prelude hiding (log, Parser)
import Options.Applicative
import Options.Applicative.Help.Pretty
import Data.Word (Word16)
import RIO (LogLevel(..))
import System.Environment (getProgName)
--------------------------------------------------------------------------------
@ -253,9 +251,9 @@ serfExe = optional
ethNode :: Parser String
ethNode = strOption
$ short 'e'
<> long "eth-node"
<> value "http://eth-mainnet.urbit.org:8545"
<> help "Ethereum gateway URL"
<> long "l2-endpoint"
<> value "https://l2.urbit.org/v1/azimuth" --TODO
<> help "Azimuth Layer 2 RPC API endpoint URL"
<> hidden
new :: Parser New

View File

@ -3,9 +3,8 @@
-}
module Urbit.King.TryJamPill where
import ClassyPrelude
import Control.Lens
import Urbit.Noun
import Urbit.Prelude
--------------------------------------------------------------------------------

View File

@ -2,31 +2,63 @@ module Urbit.Vere.Ames.LaneCache (cache) where
import Urbit.Prelude
import qualified Data.HashPSQ as P
import Urbit.Noun.Time
expiry :: Gap
expiry = (2 * 60) ^. from secs
bound :: Int
bound = 1_000
-- | An "upside down" time for use as a priority.
newtype New = New Wen
deriving newtype (Eq, Ord)
new :: Wen -> New
new = New . negate
wen :: New -> Wen
wen (New w) = negate w
-- | Given a new, find an older new corresponding to `expiry` ago.
lag :: New -> New
lag n = new (addGap (wen n) expiry)
trim :: (Hashable a, Ord a, Ord b) => P.HashPSQ a b c -> P.HashPSQ a b c
trim p = if P.size p > bound then P.deleteMin p else p
cache :: forall a b m n
. (Ord a, MonadIO m, MonadIO n)
. (Ord a, Hashable a, MonadIO m, MonadIO n)
=> (a -> m b)
-> n (a -> m b)
cache act = do
cas <- newTVarIO (mempty :: Map a (Wen, b))
cas <- newTVarIO (P.empty :: P.HashPSQ a New b)
let fun x = lookup x <$> readTVarIO cas >>= \case
let fun x = P.lookup x <$> readTVarIO cas >>= \case
Nothing -> thru
Just (t, v) -> do
Just (n, v) -> do
let t = wen n
t' <- io now
if gap t' t > expiry
then thru
else pure v
where
-- Insert a key into the map, simultaneously removing *all* stale
-- entries. Since insertion is linear in the size of the map,
-- presumably it's not horrible to do it this way. The alternative
-- would be to have a thread doing a purge every 10s or something, and
-- then we'd have to be in RAcquire.
up :: a -> New -> b -> P.HashPSQ a New b -> P.HashPSQ a New b
up k n v ps = trim
$ P.insert k n v
$ snd $ P.atMostView (lag n) ps
thru :: m b
thru = do
t <- io now
n <- new <$> io now
v <- act x
atomically $ modifyTVar' cas (insertMap x (t, v))
atomically $ modifyTVar' cas $ up x n v
pure v
pure fun

View File

@ -158,8 +158,8 @@ instance Serialize Packet where
putByteString body
where
putShipGetRank s@(Ship (LargeKey p q)) = case () of
_ | s < 2 ^ 16 -> (0, putWord16le $ fromIntegral s) -- lord
| s < 2 ^ 32 -> (1, putWord32le $ fromIntegral s) -- planet
| s < 2 ^ 64 -> (2, putWord64le $ fromIntegral s) -- moon
| otherwise -> (3, putWord64le p >> putWord64le q) -- comet
putShipGetRank (Ship (LargeKey p q)) = case q of
0 | p < 2 ^ 16 -> (0, putWord16le $ fromIntegral p) -- lord
| p < 2 ^ 32 -> (1, putWord32le $ fromIntegral p) -- planet
| otherwise -> (2, putWord64le $ fromIntegral p) -- moon
_ -> (3, putWord64le p >> putWord64le q) -- comet

View File

@ -1,5 +1,5 @@
{-|
Use etherium to access PKI information.
Use L2 to access PKI information.
-}
module Urbit.Vere.Dawn ( dawnVent
@ -19,12 +19,13 @@ import Urbit.Arvo.Common
import Urbit.Arvo.Event hiding (Address)
import Urbit.Prelude hiding (rights, to, (.=))
import Prelude (read)
import Data.Bits (xor)
import Data.List (nub)
import Data.Text (splitOn)
import Data.Aeson
import Data.HexString
import Numeric (showHex)
import qualified Crypto.Hash.SHA256 as SHA256
import qualified Crypto.Hash.SHA512 as SHA512
@ -39,19 +40,12 @@ import qualified Urbit.Ob as Ob
import qualified Network.HTTP.Client.TLS as TLS
import qualified Network.HTTP.Types as HT
-- The address of the azimuth contract as a string.
azimuthAddr :: Text
azimuthAddr = "0x223c067f8cf28ae173ee5cafea60ca44c335fecb"
-- Conversion Utilities --------------------------------------------------------
passFromBS :: ByteString -> ByteString -> ByteString -> Pass
passFromBS enc aut sut
| bytesAtom sut /= 1 = Pass (Ed.PublicKey mempty) (Ed.PublicKey mempty)
| otherwise = Pass (Ed.PublicKey aut) (Ed.PublicKey enc)
bsToBool :: ByteString -> Bool
bsToBool bs = bytesAtom bs == 1
passFromBytes :: ByteString -> ByteString -> Int -> Pass
passFromBytes enc aut sut
| sut /= 1 = Pass (Ed.PublicKey mempty) (Ed.PublicKey mempty)
| otherwise = Pass (Ed.PublicKey aut) (Ed.PublicKey enc)
clanFromShip :: Ship -> Ob.Class
clanFromShip = Ob.clan . Ob.patp . fromIntegral
@ -62,10 +56,6 @@ shipSein = Ship . fromIntegral . Ob.fromPatp . Ob.sein . Ob.patp . fromIntegral
renderShip :: Ship -> Text
renderShip = Ob.renderPatp . Ob.patp . fromIntegral
hexStrToAtom :: Text -> Atom
hexStrToAtom =
bytesAtom . reverse . toBytes . hexString . removePrefix . encodeUtf8
onLeft :: (a -> b) -> Either a c -> Either b c
onLeft fun = bimap fun id
@ -84,18 +74,8 @@ ringToPass Ring{..} = Pass{..}
-- JSONRPC Functions -----------------------------------------------------------
-- The big problem here is that we can't really use the generated web3 wrappers
-- around the azimuth contracts, especially for the galaxy table request. They
-- make multiple rpc invocations per galaxy request (which aren't even
-- batched!), while Vere built a single batched rpc call to fetch the entire
-- galaxy table.
--
-- The included Network.JsonRpc.TinyClient that Network.Web3 embeds can't do
-- batches, so calling that directly is out.
--
-- Network.JSONRPC appears to not like something about the JSON that Infura
-- returns; it's just hanging? Also no documentation.
--
-- Our use case here is simple enough.
-- Network.JSONRPC appeared fragile and has no documentation.
-- So, like with Vere, we roll our own.
dawnSendHTTP :: String -> L.ByteString -> RIO e (Either Int L.ByteString)
@ -122,24 +102,25 @@ dawnSendHTTP endpoint requestData = liftIO do
class RequestMethod m where
getRequestMethod :: m -> Text
data RawResponse = RawResponse
data RawResponse r = RawResponse
{ rrId :: Int
, rrResult :: Text
, rrResult :: r
}
deriving (Show)
instance FromJSON RawResponse where
instance FromJSON r => FromJSON (RawResponse r) where
parseJSON = withObject "Response" $ \v -> do
rrId <- v .: "id"
rawId <- v .: "id"
rrResult <- v .: "result"
let rrId = read rawId
pure RawResponse{..}
-- Given a list of methods and parameters, return a list of decoded responses.
dawnPostRequests :: forall req e resp
. (ToJSON req, RequestMethod req)
dawnPostRequests :: forall req e resp res
. (ToJSON req, RequestMethod req, FromJSON res)
=> String
-> (req -> Text -> resp)
-> (req -> res -> resp)
-> [req]
-> RIO e [resp]
dawnPostRequests endpoint responseBuilder requests = do
@ -168,122 +149,114 @@ dawnPostRequests endpoint responseBuilder requests = do
toFullRequest (rid, req) = object [ "jsonrpc" .= ("2.0" :: Text)
, "method" .= getRequestMethod req
, "params" .= req
, "id" .= rid
, "id" .= (show rid)
]
-- Azimuth JSON Requests -------------------------------------------------------
-- Not a full implementation of the Ethereum ABI, but just the ability to call
-- a method by encoded id (like 0x63fa9a87 for `points(uint32)`), and a single
-- UIntN 32 parameter.
encodeCall :: Text -> Int -> Text
encodeCall method idx = method <> leadingZeroes <> renderedNumber
where
renderedNumber = pack $ showHex idx ""
leadingZeroes = replicate (64 - length renderedNumber) '0'
data PointResponse = PointResponse
-- NOTE also contains dominion and ownership, but not actually used here
{ prNetwork :: PointNetwork
} deriving (Show, Eq, Generic)
data BlockRequest = BlockRequest
deriving (Show, Eq)
instance FromJSON PointResponse where
parseJSON = withObject "PointResponse" $ \o -> do
prNetwork <- o .: "network"
pure PointResponse{..}
instance RequestMethod BlockRequest where
getRequestMethod BlockRequest = "eth_blockNumber"
data PointNetwork = PointNetwork
{ pnKeys :: PointKeys
, pnSponsor :: PointSponsor
, pnRift :: ContNum
} deriving (Show, Eq, Generic)
instance ToJSON BlockRequest where
toJSON BlockRequest = Array $ fromList []
instance FromJSON PointNetwork where
parseJSON = withObject "PointNetwork" $ \o -> do
pnKeys <- o .: "keys"
pnSponsor <- o .: "sponsor"
pnRift <- o .: "rift"
pure PointNetwork{..}
-- No need to parse, it's already in the format we'll pass as an argument to
-- eth calls which take a block number.
parseBlockRequest :: BlockRequest -> Text -> TextBlockNum
parseBlockRequest _ txt = txt
data PointKeys = PointKeys
{ pkLife :: Life
, pkSuite :: Int
, pkAuth :: ByteString
, pkCrypt :: ByteString
} deriving (Show, Eq, Generic)
type TextBlockNum = Text
instance FromJSON PointKeys where
parseJSON = withObject "PointKeys" $ \o -> do
pkLife <- o .: "life"
pkSuite <- o .: "suite"
rawAuth <- o .: "auth"
rawCrypt <- o .: "crypt"
let pkAuth = parseKey rawAuth
let pkCrypt = parseKey rawCrypt
pure PointKeys{..}
where
parseKey = reverse . toBytes . hexString . removePrefix . encodeUtf8
data PointRequest = PointRequest
{ grqHexBlockNum :: TextBlockNum
, grqPointId :: Int
} deriving (Show, Eq)
data PointSponsor = PointSponsor
{ psHas :: Bool
, psWho :: Text
} deriving (Show, Eq, Generic)
instance FromJSON PointSponsor where
parseJSON = withObject "PointSponsor" $ \o -> do
psHas <- o .: "has"
psWho <- o .: "who"
pure PointSponsor{..}
data PointRequest = PointRequest Ship
instance RequestMethod PointRequest where
getRequestMethod PointRequest{..} = "eth_call"
getRequestMethod PointRequest{} = "getPoint"
instance ToJSON PointRequest where
-- 0x63fa9a87 is the points(uint32) call.
toJSON PointRequest{..} =
Array $ fromList [object [ "to" .= azimuthAddr
, "data" .= encodeCall "0x63fa9a87" grqPointId],
String grqHexBlockNum
]
toJSON (PointRequest point) = object [ "ship" .= renderShip point ]
parseAndChunkResultToBS :: Text -> [ByteString]
parseAndChunkResultToBS result =
map reverse $
chunkBytestring 32 $
toBytes $
hexString $
removePrefix $
encodeUtf8 result
-- The incoming result is a text bytestring. We need to take that text, and
-- spit out the parsed data.
--
-- We're sort of lucky here. After removing the front "0x", we can just chop
-- the incoming text string into 10 different 64 character chunks and then
-- parse them as numbers.
parseEthPoint :: PointRequest -> Text -> EthPoint
parseEthPoint PointRequest{..} result = EthPoint{..}
parseAzimuthPoint :: PointRequest -> PointResponse -> EthPoint
parseAzimuthPoint (PointRequest point) response = EthPoint{..}
where
[rawEncryptionKey,
rawAuthenticationKey,
rawHasSponsor,
rawActive,
rawEscapeRequested,
rawSponsor,
rawEscapeTo,
rawCryptoSuite,
rawKeyRevision,
rawContinuityNum] = parseAndChunkResultToBS result
escapeState = if bsToBool rawEscapeRequested
then Just $ Ship $ fromIntegral $ bytesAtom rawEscapeTo
else Nothing
net = prNetwork response
key = pnKeys net
-- Vere doesn't set ownership information, neither did the old Dawn.hs
-- implementation.
epOwn = (0, 0, 0, 0)
epNet = if not $ bsToBool rawActive
sponsorShip = Ob.parsePatp $ psWho $ pnSponsor net
epNet = if pkLife key == 0
then Nothing
else Just
( fromIntegral $ bytesAtom rawKeyRevision
, passFromBS rawEncryptionKey rawAuthenticationKey rawCryptoSuite
, fromIntegral $ bytesAtom rawContinuityNum
, (bsToBool rawHasSponsor,
Ship (fromIntegral $ bytesAtom rawSponsor))
, escapeState
)
else case sponsorShip of
Left _ -> Nothing
Right s -> Just
( fromIntegral $ pkLife key
, passFromBytes (pkCrypt key) (pkAuth key) (pkSuite key)
, fromIntegral $ pnRift net
, (psHas $ pnSponsor net, Ship $ fromIntegral $ Ob.fromPatp s)
, Nothing -- NOTE goes unused currently, so we simply put Nothing
)
-- I don't know what this is supposed to be, other than the old Dawn.hs and
-- dawn.c do the same thing.
epKid = case clanFromShip (Ship $ fromIntegral grqPointId) of
-- zero-fill spawn data
epKid = case clanFromShip (Ship $ fromIntegral point) of
Ob.Galaxy -> Just (0, setToHoonSet mempty)
Ob.Star -> Just (0, setToHoonSet mempty)
_ -> Nothing
-- Preprocess data from a point request into the form used in the galaxy table.
parseGalaxyTableEntry :: PointRequest -> Text -> (Ship, (Rift, Life, Pass))
parseGalaxyTableEntry PointRequest{..} result = (ship, (rift, life, pass))
parseGalaxyTableEntry :: PointRequest -> PointResponse -> (Ship, (Rift, Life, Pass))
parseGalaxyTableEntry (PointRequest point) response = (ship, (rift, life, pass))
where
[rawEncryptionKey,
rawAuthenticationKey,
_, _, _, _, _,
rawCryptoSuite,
rawKeyRevision,
rawContinuityNum] = parseAndChunkResultToBS result
net = prNetwork response
keys = pnKeys net
ship = Ship $ fromIntegral grqPointId
rift = fromIntegral $ bytesAtom rawContinuityNum
life = fromIntegral $ bytesAtom rawKeyRevision
pass = passFromBS rawEncryptionKey rawAuthenticationKey rawCryptoSuite
ship = Ship $ fromIntegral point
rift = fromIntegral $ pnRift net
life = fromIntegral $ pkLife keys
pass = passFromBytes (pkCrypt keys) (pkAuth keys) (pkSuite keys)
removePrefix :: ByteString -> ByteString
removePrefix withOhEx
@ -292,54 +265,32 @@ removePrefix withOhEx
where
(prefix, suffix) = splitAt 2 withOhEx
chunkBytestring :: Int -> ByteString -> [ByteString]
chunkBytestring size bs
| null rest = [cur]
| otherwise = (cur : chunkBytestring size rest)
where
(cur, rest) = splitAt size bs
data TurfRequest = TurfRequest
{ trqHexBlockNum :: TextBlockNum
, trqTurfId :: Int
} deriving (Show, Eq)
instance RequestMethod TurfRequest where
getRequestMethod TurfRequest{..} = "eth_call"
getRequestMethod TurfRequest = "getDns"
instance ToJSON TurfRequest where
-- 0xeccc8ff1 is the dnsDomains(uint32) call.
toJSON TurfRequest{..} =
Array $ fromList [object [ "to" .= azimuthAddr
, "data" .= encodeCall "0xeccc8ff1" trqTurfId],
String trqHexBlockNum
]
toJSON TurfRequest = object [] -- NOTE getDns takes no parameters
-- This is another hack instead of a full Ethereum ABI response.
parseTurfResponse :: TurfRequest -> Text -> Turf
parseTurfResponse a raw = turf
where
without0x = removePrefix $ encodeUtf8 raw
(_, blRest) = splitAt 64 without0x
(utfLenStr, utfStr) = splitAt 64 blRest
utfLen = fromIntegral $ bytesAtom $ reverse $ toBytes $ hexString utfLenStr
dnsStr = decodeUtf8 $ BS.take utfLen $ toBytes $ hexString utfStr
turf = Turf $ fmap Cord $ reverse $ splitOn "." dnsStr
parseTurfResponse :: TurfRequest -> [Text] -> [Turf]
parseTurfResponse TurfRequest = map turf
where
turf t = Turf $ fmap Cord $ reverse $ splitOn "." t
-- Azimuth Functions -----------------------------------------------------------
retrievePoint :: String -> TextBlockNum -> Ship -> RIO e EthPoint
retrievePoint endpoint block ship =
dawnPostRequests endpoint parseEthPoint
[PointRequest block (fromIntegral ship)] >>= \case
retrievePoint :: String -> Ship -> RIO e EthPoint
retrievePoint endpoint ship =
dawnPostRequests endpoint parseAzimuthPoint [PointRequest ship]
>>= \case
[x] -> pure x
_ -> error "JSON server returned multiple return values."
validateFeedAndGetSponsor :: String
-> TextBlockNum
-> Feed
-> RIO e (Seed, Ship)
validateFeedAndGetSponsor endpoint block = \case
validateFeedAndGetSponsor endpoint = \case
Feed0 s -> do
r <- validateSeed s
case r of
@ -387,7 +338,7 @@ validateFeedAndGetSponsor endpoint block = \case
putStrLn ("boot: retrieving " <> renderShip ship <> "'s public keys")
--TODO could cache this lookup
whoP <- retrievePoint endpoint block ship
whoP <- retrievePoint endpoint ship
case epNet whoP of
Nothing -> pure $ Left "ship not keyed"
Just (netLife, pass, contNum, (hasSponsor, who), _) -> do
@ -396,7 +347,7 @@ validateFeedAndGetSponsor endpoint block = \case
show life <> ", but Azimuth claims life " <>
show netLife)
else if ((ringToPass ring) /= pass) then
pure $ Left "keyfile does not match blockchain"
pure $ Left "keyfile does not match Azimuth"
-- TODO: The hoon code does a breach check, but the C code never
-- supplies the data necessary for it to function.
else
@ -404,13 +355,13 @@ validateFeedAndGetSponsor endpoint block = \case
-- Walk through the sponsorship chain retrieving the actual sponsorship chain
-- as it exists on Ethereum.
getSponsorshipChain :: String -> TextBlockNum -> Ship -> RIO e [(Ship,EthPoint)]
getSponsorshipChain endpoint block = loop
-- as it exists on Azimuth.
getSponsorshipChain :: String -> Ship -> RIO e [(Ship,EthPoint)]
getSponsorshipChain endpoint = loop
where
loop ship = do
putStrLn ("boot: retrieving keys for sponsor " <> renderShip ship)
ethPoint <- retrievePoint endpoint block ship
ethPoint <- retrievePoint endpoint ship
case (clanFromShip ship, epNet ethPoint) of
(Ob.Comet, _) -> error "Comets cannot be sponsors"
@ -433,32 +384,29 @@ dawnVent :: HasLogFunc e => String -> Feed -> RIO e (Either Text Dawn)
dawnVent provider feed =
-- The type checker can't figure this out on its own.
(onLeft tshow :: Either SomeException Dawn -> Either Text Dawn) <$> try do
putStrLn ("boot: requesting ethereum information from " <> pack provider)
blockResponses
<- dawnPostRequests provider parseBlockRequest [BlockRequest]
hexStrBlock <- case blockResponses of
[num] -> pure num
x -> error "Unexpected multiple returns from block # request"
let dBloq = hexStrToAtom hexStrBlock
putStrLn ("boot: ethereum block #" <> tshow dBloq)
putStrLn ("boot: requesting L2 Azimuth information from " <> pack provider)
(dSeed, immediateSponsor)
<- validateFeedAndGetSponsor provider hexStrBlock feed
dSponsor <- getSponsorshipChain provider hexStrBlock immediateSponsor
<- validateFeedAndGetSponsor provider feed
dSponsor <- getSponsorshipChain provider immediateSponsor
putStrLn "boot: retrieving galaxy table"
dCzar <- (mapToHoonMap . mapFromList) <$>
(dawnPostRequests provider parseGalaxyTableEntry $
map (PointRequest hexStrBlock) [0..255])
(dawnPostRequests provider parseGalaxyTableEntry (map (PointRequest . Ship . fromIntegral) [0..255]))
putStrLn "boot: retrieving network domains"
dTurf <- nub <$> (dawnPostRequests provider parseTurfResponse $
map (TurfRequest hexStrBlock) [0..2])
dTurf <- (dawnPostRequests provider parseTurfResponse [TurfRequest])
>>= \case
[] -> pure []
[t] -> pure (nub t)
_ -> error "too many turf responses"
let dNode = Nothing
-- NOTE blocknum of 0 is fine because jael ignores it.
-- should probably be removed from dawn event.
let dBloq = 0
pure MkDawn{..}

View File

@ -4,9 +4,8 @@
module Urbit.Vere.Http where
import ClassyPrelude
import Urbit.Prelude
import Urbit.Arvo
import Urbit.Noun
import qualified Data.CaseInsensitive as CI
import qualified Network.HTTP.Types as HT

View File

@ -278,11 +278,11 @@ pier (serf, log) vSlog startedSig injected = do
-- TODO Instead of using a TMVar, pull directly from the IO driver
-- event sources.
computeQ :: TMVar RunReq <- newEmptyTMVarIO
persistQ :: TQueue (Fact, FX) <- newTQueueIO
executeQ :: TQueue FX <- newTQueueIO
saveSig :: TMVar () <- newEmptyTMVarIO
kingApi :: King.King <- King.kingAPI
computeQ :: TMVar RunReq <- newEmptyTMVarIO
persistQ :: TBQueue (Fact, FX) <- newTBQueueIO 10 -- TODO tuning?
executeQ :: TBQueue FX <- newTBQueueIO 10
saveSig :: TMVar () <- newEmptyTMVarIO
kingApi :: King.King <- King.kingAPI
termApiQ :: TQueue TermConn <- atomically $ do
q <- newTQueue
@ -310,8 +310,8 @@ pier (serf, log) vSlog startedSig injected = do
-- the c serf code. Logging output from our haskell process must manually
-- add them.
let compute = putTMVar computeQ
let execute = writeTQueue executeQ
let persist = writeTQueue persistQ
let execute = writeTBQueue executeQ
let persist = writeTBQueue persistQ
let sigint = Serf.sendSIGINT serf
let scry = \g r -> do
res <- newEmptyMVar
@ -378,7 +378,7 @@ pier (serf, log) vSlog startedSig injected = do
fn (0, textToTank txt)
drivz <- startDrivers
tExec <- acquireWorker "Effects" (router slog (readTQueue executeQ) drivz)
tExec <- acquireWorker "Effects" (router slog (readTBQueue executeQ) drivz)
tDisk <- acquireWorkerBound "Persist" (runPersist log persistQ execute)
-- Now that the Serf is configured, the IO drivers are hooked up, their
@ -667,12 +667,14 @@ runPersist
:: forall e
. HasPierEnv e
=> EventLog
-> TQueue (Fact, FX)
-> TBQueue (Fact, FX)
-> (FX -> STM ())
-> RIO e ()
runPersist log inpQ out = do
dryRun <- view dryRunL
forever $ do
-- This is not a memory leak because eventually the TBQueue at out will
-- fill up, blocking the loop.
writs <- atomically getBatchFromQueue
events <- validateFactsAndGetBytes (fst <$> toNullable writs)
unless dryRun (Log.appendEvents log events)
@ -690,9 +692,11 @@ runPersist log inpQ out = do
pure $ buildLogEvent mug $ toNoun (wen, non)
pure (fromList lis)
-- Read as much out of the queue as possible (i.e. the entire contents),
-- blocking if empty.
getBatchFromQueue :: STM (NonNull [(Fact, FX)])
getBatchFromQueue = readTQueue inpQ >>= go . singleton
getBatchFromQueue = readTBQueue inpQ >>= go . singleton
where
go acc = tryReadTQueue inpQ >>= \case
go acc = tryReadTBQueue inpQ >>= \case
Nothing -> pure (reverse acc)
Just item -> go (item <| acc)

View File

@ -566,7 +566,6 @@ localClient doneSignal = fst <$> mkRAcquire start stop
loop rd
else if w <= 26 then do
case pack [BS.w2c (w + 97 - 1)] of
"d" -> atomically doneSignal
c -> do sendBelt $ Ctl $ Cord c
loop rd
else if w == 27 then do

View File

@ -9,7 +9,7 @@ module Urbit.Vere.Term.Render
, soundBell
) where
import ClassyPrelude
import Urbit.Prelude
import qualified System.Console.ANSI as ANSI

View File

@ -79,6 +79,7 @@ dependencies:
- pretty-show
- primitive
- process
- psqueues
- QuickCheck
- racquire
- random

View File

@ -30,6 +30,7 @@ newtype Parser a = Parser {
runParser :: forall r. ParseStack -> Failure r -> Success a r -> r
}
{-# INLINE named #-} -- keep out of the cost centers
named :: Text -> Parser a -> Parser a
named nm (Parser cb) =
Parser $ \path kf ks -> cb (nm:path) kf ks

View File

@ -84,15 +84,15 @@ flush = Put $ \tbl s@S{..} -> do
{-# INLINE update #-}
update :: (S -> S) -> Put ()
update f = Put $ \tbl s@S{..} -> pure (PutResult (f s) ())
update f = Put $ \tbl s@S{} -> pure (PutResult (f s) ())
{-# INLINE setRegOff #-}
setRegOff :: Word -> Int -> Put ()
setRegOff r o = update $ \s@S{..} -> (s {reg=r, off=o})
setRegOff r o = update $ \s@S{} -> (s {reg=r, off=o})
{-# INLINE setReg #-}
setReg :: Word -> Put ()
setReg r = update $ \s@S{..} -> (s { reg=r })
setReg r = update $ \s@S{} -> (s { reg=r })
{-# INLINE getS #-}
getS :: Put S

View File

@ -110,9 +110,22 @@ deriveToNounFunc tyName = do
addErrTag :: String -> Exp -> Exp
addErrTag tag exp =
InfixE (Just $ AppE (VarE 'named) str) (VarE (mkName ".")) (Just exp)
-- This spurious let is inserted so we can get better cost center data
-- during heap profiling.
LetE [ValD (VarP nom) (NormalB nam) []]
$ InfixE (Just $ VarE nom) (VarE (mkName ".")) (Just exp)
where
-- XX arguably we should use newName rather than mkName here
nom = mkName $ "named_" ++ filter C.isAlphaNum tag
str = LitE $ StringL tag
nam = LamE [VarP $ mkName "x"] $ AppE (AppE (VarE 'named) str)
$ VarE (mkName "x")
addCostCenter :: String -> Exp -> Exp
addCostCenter tag exp =
LetE [ValD (VarP nom) (NormalB exp) []] (VarE nom)
where
nom = mkName $ "scc_" ++ filter C.isAlphaNum tag
deriveFromNoun :: Name -> Q [Dec]
deriveFromNoun tyName = do
@ -124,7 +137,8 @@ deriveFromNoun tyName = do
let ty = foldl' (\acc v -> AppT acc (VarT v)) (ConT tyName) params
let overlap = Nothing
body = NormalB (addErrTag (nameStr tyName) exp)
body = NormalB (addCostCenter nom $ addErrTag nom exp)
nom = nameStr tyName
ctx = params <&> \t -> AppT (ConT ''FromNoun) (VarT t)
inst = AppT (ConT ''FromNoun) ty
@ -214,12 +228,13 @@ taggedFromNoun cons = LamE [VarP n] (DoE [getHead, getTag, examine])
matches = mkMatch <$> cons
mkMatch = \(tag, (n, tys)) ->
let body = AppE (addErrTag ('%':tag) (tupFromNoun (n, tys)))
let body = addCostCenter tag
$ AppE (addErrTag ('%':tag) (tupFromNoun (n, tys)))
(VarE t)
in Match (LitP $ StringL tag) (NormalB body) []
fallback = Match WildP (NormalB $ AppE (VarE 'fail) matchFail) []
matchFail = unexpectedTag (fst <$> cons) (VarE c)
matchFail = addCostCenter "matchFail" $ unexpectedTag (fst <$> cons) (VarE c)
tagFail = LitE $ StringL (intercalate " " (('%':) <$> (fst <$> cons)))

View File

@ -1,3 +1,5 @@
{-# LANGUAGE StrictData #-}
{-|
Large Library of conversion between various types and Nouns.
-}
@ -29,7 +31,7 @@ import Urbit.Noun.Convert
import Urbit.Noun.Core
import Urbit.Noun.TH
import Data.LargeWord (LargeKey, Word128, Word256)
import Data.LargeWord (LargeKey(..), Word128, Word256)
import GHC.Exts (chr#, isTrue#, leWord#, word2Int#)
import GHC.Natural (Natural)
import GHC.Types (Char(C#))
@ -600,6 +602,9 @@ newtype Ship = Ship Word128 -- @p
instance Show Ship where
show = show . patp . fromIntegral
instance Hashable Ship where
hashWithSalt s (Ship (LargeKey a b)) = s `hashWithSalt` a `hashWithSalt` b
-- Path ------------------------------------------------------------------------

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,9 +13,9 @@
"@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": "^1.1.1",
"@urbit/api": "^1.4.0",
"@urbit/http-api": "^1.2.1",
"any-ascii": "^0.1.7",
"aws-sdk": "^2.830.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/dist';
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

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