diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 0f1200711..ed0ab9842 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,11 +1,8 @@ blank_issues_enabled: true contact_links: - - name: Landscape design issue - url: https://github.com/urbit/landscape/issues/new?assignees=&labels=design+issue&template=report-a-design-issue.md&title= - about: Submit non-functionality, design-specific issues to the Landscape team here. - - name: Landscape feature request - url: https://github.com/urbit/landscape/issues/new?assignees=&labels=feature+request&template=feature_request.md&title= - about: Landscape is comprised of Tlon's user applications and client for Urbit. Submit Landscape feature requests here. + - name: Submit a Landscape issue + url: https://github.com/urbit/landscape/issues/new/choose + about: Issues with Landscape (Tlon's flagship client) should be filed at urbit/landscape. This includes groups, chats, collections, notebooks, and more. - name: urbit-dev mailing list url: https://groups.google.com/a/urbit.org/g/dev about: Developer questions and discussions also take place on the urbit-dev mailing list. diff --git a/.github/ISSUE_TEMPLATE/os1-bug-report.md b/.github/ISSUE_TEMPLATE/os1-bug-report.md deleted file mode 100644 index b6800a7a7..000000000 --- a/.github/ISSUE_TEMPLATE/os1-bug-report.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -name: Landscape bug report -about: 'Use this template to file a bug for any Landscape app: Chat, Publish, Links, Groups, - Weather or Clock' -title: '' -labels: landscape -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. If possible, please also screenshot your browser's dev console. Here are [Chrome's docs](https://developers.google.com/web/tools/chrome-devtools/open) for using this feature. - -**Desktop (please complete the following information):** - - OS: [e.g. MacOS 10.15.3] - - Browser [e.g. chrome, safari] - - Base hash of your urbit ship. Run `+trouble` in Dojo to see this. - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Base hash of your urbit ship. Run `+trouble` in Dojo to see this. - -**Additional context** -Add any other context about the problem here. diff --git a/.github/actions/glob/Dockerfile b/.github/actions/glob/Dockerfile index 6e2a3833c..fda34e039 100644 --- a/.github/actions/glob/Dockerfile +++ b/.github/actions/glob/Dockerfile @@ -1,4 +1,4 @@ -FROM jaredtobin/janeway:v0.13.1 +FROM jaredtobin/janeway:v0.13.3 COPY entrypoint.sh /entrypoint.sh EXPOSE 22/tcp ENTRYPOINT ["/entrypoint.sh"] diff --git a/.github/actions/glob/entrypoint.sh b/.github/actions/glob/entrypoint.sh index ca01a4dce..cd9ecea22 100755 --- a/.github/actions/glob/entrypoint.sh +++ b/.github/actions/glob/entrypoint.sh @@ -10,13 +10,7 @@ chmod 600 service-account chmod 600 id_ssh chmod 600 id_ssh.pub -LANDSCAPE_STREAM="development" -export LANDSCAPE_STREAM - -LANDSCAPE_SHORTHASH="${GITHUB_SHA:0:7}" -export LANDSCAPE_SHORTHASH - -janeway release glob --no-pill \ +janeway release glob --dev --no-pill \ --credentials service-account \ --ssh-key id_ssh \ --do-it-live \ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7b89b59c5..b7b954227 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,19 @@ jobs: steps: - uses: actions/checkout@v2 + + # We only want the extra nix config on linux, where it is necessary + # 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@v12 + with: + extra_nix_config: | + system-features = nixos-test benchmark big-parallel kvm + if: ${{ matrix.os == 'ubuntu-latest' }} + - uses: cachix/install-nix-action@v12 + if: ${{ matrix.os != 'ubuntu-latest' }} + - uses: cachix/cachix-action@v8 with: name: ares @@ -58,6 +70,9 @@ jobs: - if: ${{ matrix.os == 'ubuntu-latest' }} run: nix-build -A urbit-tests + - if: ${{ matrix.os == 'ubuntu-latest' }} + run: nix-build -A docker-image + haskell: strategy: fail-fast: false diff --git a/.github/workflows/glob.yml b/.github/workflows/glob.yml index 8535724c1..3aabb75c9 100644 --- a/.github/workflows/glob.yml +++ b/.github/workflows/glob.yml @@ -3,9 +3,6 @@ on: push: branches: - 'release/next-js' - pull_request: - branches: - - 'release/next-js' jobs: glob: runs-on: ubuntu-latest diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml new file mode 100644 index 000000000..ff520c265 --- /dev/null +++ b/.github/workflows/merge.yml @@ -0,0 +1,17 @@ +name: merge +on: + push: + branches: + - 'master' +jobs: + merge-to-next-js: + runs-on: ubuntu-latest + name: "Merge master to release/next-js" + steps: + - uses: actions/checkout@v2 + - uses: devmasx/merge-branch@v1.3.1 + with: + type: now + target_branch: release/next-js + github_token: ${{ secrets.JANEWAY_BOT_TOKEN }} + diff --git a/.github/workflows/release-docker.yml b/.github/workflows/release-docker.yml new file mode 100644 index 000000000..bae515601 --- /dev/null +++ b/.github/workflows/release-docker.yml @@ -0,0 +1,51 @@ +name: release-docker + +on: + release: null + push: + tags: ['urbit-v*'] + +jobs: + upload: + strategy: + matrix: + include: + - { os: ubuntu-latest, system: x86_64-linux } + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v2 + - uses: cachix/install-nix-action@v12 + with: + extra_nix_config: | + system-features = nixos-test benchmark big-parallel kvm + - uses: cachix/cachix-action@v8 + with: + name: ares + authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} + - uses: docker/docker-login-action@v1.8.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - uses: christian-korneck/update-container-description-action@v1 + env: + DOCKER_USER: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKER_PASS: ${{ secrets.DOCKERHUB_TOKEN }} + with: + destination_container_repo: ${{ secrets.DOCKERHUB_USERNAME }}/urbit + provider: dockerhub + short_description: 'Urbit: a clean-slate OS and network for the 21st century' + readme_file: 'pkg/docker-image/README.md' + + - run: | + version="$(cat ./pkg/urbit/version)" + image="$(nix-build -A docker-image)" + imageName="$(nix-instantiate --eval -A docker-image.imageName | cut -d'"' -f2)" + imageTag="$(nix-instantiate --eval -A docker-image.imageTag | cut -d'"' -f2)" + # Load the image from the nix-built tarball + docker load -i $image + docker tag "$imageName:$imageTag" ${{secrets.DOCKERHUB_USERNAME }}/urbit:v$version + docker tag "$imageName:$imageTag" ${{secrets.DOCKERHUB_USERNAME }}/urbit:latest + docker push ${{secrets.DOCKERHUB_USERNAME }}/urbit:v$version + docker push ${{secrets.DOCKERHUB_USERNAME }}/urbit:latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 78a8c0103..db22e4b8d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: - uses: cachix/install-nix-action@v12 - uses: cachix/cachix-action@v8 with: - name: ares + name: ${{ secrets.CACHIX_NAME }} authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - uses: google-github-actions/setup-gcloud@v0.2.0 diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 85671118a..78b2eda4a 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -180,9 +180,9 @@ new fakezod with `urbit -F zod -B bin/solid.pill -A pkg/arvo`). Run `:glob|make`, and this will output a file in `fakezod/.urb/put/glob-0vXXX.glob`. Upload this file to bootstrap.urbit.org, and modify `+hash` at the top of -`pkg/arvo/app/glob.hoon` to match the hash in the filename of the `.glob` file. +`pkg/arvo/app/glob.hoon` to match the hash in the filename of the `.glob` file. Amend `pkg/arvo/app/landscape/index.html` to import the hashed JS bundle, instead -of the unversioned index.js. Do not commit the produced `index.js` and +of the unversioned index.js. Do not commit the produced `index.js` and make sure it doesn't end up in your pills (they should be less than 10MB each). ### Tag the resulting commit @@ -306,6 +306,13 @@ $ herb zod -p hood -d "+hood/merge %kids our %home" For Vere updates, this means simply shutting down each desired ship, installing the new binary, and restarting the pier with it. +#### Continuous deployment + +A subset of release branches are deployed continuously to the network. Thus far +this only includes `release/next-js`, which deploys livenet-compatible +JavaScript changes to select QA ships. Any push to master will automatically +merge master into `release/next-js` to keep the streams at parity. + ### Announce the update Post an announcement to urbit-dev. The tag annotation, basically, is fine here diff --git a/bin/solid.pill b/bin/solid.pill index 00dd9888e..254643bb2 100644 --- a/bin/solid.pill +++ b/bin/solid.pill @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17eb2f5a123f5ad29b0cc9ff9069540c349dd97c6133a9ea33cbf81e0bfa4d6b -size 8483784 +oid sha256:fd78b54d1c825f14b51c4dff001e17368953ecf9fc5a7b6792c059477761076a +size 9001760 diff --git a/default.nix b/default.nix index b0029f1f7..668b01702 100644 --- a/default.nix +++ b/default.nix @@ -115,6 +115,8 @@ let urbit = callPackage ./nix/pkgs/urbit { inherit enableStatic; }; + docker-image = callPackage ./nix/pkgs/docker-image { }; + hs = callPackage ./nix/pkgs/hs { inherit enableStatic; inherit (pkgsCross) haskell-nix; @@ -158,6 +160,8 @@ let }; }; + inherit (pkgsNative) skopeo; + # A convenience function for constructing a shell.nix for any of the # pkgsLocal derivations by automatically propagating any dependencies # to the nix-shell. diff --git a/nix/pkgs/docker-image/default.nix b/nix/pkgs/docker-image/default.nix new file mode 100644 index 000000000..467c06ac4 --- /dev/null +++ b/nix/pkgs/docker-image/default.nix @@ -0,0 +1,68 @@ +{ urbit, libcap, coreutils, bashInteractive, dockerTools, writeScriptBin, amesPort ? 34343 }: +let + startUrbit = writeScriptBin "start-urbit" '' + #!${bashInteractive}/bin/bash + + set -eu + + # If the container is not started with the `-i` flag + # then STDIN will be closed and we need to start + # Urbit/vere with the `-t` flag. + ttyflag="" + if [ ! -t 0 ]; then + echo "Running with no STDIN" + ttyflag="-t" + fi + + # Check if there is a keyfile, if so boot a ship with its name, and then remove the key + if [ -e *.key ]; then + # Get the name of the key + keynames="*.key" + keys=( $keynames ) + keyname=''${keys[0]} + mv $keyname /tmp + + # Boot urbit with the key, exit when done booting + urbit $ttyflag -w $(basename $keyname .key) -k /tmp/$keyname -c $(basename $keyname .key) -p ${toString amesPort} -x + + # Remove the keyfile for security + rm /tmp/$keyname + rm *.key || true + elif [ -e *.comet ]; then + cometnames="*.comet" + comets=( $cometnames ) + cometname=''${comets[0]} + rm *.comet + + urbit $ttyflag -c $(basename $cometname .comet) -p ${toString amesPort} -x + fi + + # Find the first directory and start urbit with the ship therein + dirnames="*/" + dirs=( $dirnames ) + dirname=''${dirnames[0]} + + urbit $ttyflag -p ${toString amesPort} $dirname + ''; + + +in dockerTools.buildImage { + name = "urbit"; + tag = "v${urbit.version}"; + contents = [ bashInteractive urbit startUrbit coreutils ]; + runAsRoot = '' + #!${bashInteractive} + mkdir -p /urbit + mkdir -p /tmp + ${libcap}/bin/setcap 'cap_net_bind_service=+ep' /bin/urbit + ''; + config = { + Cmd = [ "/bin/start-urbit" ]; + Env = [ "PATH=/bin" ]; + WorkingDir = "/urbit"; + Volumes = { + "/urbit" = {}; + }; + Expose = [ "80/tcp" "${toString amesPort}/udp" ]; + }; +} diff --git a/pkg/arvo/app/glob.hoon b/pkg/arvo/app/glob.hoon index bb1e27236..b9d80174b 100644 --- a/pkg/arvo/app/glob.hoon +++ b/pkg/arvo/app/glob.hoon @@ -5,7 +5,7 @@ /- glob /+ default-agent, verb, dbug |% -++ hash 0v5.hvt1e.ie7it.b7i7l.1r7jj.dn9ib +++ hash 0v7.9mc9i.jbk7p.smfcl.3aose.b6dat +$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))] +$ all-states $% state-0 diff --git a/pkg/arvo/app/graph-push-hook.hoon b/pkg/arvo/app/graph-push-hook.hoon index 3e5f36ac1..92076e355 100644 --- a/pkg/arvo/app/graph-push-hook.hoon +++ b/pkg/arvo/app/graph-push-hook.hoon @@ -92,27 +92,6 @@ %run-updates (is-allowed resource.q.update bowl %.y) == :: -++ resource-for-update - |= =vase - ^- (unit resource:res) - =/ =update:store !<(update:store vase) - ?- -.q.update - %add-graph `resource.q.update - %remove-graph `resource.q.update - %add-nodes `resource.q.update - %remove-nodes `resource.q.update - %add-signatures `resource.uid.q.update - %remove-signatures `resource.uid.q.update - %archive-graph `resource.q.update - %unarchive-graph ~ - %add-tag ~ - %remove-tag ~ - %keys ~ - %tags ~ - %tag-queries ~ - %run-updates `resource.q.update - == -:: ++ initial-watch |= [=path =resource:res] ^- vase diff --git a/pkg/arvo/app/graph-store.hoon b/pkg/arvo/app/graph-store.hoon index 46eb4cae1..12baed69c 100644 --- a/pkg/arvo/app/graph-store.hoon +++ b/pkg/arvo/app/graph-store.hoon @@ -230,7 +230,7 @@ ?> ?=(%0 -.update) =? p.update =(p.update *time) now.bowl ?- -.q.update - %add-graph (add-graph +.q.update) + %add-graph (add-graph p.update +.q.update) %remove-graph (remove-graph +.q.update) %add-nodes (add-nodes p.update +.q.update) %remove-nodes (remove-nodes p.update +.q.update) @@ -247,7 +247,8 @@ == :: ++ add-graph - |= $: =resource:store + |= $: =time + =resource:store =graph:store mark=(unit mark:store) overwrite=? @@ -258,9 +259,13 @@ !(~(has by graphs) resource) == == ?> (validate-graph graph mark) + =/ =logged-update:store + [%0 time %add-graph resource graph mark overwrite] + =/ =update-log:store + (gas:orm-log ~ [time logged-update] ~) :_ %_ state graphs (~(put by graphs) resource [graph mark]) - update-logs (~(put by update-logs) resource (gas:orm-log ~ ~)) + update-logs (~(put by update-logs) resource update-log) archive (~(del by archive) resource) :: validators @@ -418,43 +423,81 @@ =/ =update-log:store (~(got by update-logs) resource) =. update-log (put:orm-log update-log time [%0 time [%remove-nodes resource indices]]) + =/ [affected-indices=(set index:store) new-graph=graph:store] + (remove-indices resource graph (sort ~(tap in indices) by-lent)) :: - :- (give [/updates]~ [%remove-nodes resource indices]) + :- (give [/updates]~ [%remove-nodes resource (~(uni in indices) affected-indices)]) %_ state update-logs (~(put by update-logs) resource update-log) graphs %+ ~(put by graphs) resource - [(remove-indices resource graph ~(tap in indices)) mark] + [new-graph mark] == :: + :: we always want to remove the deepest node first, + :: so we don't remove parents before children + ++ by-lent + |* [a=(list) b=(list)] + ^- ? + (gth (lent a) (lent b)) + :: ++ remove-indices + =| affected=(set index:store) |= [=resource:store =graph:store indices=(list index:store)] - ^- graph:store - ?~ indices graph + ^- [(set index:store) graph:store] + ?~ indices [affected graph] + =^ new-affected graph + (remove-index graph i.indices) %_ $ indices t.indices - graph (remove-index graph i.indices) + affected (~(uni in affected) new-affected) + == + :: + ++ get-descendants + |= =graph:store + =| indices=(list index:store) + =/ nodes (tap:orm:store graph) + %- ~(gas in *(set index:store)) + |- =* tap-nodes $ + ^+ indices + %- zing + %+ turn nodes + |= [atom =node:store] + ^- (list index:store) + %+ welp + index.post.node^~ + ?. ?=(%graph -.children.node) + ~ + %_ tap-nodes + nodes (tap:orm p.children.node) == :: ++ remove-index + =| indices=(set index:store) |= [=graph:store =index:store] - ^- graph:store - ?~ index graph + ^- [(set index:store) graph:store] + ?~ index [indices graph] =* atom i.index :: last index in list :: ?~ t.index - +:`[* graph:store]`(del:orm graph atom) + =^ rm-node graph (del:orm graph atom) + ?~ rm-node `graph + ?. ?=(%graph -.children.u.rm-node) + `graph + =/ new-indices + (get-descendants p.children.u.rm-node) + [(~(uni in indices) new-indices) graph] =/ =node:store ~| "parent index does not exist to remove a node from!" (need (get:orm graph atom)) ~| "child index does not exist to remove a node from!" ?> ?=(%graph -.children.node) - %^ put:orm - graph - atom - node(p.children $(graph p.children.node, index t.index)) + =^ new-indices p.children.node + $(graph p.children.node, index t.index) + :- (~(uni in indices) new-indices) + (put:orm graph atom node) -- :: ++ add-signatures @@ -605,6 +648,7 @@ %- graph-update ^- update:store ?- -.q.update + %add-graph update(resource.q resource) %add-nodes update(resource.q resource) %remove-nodes update(resource.q resource) %add-signatures update(resource.uid.q resource) @@ -868,6 +912,15 @@ |= [=atom =node:store] ^- [index:store node:store] [~[atom] node] + :: + [%x %node-exists @ @ @ *] + =/ =ship (slav %p i.t.t.path) + =/ =term i.t.t.t.path + =/ =index:store + (turn t.t.t.t.path (cury slav %ud)) + =/ node=(unit node:store) + (get-node ship term index) + ``noun+!>(?=(^ node)) :: [%x %node @ @ @ *] =/ =ship (slav %p i.t.t.path) diff --git a/pkg/arvo/app/group-push-hook.hoon b/pkg/arvo/app/group-push-hook.hoon index 031c1dc98..b4d0d69ca 100644 --- a/pkg/arvo/app/group-push-hook.hoon +++ b/pkg/arvo/app/group-push-hook.hoon @@ -142,15 +142,6 @@ == -- :: -++ resource-for-update - |= =vase - ^- (unit resource) - =/ =update:store - !<(update:store vase) - ?: ?=(%initial -.update) - ~ - `resource.update -:: ++ take-update |= =vase ^- [(list card) agent] diff --git a/pkg/arvo/app/hark-graph-hook.hoon b/pkg/arvo/app/hark-graph-hook.hoon index 68dfd65e3..803b18562 100644 --- a/pkg/arvo/app/hark-graph-hook.hoon +++ b/pkg/arvo/app/hark-graph-hook.hoon @@ -9,11 +9,17 @@ +$ card card:agent:gall +$ versioned-state $% state-0 + state-1 == :: +$ state-0 - $: %0 - watching=(set [resource index:post]) + [%0 base-state-0] +:: ++$ state-1 + [%1 base-state-0] +:: ++$ base-state-0 + $: watching=(set [resource index:post]) mentions=_& watch-on-self=_& == @@ -36,7 +42,7 @@ :: -- :: -=| state-0 +=| state-1 =* state - :: =< @@ -57,13 +63,25 @@ :: ++ on-save !>(state) ++ on-load - |= old=vase + |= =vase ^- (quip card _this) - :_ this(state !<(state-0 old)) + =+ !<(old=versioned-state vase) + =| cards=(list card) + |- + ?: ?=(%0 -.old) + %_ $ + -.old %1 + :: + cards + :_ cards + [%pass / %agent [our dap]:bowl %poke noun+!>(%rewatch-dms)] + == + :_ this(state old) + =. cards (flop cards) %+ welp ?: (~(has by wex.bowl) [/graph our.bowl %graph-store]) - ~ - ~[watch-graph:ha] + cards + [watch-graph:ha cards] %+ turn ^- (list mark) :~ %graph-validator-chat @@ -103,9 +121,23 @@ ?+ mark (on-poke:def mark vase) %hark-graph-hook-action (hark-graph-hook-action !<(action:hook vase)) + %noun + (poke-noun !<(* vase)) == [cards this] :: + ++ poke-noun + |= non=* + ?> ?=(%rewatch-dms non) + =/ graphs=(list resource) + ~(tap in get-keys:gra) + :- ~ + %_ state + watching + %- ~(gas in watching) + (murn graphs |=(rid=resource ?:((should-watch:ha rid) `[rid ~] ~))) + == + :: ++ hark-graph-hook-action |= =action:hook ^- (quip card _state) @@ -167,16 +199,48 @@ :: ?(%remove-graph %archive-graph) (remove-graph resource.q.update) + :: + %remove-nodes + (remove-nodes resource.q.update indices.q.update) :: %add-nodes =* rid resource.q.update (check-nodes ~(val by nodes.q.update) rid) == + :: this is awful, but notification kind should always switch + :: on the index, so hopefully doesn't matter + :: TODO: rethink this + ++ remove-nodes + |= [rid=resource indices=(set index:graph-store)] + =/ to-remove + %- ~(gas by *(set [resource index:graph-store])) + (turn ~(tap in indices) (lead rid)) + :_ state(watching (~(dif in watching) to-remove)) + =/ =tube:clay + (get-conversion:ha rid) + %+ roll + ~(tap in indices) + |= [=index:graph-store out=(list card)] + =| =indexed-post:graph-store + =. index.p.indexed-post index + =+ !<(u-notif-kind=(unit notif-kind) (tube !>(indexed-post))) + ?~ u-notif-kind out + =* notif-kind u.u-notif-kind + =/ =stats-index:store + [%graph rid (scag parent-lent.notif-kind index)] + ?. ?=(%each mode.notif-kind) out + :_ out + (poke-hark %read-each stats-index index) + :: + ++ poke-hark + |= =action:store + ^- card + [%pass / %agent [our.bowl %hark-store] %poke hark-action+!>(action)] :: ++ remove-graph |= rid=resource =/ unwatched - %- ~(gas in *_watching) + %- ~(gas in *(set [resource index:graph-store])) %+ skim ~(tap in watching) |= [r=resource idx=index:graph-store] =(r rid) @@ -191,23 +255,14 @@ ++ add-graph |= rid=resource ^- (quip card _state) - =/ group-rid=(unit resource) - (group-from-app-resource:met %graph rid) - ?~ group-rid - ~& no-group+rid - `state - =/ is-hidden=? - !(is-managed:grp u.group-rid) - =/ should-watch - |(is-hidden &(watch-on-self =(our.bowl entity.rid))) - ?. should-watch - `state =/ graph=graph:graph-store :: graph in subscription is bunted (get-graph-mop:gra rid) =/ node=(unit node:graph-store) (bind (peek:orm:graph-store graph) |=([@ =node:graph-store] node)) =^ cards state (check-nodes (drop node) rid) + ?. (should-watch:ha rid) + [cards state] :_ state(watching (~(put in watching) [rid ~])) (weld cards (give:ha ~[/updates] %listen [rid ~])) :: @@ -245,7 +300,19 @@ -- :: |_ =bowl:gall ++* met ~(. metadata bowl) + grp ~(. grouplib bowl) + gra ~(. graph bowl) :: +++ get-conversion + |= rid=resource + ^- tube:clay + =+ %^ scry [our now]:bowl + ,mark=(unit mark) + /gx/graph-store/graph-mark/(scot %p entity.rid)/[name.rid]/noun + ?~ mark + |=(v=vase !>(~)) + (scry-conversion [our now]:bowl q.byk.bowl u.mark) :: ++ give |= [paths=(list path) =update:hook] @@ -273,6 +340,16 @@ %.y $(contents t.contents) :: +++ should-watch + |= rid=resource + ^- ? + =/ group-rid=(unit resource) + (group-from-app-resource:met %graph rid) + ?~ group-rid %.n + ?| !(is-managed:grp u.group-rid) + &(watch-on-self =(our.bowl entity.rid)) + == +:: ++ handle-update |_ $: rid=resource :: input updates=(list node:graph-store) @@ -288,13 +365,7 @@ update-core(rid r, updates upds, group grp, module mod) :: ++ get-conversion - ^- tube:clay - =+ %^ scry [our now]:bowl - ,mark=(unit mark) - /gx/graph-store/graph-mark/(scot %p entity.rid)/[name.rid]/noun - ?~ mark - |=(v=vase !>(~)) - (scry-conversion [our now]:bowl q.byk.bowl u.mark) + (^get-conversion rid) :: ++ abet ^- (quip card _state) diff --git a/pkg/arvo/app/hark-group-hook.hoon b/pkg/arvo/app/hark-group-hook.hoon index 96eb60ca2..e644a22f0 100644 --- a/pkg/arvo/app/hark-group-hook.hoon +++ b/pkg/arvo/app/hark-group-hook.hoon @@ -148,7 +148,9 @@ |= [=index:store =notification:store] ^- card =- [%pass / %agent [our.bowl %hark-store] %poke -] - hark-action+!>([%add index notification]) + :- %hark-action + !> ^- action:store + [%add-note index notification] -- :: ++ on-peek on-peek:def diff --git a/pkg/arvo/app/hark-store.hoon b/pkg/arvo/app/hark-store.hoon index 031a20284..106e62045 100644 --- a/pkg/arvo/app/hark-store.hoon +++ b/pkg/arvo/app/hark-store.hoon @@ -21,13 +21,13 @@ $% state:state-zero:store state:state-one:store state-2 + state-3 == +$ unread-stats [indices=(set index:graph-store) last=@da] :: -+$ state-2 - $: %2 - unreads-each=(jug stats-index:store index:graph-store) ++$ base-state + $: unreads-each=(jug stats-index:store index:graph-store) unreads-count=(map stats-index:store @ud) last-seen=(map stats-index:store @da) =notifications:store @@ -36,14 +36,20 @@ dnd=_| == :: ++$ state-2 + [%2 base-state] +:: ++$ state-3 + [%3 base-state] +:: +$ inflated-state - $: state-2 + $: state-3 cache == :: $cache: useful to have precalculated, but can be derived from state :: albeit expensively +$ cache - $: by-index=(jug stats-index:store @da) + $: by-index=(jug stats-index:store [time=@da =index:store]) ~ == :: @@ -78,9 +84,19 @@ =| cards=(list card) |^ ?- -.old - %2 - :- cards + %3 + :- (flop cards) this(-.state old, +.state (inflate-cache:ha old)) + :: + %2 + %_ $ + -.old %3 + :: + cards + :_ cards + [%pass / %agent [our dap]:bowl %poke noun+!>(%fix-dangling)] + == + :: %1 %_ $ @@ -212,6 +228,7 @@ [%count count] (~(gut by last-seen) stats-index *time) == + :: ++ give-each-unreads ^- (list [stats-index:store stats:store]) %+ turn @@ -264,10 +281,41 @@ =^ cards state ?+ mark (on-poke:def mark vase) %hark-action (hark-action !<(action:store vase)) - %noun ~& +.state [~ state] + %noun (poke-noun !<(* vase)) == [cards this] :: + ++ poke-noun + |= val=* + ?+ val ~|(%bad-noun-poke !!) + %fix-dangling fix-dangling + %print ~&(+.state [~ state]) + == + :: + ++ fix-dangling + =/ graphs get-keys:gra + :_ state + %+ roll + ~(tap by unreads-each) + |= $: [=stats-index:store indices=(set index:graph-store)] + out=(list card) + == + ?. ?=(%graph -.stats-index) out + ?. (~(has in graphs) graph.stats-index) + :_(out (poke-us %remove-graph graph.stats-index)) + %+ welp out + %+ turn + %+ skip + ~(tap in indices) + |= =index:graph-store + (check-node-existence:gra graph.stats-index index) + |=(=index:graph-store (poke-us %read-each stats-index index)) + :: + ++ poke-us + |= =action:store + ^- card + [%pass / %agent [our dap]:bowl %poke hark-action+!>(action)] + :: ++ hark-action |= =action:store ^- (quip card _state) @@ -338,6 +386,9 @@ |= [read=? time=@da =index:store] poke-core(+.state (^upd-cache read time index)) :: + ++ rebuild-cache + poke-core(+.state (inflate-cache -.state)) + :: ++ put-notifs |= [time=@da =timebox:store] poke-core(notifications (put:orm notifications time timebox)) @@ -380,17 +431,28 @@ (~(put by archive-box) index notification(read %.y)) (give %archive time index) :: + :: if we detect cache inconsistencies, wipe and rebuild ++ change-read-status |= [time=@da =index:store read=?] + ^+ poke-core =. poke-core (upd-cache read time index) - %_ poke-core - notifications - %^ jub-orm notifications time - |= =timebox:store - %+ ~(jab by timebox) index - |= n=notification:store - ?>(!=(read read.n) n(read read)) - == + =/ tib=(unit timebox:store) + (get:orm notifications time) + ?~ tib poke-core + =/ not=(unit notification:store) + (~(get by u.tib) index) + ?~ not poke-core + =? poke-core + :: cache is inconsistent iff we didn't directly + :: call this through %read-note or %unread-note + &(=(read read.u.not) !?=(?(%read-note %unread-note) -.in)) + ~& >> "Inconsistent hark cache, rebuilding" + rebuild-cache + =. u.tib + (~(put by u.tib) index u.not(read read)) + =. notifications + (put:orm notifications time u.tib) + poke-core :: ++ read-note |= [time=@da =index:store] @@ -416,19 +478,16 @@ :: ++ read-index-each |= [=stats-index:store ref=index:graph-store] - %+ read-index stats-index + %- read-indices %+ skim ~(tap ^in (~(get ju by-index) stats-index)) - |= time=@da + |= [time=@da =index:store] =/ =timebox:store (gut-orm notifications time) - %+ roll - ~(tap ^in timebox) - |= [[=index:store not=notification:store] out=?] - ?: out out - ?. (stats-index-is-index:store stats-index index) out - ?. ?=(%graph -.index) out - ?. ?=(%graph -.contents.not) out + =/ not=notification:store + (~(got by timebox) index) + ?. ?=(%graph -.index) %.n + ?. ?=(%graph -.contents.not) %.n (lien list.contents.not |=(p=post:post =(index.p ref))) :: ++ read-each @@ -456,31 +515,18 @@ ++ read-count |= =stats-index:store =. unreads-count (~(put by unreads-count) stats-index 0) - =/ times=(list @da) + =/ times=(list [@da index:store]) ~(tap ^in (~(get ju by-index) stats-index)) - (give:(read-index stats-index times) %read-count stats-index) + (give:(read-indices times) %read-count stats-index) :: - ++ read-index - |= [=stats-index:store times=(list @da)] + ++ read-indices + |= times=(list [time=@da =index:store]) |- ?~ times poke-core =/ core - (read-stats-index i.times stats-index) + (read-note i.times) $(poke-core core, times t.times) :: - ++ read-stats-index - |= [time=@da =stats-index:store] - =/ keys - ~(tap ^in ~(key by (gut-orm notifications time))) - |- ^+ poke-core - ?~ keys - poke-core - ?. (stats-index-is-index:store stats-index i.keys) - $(keys t.keys) - =/ core - (read-note time i.keys) - $(poke-core core, keys t.keys) - :: ++ seen-index |= [time=@da =stats-index:store] =/ new-time=@da @@ -505,7 +551,7 @@ =. last-seen ((dif-map-by-key ,@da) last-seen indices) =. by-index - ((dif-map-by-key ,(set @da)) by-index indices) + ((dif-map-by-key ,(set [@da =index:store])) by-index indices) poke-core :: ++ get-stats-indices @@ -538,10 +584,10 @@ ~(tap ^in set) |- ?~ indices poke-core - =/ times=(list @da) + =/ times=(list [time=@da =index:store]) ~(tap ^in (~(get ju by-index) i.indices)) =. poke-core - (read-index i.indices times) + (read-indices times) $(indices t.indices) -- :: @@ -629,14 +675,14 @@ %_ +.state :: by-index - %. [(to-stats-index:store index) time] + %. [(to-stats-index:store index) time index] ?: read ~(del ju by-index) ~(put ju by-index) == :: ++ inflate-cache - |= state-2 + |= state-3 ^+ +.state =/ nots=(list [p=@da =timebox:store]) (tap:orm notifications) diff --git a/pkg/arvo/app/landscape/index.html b/pkg/arvo/app/landscape/index.html index 2dab9db71..04509a540 100644 --- a/pkg/arvo/app/landscape/index.html +++ b/pkg/arvo/app/landscape/index.html @@ -1,7 +1,7 @@ - OS1 + Landscape @@ -12,8 +12,8 @@ ?=(%add-graph -.q.upd) + =/ mo ((ordered-map atom node:ga) gth) + =/ week=(list [@da node:ga]) + (tap:mo (subset:mo graph.q.upd ~ `(sub now ~d7))) + :- (lent week) + %~ wyt in + %+ roll week + |= [[* [author=ship *] *] a=(set ship)] + (~(put in a) author) +:: render results +:: +:- (tac 'the date is ' (scot %da now)) +:- :(tac 'you are in ' (render-number (lent groups)) ' group(s):') +:- =- (roll - tac) + %+ join ', ' + %+ turn groups + |=([* r=resource:re *] (render-resource r)) +:- :(tac 'you are hosting ' (render-number (lent crowds)) ' group(s):') +%- zing +%+ turn activity +|= [g=resource:re m=@ud chans=(list [resource:re term @ud @ud])] +^- (list @t) +:- :(tac 'group, ' (render-resource g) ', ' (render-number m)) +%+ turn chans +|= [c=resource:re m=term w=@ud a=@ud] +;: tac ' chan, ' + (render-resource c) ', ' + m ', ' + (render-number w) ', ' + (render-number a) +== +:: +++ scry + |* [=mold care=term app=term =path] + .^(mold (tac %g care) (scot %p our) app (scot %da now) path) +:: +++ tac (cury cat 3) +:: +++ render-resource + |= r=resource:re + ?: shy + (crip ((x-co:co 8) (mug r))) + :(tac (scot %p entity.r) '/' name.r) +:: +++ render-number + |= n=@ud + (crip ((d-co:co 1) n)) +-- diff --git a/pkg/arvo/lib/graph.hoon b/pkg/arvo/lib/graph.hoon index fc84be6fc..6d78a206f 100644 --- a/pkg/arvo/lib/graph.hoon +++ b/pkg/arvo/lib/graph.hoon @@ -49,6 +49,14 @@ ?> ?=(^ nodes.q.update) q.n.nodes.q.update :: +++ check-node-existence + |= [res=resource =index:store] + ^- ? + %+ scry-for ,? + %+ weld + /node-exists/(scot %p entity.res)/[name.res] + (turn index (cury scot %ud)) +:: ++ get-update-log |= rid=resource ^- update-log:store diff --git a/pkg/arvo/lib/pull-hook.hoon b/pkg/arvo/lib/pull-hook.hoon index f5fb8f273..a8baae867 100644 --- a/pkg/arvo/lib/pull-hook.hoon +++ b/pkg/arvo/lib/pull-hook.hoon @@ -293,7 +293,7 @@ (on-agent:og wire sign) [cards this] :_ this - ~[(update-store:hc q.cage.sign)] + ~[(update-store:hc rid q.cage.sign)] == ++ on-leave |= =path @@ -469,15 +469,24 @@ /helper/pull-hook wire :: + ++ get-conversion + .^ tube:clay + %cc (scot %p our.bowl) %home (scot %da now.bowl) + /[update-mark.config]/resource + == + :: ++ give-update ^- card [%give %fact ~[/tracking] %pull-hook-update !>(tracking)] :: ++ update-store - |= =vase + |= [wire-rid=resource =vase] ^- card =/ =wire (make-wire /store) + =+ !<(rid=resource (get-conversion vase)) + ?> =(src.bowl (~(got by tracking) rid)) + ?> =(wire-rid rid) [%pass wire %agent [our.bowl store-name.config] %poke update-mark.config vase] -- -- diff --git a/pkg/arvo/lib/push-hook.hoon b/pkg/arvo/lib/push-hook.hoon index dff65aac6..7a77e0736 100644 --- a/pkg/arvo/lib/push-hook.hoon +++ b/pkg/arvo/lib/push-hook.hoon @@ -67,16 +67,6 @@ |* =config $_ ^| |_ bowl:gall - :: - :: +resource-for-update: get affected resource from an update - :: - :: Given a vase of the update, the mark of which is - :: update-mark.config, produce the affected resource, if any. - :: - ++ resource-for-update - |~ vase - *(unit resource) - :: :: +take-update: handle update from store :: :: Given an update from the store, do other things after proxying @@ -175,9 +165,11 @@ |^ ?- -.old %1 + =. cards + :_(cards (build-mark:hc %sing)) =^ og-cards push-hook (on-load:og inner-state.old) - [(weld cards og-cards) this(state old)] + [(weld (flop cards) og-cards) this(state old)] :: %0 %_ $ @@ -274,11 +266,18 @@ =^ cards push-hook (on-leave:og path) [cards this] + :: ++ on-arvo |= [=wire =sign-arvo] - =^ cards push-hook - (on-arvo:og wire sign-arvo) - [cards this] + ?. ?=([%helper %push-hook @ *] wire) + =^ cards push-hook + (on-arvo:og wire sign-arvo) + [cards this] + ?. ?=(%resource-conversion i.t.t.wire) + (on-arvo:def wire sign-arvo) + :_ this + ~[(build-mark:hc %next)] + :: ++ on-fail |= [=term =tang] =^ cards push-hook @@ -373,7 +372,7 @@ |= =vase ^- (list card:agent:gall) =/ rid=(unit resource) - (resource-for-update:og vase) + (resource-for-update vase) ?~ rid ~ =/ prefix=path resource+(en-path:resource u.rid) @@ -390,7 +389,7 @@ |= =vase ^- (list card:agent:gall) =/ rid=(unit resource) - (resource-for-update:og vase) + (resource-for-update vase) ?~ rid ~ =/ =path resource+(en-path:resource u.rid) @@ -399,5 +398,30 @@ =/ dap=term ?:(=(our.bowl entity.u.rid) store-name.config dap.bowl) [%pass wire %agent [entity.u.rid dap] %poke update-mark.config vase]~ + :: + ++ get-conversion + .^ tube:clay + %cc (scot %p our.bowl) %home (scot %da now.bowl) + /[update-mark.config]/resource + == + :: + ++ resource-for-update + |= update=vase + =/ =tube:clay + get-conversion + %+ bind + (mole |.((tube update))) + |=(=vase !<(resource vase)) + :: + ++ build-mark + |= rav=?(%sing %next) + ^- card + =/ =wire + (make-wire /resource-conversion) + =/ =mood:clay + [%c da+now.bowl /[update-mark.config]/resource] + =/ =rave:clay + ?:(?=(%next rav) [rav mood] [rav mood]) + [%pass wire %arvo %c %warp our.bowl [%home `rave]] -- -- diff --git a/pkg/arvo/mar/graph/update.hoon b/pkg/arvo/mar/graph/update.hoon index e6766edb5..405ae76c8 100644 --- a/pkg/arvo/mar/graph/update.hoon +++ b/pkg/arvo/mar/graph/update.hoon @@ -7,6 +7,13 @@ |% ++ noun upd ++ json (update:enjs upd) + ++ resource + ?+ -.q.upd !! + ?(%run-updates %add-nodes %remove-nodes %add-graph) resource.q.upd + ?(%remove-graph %archive-graph %unarchive-graph) resource.q.upd + ?(%add-tag %remove-tag) resource.q.upd + ?(%add-signatures %remove-signatures) resource.uid.q.upd + == ++ mime [/application/x-urb-graph-update (as-octs (jam upd))] -- :: diff --git a/pkg/arvo/mar/group/update.hoon b/pkg/arvo/mar/group/update.hoon index b8b7659e7..62fdc5af2 100644 --- a/pkg/arvo/mar/group/update.hoon +++ b/pkg/arvo/mar/group/update.hoon @@ -4,9 +4,13 @@ ++ grow |% ++ noun upd + ++ resource + ?< ?=(%initial -.upd) + resource.upd + :: ++ json - %+ frond:enjs:format 'groupUpdate' - (update:enjs upd) + %+ frond:enjs:format 'groupUpdate' + (update:enjs upd) -- ++ grab |% diff --git a/pkg/arvo/mar/resource.hoon b/pkg/arvo/mar/resource.hoon new file mode 100644 index 000000000..9edd4e0ae --- /dev/null +++ b/pkg/arvo/mar/resource.hoon @@ -0,0 +1,14 @@ +/+ resource +|_ rid=resource +++ grad %noun +++ grow + |% + ++ noun rid + ++ json (enjs:resource rid) + -- +++ grab + |% + ++ noun resource + ++ json dejs:resource + -- +-- diff --git a/pkg/arvo/sur/graph-store.hoon b/pkg/arvo/sur/graph-store.hoon index e2570fed8..6780e4210 100644 --- a/pkg/arvo/sur/graph-store.hoon +++ b/pkg/arvo/sur/graph-store.hoon @@ -34,7 +34,8 @@ == :: +$ logged-update-0 - $% [%add-nodes =resource nodes=(map index node)] + $% [%add-graph =resource =graph mark=(unit mark) overwrite=?] + [%add-nodes =resource nodes=(map index node)] [%remove-nodes =resource indices=(set index)] [%add-signatures =uid =signatures] [%remove-signatures =uid =signatures] @@ -42,7 +43,6 @@ :: +$ update-0 $% logged-update-0 - [%add-graph =resource =graph mark=(unit mark) overwrite=?] [%remove-graph =resource] :: [%add-tag =term =resource] diff --git a/pkg/arvo/sys/vane/ames.hoon b/pkg/arvo/sys/vane/ames.hoon index 64d410698..ad799da54 100644 --- a/pkg/arvo/sys/vane/ames.hoon +++ b/pkg/arvo/sys/vane/ames.hoon @@ -180,6 +180,9 @@ $(index +(index), sorted [(~(got by fragments) index) sorted]) :: (cue (rep 13 (flop sorted))) +:: +jim: caching +jam +:: +++ jim |=(n=* ~+((jam n))) :: +bind-duct: find or make new $bone for .duct in .ossuary :: ++ bind-duct @@ -1931,7 +1934,7 @@ == now :: - =/ =message-blob (dedup-message (jam payload)) + =/ =message-blob (dedup-message (jim payload)) =. peer-core (run-message-pump bone %memo message-blob) :: ?: &(=(%boon valence) ?=(?(%dead %unborn) -.qos.peer-state)) diff --git a/pkg/arvo/ted/graph/delete.hoon b/pkg/arvo/ted/graph/delete.hoon index 8a3ea1b78..f98ddf075 100644 --- a/pkg/arvo/ted/graph/delete.hoon +++ b/pkg/arvo/ted/graph/delete.hoon @@ -33,7 +33,7 @@ (pure:m (need ugroup)) :: ++ delete-graph - |= rid=resource + |= [group-rid=resource rid=resource] =/ m (strand ,~) ^- form:m ;< =bowl:spider bind:m get-bowl:strandio @@ -43,12 +43,9 @@ (poke-our %graph-push-hook %push-hook-action !>([%remove rid])) ;< ~ bind:m %+ poke-our %metadata-hook - metadata-hook-action+!>([%remove (en-path:resource rid)]) - ;< ~ bind:m - %+ poke-our %metadata-store :- %metadata-action !> :+ %remove - (en-path:resource rid) + (en-path:resource group-rid) [%graph (en-path:resource rid)] (pure:m ~) -- @@ -69,11 +66,14 @@ (scry-group u.ugroup-rid) ?. hidden.group ;< ~ bind:m - (delete-graph rid.action) + (delete-graph u.ugroup-rid rid.action) (pure:m !>(~)) ;< ~ bind:m (poke-our %group-store %group-action !>([%remove-group rid.action ~])) ;< ~ bind:m (poke-our %group-push-hook %push-hook-action !>([%remove rid.action])) -;< ~ bind:m (delete-graph rid.action) +;< ~ bind:m (delete-graph u.ugroup-rid rid.action) +;< ~ bind:m + %+ poke-our %metadata-hook + metadata-hook-action+!>([%remove (en-path:resource u.ugroup-rid)]) (pure:m !>(~)) diff --git a/pkg/arvo/ted/graph/join.hoon b/pkg/arvo/ted/graph/join.hoon index 5837409ae..25a18e3b7 100644 --- a/pkg/arvo/ted/graph/join.hoon +++ b/pkg/arvo/ted/graph/join.hoon @@ -29,13 +29,16 @@ ^- form:m =/ pax (en-path:resource rid) + =/ hold=@dr ~s0..8000 |- ^- form:m + ?> (lte hold ~m5) =* loop $ ;< u-group=(unit group) bind:m (scry:strandio ,(unit group) (weld /gx/group-store/groups (snoc pax %noun))) ?^ u-group (pure:m ~) - ;< ~ bind:m (sleep:strandio `@dr`(div ~s1 2)) + ;< ~ bind:m (sleep:strandio hold) + =. hold (mul hold 2) loop :: ++ wait-for-md @@ -44,13 +47,16 @@ ^- form:m =/ pax (en-path:resource rid) + =/ hold=@dr ~s0..8000 |- ^- form:m + ?> (lte hold ~m5) =* loop $ - ;< groups=(set path) bind:m - (scry:strandio ,(set path) /gy/metadata-store/group-indices) - ?: (~(has in groups) pax) + ;< groups=(jug path md-resource) bind:m + (scry:strandio ,(jug path md-resource) /gy/metadata-store/group-indices) + ?: (~(has by groups) pax) (pure:m ~) - ;< ~ bind:m (sleep:strandio `@dr`(div ~s1 2)) + ;< ~ bind:m (sleep:strandio hold) + =. hold (mul hold 2) loop -- :: diff --git a/pkg/docker-image/README.md b/pkg/docker-image/README.md new file mode 100644 index 000000000..e5e2163ee --- /dev/null +++ b/pkg/docker-image/README.md @@ -0,0 +1,37 @@ +# Official Urbit Docker Image + +This is the official Docker image for [Urbit](https://urbit.org). + +Urbit is a clean-slate OS and network for the 21st century. + +## Using + +To use this image, you should mount a volume with a keyfile, comet file, or existing pier at `/urbit`, and map ports +as described below. + +### Volume Mount +This image expects a volume mounted at `/urbit`. This volume should initially contain one of + +- A keyfile `.key` for a galaxy, star, planet, or moon. See the setup instructions for Urbit for information on [obtaining a keyfile](https://urbit.org/using/install/). + * e.g. `sampel-palnet.key` for the planet `sampel-palnet`. +- An empty file with the extension `.comet`. This will cause Urbit to boot a [comet](https://urbit.org/docs/glossary/comet/) in a pier named for the `.comet` file (less the extension). + * e.g. starting with an empty file `my-urbit-bot.comet` will result in Urbit booting a comet into the pier + `my-urbit-bot` under your volume. +- An existing pier as a directory ``. You can migrate an existing ship to a new docker container in this way by placing its pier under the volume. + * e.g. if your ship is `sampel-palnet` then you likely have a directory `sampel-palnet` whose path you pass to `./urbit` when starting. [Move your pier](https://urbit.org/using/operations/using-your-ship/#moving-your-pier) directory to the volume and then start the container. + +The first two options result in Urbit attempting to boot either the ship named by the name of the keyfile, or a comet. In both cases, after that boot is successful, the `.key` or `.comet` file will be removed from the volume and the pier will take its place. + +In consequence, it is safe to remove the container and start a new container which mounts the same volume, e.g. to upgrade the version of the urbit binary by running a later container version. It is also possible to stop the container and then move the pier away e.g. to a location where you will run it directly with the Urbit binary. + +### Ports +The image includes `EXPOSE` directives for TCP port 80 and UDP port 34343. Port `80` is used for Urbit's HTTP interface for both [Landscape](https://urbit.org/docs/glossary/landscape/) and for [API calls](https://urbit.org/using/integrating-api/) to the ship. Port `34343` is used by [Ames](https://urbit.org/docs/glossary/ames/) for ship-to-ship communication. + +You can either pass the `-P` flag to docker to map ports directly to the corresponding ports on the host, or map them individually with `-p` flags. For local testing the latter is often convenient, for instance to remap port 80 to an unprivileged port. + +## Extending + +You likely do not want to extend this image. External applications which interact with Urbit do so primarily via an HTTP API, which should be exposed as described above. For containerized applications using Urbit, it is more appropriate to use a container orchestration service such as Docker Compose or Kubernetes to run Urbit alongside other containers which will interface with its API. + +## Development +The docker image is built by a Nix derivation in the [`nix/pkgs/docker-image/default.nix`](https://github.com/urbit/urbit/tree/master/nix/pkgs/docker-image/default.nix) file under the Urbit git repository. diff --git a/pkg/interface/config/webpack.dev.js b/pkg/interface/config/webpack.dev.js index 9bdb61031..fe97e7c07 100644 --- a/pkg/interface/config/webpack.dev.js +++ b/pkg/interface/config/webpack.dev.js @@ -96,7 +96,7 @@ module.exports = { ] } }, - exclude: /node_modules/ + exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light)\/).*/ }, { test: /\.css$/i, diff --git a/pkg/interface/config/webpack.prod.js b/pkg/interface/config/webpack.prod.js index 8e8e6191c..005e4cad5 100644 --- a/pkg/interface/config/webpack.prod.js +++ b/pkg/interface/config/webpack.prod.js @@ -26,7 +26,7 @@ module.exports = { ] } }, - exclude: /node_modules/ + exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light)\/).*/ }, { test: /\.css$/i, diff --git a/pkg/interface/package-lock.json b/pkg/interface/package-lock.json index cd806bbc0..d4a06d132 100644 Binary files a/pkg/interface/package-lock.json and b/pkg/interface/package-lock.json differ diff --git a/pkg/interface/package.json b/pkg/interface/package.json index b99291738..a65578c43 100644 --- a/pkg/interface/package.json +++ b/pkg/interface/package.json @@ -4,83 +4,85 @@ "description": "", "main": "index.js", "dependencies": { - "@babel/runtime": "^7.10.5", + "@babel/runtime": "^7.12.5", "@reach/disclosure": "^0.10.5", "@reach/menu-button": "^0.10.5", "@reach/tabs": "^0.10.5", - "@tlon/indigo-light": "^1.0.3", - "@tlon/indigo-react": "1.2.15", - "@tlon/sigil-js": "^1.4.2", - "aws-sdk": "^2.726.0", + "@tlon/indigo-dark": "^1.0.6", + "@tlon/indigo-light": "^1.0.6", + "@tlon/indigo-react": "1.2.17", + "@tlon/sigil-js": "^1.4.3", + "aws-sdk": "^2.830.0", "big-integer": "^1.6.48", "classnames": "^2.2.6", - "codemirror": "^5.55.0", - "css-loader": "^3.5.3", - "file-saver": "^2.0.2", - "formik": "^2.1.4", - "immer": "^8.0.0", - "lodash": "^4.17.15", + "codemirror": "^5.59.2", + "css-loader": "^3.6.0", + "file-saver": "^2.0.5", + "formik": "^2.1.5", + "immer": "^8.0.1", + "lodash": "^4.17.20", "markdown-to-jsx": "^6.11.4", - "moment": "^2.20.1", + "moment": "^2.29.1", "mousetrap": "^1.6.5", "mousetrap-global-bind": "^1.1.0", "normalize-wheel": "1.0.1", - "oembed-parser": "^1.4.1", + "oembed-parser": "^1.4.5", "prop-types": "^15.7.2", - "react": "^16.5.2", + "react": "^16.14.0", "react-codemirror2": "^6.0.1", - "react-dom": "^16.8.6", + "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.0.0", - "react-virtuoso": "^0.20.0", + "react-router-dom": "^5.2.0", + "react-virtuoso": "^0.20.3", "react-visibility-sensor": "^5.1.1", + "remark-breaks": "^2.0.1", "remark-disable-tokenizers": "^1.0.24", - "style-loader": "^1.2.1", - "styled-components": "^5.1.0", + "style-loader": "^1.3.0", + "styled-components": "^5.1.1", "styled-system": "^5.1.5", "suncalc": "^1.8.0", - "urbit-ob": "^5.0.0", + "urbit-ob": "^5.0.1", "yup": "^0.29.3", - "zustand": "^3.2.0" + "zustand": "^3.3.1" }, "devDependencies": { - "@babel/core": "^7.9.0", - "@babel/plugin-proposal-class-properties": "^7.8.3", - "@babel/plugin-proposal-object-rest-spread": "^7.9.5", - "@babel/plugin-proposal-optional-chaining": "^7.9.0", - "@babel/plugin-transform-runtime": "^7.10.5", - "@babel/preset-env": "^7.9.5", - "@babel/preset-react": "^7.9.4", - "@babel/preset-typescript": "^7.10.1", - "@types/lodash": "^4.14.155", - "@types/react": "^16.9.38", - "@types/react-dom": "^16.9.8", - "@types/react-router-dom": "^5.1.5", - "@types/styled-components": "^5.1.2", + "@babel/core": "^7.12.10", + "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/plugin-proposal-object-rest-spread": "^7.12.1", + "@babel/plugin-proposal-optional-chaining": "^7.12.7", + "@babel/plugin-transform-runtime": "^7.12.10", + "@babel/preset-env": "^7.12.11", + "@babel/preset-react": "^7.12.10", + "@babel/preset-typescript": "^7.12.7", + "@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.7", - "@typescript-eslint/eslint-plugin": "^3.8.0", - "@typescript-eslint/parser": "^3.8.0", + "@types/yup": "^0.29.11", + "@typescript-eslint/eslint-plugin": "^3.10.1", + "@typescript-eslint/parser": "^3.10.1", "babel-eslint": "^10.1.0", - "babel-loader": "^8.1.0", + "babel-loader": "^8.2.2", "babel-plugin-lodash": "^3.3.4", - "babel-plugin-root-import": "^6.5.0", + "babel-plugin-root-import": "^6.6.0", "clean-webpack-plugin": "^3.0.0", - "cross-env": "^7.0.2", + "cross-env": "^7.0.3", "eslint": "^6.8.0", - "eslint-plugin-react": "^7.19.0", - "file-loader": "^6.0.0", - "html-webpack-plugin": "^4.2.0", + "eslint-plugin-react": "^7.22.0", + "file-loader": "^6.2.0", + "html-webpack-plugin": "^4.5.1", "moment-locales-webpack-plugin": "^1.2.0", - "react-hot-loader": "^4.12.21", - "sass": "^1.26.5", + "react-hot-loader": "^4.13.0", + "sass": "^1.32.5", "sass-loader": "^8.0.2", "typescript": "^3.9.7", - "webpack": "^4.43.0", - "webpack-cli": "^3.3.11", - "webpack-dev-server": "^3.10.3" + "webpack": "^4.46.0", + "webpack-cli": "^3.3.12", + "webpack-dev-server": "^3.11.2" }, "scripts": { "lint": "eslint ./src/**/*.{js,ts,tsx}", diff --git a/pkg/interface/public/index.html b/pkg/interface/public/index.html index e84636f1c..4a2da1bf3 100644 --- a/pkg/interface/public/index.html +++ b/pkg/interface/public/index.html @@ -10,7 +10,7 @@ - OS1 + Landscape diff --git a/pkg/interface/src/logic/api/graph.ts b/pkg/interface/src/logic/api/graph.ts index 40b9c1aad..0606a2c1e 100644 --- a/pkg/interface/src/logic/api/graph.ts +++ b/pkg/interface/src/logic/api/graph.ts @@ -3,8 +3,8 @@ import { StoreState } from '../store/type'; import { Patp, Path, PatpNoSig } from '~/types/noun'; import _ from 'lodash'; import {makeResource, resourceFromPath} from '../lib/group'; -import {GroupPolicy, Enc, Post, NodeMap, Content} from '~/types'; -import { numToUd, unixToDa, decToUd, deSig } from '~/logic/lib/util'; +import {GroupPolicy, Enc, Post, NodeMap, Content, Resource} from '~/types'; +import { numToUd, unixToDa, decToUd, deSig, resourceAsPath } from '~/logic/lib/util'; export const createBlankNodeWithChildPost = ( parentIndex: string = '', @@ -81,6 +81,8 @@ function moduleToMark(mod: string): string | undefined { export default class GraphApi extends BaseApi { + joiningGraphs = new Set(); + private storeAction(action: any): Promise { return this.action('graph-store', 'graph-update', action) } @@ -138,11 +140,19 @@ export default class GraphApi extends BaseApi { joinGraph(ship: Patp, name: string) { const resource = makeResource(ship, name); + const rid = resourceAsPath(resource); + if(this.joiningGraphs.has(rid)) { + return Promise.resolve(); + } + this.joiningGraphs.add(rid); return this.viewAction('graph-join', { join: { resource, ship, } + }).then(res => { + this.joiningGraphs.delete(rid); + return res; }); } diff --git a/pkg/interface/src/logic/api/hark.ts b/pkg/interface/src/logic/api/hark.ts index 498665bab..96111757c 100644 --- a/pkg/interface/src/logic/api/hark.ts +++ b/pkg/interface/src/logic/api/hark.ts @@ -196,10 +196,11 @@ export class HarkApi extends BaseApi { }); } - getMore() { + async getMore(): Promise { const offset = this.store.state['notifications']?.size || 0; const count = 3; - return this.getSubset(offset, count, false); + await this.getSubset(offset, count, false); + return offset === (this.store.state.notifications?.size || 0); } async getSubset(offset:number, count:number, isArchive: boolean) { diff --git a/pkg/interface/src/logic/lib/omnibox.js b/pkg/interface/src/logic/lib/omnibox.js index 1bc38a4e2..e636d3d6d 100644 --- a/pkg/interface/src/logic/lib/omnibox.js +++ b/pkg/interface/src/logic/lib/omnibox.js @@ -1,6 +1,7 @@ import { cite } from '~/logic/lib/util'; const indexes = new Map([ + ['ships', []], ['commands', []], ['subscriptions', []], ['groups', []], @@ -18,6 +19,14 @@ const result = function(title, link, app, host) { }; }; +const shipIndex = function(contacts) { + const ships = []; + Object.keys(contacts).map((e) => { + return ships.push(result(e, `/~profile/${e}`, 'profile', contacts[e]?.status)); + }); + return ships; +}; + const commandIndex = function (currentGroup) { // commands are special cased for default suite const commands = []; @@ -62,7 +71,8 @@ const otherIndex = function() { return other; }; -export default function index(associations, apps, currentGroup, groups) { +export default function index(contacts, associations, apps, currentGroup, groups) { + indexes.set('ships', shipIndex(contacts)); // all metadata from all apps is indexed // into subscriptions and landscape const subscriptions = []; diff --git a/pkg/interface/src/logic/lib/tokenizeMessage.js b/pkg/interface/src/logic/lib/tokenizeMessage.js index 39154c6ff..dc1ffeffc 100644 --- a/pkg/interface/src/logic/lib/tokenizeMessage.js +++ b/pkg/interface/src/logic/lib/tokenizeMessage.js @@ -52,13 +52,16 @@ const tokenizeMessage = (text) => { } messages.push({ url: str }); message = []; - } else if(urbitOb.isValidPatp(str) && !isInCodeBlock) { + } else if (urbitOb.isValidPatp(str.replace(/[^a-z\-\~]/g, '')) && !isInCodeBlock) { if (message.length > 0) { // If we're in the middle of a message, add it to the stack and reset messages.push({ text: message.join(' ') }); message = []; } - messages.push({ mention: str }); + messages.push({ mention: str.replace(/[^a-z\-\~]/g, '') }); + if (str.replace(/[a-z\-\~]/g, '').length > 0) { + messages.push({ text: str.replace(/[a-z\-\~]/g, '') }); + } message = []; } else { diff --git a/pkg/interface/src/logic/lib/useLazyScroll.ts b/pkg/interface/src/logic/lib/useLazyScroll.ts new file mode 100644 index 000000000..f9e8c10eb --- /dev/null +++ b/pkg/interface/src/logic/lib/useLazyScroll.ts @@ -0,0 +1,51 @@ +import { useEffect, RefObject, useRef, useState } from "react"; +import _ from "lodash"; + +export function distanceToBottom(el: HTMLElement) { + const { scrollTop, scrollHeight, clientHeight } = el; + const scrolledPercent = + (scrollHeight - scrollTop - clientHeight) / scrollHeight; + return _.isNaN(scrolledPercent) ? 0 : scrolledPercent; +} + +export function useLazyScroll( + ref: RefObject, + margin: number, + loadMore: () => Promise +) { + const [isDone, setIsDone] = useState(false); + useEffect(() => { + if (!ref.current) { + return; + } + setIsDone(false); + const scroll = ref.current; + const loadUntil = (el: HTMLElement) => { + if (!isDone && distanceToBottom(el) < margin) { + return loadMore().then((done) => { + if (done) { + setIsDone(true); + return Promise.resolve(); + } + return loadUntil(el); + }); + } + return Promise.resolve(); + }; + + loadUntil(scroll); + + const onScroll = (e: Event) => { + const el = e.currentTarget! as HTMLElement; + loadUntil(el); + }; + + ref.current.addEventListener("scroll", onScroll); + + return () => { + ref.current?.removeEventListener("scroll", onScroll); + }; + }, [ref?.current]); + + return isDone; +} diff --git a/pkg/interface/src/logic/lib/useS3.ts b/pkg/interface/src/logic/lib/useS3.ts index b5d4993d5..0717d52f7 100644 --- a/pkg/interface/src/logic/lib/useS3.ts +++ b/pkg/interface/src/logic/lib/useS3.ts @@ -50,11 +50,11 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => { ACL: "public-read", ContentType: file.type, }; - + setUploading(true); const { Location } = await client.current.upload(params).promise(); - + setUploading(false); return Location; @@ -75,6 +75,7 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => { const fileSelector = document.createElement('input'); fileSelector.setAttribute('type', 'file'); fileSelector.setAttribute('accept', accept); + fileSelector.style.visibility = 'hidden'; fileSelector.addEventListener('change', () => { const files = fileSelector.files; if (!files || files.length <= 0) { @@ -82,10 +83,12 @@ const useS3 = (s3: S3State, { accept = '*' } = { accept: '*' }): IuseS3 => { return; } uploadDefault(files[0]).then(resolve); + document.body.removeChild(fileSelector); }) + document.body.appendChild(fileSelector); fileSelector.click(); }) - + }, [uploadDefault] ); diff --git a/pkg/interface/src/logic/lib/util.ts b/pkg/interface/src/logic/lib/util.ts index d4124a371..ba2bc67c8 100644 --- a/pkg/interface/src/logic/lib/util.ts +++ b/pkg/interface/src/logic/lib/util.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import _ from "lodash"; import f, { memoize } from "lodash/fp"; import bigInt, { BigInteger } from "big-integer"; @@ -13,7 +13,7 @@ export const MOMENT_CALENDAR_DATE = { nextWeek: "dddd", lastDay: "[Yesterday]", lastWeek: "[Last] dddd", - sameElse: "DD/MM/YYYY", + sameElse: "~YYYY.M.D", }; export function appIsGraph(app: string) { @@ -357,7 +357,17 @@ export function pluralize(text: string, isPlural = false, vowel = false) { return isPlural ? `${text}s`: `${vowel ? 'an' : 'a'} ${text}`; } -export function useShowNickname(contact: Contact | null): boolean { - const hideNicknames = useLocalState(state => state.hideNicknames); +// Hide is an optional second parameter for when this function is used in class components +export function useShowNickname(contact: Contact | null, hide?: boolean): boolean { + const hideNicknames = typeof hide !== 'undefined' ? hide : useLocalState(state => state.hideNicknames); return !!(contact && contact.nickname && !hideNicknames); +} + +export function useHovering() { + const [hovering, setHovering] = useState(false); + const bind = { + onMouseEnter: () => setHovering(true), + onMouseLeave: () => setHovering(false) + }; + return { hovering, bind }; } \ No newline at end of file diff --git a/pkg/interface/src/logic/reducers/hark-update.ts b/pkg/interface/src/logic/reducers/hark-update.ts index 62dfbe42e..8db766f53 100644 --- a/pkg/interface/src/logic/reducers/hark-update.ts +++ b/pkg/interface/src/logic/reducers/hark-update.ts @@ -386,5 +386,7 @@ function archive(json: any, state: HarkState) { notifIdxEqual(index, idxNotif.index) ); state.notifications.set(time, unarchived); + const newlyRead = archived.filter(x => !x.notification.read).length; + updateNotificationStats(state, index, 'notifications', (x) => x - newlyRead); } } diff --git a/pkg/interface/src/logic/state/local.tsx b/pkg/interface/src/logic/state/local.tsx index b9dfe5794..8b3260904 100644 --- a/pkg/interface/src/logic/state/local.tsx +++ b/pkg/interface/src/logic/state/local.tsx @@ -31,7 +31,7 @@ const useLocalState = create(persist((set, get) => ({ suspendedFocus: undefined, toggleOmnibox: () => set(produce(state => { state.omniboxShown = !state.omniboxShown; - if (state.suspendedFocus) { + if (typeof state.suspendedFocus?.focus === 'function') { state.suspendedFocus.focus(); state.suspendedFocus = undefined; } else { @@ -40,7 +40,8 @@ const useLocalState = create(persist((set, get) => ({ } })), set: fn => set(produce(fn)) -}), { + }), { + blacklist: ['suspendedFocus', 'toggleOmnibox', 'omniboxShown'], name: 'localReducer' })); @@ -55,4 +56,4 @@ function withLocalState(Component: any, stateMemb }); } -export { useLocalState as default, withLocalState }; \ No newline at end of file +export { useLocalState as default, withLocalState }; diff --git a/pkg/interface/src/types/invite-update.ts b/pkg/interface/src/types/invite-update.ts index a1eb2ed91..b897687a4 100644 --- a/pkg/interface/src/types/invite-update.ts +++ b/pkg/interface/src/types/invite-update.ts @@ -1,4 +1,5 @@ import { Serial, PatpNoSig, Path } from './noun'; +import {Resource} from './group-update'; export type InviteUpdate = InviteUpdateInitial @@ -60,8 +61,8 @@ export type AppInvites = { export interface Invite { app: string; - path: Path; - recipeint: PatpNoSig; + recipient: PatpNoSig; + resource: Resource; ship: PatpNoSig; text: string; } diff --git a/pkg/interface/src/views/App.js b/pkg/interface/src/views/App.js index 19f4ffc8c..0e70bce4f 100644 --- a/pkg/interface/src/views/App.js +++ b/pkg/interface/src/views/App.js @@ -11,8 +11,8 @@ import 'mousetrap-global-bind'; import './css/indigo-static.css'; import './css/fonts.css'; -import light from './themes/light'; -import dark from './themes/old-dark'; +import light from '@tlon/indigo-light'; +import dark from '@tlon/indigo-dark'; import { Text, Anchor, Row } from '@tlon/indigo-react'; @@ -40,7 +40,7 @@ const Root = styled.div` background-size: cover; ` : p.background?.type === 'color' ? ` background-color: ${p.background.color}; - ` : '' + ` : `background-color: ${p.theme.colors.white};` } display: flex; flex-flow: column nowrap; @@ -90,7 +90,7 @@ class App extends React.Component { this.themeWatcher.onchange = this.updateTheme; setTimeout(() => { // Something about how the store works doesn't like changing it - // before the app has actually rendered, hence the timeout + // before the app has actually rendered, hence the timeout. this.updateTheme(this.themeWatcher); }, 500); this.api.local.getBaseHash(); @@ -139,9 +139,6 @@ class App extends React.Component { const doNotDisturb = state.doNotDisturb || false; const ourContact = this.state.contacts[this.ship] || null; - const showBanner = localStorage.getItem("2020BreachBanner") || "flex"; - let banner = null; - return ( @@ -170,6 +167,7 @@ class App extends React.Component { associations={state.associations} apps={state.launch} api={this.api} + contacts={state.contacts} notifications={state.notificationsCount} invites={state.invites} groups={state.groups} diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index 7203d6cfe..30ed1c2c1 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx @@ -4,7 +4,7 @@ import _ from "lodash"; import { Box, Row, Text, Rule } from "@tlon/indigo-react"; import OverlaySigil from '~/views/components/OverlaySigil'; -import { uxToHex, cite, writeText, useShowNickname } from '~/logic/lib/util'; +import { uxToHex, cite, writeText, useShowNickname, useHovering } from '~/logic/lib/util'; import { Group, Association, Contacts, Post } from "~/types"; import TextContent from './content/text'; import CodeContent from './content/code'; @@ -134,6 +134,7 @@ export default class ChatMessage extends Component { className={containerClass} style={style} mb={1} + position="relative" > {dayBreak && !isLastRead ? : null} {renderSigil @@ -194,6 +195,8 @@ export const MessageWithSigil = (props) => { } }; + const { hovering, bind } = useHovering(); + return ( <> { history={history} api={api} bg="white" - className="fl pr3 v-top pt1" + className="fl v-top pt1" + pr={3} + pl={2} /> - + { }} title={`~${msg.author}`} >{name} - {timestamp} - {datestamp} + {timestamp} + {datestamp} {msg.contents.map(c => @@ -257,20 +269,40 @@ const ContentBox = styled(Box)` `; -export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }) => ( - <> - {timestamp} - - {msg.contents.map((c, i) => ( - ))} - - -); +export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }) => { + const { hovering, bind } = useHovering(); + return ( + <> + {timestamp} + + {msg.contents.map((c, i) => ( + ))} + + + ) +}; export const MessageContent = ({ content, contacts, measure, fontSize, group }) => { if ('code' in content) { @@ -282,7 +314,7 @@ export const MessageContent = ({ content, contacts, measure, fontSize, group }) url={content.url} onLoad={measure} imageProps={{style: { - maxWidth: '18rem', + maxWidth: 'min(100%,18rem)', display: 'block' }}} videoProps={{style: { @@ -292,7 +324,8 @@ export const MessageContent = ({ content, contacts, measure, fontSize, group }) }} textProps={{style: { fontSize: 'inherit', - textDecoration: 'underline' + borderBottom: '1px solid', + textDecoration: 'none' }}} /> diff --git a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx index 92d2c48fa..5073b8549 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx @@ -258,7 +258,7 @@ export default class ChatWindow extends Component new Proxy(input, { if (property === 'setValue') { return (val) => target.value = val; } + if (property === 'element') { + return input; + } } }); +const MobileBox = styled(Box)` + display: inline-grid; + vertical-align: center; + align-items: stretch; + position: relative; + justify-content: flex-start; + width: 100%; + + &:after, + textarea { + grid-area: 2 / 1; + width: auto; + min-width: 1em; + font: inherit; + padding: 0.25em; + margin: 0; + resize: none; + background: none; + appearance: none; + border: none; + } + &::after { + content: attr(data-value) ' '; + visibility: hidden; + white-space: pre-wrap; + } +`; + export default class ChatEditor extends Component { constructor(props) { super(props); @@ -161,31 +193,49 @@ export default class ChatEditor extends Component { alignItems='center' flexGrow='1' height='100%' + paddingTop={MOBILE_BROWSER_REGEX.test(navigator.userAgent) ? '16px' : '0'} + paddingBottom={MOBILE_BROWSER_REGEX.test(navigator.userAgent) ? '16px' : '0'} maxHeight='224px' width='calc(100% - 88px)' className={inCodeMode ? 'chat code' : 'chat'} color="black" > {MOBILE_BROWSER_REGEX.test(navigator.userAgent) - ? { - if (event.key === 'Enter') { - this.submit(); - } else { + ? { + if (this.editor) { + this.editor.element.focus(); + } + }} + > + { this.messageChange(null, null, event.target.value); - } - }} - ref={input => { - if (!input) return; - this.editor = inputProxy(input); - }} - {...props} - /> + }} + onKeyDown={event => { + if (event.key === 'Enter') { + event.preventDefault(); + this.submit(); + } else { + this.messageChange(null, null, event.target.value); + } + }} + ref={input => { + if (!input) return; + this.editor = inputProxy(input); + }} + {...props} + /> + : {content.code.expression} diff --git a/pkg/interface/src/views/apps/chat/components/content/text.js b/pkg/interface/src/views/apps/chat/components/content/text.js index d5c5c58af..2fe98c6f8 100644 --- a/pkg/interface/src/views/apps/chat/components/content/text.js +++ b/pkg/interface/src/views/apps/chat/components/content/text.js @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import { Link } from 'react-router-dom'; import ReactMarkdown from 'react-markdown'; import RemarkDisableTokenizers from 'remark-disable-tokenizers'; +import RemarkBreaks from 'remark-breaks'; import urbitOb from 'urbit-ob'; import { Text } from '@tlon/indigo-react'; @@ -26,10 +27,10 @@ const DISABLED_INLINE_TOKENS = [ const renderers = { inlineCode: ({language, value}) => { - return {value} + return {value} }, paragraph: ({ children }) => { - return ({children}); + return ({children}); }, code: ({language, value}) => { return @@ -51,6 +53,9 @@ const MessageMarkdown = React.memo(props => ( {...props} unwrapDisallowed={true} renderers={renderers} + // shim until we uncover why RemarkBreaks and + // RemarkDisableTokenizers can't be loaded simultaneously + disallowedTypes={['heading', 'list', 'listItem', 'link']} allowNode={(node, index, parent) => { if ( node.type === 'blockquote' @@ -65,10 +70,7 @@ const MessageMarkdown = React.memo(props => ( return true; }} - plugins={[[ - RemarkDisableTokenizers, - { block: DISABLED_BLOCK_TOKENS, inline: DISABLED_INLINE_TOKENS } - ]]} /> + plugins={[RemarkBreaks]} /> )); diff --git a/pkg/interface/src/views/apps/chat/css/custom.css b/pkg/interface/src/views/apps/chat/css/custom.css index 898422b79..702106082 100644 --- a/pkg/interface/src/views/apps/chat/css/custom.css +++ b/pkg/interface/src/views/apps/chat/css/custom.css @@ -277,9 +277,6 @@ pre.CodeMirror-placeholder.CodeMirror-line-like { color: var(--gray); } /* dark */ @media (prefers-color-scheme: dark) { - body { - background-color: #333; - } .bg-black-d { background-color: black; } diff --git a/pkg/interface/src/views/apps/launch/app.js b/pkg/interface/src/views/apps/launch/app.js index bd1877a1c..7861d905f 100644 --- a/pkg/interface/src/views/apps/launch/app.js +++ b/pkg/interface/src/views/apps/launch/app.js @@ -1,5 +1,4 @@ import React, { useState } from 'react'; -import Helmet from 'react-helmet'; import styled from 'styled-components'; import { Box, Row, Icon, Text } from '@tlon/indigo-react'; @@ -14,6 +13,7 @@ import ModalButton from './components/ModalButton'; import { writeText } from '~/logic/lib/util'; import { NewGroup } from "~/views/landscape/components/NewGroup"; import { JoinGroup } from "~/views/landscape/components/JoinGroup"; +import { Helmet } from 'react-helmet'; const ScrollbarLessBox = styled(Box)` scrollbar-width: none !important; @@ -25,13 +25,38 @@ const ScrollbarLessBox = styled(Box)` export default function LaunchApp(props) { const [hashText, setHashText] = useState(props.baseHash); - + const hashBox = ( + { + writeText(props.baseHash); + setHashText('copied'); + setTimeout(() => { + setHashText(props.baseHash); + }, 2000); + }} + > + {hashText || props.baseHash} + + ); return ( <> - - OS1 - Home + + { props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - + - DMs + Drafts + DMs + Drafts @@ -77,36 +102,15 @@ export default function LaunchApp(props) { icon="CreateGroup" bg="green" color="#fff" - text="Create a Group" + text="Create Group" > + {hashBox} - { - writeText(props.baseHash); - setHashText('copied'); - setTimeout(() => { - setHashText(props.baseHash); - }, 2000); - }} - > - {hashText || props.baseHash} - + {hashBox} ); } diff --git a/pkg/interface/src/views/apps/launch/components/tiles/basic.js b/pkg/interface/src/views/apps/launch/components/tiles/basic.js index 6206dd78b..d90dc29b0 100644 --- a/pkg/interface/src/views/apps/launch/components/tiles/basic.js +++ b/pkg/interface/src/views/apps/launch/components/tiles/basic.js @@ -20,8 +20,8 @@ export default class BasicTile extends React.PureComponent { size='12px' display='inline-block' verticalAlign='top' - pt='5px' - pr='2px' + mt='5px' + mr='2' /> : null }{props.title} diff --git a/pkg/interface/src/views/apps/launch/components/tiles/weather.js b/pkg/interface/src/views/apps/launch/components/tiles/weather.js index 68e91f815..7e409edd8 100644 --- a/pkg/interface/src/views/apps/launch/components/tiles/weather.js +++ b/pkg/interface/src/views/apps/launch/components/tiles/weather.js @@ -171,7 +171,7 @@ export default class WeatherTile extends React.Component { onClick={() => this.setState({ manualEntry: !this.state.manualEntry })} > - + Weather @@ -217,15 +217,14 @@ export default class WeatherTile extends React.Component { title={`${locationName} Weather`} > - - Weather + this.setState({ manualEntry: !this.state.manualEntry }) } > - -> + Weather -> @@ -268,7 +267,7 @@ export default class WeatherTile extends React.Component { flexDirection="column" justifyContent="flex-start" > - Weather + Weather Loading, please check again later... diff --git a/pkg/interface/src/views/apps/launch/css/custom.css b/pkg/interface/src/views/apps/launch/css/custom.css index 086247060..8df17cad1 100644 --- a/pkg/interface/src/views/apps/launch/css/custom.css +++ b/pkg/interface/src/views/apps/launch/css/custom.css @@ -40,12 +40,12 @@ button { /* stolen from indigo-react reset.css * TODO: remove and add reset.css properly */ - + @keyframes loadingSpinnerRotation { from { transform: rotate(0deg); } - + to { transform: rotate(360deg); } @@ -53,9 +53,6 @@ button { /* dark */ @media all and (prefers-color-scheme: dark) { - body { - background-color: #333; - } .bg-gray0-d { background-color: #333; } diff --git a/pkg/interface/src/views/apps/links/LinkResource.tsx b/pkg/interface/src/views/apps/links/LinkResource.tsx index 962871f03..0bf62c845 100644 --- a/pkg/interface/src/views/apps/links/LinkResource.tsx +++ b/pkg/interface/src/views/apps/links/LinkResource.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useCallback } from "react"; import { Box, Row, Col, Center, LoadingSpinner, Text } from "@tlon/indigo-react"; import { Switch, Route, Link } from "react-router-dom"; import bigInt from 'big-integer'; @@ -10,10 +10,14 @@ import { RouteComponentProps } from "react-router-dom"; import { LinkItem } from "./components/LinkItem"; import LinkSubmit from "./components/LinkSubmit"; +import { LinkPreview } from "./components/link-preview"; +import { LinkWindow } from "./LinkWindow"; import { Comments } from "~/views/components/Comments"; import "./css/custom.css"; +const emptyMeasure = () => {}; + type LinkResourceProps = StoreState & { association: Association; api: GlobalApi; @@ -57,39 +61,28 @@ export function LinkResource(props: LinkResourceProps) { return
; } + return ( - + { return ( - - - - - {Array.from(graph).map(([date, node]) => { - const contact = contactDetails[node.post.author]; - return ( - - ); - })} - + ); }} /> @@ -112,6 +105,7 @@ export function LinkResource(props: LinkResourceProps) { const contact = contactDetails[node.post.author]; return ( + {"<- Back"} + ); }} /> diff --git a/pkg/interface/src/views/apps/links/LinkWindow.tsx b/pkg/interface/src/views/apps/links/LinkWindow.tsx new file mode 100644 index 000000000..71ab000d6 --- /dev/null +++ b/pkg/interface/src/views/apps/links/LinkWindow.tsx @@ -0,0 +1,104 @@ +import React, { useRef, useCallback, useEffect, useMemo } from "react"; +import { Col } from "@tlon/indigo-react"; +import bigInt from 'big-integer'; +import { + Association, + Graph, + Contacts, + Unreads, + LocalUpdateRemoteContentPolicy, + Group, + Rolodex, + S3State, +} from "~/types"; +import GlobalApi from "~/logic/api/global"; +import VirtualScroller from "~/views/components/VirtualScroller"; +import { LinkItem } from "./components/LinkItem"; +import LinkSubmit from "./components/LinkSubmit"; + +interface LinkWindowProps { + association: Association; + contacts: Rolodex; + resource: string; + graph: Graph; + unreads: Unreads; + hideNicknames: boolean; + hideAvatars: boolean; + baseUrl: string; + group: Group; + path: string; + api: GlobalApi; + s3: S3State; +} +export function LinkWindow(props: LinkWindowProps) { + const { graph, api, association } = props; + const loadedNewest = useRef(true); + const loadedOldest = useRef(false); + const virtualList = useRef(); + const fetchLinks = useCallback( + async (newer: boolean) => { + /* stubbed, should we generalize the display of graphs in virtualscroller? */ + }, [] + ); + + useEffect(() => { + const list = virtualList?.current; + if(!list) return; + list.calculateVisibleItems(); + }, [graph.size]); + + const first = graph.peekLargest()?.[0]; + const [,,ship, name] = association['app-path'].split('/'); + + const style = useMemo(() => + ({ + height: "100%", + width: "100%", + display: 'flex', + flexDirection: 'column', + alignItems: 'center' + }), []); + + if (!first) { + return ( + + + + ); + } + + return ( + (virtualList.current = l ?? undefined)} + origin="top" + style={style} + onStartReached={() => {}} + onScroll={() => {}} + data={graph} + size={graph.size} + renderer={({ index, measure, scrollWindow }) => { + const node = graph.get(index); + const post = node?.post; + if (!node || !post) return null; + const linkProps = { + ...props, + node, + measure, + key: index.toString() + }; + if(index.eq(first ?? bigInt.zero)) { + return ( + <> + + + + + + ) + } + return ; + }} + loadRows={fetchLinks} + /> + ); +} diff --git a/pkg/interface/src/views/apps/links/components/LinkItem.tsx b/pkg/interface/src/views/apps/links/components/LinkItem.tsx index e27e45365..83bfc2dc8 100644 --- a/pkg/interface/src/views/apps/links/components/LinkItem.tsx +++ b/pkg/interface/src/views/apps/links/components/LinkItem.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import { Link } from 'react-router-dom'; import { Row, Col, Anchor, Box, Text, Icon, Action } from '@tlon/indigo-react'; @@ -17,8 +17,9 @@ interface LinkItemProps { api: GlobalApi; group: Group; path: string; - contacts: Rolodex[]; + contacts: Rolodex; unreads: Unreads; + measure: (el: any) => void; } export const LinkItem = (props: LinkItemProps) => { @@ -29,9 +30,12 @@ export const LinkItem = (props: LinkItemProps) => { group, path, contacts, + measure, ...rest } = props; + const ref = useRef(null); + const URLparser = new RegExp( /((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/ ); @@ -70,9 +74,18 @@ export const LinkItem = (props: LinkItemProps) => { const markRead = () => { api.hark.markEachAsRead(props.association, '/', `/${index}`, 'link', 'link'); } + + + const onMeasure = useCallback(() => { + ref.current && measure(ref.current); + }, [ref.current, measure]) + + useEffect(() => { + onMeasure(); + }, [onMeasure]); + return ( - - + { url={contents[1].url} text={contents[0].text} unfold={true} + onLoad={onMeasure} style={{ alignSelf: 'center' }} oembedProps={{ p: 2, @@ -116,9 +130,9 @@ export const LinkItem = (props: LinkItemProps) => {
- + - + { - + { > - + ); }; diff --git a/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx b/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx index ab9a78b0f..2eaba4555 100644 --- a/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx +++ b/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx @@ -132,7 +132,6 @@ const LinkSubmit = (props: LinkSubmitProps) => { position="absolute" px={2} pt={2} - fontSize={0} style={{ pointerEvents: 'none' }} >{canUpload ? <> @@ -180,7 +179,6 @@ const LinkSubmit = (props: LinkSubmitProps) => { type="url" pl={2} width="100%" - fontSize={0} py={2} color="black" backgroundColor="transparent" @@ -198,8 +196,8 @@ const LinkSubmit = (props: LinkSubmitProps) => { pl={2} backgroundColor="transparent" width="100%" - fontSize={0} color="black" + fontSize={1} style={{ resize: 'none', height: 40 diff --git a/pkg/interface/src/views/apps/notifications/chat.tsx b/pkg/interface/src/views/apps/notifications/chat.tsx deleted file mode 100644 index 87e870fcc..000000000 --- a/pkg/interface/src/views/apps/notifications/chat.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React, { useCallback } from "react"; -import _ from "lodash"; -import { Link } from "react-router-dom"; -import GlobalApi from "~/logic/api/global"; -import { - Rolodex, - Associations, - ChatNotifIndex, - ChatNotificationContents, - Groups, -} from "~/types"; -import { BigInteger } from "big-integer"; -import { Box, Col } from "@tlon/indigo-react"; -import { Header } from "./header"; -import { pluralize } from "~/logic/lib/util"; -import ChatMessage from "../chat/components/ChatMessage"; - -function describeNotification(mention: boolean, lent: number) { - const msg = pluralize("message", lent !== 1); - if (mention) { - return `mentioned you in ${msg} in`; - } - return `sent ${msg} in`; -} - -export function ChatNotification(props: { - index: ChatNotifIndex; - contents: ChatNotificationContents; - archived: boolean; - read: boolean; - time: number; - timebox: BigInteger; - associations: Associations; - contacts: Rolodex; - groups: Groups; - api: GlobalApi; -}) { - const { index, contents, read, time, api, timebox } = props; - const authors = _.map(contents, "author"); - - const { chat, mention } = index; - const association = props?.associations?.chat?.[chat]; - const groupPath = association?.["group-path"]; - const appPath = index?.chat; - - const group = props?.groups?.[groupPath]; - - const desc = describeNotification(mention, contents.length); - const groupContacts = props.contacts[groupPath] || {}; - - const onClick = useCallback(() => { - if (props.archived) { - return; - } - - const func = read ? "unread" : "read"; - return api.hark[func](timebox, { chat: index }); - }, [api, timebox, index, read]); - - return ( - -
- - {_.map(_.take(contents, 5), (content, idx) => { - let workspace = groupPath; - if (workspace === undefined || group?.hidden) { - workspace = '/home'; - } - const to = `/~landscape${workspace}/resource/chat${appPath}?msg=${content.number}`; - return ( - - {}} - msg={content} - isLastRead={false} - group={group} - contacts={groupContacts} - fontSize='0' - pt='2' - /> - - ); - })} - {contents.length > 5 && ( - - and {contents.length - 5} other message - {contents.length > 6 ? "s" : ""} - - )} - - - ); -} diff --git a/pkg/interface/src/views/apps/notifications/graph.tsx b/pkg/interface/src/views/apps/notifications/graph.tsx index 829f96bbc..8358be149 100644 --- a/pkg/interface/src/views/apps/notifications/graph.tsx +++ b/pkg/interface/src/views/apps/notifications/graph.tsx @@ -25,7 +25,7 @@ import ChatMessage, {MessageWithoutSigil} from "../chat/components/ChatMessage"; function getGraphModuleIcon(module: string) { if (module === "link") { - return "Links"; + return "Collection"; } return _.capitalize(module); } @@ -90,6 +90,8 @@ const GraphNodeContent = ({ group, post, contacts, mod, description, index, remo content={contents} group={group} contacts={contacts} + fontSize='14px' + lineHeight="tall" /> } else if (idx[1] === "1") { const [{ text: header }, { text: body }] = contents; @@ -164,13 +166,14 @@ const GraphNode = ({ group, read, onRead, + showContact = false, remoteContentPolicy }) => { const { contents } = post; author = deSig(author); const history = useHistory(); - const img = ( + const img = showContact ? ( - ); + ) : ; const groupContacts = contacts[groupPath] ?? {}; @@ -192,10 +195,10 @@ const GraphNode = ({ }, [read, onRead]); return ( - + {img} - {moment(time).format("HH:mm")} - - + } + { - if (props.archived) { + if (props.archived || read) { return; } - const func = read ? "unread" : "read"; - return api.hark[func](timebox, { graph: index }); + return api.hark["read"](timebox, { graph: index }); }, [api, timebox, index, read]); return ( - + <>
- + {_.map(contents, (content, idx) => ( ))} - - + + ); } diff --git a/pkg/interface/src/views/apps/notifications/header.tsx b/pkg/interface/src/views/apps/notifications/header.tsx index e930d9b19..b987a0693 100644 --- a/pkg/interface/src/views/apps/notifications/header.tsx +++ b/pkg/interface/src/views/apps/notifications/header.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Text as NormalText, Row, Icon, Rule } from "@tlon/indigo-react"; +import { Text as NormalText, Row, Icon, Rule, Box } from "@tlon/indigo-react"; import f from "lodash/fp"; import _ from "lodash"; import moment from "moment"; @@ -71,12 +71,13 @@ export function Header(props: { channel; return ( - + {!props.archived && ( )} @@ -84,13 +85,13 @@ export function Header(props: { {authorDesc} {description} - {!!moduleIcon && } - {!!channel && {channelTitle}} - + {!!moduleIcon && } + {!!channel && {channelTitle}} + {groupTitle && <> - {groupTitle} - + {groupTitle} + } diff --git a/pkg/interface/src/views/apps/notifications/inbox.tsx b/pkg/interface/src/views/apps/notifications/inbox.tsx index 4b55a7e49..3bc95e7ec 100644 --- a/pkg/interface/src/views/apps/notifications/inbox.tsx +++ b/pkg/interface/src/views/apps/notifications/inbox.tsx @@ -1,18 +1,16 @@ -import React, { useEffect, useCallback } from "react"; +import React, { useEffect, useCallback, useRef, useState } from "react"; import f from "lodash/fp"; import _ from "lodash"; -import { Icon, Col, Row, Box, Text, Anchor, Rule } from "@tlon/indigo-react"; +import { Icon, Col, Row, Box, Text, Anchor, Rule, Center } from "@tlon/indigo-react"; import moment from "moment"; -import { Notifications, Rolodex, Timebox, IndexedNotification, Groups } from "~/types"; +import { Notifications, Rolodex, Timebox, IndexedNotification, Groups, GroupNotificationsConfig, NotificationGraphConfig } from "~/types"; import { MOMENT_CALENDAR_DATE, daToUnix, resourceAsPath } from "~/logic/lib/util"; import { BigInteger } from "big-integer"; import GlobalApi from "~/logic/api/global"; import { Notification } from "./notification"; import { Associations } from "~/types"; -import { cite } from '~/logic/lib/util'; -import { InviteItem } from '~/views/components/Invite'; -import { useWaitForProps } from "~/logic/lib/useWaitForProps"; -import { useHistory } from "react-router-dom"; +import {Invites} from "./invites"; +import {useLazyScroll} from "~/logic/lib/useLazyScroll"; type DatedTimebox = [BigInteger, Timebox]; @@ -45,10 +43,10 @@ export default function Inbox(props: { contacts: Rolodex; filter: string[]; invites: any; + notificationsGroupConfig: GroupNotificationsConfig; + notificationsGraphConfig: NotificationGraphConfig; }) { const { api, associations, invites } = props; - const waiter = useWaitForProps(props) - const history = useHistory(); useEffect(() => { let seen = false; setTimeout(() => { @@ -61,109 +59,73 @@ export default function Inbox(props: { }; }, []); - const [newNotifications, ...notifications] = + const notifications = Array.from(props.showArchive ? props.archive : props.notifications) || []; + + const calendar = { + ...MOMENT_CALENDAR_DATE, sameDay: function (now) { + if (this.subtract(6, 'hours').isBefore(now)) { + return "[Earlier Today]"; + } else { + return MOMENT_CALENDAR_DATE.sameDay; + } + } + }; - const notificationsByDay = f.flow( - f.map(([date, nots]) => [ + let notificationsByDay = f.flow( + f.map(([date, nots]) => [ date, nots.filter(filterNotification(associations, props.filter)), ]), - f.groupBy(([date]) => - moment(daToUnix(date)).format("DDMMYYYY") - ), - f.values, - f.reverse + f.groupBy(([d]) => { + const date = moment(daToUnix(d)); + if (moment().subtract(6, 'hours').isBefore(date)) { + return 'latest'; + } else { + return date.format("YYYYMMDD"); + } + }), )(notifications); - useEffect(() => { - api.hark.getMore(props.showArchive); - }, [props.showArchive]); + const notificationsByDayMap = new Map( + Object.keys(notificationsByDay).map(timebox => { + return [timebox, notificationsByDay[timebox]]; + }) + ); - const onScroll = useCallback((e) => { - let container = e.target; - const { scrollHeight, scrollTop, clientHeight } = container; - if((scrollHeight - scrollTop) < 1.5 * clientHeight) { - api.hark.getMore(props.showArchive); - } - }, [props.showArchive]); + const scrollRef = useRef(null); - const acceptInvite = (app: string, uid: string) => async (invite) => { - const resource = { - ship: `~${invite.resource.ship}`, - name: invite.resource.name - }; + const loadMore = useCallback(async () => { + return api.hark.getMore(); + }, [api]); - const resourcePath = resourceAsPath(invite.resource); - if(app === 'contacts') { - await api.contacts.join(resource); - await waiter(p => resourcePath in p.associations?.contacts); - await api.invite.accept(app, uid); - history.push(`/~landscape${resourcePath}`); - } else if ( app === 'chat') { - await api.invite.accept(app, uid); - history.push(`/~landscape/home/resource/chat${resourcePath.slice(5)}`); - } else if ( app === 'graph') { - await api.invite.accept(app, uid); - history.push(`/~graph/join${resourcePath}`); - } - }; + const loadedAll = useLazyScroll(scrollRef, 0.2, loadMore); - const inviteItems = (invites, api) => { - const returned = []; - Object.keys(invites).map((appKey) => { - const app = invites[appKey]; - Object.keys(app).map((uid) => { - const invite = app[uid]; - const inviteItem = - api.invite.decline(appKey, uid)} - />; - returned.push(inviteItem); - }); - }); - return returned; - }; return ( - - - {inviteItems(invites, api)} - - {newNotifications && ( - - )} - {_.map( - notificationsByDay, - (timeboxes, idx) => - timeboxes.length > 0 && ( - - ) + + + {[...notificationsByDayMap.keys()].sort().reverse().map((day, index) => { + const timeboxes = notificationsByDayMap.get(day)!; + return timeboxes.length > 0 && ( + + ); + })} + {loadedAll && ( +
+ No more notifications +
)} ); @@ -181,21 +143,17 @@ function sortIndexedNotification( } function DaySection({ + label, contacts, groups, archive, timeboxes, - latest = false, associations, api, groupConfig, graphConfig, - chatConfig, - remoteContentPolicy }) { - const calendar = latest - ? MOMENT_CALENDAR_DATE - : { ...MOMENT_CALENDAR_DATE, sameDay: "[Earlier Today]" }; + const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0); if (lent === 0 || timeboxes.length === 0) { return null; @@ -203,23 +161,22 @@ function DaySection({ return ( <> - + - {moment(daToUnix(timeboxes[0][0])).calendar(null, calendar)} + {label} - {_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i) => + {_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i: number) => _.map(nots.sort(sortIndexedNotification), (not, j: number) => ( {(i !== 0 || j !== 0) && ( - + )} async () => { + const resource = { + ship: `~${invite.resource.ship}`, + name: invite.resource.name, + }; + + const resourcePath = resourceAsPath(invite.resource); + if (app === "contacts") { + await api.contacts.join(resource); + await waiter((p) => resourcePath in p.associations?.contacts); + await api.invite.accept(app, uid); + history.push(`/~landscape${resourcePath}`); + } else if (app === "graph") { + await api.invite.accept(app, uid); + history.push(`/~graph/join${resourcePath}`); + } + }; + + const declineInvite = useCallback( + (app: string, uid: string) => () => api.invite.decline(app, uid), + [api] + ); + + return ( + + {Object.keys(invites).reduce((items, appKey) => { + const app = invites[appKey]; + let appItems = Object.keys(app).map((uid) => { + const invite = app[uid]; + return ( + + ); + }); + return [...items, ...appItems]; + }, [] as JSX.Element[])} + + ); +} diff --git a/pkg/interface/src/views/apps/notifications/notification.tsx b/pkg/interface/src/views/apps/notifications/notification.tsx index 6f2878b2b..c9ca7455f 100644 --- a/pkg/interface/src/views/apps/notifications/notification.tsx +++ b/pkg/interface/src/views/apps/notifications/notification.tsx @@ -1,5 +1,5 @@ -import React, { ReactNode, useCallback, useMemo } from "react"; -import { Row, Box, Col, Text, Anchor, Icon, Action } from "@tlon/indigo-react"; +import React, { ReactNode, useCallback, useMemo, useState } from "react"; +import { Row, Box } from "@tlon/indigo-react"; import _ from "lodash"; import { GraphNotificationContents, @@ -7,7 +7,6 @@ import { GroupNotificationContents, NotificationGraphConfig, GroupNotificationsConfig, - NotifIndex, Groups, Associations, Contacts, @@ -17,8 +16,8 @@ import { getParentIndex } from "~/logic/lib/notification"; import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction"; import { GroupNotification } from "./group"; import { GraphNotification } from "./graph"; -import { ChatNotification } from "./chat"; import { BigInteger } from "big-integer"; +import { useHovering } from "~/logic/lib/util"; interface NotificationProps { notification: IndexedNotification; @@ -30,7 +29,6 @@ interface NotificationProps { contacts: Contacts; graphConfig: NotificationGraphConfig; groupConfig: GroupNotificationsConfig; - chatConfig: string[]; } function getMuted( @@ -55,9 +53,6 @@ function getMuted( if ("group" in index) { return _.findIndex(groups || [], (g) => g === index.group.group) === -1; } - if ("chat" in index) { - return _.findIndex(chat || [], (c) => c === index.chat.chat) === -1; - } return false; } @@ -89,11 +84,21 @@ function NotificationWrapper(props: { return api.hark[func](notif); }, [notif, api, isMuted]); + const { hovering, bind } = useHovering(); + const changeMuteDesc = isMuted ? "Unmute" : "Mute"; return ( - + {children} - + {changeMuteDesc} @@ -103,7 +108,7 @@ function NotificationWrapper(props: { )} - + ); } @@ -166,26 +171,6 @@ export function Notification(props: NotificationProps) { ); } - if ("chat" in notification.index) { - const index = notification.index.chat; - const c: ChatNotificationContents = (contents as any).chat; - return ( - - - - ); - } return null; } diff --git a/pkg/interface/src/views/apps/notifications/notifications.tsx b/pkg/interface/src/views/apps/notifications/notifications.tsx index cac2ff16a..c4567cc09 100644 --- a/pkg/interface/src/views/apps/notifications/notifications.tsx +++ b/pkg/interface/src/views/apps/notifications/notifications.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useState } from "react"; import _ from 'lodash'; import { Box, Col, Text, Row } from "@tlon/indigo-react"; import { Link, Switch, Route } from "react-router-dom"; +import Helmet from "react-helmet"; import { Body } from "~/views/components/Body"; import { PropFunc } from "~/types/util"; @@ -52,74 +53,79 @@ export default function NotificationsScreen(props: any) { render={(routeProps) => { const { view } = routeProps.match.params; return ( - - - - Updates - - - - Inbox - - - - - Preferences - - - - - - - - - } + <> + + { props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Notifications + + + + - - - Filter: - - {groupFilterDesc} - - - - {view === "preferences" && ( - - )} - {!view && } - - + Updates + + + + Inbox + + + + + Preferences + + + + + + + + + } + > + + + Filter: + + {groupFilterDesc} + + + + {view === "preferences" && ( + + )} + {!view && } + + + ); }} /> diff --git a/pkg/interface/src/views/apps/profile/components/Profile.tsx b/pkg/interface/src/views/apps/profile/components/Profile.tsx index 9cac884b2..cb46b3514 100644 --- a/pkg/interface/src/views/apps/profile/components/Profile.tsx +++ b/pkg/interface/src/views/apps/profile/components/Profile.tsx @@ -29,7 +29,7 @@ export function Profile(props: any) { const image = (!hideAvatars && contact?.avatar) ? - : ; + : ; return (
- {`~${ship}`} + {`${ship}`}
@@ -63,7 +63,7 @@ export function ViewProfile(props: any) {
- ) : null + ) : null }
- {`~${ship} `} + {`${ship} `} remains private
diff --git a/pkg/interface/src/views/apps/profile/profile.tsx b/pkg/interface/src/views/apps/profile/profile.tsx index d271f9f9f..7baa1724d 100644 --- a/pkg/interface/src/views/apps/profile/profile.tsx +++ b/pkg/interface/src/views/apps/profile/profile.tsx @@ -15,7 +15,7 @@ export default function ProfileScreen(props: any) { return ( <> - OS1 - Profile + { props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Profile = useRef(); diff --git a/pkg/interface/src/views/apps/publish/components/Note.tsx b/pkg/interface/src/views/apps/publish/components/Note.tsx index 7035fa26c..3c71ecd9e 100644 --- a/pkg/interface/src/views/apps/publish/components/Note.tsx +++ b/pkg/interface/src/views/apps/publish/components/Note.tsx @@ -47,7 +47,7 @@ export function Note(props: NoteProps & RouteComponentProps) { const noteId = bigInt(index[1]); useEffect(() => { api.hark.markEachAsRead(props.association, '/',`/${index[1]}/1/1`, 'note', 'publish'); - }, [props.association]); + }, [props.association, props.note]); @@ -67,7 +67,7 @@ export function Note(props: NoteProps & RouteComponentProps) { color="red" ml={2} onClick={deletePost} - css={{ cursor: "pointer" }} + style={{ cursor: "pointer" }} > Delete @@ -75,6 +75,13 @@ export function Note(props: NoteProps & RouteComponentProps) { ); } + const windowRef = React.useRef(null); + useEffect(() => { + if (windowRef.current) { + windowRef.current.parentElement.scrollTop = 0; + } + }, [windowRef, note]); + return ( {"<- Notebook Index"} diff --git a/pkg/interface/src/views/apps/publish/components/NotePreview.tsx b/pkg/interface/src/views/apps/publish/components/NotePreview.tsx index 163556e9f..1820188c4 100644 --- a/pkg/interface/src/views/apps/publish/components/NotePreview.tsx +++ b/pkg/interface/src/views/apps/publish/components/NotePreview.tsx @@ -61,9 +61,9 @@ export function NotePreview(props: NotePreviewProps) { overflow='hidden' p='2' > - {title} + {title} - + props.baseUrl + p; - - const contact = notebookContacts?.[ship]; - const isOwn = `~${window.ship}` === ship; - let isWriter = true; - - if (group.tags?.publish?.[`writers-${book}`]) { - isWriter = isOwn || group.tags?.publish?.[`writers-${book}`]?.has(window.ship); - } - - const showNickname = useShowNickname(contact); - return ( - - - {metadata?.title} - by - - {showNickname ? contact?.nickname : ship} - - - {isWriter && ( - - - - )} - - - - OS1 - Terminal + + { this.props.notificationsCount ? `(${String(this.props.notificationsCount) }) `: '' }Landscape + )} @@ -147,4 +149,4 @@ class ProfileOverlay extends PureComponent { } } -export default withLocalState(ProfileOverlay, ['hideAvatars']); \ No newline at end of file +export default withLocalState(ProfileOverlay, ['hideAvatars', 'hideNicknames']); \ No newline at end of file diff --git a/pkg/interface/src/views/components/ReconnectButton.js b/pkg/interface/src/views/components/ReconnectButton.js index 7495509dc..1038655b8 100644 --- a/pkg/interface/src/views/components/ReconnectButton.js +++ b/pkg/interface/src/views/components/ReconnectButton.js @@ -8,14 +8,14 @@ const ReconnectButton = ({ connection, subscription }) => { if (connectedStatus === "disconnected") { return ( ); } else if (connectedStatus === "reconnecting") { return ( ) diff --git a/pkg/interface/src/views/components/RichText.js b/pkg/interface/src/views/components/RichText.js index ecffcf0d7..4b40655f3 100644 --- a/pkg/interface/src/views/components/RichText.js +++ b/pkg/interface/src/views/components/RichText.js @@ -3,8 +3,11 @@ import RemoteContent from '~/views/components/RemoteContent'; import { hasProvider } from 'oembed-parser'; import ReactMarkdown from 'react-markdown'; import RemarkDisableTokenizers from 'remark-disable-tokenizers'; - import { BaseAnchor, Text } from '@tlon/indigo-react'; +import { isValidPatp } from 'urbit-ob'; + +import { deSig } from '~/logic/lib/util'; +import { Mention } from '~/views/components/MentionText'; const DISABLED_BLOCK_TOKENS = [ 'indentedCode', @@ -23,19 +26,25 @@ const RichText = React.memo(({ disableRemoteContent, ...props }) => ( { - if (disableRemoteContent) { - props.remoteContentPolicy = { - imageShown: false, - audioShown: false, - videoShown: false, - oembedShown: false - }; + link: (linkProps) => { + const remoteContentPolicy = disableRemoteContent ? { + imageShown: false, + audioShown: false, + videoShown: false, + oembedShown: false + } : null; + if (hasProvider(linkProps.href)) { + return ; } - if (hasProvider(props.href)) { - return ; + + return {linkProps.children}; + }, + linkReference: (linkProps) => { + const linkText = String(linkProps.children[0].props.children); + if (isValidPatp(linkText)) { + return ; } - return {props.children}; + return linkText; }, paragraph: (paraProps) => { return {paraProps.children}; diff --git a/pkg/interface/src/views/components/ShipSearch.tsx b/pkg/interface/src/views/components/ShipSearch.tsx index 42efda447..f26ed4253 100644 --- a/pkg/interface/src/views/components/ShipSearch.tsx +++ b/pkg/interface/src/views/components/ShipSearch.tsx @@ -64,6 +64,8 @@ export function ShipSearch(props: InviteSearchProps) { if(valid) { setInputShip(ship); setError(error === INVALID_SHIP_ERR ? undefined : error); + } else if (ship === undefined) { + return; } else { setError(INVALID_SHIP_ERR); setInputTouched(false); @@ -190,9 +192,9 @@ export function ShipSearch(props: InviteSearchProps) { alignItems="center" py={1} px={2} - border={1} - borderColor="washedGrey" color="black" + borderRadius='2' + bg='washedGray' fontSize={0} mt={2} mr={2} diff --git a/pkg/interface/src/views/components/StatelessAsyncAction.tsx b/pkg/interface/src/views/components/StatelessAsyncAction.tsx index 818410cfc..b5e7936b3 100644 --- a/pkg/interface/src/views/components/StatelessAsyncAction.tsx +++ b/pkg/interface/src/views/components/StatelessAsyncAction.tsx @@ -8,6 +8,7 @@ import { useFormikContext } from "formik"; interface AsyncActionProps { children: ReactNode; name: string; + disabled?: boolean; onClick: (e: React.MouseEvent) => Promise; } @@ -15,6 +16,7 @@ export function StatelessAsyncAction({ children, onClick, name = '', + disabled = false, ...rest }: AsyncActionProps & Parameters[0]) { const { @@ -23,7 +25,10 @@ export function StatelessAsyncAction({ } = useStatelessAsyncClickable(onClick, name); return ( - + {state === "error" ? ( "Error" ) : state === "loading" ? ( diff --git a/pkg/interface/src/views/components/StatelessAsyncButton.tsx b/pkg/interface/src/views/components/StatelessAsyncButton.tsx index d6d041ca6..faf933ed3 100644 --- a/pkg/interface/src/views/components/StatelessAsyncButton.tsx +++ b/pkg/interface/src/views/components/StatelessAsyncButton.tsx @@ -7,7 +7,7 @@ import { useStatelessAsyncClickable } from "~/logic/lib/useStatelessAsyncClickab interface AsyncButtonProps { children: ReactNode; - name: string; + name?: string; onClick: (e: React.MouseEvent) => Promise; } @@ -15,6 +15,7 @@ export function StatelessAsyncButton({ children, onClick, name = "", + disabled = false, ...rest }: AsyncButtonProps & Parameters[0]) { const { @@ -23,7 +24,12 @@ export function StatelessAsyncButton({ } = useStatelessAsyncClickable(onClick, name); return ( - toggleOmnibox()}> @@ -56,7 +56,7 @@ const StatusBar = (props) => { onClick={() => window.open( 'https://github.com/urbit/landscape/issues/new' + '?assignees=&labels=development-stream&title=&' + - `body=commit:%20${process.env.LANDSCAPE_SHORTHASH}` + `body=commit:%20urbit/urbit@${process.env.LANDSCAPE_SHORTHASH}` )} > Submit an issue @@ -65,28 +65,37 @@ const StatusBar = (props) => { + props.history.push('/~profile/' + window.ship)}> + cursor='pointer' + fontSize={0} + onClick={() => props.history.push('/~profile/~' + window.ship)}> View Profile props.history.push('/~profile/' + window.ship)}> + cursor='pointer' + fontSize={0} + onClick={() => props.history.push('/~profile/~' + window.ship)}> Set Status props.history.push('/~profile/' + window.ship)}> + cursor='pointer' + fontSize={0} + onClick={() => props.history.push('/~profile/~' + window.ship)}> System Settings - + }> ; } | undefined; + overscan = 150; + OVERSCAN_SIZE = 100; // Minimum number of messages on either side before loadRows is called constructor(props: VirtualScrollerProps) { @@ -53,7 +55,7 @@ export default class VirtualScroller extends Component { - totalHeight += this.heightOf(index); + totalHeight += Math.max(this.heightOf(index), 0); }); averageHeight = Number((totalHeight / this.props.data.size).toFixed()); totalHeight += (this.props.size - this.props.data.size) * averageHeight; @@ -136,41 +140,23 @@ export default class VirtualScroller extends Component(); - let startBuffer = new BigIntOrderedMap(); - let endBuffer = new BigIntOrderedMap(); const { scrollTop, offsetHeight: windowHeight } = this.window; - const { averageHeight } = this.state; + const { averageHeight, totalHeight } = this.state; const { data, size: totalSize, onCalculateVisibleItems } = this.props; - console.log(windowHeight); - - const overscan = Math.max(windowHeight / 2, 200); [...data].forEach(([index, datum]) => { const height = this.heightOf(index); - if (startgap < (scrollTop - overscan) && !startGapFilled) { - startBuffer.set(index, datum); + if (startgap < (scrollTop - this.overscan) && !startGapFilled) { startgap += height; - } else if (heightShown < (windowHeight + overscan)) { + } else if (heightShown < (windowHeight + this.overscan)) { startGapFilled = true; visibleItems.set(index, datum); heightShown += height; - } else if (endBuffer.size < visibleItems.size) { - endBuffer.set(index, data.get(index)); - } else { - endgap += height; - } - }); - - startBuffer = new BigIntOrderedMap( - [...startBuffer].reverse().slice(0, (visibleItems.size - visibleItems.size % 5)) - ); - - - startBuffer.forEach((_datum, index) => { - startgap -= this.heightOf(index); + } }); + endgap = totalHeight - heightShown - startgap; const firstVisibleKey = visibleItems.peekSmallest()?.[0] ?? this.estimateIndexFromScrollTop(scrollTop)!; const smallest = data.peekSmallest(); @@ -189,7 +175,7 @@ export default class VirtualScroller extends Component { @@ -303,7 +291,7 @@ export default class VirtualScroller extends Component - + {indexesToRender.map(render)} diff --git a/pkg/interface/src/views/components/leap/Omnibox.js b/pkg/interface/src/views/components/leap/Omnibox.js index a58787d43..f11eb444d 100644 --- a/pkg/interface/src/views/components/leap/Omnibox.js +++ b/pkg/interface/src/views/components/leap/Omnibox.js @@ -32,7 +32,7 @@ export class Omnibox extends Component { const { pathname } = this.props.location; const selectedGroup = pathname.startsWith('/~landscape/ship/') ? '/' + pathname.split('/').slice(2,5).join('/') : null; - this.setState({ index: index(this.props.associations, this.props.apps.tiles, selectedGroup, this.props.groups) }); + this.setState({ index: index(this.props.contacts, this.props.associations, this.props.apps.tiles, selectedGroup, this.props.groups) }); } if (prevProps && (prevProps.apps !== this.props.apps) && (this.state.query === '')) { @@ -56,7 +56,7 @@ export class Omnibox extends Component { } getSearchedCategories() { - return ['other', 'commands', 'groups', 'subscriptions', 'apps']; + return ['ships', 'other', 'commands', 'groups', 'subscriptions', 'apps']; } control(evt) { @@ -234,10 +234,9 @@ export class Omnibox extends Component { .filter(category => category.categoryResults.length > 0) .map(({ category, categoryResults }, i) => { const categoryTitle = (category === 'other') - ? null : {category.charAt(0).toUpperCase() + category.slice(1)}; + ? null : {category.charAt(0).toUpperCase() + category.slice(1)}; const selected = this.state.selected?.length ? this.state.selected[1] : ''; return ( - {categoryTitle} {categoryResults.map((result, i2) => ( ))} @@ -264,6 +264,7 @@ export class Omnibox extends Component { if (state?.selected?.length === 0 && Array.from(this.state.results.values()).flat().length) { this.setNextSelected(); } + return ( Object.values(obj))); @@ -37,24 +37,25 @@ export class OmniboxResult extends Component { || icon.toLowerCase() === 'links' || icon.toLowerCase() === 'terminal') { - icon = (icon === 'Link') ? 'Links' : + icon = (icon === 'Link') ? 'Collection' : (icon === 'Terminal') ? 'Dojo' : icon; - graphic = ; + graphic = ; } else if (icon === 'inbox') { graphic = - + {(notifications > 0 || inviteCount.length > 0) && ( )} ; } else if (icon === 'logout') { - graphic = ; + graphic = ; } else if (icon === 'profile') { - graphic = ; + text = text.startsWith('Profile') ? window.ship : text; + graphic = ; } else if (icon === 'home') { - graphic = ; + graphic = ; } else if (icon === 'notifications') { - graphic = ; + graphic = ; } else { graphic = ; } @@ -67,9 +68,10 @@ export class OmniboxResult extends Component { } render() { - const { icon, text, subtext, link, navigate, selected, invites, notifications } = this.props; + const { icon, text, subtext, link, navigate, selected, invites, notifications, contacts } = this.props; - const graphic = this.getIcon(icon, selected, link, invites, notifications); + const color = contacts?.[text] ? `#${uxToHex(contacts[text].color)}` : "#000000"; + const graphic = this.getIcon(icon, selected, link, invites, notifications, text, color); return ( a.graph === appPath && a.index === "/" ) === -1; @@ -63,8 +63,10 @@ export function ChannelMenu(props: ChannelMenuProps) { }, [api, association]); const onDelete = useCallback(async () => { - await api.graph.deleteGraph(name); - history.push(`/~landscape${workspace}`); + if (confirm('Are you sure you want to delete this channel?')) { + await api.graph.deleteGraph(name); + history.push(`/~landscape${workspace}`); + } }, [api, association]); return ( @@ -100,7 +102,7 @@ export function ChannelMenu(props: ChannelMenuProps) { - + Channel Settings @@ -117,9 +119,9 @@ export function ChannelMenu(props: ChannelMenuProps) { } alignX="right" alignY="top" - width="250px" + dropWidth="250px" > - + ); } diff --git a/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx b/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx index d7601cf59..356aa0313 100644 --- a/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx +++ b/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx @@ -1,21 +1,18 @@ -import React from "react"; +import React from 'react'; import { - Center, Box, Col, Row, Text, - IconButton, - Button, - Icon, -} from "@tlon/indigo-react"; -import { uxToHex } from "~/logic/lib/util"; -import { Link } from "react-router-dom"; + Icon +} from '@tlon/indigo-react'; +import { uxToHex } from '~/logic/lib/util'; +import { Link } from 'react-router-dom'; -import { Association, Associations } from "~/types/metadata-update"; -import { Dropdown } from "~/views/components/Dropdown"; -import { Workspace } from "~/types"; -import { getTitleFromWorkspace } from "~/logic/lib/workspace"; +import { Associations } from '~/types/metadata-update'; +import { Dropdown } from '~/views/components/Dropdown'; +import { Workspace } from '~/types'; +import { getTitleFromWorkspace } from '~/logic/lib/workspace'; const GroupSwitcherItem = ({ to, children, bottom = false, ...rest }) => ( @@ -47,7 +44,7 @@ function RecentGroups(props: { recent: string[]; associations: Associations }) { return (e in associations?.contacts); }).slice(1, 5).map((g) => { const assoc = associations.contacts[g]; - const color = uxToHex(assoc?.metadata?.color || "0x0"); + const color = uxToHex(assoc?.metadata?.color || '0x0'); return ( @@ -60,7 +57,7 @@ function RecentGroups(props: { recent: string[]; associations: Associations }) { bg={`#${color}`} mr={2} display="block" - flexShrink='0' + flexShrink={0} /> {assoc?.metadata?.title} @@ -76,22 +73,20 @@ export function GroupSwitcher(props: { workspace: Workspace; baseUrl: string; recentGroups: string[]; + isAdmin: any; }) { const { associations, workspace, isAdmin } = props; const title = getTitleFromWorkspace(associations, workspace); const navTo = (to: string) => `${props.baseUrl}${to}`; return ( - + - + Join Group - {workspace.type === "group" && ( + {workspace.type === 'group' && ( <> - + Participants - + Group Settings - {isAdmin && ( + {isAdmin && ( } > - - - {title} - + + + {title} - - {(workspace.type === "group") && ( + + {(workspace.type === 'group') && ( <> - {isAdmin && ( + {isAdmin && ( )} - - + + )} diff --git a/pkg/interface/src/views/landscape/components/GroupsPane.tsx b/pkg/interface/src/views/landscape/components/GroupsPane.tsx index 24a2b63e2..34e2af007 100644 --- a/pkg/interface/src/views/landscape/components/GroupsPane.tsx +++ b/pkg/interface/src/views/landscape/components/GroupsPane.tsx @@ -7,6 +7,7 @@ import { } from "react-router-dom"; import { Col, Box, Text } from "@tlon/indigo-react"; import _ from "lodash"; +import Helmet from 'react-helmet'; import { Resource } from "./Resource"; import { PopoverRoutes } from "./PopoverRoutes"; @@ -131,28 +132,36 @@ export function GroupsPane(props: GroupsPaneProps) { const appPath = `/ship/${host}/${name}`; const association = associations.graph[appPath]; const resourceUrl = `${baseUrl}/join/${app}${appPath}`; + let title = groupAssociation?.metadata?.title ?? 'Landscape'; if (!association) { return ; } + + title += ` - ${association.metadata.title}`; return ( - - + + {props.notificationsCount ? `(${String(props.notificationsCount)}) ` : ''}{ title } + + - {popovers(routeProps, resourceUrl)} - + > + + {popovers(routeProps, resourceUrl)} + + ); }} /> @@ -184,20 +193,26 @@ export function GroupsPane(props: GroupsPaneProps) { const hasDescription = groupAssociation?.metadata?.description; const description = (hasDescription && hasDescription !== "") ? hasDescription : "Create or select a channel to get started" + const title = groupAssociation?.metadata?.title ?? 'Landscape'; return ( - - - - {description} - - - {popovers(routeProps, baseUrl)} - + <> + + {props.notificationsCount ? `(${String(props.notificationsCount)}) ` : ''}{ title } + + + + + {description} + + + {popovers(routeProps, baseUrl)} + + ); }} /> diff --git a/pkg/interface/src/views/landscape/components/NewChannel.tsx b/pkg/interface/src/views/landscape/components/NewChannel.tsx index fa3725df4..a7fc45bf6 100644 --- a/pkg/interface/src/views/landscape/components/NewChannel.tsx +++ b/pkg/interface/src/views/landscape/components/NewChannel.tsx @@ -85,7 +85,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) { moduleType ); } - + if (!group) { await waiter(p => Boolean(p?.groups?.[`/ship/~${window.ship}/${resId}`])); } @@ -99,11 +99,11 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps) { actions.setStatus({ error: 'Channel creation failed' }); } }; - + return ( history.push(props.baseUrl)}> - {'<- Back'} + {'<- Back'} New Channel diff --git a/pkg/interface/src/views/landscape/components/NewGroup.tsx b/pkg/interface/src/views/landscape/components/NewGroup.tsx index fd1989e1e..ea0a75f7f 100644 --- a/pkg/interface/src/views/landscape/components/NewGroup.tsx +++ b/pkg/interface/src/views/landscape/components/NewGroup.tsx @@ -105,7 +105,7 @@ export function NewGroup(props: NewGroupProps & RouteComponentProps) { Create Group diff --git a/pkg/interface/src/views/landscape/components/PopoverRoutes.tsx b/pkg/interface/src/views/landscape/components/PopoverRoutes.tsx index 4a50a5d44..bddf08b63 100644 --- a/pkg/interface/src/views/landscape/components/PopoverRoutes.tsx +++ b/pkg/interface/src/views/landscape/components/PopoverRoutes.tsx @@ -79,7 +79,7 @@ export function PopoverRoutes( diff --git a/pkg/interface/src/views/landscape/components/Resource.tsx b/pkg/interface/src/views/landscape/components/Resource.tsx index a78049545..973627f28 100644 --- a/pkg/interface/src/views/landscape/components/Resource.tsx +++ b/pkg/interface/src/views/landscape/components/Resource.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from "react"; import { Row, Box, Col } from "@tlon/indigo-react"; import styled from "styled-components"; -import { Link } from "react-router-dom"; +import Helmet from 'react-helmet'; import { ChatResource } from "~/views/apps/chat/ChatResource"; import { PublishResource } from "~/views/apps/publish/PublishResource"; @@ -34,47 +34,60 @@ export function Resource(props: ResourceProps) { const relativePath = (p: string) => `${props.baseUrl}/resource/${app}${appPath}${p}`; const skelProps = { api, association }; + let title = props.association.metadata.title; + if ('workspace' in props) { + if ('group' in props.workspace && props.workspace.group in props.associations.contacts) { + title = `${props.associations.contacts[props.workspace.group].metadata.title} - ${props.association.metadata.title}`; + } + } return ( - - { - return ( + <> + + {props.notificationsCount ? `(${String(props.notificationsCount)}) ` : ''}{ title } + + + { + return ( + + + + ); + }} + /> + ( - + {app === "chat" ? ( + + ) : app === "publish" ? ( + + ) : ( + + )} - ); - }} - /> - ( - - {app === "chat" ? ( - - ) : app === "publish" ? ( - - ) : ( - - )} - - )} - /> - + )} + /> + + ); } diff --git a/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx b/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx index 7ad8d8842..f2d8329ce 100644 --- a/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx +++ b/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx @@ -16,7 +16,7 @@ import { ChannelMenu } from "./ChannelMenu"; import { NotificationGraphConfig } from "~/types"; const TruncatedBox = styled(Box)` - white-space: nowrap; + white-space: pre; text-overflow: ellipsis; overflow: hidden; `; @@ -29,19 +29,33 @@ type ResourceSkeletonProps = { children: ReactNode; atRoot?: boolean; title?: string; + groupTags?: any; }; export function ResourceSkeleton(props: ResourceSkeletonProps) { - const { association, api, baseUrl, children, atRoot } = props; + const { association, api, baseUrl, children, atRoot, groupTags } = props; const app = association?.metadata?.module || association["app-name"]; const appPath = association["app-path"]; const workspace = baseUrl === "/~landscape/home" ? "/home" : association["group-path"]; const title = props.title || association?.metadata?.title; + + const [, , ship, resource] = appPath.split("/"); + + const resourcePath = (p: string) => baseUrl + `/resource/${app}/ship/${ship}/${resource}` + p; + + const isOwn = `~${window.ship}` === ship; + let isWriter = (app === 'publish') ? true : false; + + if (groupTags?.publish?.[`writers-${resource}`]) { + isWriter = isOwn || groupTags?.publish?.[`writers-${resource}`]?.has(window.ship); + } + return ( {"<- Back"} @@ -70,15 +86,15 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) { {atRoot && ( <> - - + + {title} + {isWriter && ( + + + New Post + + )} { - const unreads = graphUnreads?.[s]?.['/']?.unreads; - if(typeof unreads === 'number' ? unreads > 0 : unreads?.size ?? 0 > 0) { - return 'unread'; - } const [, , host, name] = s.split("/"); const graphKey = `${host.slice(1)}/${name}`; - if (!graphKeys.has(graphKey)) { return "unsubscribed"; } + + const unreads = graphUnreads?.[s]?.['/']?.unreads; + if (typeof unreads === 'number' ? unreads > 0 : unreads?.size ?? 0 > 0) { + return 'unread'; + } + return undefined; }, [graphs, graphKeys, graphUnreads] diff --git a/pkg/interface/src/views/landscape/components/Sidebar/Sidebar.tsx b/pkg/interface/src/views/landscape/components/Sidebar/Sidebar.tsx index 07b96e02a..61380aaee 100644 --- a/pkg/interface/src/views/landscape/components/Sidebar/Sidebar.tsx +++ b/pkg/interface/src/views/landscape/components/Sidebar/Sidebar.tsx @@ -1,26 +1,24 @@ -import React, { ReactNode } from "react"; +import React, { ReactNode } from 'react'; import styled from 'styled-components'; import { - Box, - Col, -} from "@tlon/indigo-react"; -import { Link } from "react-router-dom"; + Col +} from '@tlon/indigo-react'; -import GlobalApi from "~/logic/api/global"; -import { GroupSwitcher } from "../GroupSwitcher"; +import GlobalApi from '~/logic/api/global'; +import { GroupSwitcher } from '../GroupSwitcher'; import { Associations, Workspace, Groups, Invites, - Rolodex, -} from "~/types"; -import { SidebarListHeader } from "./SidebarListHeader"; -import { useLocalStorageState } from "~/logic/lib/useLocalStorageState"; -import { getGroupFromWorkspace } from "~/logic/lib/workspace"; + Rolodex +} from '~/types'; +import { SidebarListHeader } from './SidebarListHeader'; +import { useLocalStorageState } from '~/logic/lib/useLocalStorageState'; +import { getGroupFromWorkspace } from '~/logic/lib/workspace'; import { SidebarAppConfigs } from './types'; -import { SidebarList } from "./SidebarList"; -import { roleForShip } from "~/logic/lib/group"; +import { SidebarList } from './SidebarList'; +import { roleForShip } from '~/logic/lib/group'; const ScrollbarLessCol = styled(Col)` scrollbar-width: none !important; @@ -30,7 +28,6 @@ const ScrollbarLessCol = styled(Col)` } `; - interface SidebarProps { contacts: Rolodex; children: ReactNode; @@ -48,38 +45,24 @@ interface SidebarProps { workspace: Workspace; } -// Magic spacer that because firefox doesn't correctly calculate -// position: sticky on a flex child -// remove when https://bugzilla.mozilla.org/show_bug.cgi?id=1488080 -// is fixed -const SidebarStickySpacer = styled(Box)` - height: 0px; - flex-grow: 1; - @-moz-document url-prefix() { - & { - height: ${p => p.theme.space[6] }px; - } - } -`; - export function Sidebar(props: SidebarProps) { - const { invites, api, associations, selected, apps, workspace } = props; + const { associations, selected, workspace } = props; const groupPath = getGroupFromWorkspace(workspace); - const display = props.mobileHide ? ["none", "flex"] : "flex"; + const display = props.mobileHide ? ['none', 'flex'] : 'flex'; if (!associations) { return null; } const [config, setConfig] = useLocalStorageState( - `group-config:${groupPath || "home"}`, + `group-config:${groupPath || 'home'}`, { - sortBy: "lastUpdated", - hideUnjoined: false, + sortBy: 'lastUpdated', + hideUnjoined: false } ); const role = props.groups?.[groupPath] ? roleForShip(props.groups[groupPath], window.ship) : undefined; - const isAdmin = (role === "admin") || (workspace?.type === 'home'); + const isAdmin = (role === 'admin') || (workspace?.type === 'home'); return ( + selected={selected || ''} + workspace={workspace} + /> - - - - - + New Channel - - - ); } diff --git a/pkg/interface/src/views/landscape/components/Sidebar/SidebarItem.tsx b/pkg/interface/src/views/landscape/components/Sidebar/SidebarItem.tsx index b0db550b2..2b18f76e3 100644 --- a/pkg/interface/src/views/landscape/components/Sidebar/SidebarItem.tsx +++ b/pkg/interface/src/views/landscape/components/Sidebar/SidebarItem.tsx @@ -27,7 +27,7 @@ function SidebarItemIndicator(props: { status?: SidebarItemStatus }) { const getAppIcon = (app: string, mod: string) => { if (app === "graph") { if (mod === "link") { - return "Links"; + return "Collection"; } return _.capitalize(mod); } @@ -93,8 +93,8 @@ export function SidebarItem(props: { justifyContent="space-between" alignItems="center" py={1} - pl={4} - pr={2} + pl={3} + pr={3} selected={selected} > @@ -105,7 +105,7 @@ export function SidebarItem(props: { /> @@ -51,27 +56,33 @@ export function SidebarListHeader(props: { - + + + + + DM - + ); } diff --git a/pkg/interface/src/views/landscape/index.tsx b/pkg/interface/src/views/landscape/index.tsx index 8cff5c278..ec85986ee 100644 --- a/pkg/interface/src/views/landscape/index.tsx +++ b/pkg/interface/src/views/landscape/index.tsx @@ -1,5 +1,6 @@ import React, { Component, useEffect, useCallback } from 'react'; import { Route, Switch, RouteComponentProps } from 'react-router-dom'; +import Helmet from 'react-helmet'; import './css/custom.css'; @@ -67,8 +68,6 @@ export function DMRedirect(props: LandscapeProps & RouteComponentProps & { ship: export default class Landscape extends Component { componentDidMount() { - document.title = 'OS1 - Landscape'; - this.props.subscription.startApp('groups'); this.props.subscription.startApp('graph'); } @@ -78,71 +77,76 @@ export default class Landscape extends Component { const { api } = props; return ( - - { - const { - host, - name - } = routeProps.match.params as Record; - const groupPath = `/ship/${host}/${name}`; - const baseUrl = `/~landscape${groupPath}`; - const ws: Workspace = { type: 'group', group: groupPath }; + <> + + { props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape + + + { + const { + host, + name + } = routeProps.match.params as Record; + const groupPath = `/ship/${host}/${name}`; + const baseUrl = `/~landscape${groupPath}`; + const ws: Workspace = { type: 'group', group: groupPath }; - return ( - - ) - }}/> - + ) + }}/> + { + const ws: Workspace = { type: 'home' }; + return ( + + ); + }} + /> + { + return ( + + + + + + ); + }} + /> + { - const ws: Workspace = { type: 'home' }; - return ( - - ); + const { ship } = routeProps.match.params; + return }} - /> - { - return ( - - - - - - ); - }} - /> - { - const { ship } = routeProps.match.params; - return - }} - /> - { - const { ship, name } = routeProps.match.params; - const autojoin = ship && name ? `${ship}/${name}` : null; - return ( - - - - - - ); - }} - /> - + /> + { + const { ship, name } = routeProps.match.params; + const autojoin = ship && name ? `${ship}/${name}` : null; + return ( + + + + + + ); + }} + /> + + ); } } diff --git a/pkg/interface/src/views/themes/light.ts b/pkg/interface/src/views/themes/light.ts deleted file mode 100644 index bd3ec6837..000000000 --- a/pkg/interface/src/views/themes/light.ts +++ /dev/null @@ -1,171 +0,0 @@ -import baseStyled, { ThemedStyledInterface } from "styled-components"; - -const base = { - white: "rgba(255,255,255,1)", - black: "rgba(0,0,0,1)", - red: "rgba(255,65,54,1)", - yellow: "rgba(255,199,0,1)", - green: "rgba(0,159,101,1)", - blue: "rgba(0,142,255,1)", -}; - -const scales = { - white10: "rgba(255,255,255,0.1)", - white20: "rgba(255,255,255,0.2)", - white30: "rgba(255,255,255,0.3)", - white40: "rgba(255,255,255,0.4)", - white50: "rgba(255,255,255,0.5)", - white60: "rgba(255,255,255,0.6)", - white70: "rgba(255,255,255,0.7)", - white80: "rgba(255,255,255,0.8)", - white90: "rgba(255,255,255,0.9)", - white100: "rgba(255,255,255,1)", - black05: "rgba(0,0,0,0.05)", - black10: "rgba(0,0,0,0.1)", - black20: "rgba(0,0,0,0.2)", - black30: "rgba(0,0,0,0.3)", - black40: "rgba(0,0,0,0.4)", - black50: "rgba(0,0,0,0.5)", - black60: "rgba(0,0,0,0.6)", - black70: "rgba(0,0,0,0.7)", - black80: "rgba(0,0,0,0.8)", - black90: "rgba(0,0,0,0.9)", - black100: "rgba(0,0,0,1)", - red10: "rgba(255,65,54,0.1)", - red20: "rgba(255,65,54,0.2)", - red30: "rgba(255,65,54,0.3)", - red40: "rgba(255,65,54,0.4)", - red50: "rgba(255,65,54,0.5)", - red60: "rgba(255,65,54,0.6)", - red70: "rgba(255,65,54,0.7)", - red80: "rgba(255,65,54,0.8)", - red90: "rgba(255,65,54,0.9)", - red100: "rgba(255,65,54,1)", - yellow10: "rgba(255,199,0,0.1)", - yellow20: "rgba(255,199,0,0.2)", - yellow30: "rgba(255,199,0,0.3)", - yellow40: "rgba(255,199,0,0.4)", - yellow50: "rgba(255,199,0,0.5)", - yellow60: "rgba(255,199,0,0.6)", - yellow70: "rgba(255,199,0,0.7)", - yellow80: "rgba(255,199,0,0.8)", - yellow90: "rgba(255,199,0,0.9)", - yellow100: "rgba(255,199,0,1)", - green10: "rgba(0,159,101,0.1)", - green20: "rgba(0,159,101,0.2)", - green30: "rgba(0,159,101,0.3)", - green40: "rgba(0,159,101,0.4)", - green50: "rgba(0,159,101,0.5)", - green60: "rgba(0,159,101,0.6)", - green70: "rgba(0,159,101,0.7)", - green80: "rgba(0,159,101,0.8)", - green90: "rgba(0,159,101,0.9)", - green100: "rgba(0,159,101,1)", - blue10: "rgba(0,142,255,0.1)", - blue20: "rgba(0,142,255,0.2)", - blue30: "rgba(0,142,255,0.3)", - blue40: "rgba(0,142,255,0.4)", - blue50: "rgba(0,142,255,0.5)", - blue60: "rgba(0,142,255,0.6)", - blue70: "rgba(0,142,255,0.7)", - blue80: "rgba(0,142,255,0.8)", - blue90: "rgba(0,142,255,0.9)", - blue100: "rgba(0,142,255,1)", -}; - -const theme = { - colors: { - white: base.white, - black: base.black, - - darkGray: scales.black80, - gray: scales.black60, - lightGray: scales.black30, - washedGray: scales.black10, - - red: base.red, - lightRed: scales.red30, - washedRed: scales.red10, - - yellow: base.yellow, - lightYellow: scales.yellow30, - washedYellow: scales.yellow10, - - green: base.green, - lightGreen: scales.green30, - washedGreen: scales.green10, - - blue: base.blue, - lightBlue: scales.blue30, - washedBlue: scales.blue10, - - none: "rgba(0,0,0,0)", - scales: scales, - }, - fonts: { - sans: `"Inter", "Inter UI", -apple-system, BlinkMacSystemFont, 'San Francisco', 'Helvetica Neue', Arial, sans-serif`, - mono: `"Source Code Pro", "Roboto mono", "Courier New", monospace`, - }, - // font-size - fontSizes: [ - 12, // 0 - 16, // 1 - 24, // 2 - 32, // 3 - 48, // 4 - 64, // 5 - ], - // font-weight - fontWeights: { - thin: 300, - regular: 400, - bold: 600, - }, - // line-height - lineHeights: { - min: 1.2, - short: 1.333333, - regular: 1.5, - tall: 1.666666, - }, - // border, border-top, border-right, border-bottom, border-left - borders: ["none", "1px solid"], - // margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, grid-gap, grid-column-gap, grid-row-gap - space: [ - 0, // 0 - 4, // 1 - 8, // 2 - 16, // 3 - 24, // 4 - 32, // 5 - 48, // 6 - 64, // 7 - 96, // 8 - ], - // border-radius - radii: [ - 0, // 0 - 2, // 1 - 4, // 2 - 8, // 3 - 16, // 4 - ], - // width, height, min-width, max-width, min-height, max-height - sizes: [ - 0, // 0 - 4, // 1 - 8, // 2 - 16, // 3 - 24, // 4 - 32, // 5 - 48, // 6 - 64, // 7 - 96, // 8 - ], - // z-index - zIndices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - breakpoints: ["550px", "750px", "960px"], -}; -export type Theme = typeof theme; -export const styled = baseStyled as ThemedStyledInterface; -export default theme; diff --git a/pkg/interface/src/views/themes/old-dark.ts b/pkg/interface/src/views/themes/old-dark.ts deleted file mode 100644 index f2eead7c9..000000000 --- a/pkg/interface/src/views/themes/old-dark.ts +++ /dev/null @@ -1,186 +0,0 @@ -import baseStyled, { ThemedStyledInterface } from "styled-components"; - -const base = { - white: "rgba(255,255,255,1)", - black: "rgba(0,0,0,1)", - red: "rgba(255,65,54,1)", - yellow: "rgba(255,199,0,1)", - green: "rgba(0,159,101,1)", - blue: "rgba(0,142,255,1)", -}; - -const scales = { - white05: "rgba(255,255,255,0.05)", - white10: "rgba(255,255,255,0.1)", - white20: "rgba(255,255,255,0.2)", - white30: "rgba(255,255,255,0.3)", - white40: "rgba(255,255,255,0.4)", - white50: "rgba(255,255,255,0.5)", - white60: "rgba(255,255,255,0.6)", - white70: "rgba(255,255,255,0.7)", - white80: "rgba(255,255,255,0.8)", - white90: "rgba(255,255,255,0.9)", - white100: "rgba(255,255,255,1)", - black05: "rgba(0,0,0,0.05)", - black10: "rgba(0,0,0,0.1)", - black20: "rgba(0,0,0,0.2)", - black30: "rgba(0,0,0,0.3)", - black40: "rgba(0,0,0,0.4)", - black50: "rgba(0,0,0,0.5)", - black60: "rgba(0,0,0,0.6)", - black70: "rgba(0,0,0,0.7)", - black80: "rgba(0,0,0,0.8)", - black90: "rgba(0,0,0,0.9)", - black100: "rgba(0,0,0,1)", - red05: "rgba(255,65,54,0.05)", - red10: "rgba(255,65,54,0.1)", - red20: "rgba(255,65,54,0.2)", - red30: "rgba(255,65,54,0.3)", - red40: "rgba(255,65,54,0.4)", - red50: "rgba(255,65,54,0.5)", - red60: "rgba(255,65,54,0.6)", - red70: "rgba(255,65,54,0.7)", - red80: "rgba(255,65,54,0.8)", - red90: "rgba(255,65,54,0.9)", - red100: "rgba(255,65,54,1)", - yellow05: "rgba(255,199,0,0.05)", - yellow10: "rgba(255,199,0,0.1)", - yellow20: "rgba(255,199,0,0.2)", - yellow30: "rgba(255,199,0,0.3)", - yellow40: "rgba(255,199,0,0.4)", - yellow50: "rgba(255,199,0,0.5)", - yellow60: "rgba(255,199,0,0.6)", - yellow70: "rgba(255,199,0,0.7)", - yellow80: "rgba(255,199,0,0.8)", - yellow90: "rgba(255,199,0,0.9)", - yellow100: "rgba(255,199,0,1)", - green05: "rgba(0,159,101,0.05)", - green10: "rgba(0,159,101,0.1)", - green20: "rgba(0,159,101,0.2)", - green30: "rgba(0,159,101,0.3)", - green40: "rgba(0,159,101,0.4)", - green50: "rgba(0,159,101,0.5)", - green60: "rgba(0,159,101,0.6)", - green70: "rgba(0,159,101,0.7)", - green80: "rgba(0,159,101,0.8)", - green90: "rgba(0,159,101,0.9)", - green100: "rgba(0,159,101,1)", - blue05: "rgba(0,142,255,0.05)", - blue10: "rgba(0,142,255,0.1)", - blue20: "rgba(0,142,255,0.2)", - blue30: "rgba(0,142,255,0.3)", - blue40: "rgba(0,142,255,0.4)", - blue50: "rgba(0,142,255,0.5)", - blue60: "rgba(0,142,255,0.6)", - blue70: "rgba(0,142,255,0.7)", - blue80: "rgba(0,142,255,0.8)", - blue90: "rgba(0,142,255,0.9)", - blue100: "rgba(0,142,255,1)", -}; - -const util = { - cyan: "#00FFFF", - magenta: "#FF00FF", - yellow: "#FFFF00", - black: "#000000", - gray0: "#333333" -}; - -const theme = { - colors: { - white: util.gray0, - black: base.white, - - darkGray: scales.white80, - gray: scales.white60, - lightGray: scales.white30, - washedGray: scales.white05, - - red: base.red, - lightRed: scales.red30, - washedRed: scales.red05, - - yellow: base.yellow, - lightYellow: scales.yellow30, - washedYellow: scales.yellow10, - - green: base.green, - lightGreen: scales.green30, - washedGreen: scales.green10, - - blue: base.blue, - lightBlue: scales.blue30, - washedBlue: scales.blue10, - - none: "rgba(0,0,0,0)", - - scales: scales, - util: util, - }, - fonts: { - sans: `"Inter", "Inter UI", -apple-system, BlinkMacSystemFont, 'San Francisco', 'Helvetica Neue', Arial, sans-serif`, - mono: `"Source Code Pro", "Roboto mono", "Courier New", monospace`, - }, - // font-size - fontSizes: [ - 12, // 0 - 16, // 1 - 24, // 2 - 32, // 3 - 48, // 4 - 64, // 5 - ], - // font-weight - fontWeights: { - thin: 300, - regular: 400, - semibold: 500, - bold: 600, - }, - // line-height - lineHeights: { - min: 1.2, - short: 1.333333, - regular: 1.5, - tall: 1.666666, - }, - // border, border-top, border-right, border-bottom, border-left - borders: ["none", "1px solid"], - // margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, grid-gap, grid-column-gap, grid-row-gap - space: [ - 0, // 0 - 4, // 1 - 8, // 2 - 16, // 3 - 24, // 4 - 32, // 5 - 48, // 6 - 64, // 7 - 96, // 8 - ], - // border-radius - radii: [ - 0, // 0 - 2, // 1 - 4, // 2 - 8, // 3 - ], - // width, height, min-width, max-width, min-height, max-height - sizes: [ - 0, // 0 - 4, // 1 - 8, // 2 - 16, // 3 - 24, // 4 - 32, // 5 - 48, // 6 - 64, // 7 - 96, // 8 - ], - // z-index - zIndices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], - breakpoints: ["550px", "750px", "960px"], -}; -export type Theme = typeof theme; -export const styled = baseStyled as ThemedStyledInterface; -export default theme;