mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-09-21 07:28:30 +03:00
Merge pull request #4337 from urbit/mp/contacts/ui-detail
contacts: UI details
This commit is contained in:
commit
c3395d4c14
9
.github/ISSUE_TEMPLATE/config.yml
vendored
9
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -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.
|
||||
|
39
.github/ISSUE_TEMPLATE/os1-bug-report.md
vendored
39
.github/ISSUE_TEMPLATE/os1-bug-report.md
vendored
@ -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.
|
2
.github/actions/glob/Dockerfile
vendored
2
.github/actions/glob/Dockerfile
vendored
@ -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"]
|
||||
|
8
.github/actions/glob/entrypoint.sh
vendored
8
.github/actions/glob/entrypoint.sh
vendored
@ -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 \
|
||||
|
15
.github/workflows/build.yml
vendored
15
.github/workflows/build.yml
vendored
@ -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
|
||||
|
3
.github/workflows/glob.yml
vendored
3
.github/workflows/glob.yml
vendored
@ -3,9 +3,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- 'release/next-js'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'release/next-js'
|
||||
jobs:
|
||||
glob:
|
||||
runs-on: ubuntu-latest
|
||||
|
17
.github/workflows/merge.yml
vendored
Normal file
17
.github/workflows/merge.yml
vendored
Normal file
@ -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 }}
|
||||
|
51
.github/workflows/release-docker.yml
vendored
Normal file
51
.github/workflows/release-docker.yml
vendored
Normal file
@ -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
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:17eb2f5a123f5ad29b0cc9ff9069540c349dd97c6133a9ea33cbf81e0bfa4d6b
|
||||
size 8483784
|
||||
oid sha256:fd78b54d1c825f14b51c4dff001e17368953ecf9fc5a7b6792c059477761076a
|
||||
size 9001760
|
||||
|
@ -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.
|
||||
|
68
nix/pkgs/docker-image/default.nix
Normal file
68
nix/pkgs/docker-image/default.nix
Normal file
@ -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" ];
|
||||
};
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -1,7 +1,7 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OS1</title>
|
||||
<title>Landscape</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no,maximum-scale=1"/>
|
||||
@ -12,8 +12,8 @@
|
||||
<link rel="icon" type="image/png" href="/~landscape/img/Favicon.png">
|
||||
<link rel="manifest"
|
||||
href='data:application/manifest+json,{
|
||||
"name": "OS1",
|
||||
"short_name": "OS1",
|
||||
"name": "Landscape",
|
||||
"short_name": "Landscape",
|
||||
"description": "An%20interface%20to%20your%20Urbit.",
|
||||
"display": "standalone",
|
||||
"background_color": "%23FFFFFF",
|
||||
@ -24,6 +24,6 @@
|
||||
<div id="portal-root"></div>
|
||||
<script src="/~landscape/js/channel.js"></script>
|
||||
<script src="/~landscape/js/session.js"></script>
|
||||
<script src="/~landscape/js/bundle/index.2ddb586104e8758c6863.js"></script>
|
||||
<script src="/~landscape/js/bundle/index.226dd4e7ed526397a662.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -63,7 +63,7 @@ class Channel {
|
||||
}
|
||||
|
||||
resetDebounceTimer() {
|
||||
if(this.debounceTimer) {
|
||||
if (this.debounceTimer) {
|
||||
clearTimeout(this.debounceTimer);
|
||||
this.debounceTimer = null;
|
||||
}
|
||||
@ -182,19 +182,19 @@ class Channel {
|
||||
// sends a JSON command command to the server.
|
||||
//
|
||||
sendJSONToChannel(j) {
|
||||
if(!j && this.outstandingJSON.length === 0) {
|
||||
return;
|
||||
}
|
||||
let req = new XMLHttpRequest();
|
||||
req.open("PUT", this.channelURL());
|
||||
req.setRequestHeader("Content-Type", "application/json");
|
||||
|
||||
if (this.lastEventId == this.lastAcknowledgedEventId) {
|
||||
if(j) {
|
||||
if (j) {
|
||||
this.outstandingJSON.push(j);
|
||||
}
|
||||
let x = JSON.stringify(this.outstandingJSON);
|
||||
req.send(x);
|
||||
|
||||
if (this.outstandingJSON.length > 0) {
|
||||
let x = JSON.stringify(this.outstandingJSON);
|
||||
req.send(x);
|
||||
}
|
||||
} else {
|
||||
// we add an acknowledgment to clear the server side queue
|
||||
//
|
||||
@ -203,15 +203,15 @@ class Channel {
|
||||
//
|
||||
let payload = [
|
||||
...this.outstandingJSON,
|
||||
{action: "ack", "event-id": parseInt(this.lastEventId)}
|
||||
{action: "ack", "event-id": this.lastEventId}
|
||||
];
|
||||
if(j) {
|
||||
if (j) {
|
||||
payload.push(j)
|
||||
}
|
||||
let x = JSON.stringify(payload);
|
||||
req.send(x);
|
||||
|
||||
this.lastEventId = this.lastAcknowledgedEventId;
|
||||
this.lastAcknowledgedEventId = this.lastEventId;
|
||||
}
|
||||
this.outstandingJSON = [];
|
||||
|
||||
@ -227,7 +227,7 @@ class Channel {
|
||||
|
||||
this.eventSource = new EventSource(this.channelURL(), {withCredentials:true});
|
||||
this.eventSource.onmessage = e => {
|
||||
this.lastEventId = e.lastEventId;
|
||||
this.lastEventId = parseInt(e.lastEventId, 10);
|
||||
|
||||
let obj = JSON.parse(e.data);
|
||||
let pokeFuncs = this.outstandingPokes.get(obj.id);
|
||||
|
126
pkg/arvo/gen/tally.hoon
Normal file
126
pkg/arvo/gen/tally.hoon
Normal file
@ -0,0 +1,126 @@
|
||||
/- gr=group, md=metadata-store, ga=graph-store
|
||||
/+ re=resource
|
||||
!:
|
||||
:- %say
|
||||
|= $: [now=@da eny=@uvJ =beak]
|
||||
args=?(~ [shy=? ~])
|
||||
~
|
||||
==
|
||||
::
|
||||
=/ shy=? ?~(args & shy.args)
|
||||
=* our=@p p.beak
|
||||
::
|
||||
|^
|
||||
=; out=(list @t)
|
||||
:- %tang
|
||||
%- flop ::NOTE tang is bottom-up
|
||||
:* ''
|
||||
'tallied your activity score! find the results below.'
|
||||
::
|
||||
?: shy
|
||||
'to show non-anonymized resource identifiers, +tally |'
|
||||
'showing plain resource identifiers, share with care.'
|
||||
::
|
||||
'counted from groups and channels that you are hosting.'
|
||||
'groups are listed with their member count.'
|
||||
'channels are listed with activity from the past week:'
|
||||
' - amount of top-level content'
|
||||
' - amount of unique authors'
|
||||
''
|
||||
(snoc out '')
|
||||
==
|
||||
:: gather local non-dm groups, sorted by size
|
||||
::
|
||||
=/ groups=(list [local=? resource:re members=@ud])
|
||||
%+ murn
|
||||
%~ tap in
|
||||
%~ key by
|
||||
dir:(scry arch %y %group-store /groups)
|
||||
|= i=@ta
|
||||
=/ r=resource:re (de-path:re (stab i))
|
||||
=/ g=(unit group:gr)
|
||||
%+ scry (unit group:gr)
|
||||
[%x %group-store [%groups (snoc (en-path:re r) %noun)]]
|
||||
?: |(?=(~ g) hidden.u.g)
|
||||
~
|
||||
`[=(our entity.r) r ~(wyt in members.u.g)]
|
||||
=/ crowds=(list [resource:re @ud])
|
||||
%+ sort (turn (skim groups head) tail)
|
||||
|= [[* a=@ud] [* b=@ud]]
|
||||
(gth a b)
|
||||
:: gather local per-group channels
|
||||
::
|
||||
=/ channels=(map resource:re (list [module=term =resource:re]))
|
||||
%- ~(gas by *(map resource:re (list [module=term =resource:re])))
|
||||
%+ turn crowds
|
||||
|= [r=resource:re *]
|
||||
:- r
|
||||
%+ murn
|
||||
%~ tap by
|
||||
%+ scry associations:md
|
||||
[%x %metadata-store [%group (snoc (en-path:re r) %noun)]]
|
||||
|= [[* m=md-resource:md] metadata:md]
|
||||
::NOTE we only count graphs for now
|
||||
?. &(=(%graph app-name.m) =(our creator)) ~
|
||||
`[module (de-path:re app-path.m)]
|
||||
:: count activity per channel
|
||||
::
|
||||
=/ activity=(list [resource:re members=@ud (list [resource:re mod=term week=@ud authors=@ud])])
|
||||
%+ turn crowds
|
||||
|= [g=resource:re m=@ud]
|
||||
:+ g m
|
||||
%+ turn (~(got by channels) g)
|
||||
|= [m=term r=resource:re]
|
||||
:+ r m
|
||||
::NOTE graph-store doesn't use the full resource-style path here!
|
||||
=/ upd=update:ga
|
||||
%+ scry update:ga
|
||||
[%x %graph-store /graph/(scot %p entity.r)/[name.r]/noun]
|
||||
?> ?=(%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))
|
||||
--
|
@ -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
|
||||
|
@ -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]
|
||||
--
|
||||
--
|
||||
|
@ -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]]
|
||||
--
|
||||
--
|
||||
|
@ -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))]
|
||||
--
|
||||
::
|
||||
|
@ -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
|
||||
|%
|
||||
|
14
pkg/arvo/mar/resource.hoon
Normal file
14
pkg/arvo/mar/resource.hoon
Normal file
@ -0,0 +1,14 @@
|
||||
/+ resource
|
||||
|_ rid=resource
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun rid
|
||||
++ json (enjs:resource rid)
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun resource
|
||||
++ json dejs:resource
|
||||
--
|
||||
--
|
@ -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]
|
||||
|
@ -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))
|
||||
|
@ -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 !>(~))
|
||||
|
@ -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
|
||||
--
|
||||
::
|
||||
|
37
pkg/docker-image/README.md
Normal file
37
pkg/docker-image/README.md
Normal file
@ -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 `<shipname>.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 `<shipname>`. 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.
|
@ -96,7 +96,7 @@ module.exports = {
|
||||
]
|
||||
}
|
||||
},
|
||||
exclude: /node_modules/
|
||||
exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light)\/).*/
|
||||
},
|
||||
{
|
||||
test: /\.css$/i,
|
||||
|
@ -26,7 +26,7 @@ module.exports = {
|
||||
]
|
||||
}
|
||||
},
|
||||
exclude: /node_modules/
|
||||
exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light)\/).*/
|
||||
},
|
||||
{
|
||||
test: /\.css$/i,
|
||||
|
BIN
pkg/interface/package-lock.json
generated
BIN
pkg/interface/package-lock.json
generated
Binary file not shown.
@ -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}",
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
<!-- <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> -->
|
||||
|
||||
<title>OS1</title>
|
||||
<title>Landscape</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -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<StoreState> {
|
||||
|
||||
joiningGraphs = new Set<string>();
|
||||
|
||||
private storeAction(action: any): Promise<any> {
|
||||
return this.action('graph-store', 'graph-update', action)
|
||||
}
|
||||
@ -138,11 +140,19 @@ export default class GraphApi extends BaseApi<StoreState> {
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -196,10 +196,11 @@ export class HarkApi extends BaseApi<StoreState> {
|
||||
});
|
||||
}
|
||||
|
||||
getMore() {
|
||||
async getMore(): Promise<boolean> {
|
||||
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) {
|
||||
|
@ -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 = [];
|
||||
|
@ -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 {
|
||||
|
51
pkg/interface/src/logic/lib/useLazyScroll.ts
Normal file
51
pkg/interface/src/logic/lib/useLazyScroll.ts
Normal file
@ -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<HTMLElement>,
|
||||
margin: number,
|
||||
loadMore: () => Promise<boolean>
|
||||
) {
|
||||
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;
|
||||
}
|
@ -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]
|
||||
);
|
||||
|
@ -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 };
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ const useLocalState = create<LocalState>(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<LocalState>(persist((set, get) => ({
|
||||
}
|
||||
})),
|
||||
set: fn => set(produce(fn))
|
||||
}), {
|
||||
}), {
|
||||
blacklist: ['suspendedFocus', 'toggleOmnibox', 'omniboxShown'],
|
||||
name: 'localReducer'
|
||||
}));
|
||||
|
||||
@ -55,4 +56,4 @@ function withLocalState<P, S extends keyof LocalState>(Component: any, stateMemb
|
||||
});
|
||||
}
|
||||
|
||||
export { useLocalState as default, withLocalState };
|
||||
export { useLocalState as default, withLocalState };
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Helmet>
|
||||
@ -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}
|
||||
|
@ -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<ChatMessageProps> {
|
||||
className={containerClass}
|
||||
style={style}
|
||||
mb={1}
|
||||
position="relative"
|
||||
>
|
||||
{dayBreak && !isLastRead ? <DayBreak when={msg['time-sent']} /> : null}
|
||||
{renderSigil
|
||||
@ -194,6 +195,8 @@ export const MessageWithSigil = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const { hovering, bind } = useHovering();
|
||||
|
||||
return (
|
||||
<>
|
||||
<OverlaySigil
|
||||
@ -206,9 +209,11 @@ export const MessageWithSigil = (props) => {
|
||||
history={history}
|
||||
api={api}
|
||||
bg="white"
|
||||
className="fl pr3 v-top pt1"
|
||||
className="fl v-top pt1"
|
||||
pr={3}
|
||||
pl={2}
|
||||
/>
|
||||
<Box flexGrow={1} display='block' className="clamp-message">
|
||||
<Box flexGrow={1} display='block' className="clamp-message" {...bind}>
|
||||
<Box
|
||||
flexShrink={0}
|
||||
className="hide-child"
|
||||
@ -231,8 +236,15 @@ export const MessageWithSigil = (props) => {
|
||||
}}
|
||||
title={`~${msg.author}`}
|
||||
>{name}</Text>
|
||||
<Text flexShrink='0' gray mono className="v-mid">{timestamp}</Text>
|
||||
<Text flexShrink={0} gray mono ml={2} className="v-mid child dn-s">{datestamp}</Text>
|
||||
<Text flexShrink={0} fontSize={0} gray mono>{timestamp}</Text>
|
||||
<Text
|
||||
flexShrink={0}
|
||||
fontSize={0}
|
||||
gray
|
||||
mono
|
||||
ml={2}
|
||||
display={['none', hovering ? 'block' : 'none']}
|
||||
>{datestamp}</Text>
|
||||
</Box>
|
||||
<ContentBox flexShrink={0} fontSize={fontSize ? fontSize : '14px'}>
|
||||
{msg.contents.map(c =>
|
||||
@ -257,20 +269,40 @@ const ContentBox = styled(Box)`
|
||||
|
||||
`;
|
||||
|
||||
export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }) => (
|
||||
<>
|
||||
<Text flexShrink={0} mono gray display='inline-block' pt='2px' lineHeight='tall' className="child">{timestamp}</Text>
|
||||
<ContentBox flexShrink={0} fontSize='14px' className="clamp-message" style={{ flexGrow: 1 }}>
|
||||
{msg.contents.map((c, i) => (
|
||||
<MessageContent
|
||||
key={i}
|
||||
contacts={contacts}
|
||||
content={c}
|
||||
group={group}
|
||||
measure={measure}/>))}
|
||||
</ContentBox>
|
||||
</>
|
||||
);
|
||||
export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }) => {
|
||||
const { hovering, bind } = useHovering();
|
||||
return (
|
||||
<>
|
||||
<Text
|
||||
flexShrink={0}
|
||||
mono
|
||||
gray
|
||||
display={hovering ? 'block': 'none'}
|
||||
pt='2px'
|
||||
lineHeight='tall'
|
||||
fontSize={0}
|
||||
position="absolute"
|
||||
left={1}
|
||||
>{timestamp}</Text>
|
||||
<ContentBox
|
||||
flexShrink={0}
|
||||
fontSize='14px'
|
||||
className="clamp-message"
|
||||
style={{ flexGrow: 1 }}
|
||||
{...bind}
|
||||
pl={6}
|
||||
>
|
||||
{msg.contents.map((c, i) => (
|
||||
<MessageContent
|
||||
key={i}
|
||||
contacts={contacts}
|
||||
content={c}
|
||||
group={group}
|
||||
measure={measure}/>))}
|
||||
</ContentBox>
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
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'
|
||||
}}}
|
||||
/>
|
||||
</Box>
|
||||
|
@ -258,7 +258,7 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
||||
const messageProps = { association, group, contacts, unreadMarkerRef, history, api };
|
||||
|
||||
const keys = graph.keys().reverse();
|
||||
const unreadIndex = keys[this.props.unreadCount];
|
||||
const unreadIndex = graph.keys()[this.props.unreadCount];
|
||||
const unreadMsg = unreadIndex && graph.get(unreadIndex);
|
||||
|
||||
return (
|
||||
|
@ -2,8 +2,9 @@ import React, { Component } from 'react';
|
||||
import { UnControlled as CodeEditor } from 'react-codemirror2';
|
||||
import { MOBILE_BROWSER_REGEX } from "~/logic/lib/util";
|
||||
import CodeMirror from 'codemirror';
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Row, BaseTextArea } from '@tlon/indigo-react';
|
||||
import { Row, BaseTextArea, Box } from '@tlon/indigo-react';
|
||||
|
||||
import 'codemirror/mode/markdown/markdown';
|
||||
import 'codemirror/addon/display/placeholder';
|
||||
@ -52,9 +53,40 @@ const inputProxy = (input) => 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)
|
||||
? <BaseTextArea
|
||||
fontFamily={inCodeMode ? 'Source Code Pro' : 'Inter'}
|
||||
fontSize="14px"
|
||||
lineHeight="tall"
|
||||
style={{ width: '100%', background: 'transparent', color: 'currentColor' }}
|
||||
placeholder={inCodeMode ? "Code..." : "Message..."}
|
||||
onKeyUp={event => {
|
||||
if (event.key === 'Enter') {
|
||||
this.submit();
|
||||
} else {
|
||||
? <MobileBox
|
||||
data-value={this.state.message}
|
||||
fontSize="1"
|
||||
lineHeight="tall"
|
||||
onClick={event => {
|
||||
if (this.editor) {
|
||||
this.editor.element.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<BaseTextArea
|
||||
fontFamily={inCodeMode ? 'Source Code Pro' : 'Inter'}
|
||||
fontSize="1"
|
||||
lineHeight="tall"
|
||||
rows="1"
|
||||
style={{ width: '100%', background: 'transparent', color: 'currentColor' }}
|
||||
placeholder={inCodeMode ? "Code..." : "Message..."}
|
||||
onChange={event => {
|
||||
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}
|
||||
/>
|
||||
</MobileBox>
|
||||
: <CodeEditor
|
||||
className="lh-copy"
|
||||
value={message}
|
||||
|
@ -12,6 +12,7 @@ export default class CodeContent extends Component {
|
||||
(
|
||||
<Text
|
||||
display='block'
|
||||
fontSize='0'
|
||||
mono
|
||||
p='1'
|
||||
my='0'
|
||||
@ -37,6 +38,7 @@ export default class CodeContent extends Component {
|
||||
overflow='auto'
|
||||
maxHeight='10em'
|
||||
maxWidth='100%'
|
||||
fontSize='0'
|
||||
style={{ whiteSpace: 'pre' }}
|
||||
>
|
||||
{content.code.expression}
|
||||
|
@ -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 <Text mono p='1' backgroundColor='washedGray' style={{ whiteSpace: 'preWrap'}}>{value}</Text>
|
||||
return <Text mono p='1' backgroundColor='washedGray' fontSize='0' style={{ whiteSpace: 'preWrap'}}>{value}</Text>
|
||||
},
|
||||
paragraph: ({ children }) => {
|
||||
return (<Text fontSize="14px">{children}</Text>);
|
||||
return (<Text fontSize="1">{children}</Text>);
|
||||
},
|
||||
code: ({language, value}) => {
|
||||
return <Text
|
||||
@ -38,6 +39,7 @@ const renderers = {
|
||||
display='block'
|
||||
borderRadius='1'
|
||||
mono
|
||||
fontSize='0'
|
||||
backgroundColor='washedGray'
|
||||
overflowX='auto'
|
||||
style={{ whiteSpace: 'pre'}}>
|
||||
@ -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]} />
|
||||
));
|
||||
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 = (
|
||||
<Box
|
||||
position={["relative", "absolute"]}
|
||||
fontFamily="mono"
|
||||
left="0"
|
||||
bottom="0"
|
||||
color="scales.black20"
|
||||
bg="white"
|
||||
ml={3}
|
||||
mb={3}
|
||||
borderRadius={2}
|
||||
fontSize={0}
|
||||
p={2}
|
||||
boxShadow="0 0 0px 1px inset"
|
||||
cursor="pointer"
|
||||
onClick={() => {
|
||||
writeText(props.baseHash);
|
||||
setHashText('copied');
|
||||
setTimeout(() => {
|
||||
setHashText(props.baseHash);
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
<Text color="gray">{hashText || props.baseHash}</Text>
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>OS1 - Home</title>
|
||||
<Helmet defer={false}>
|
||||
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape</title>
|
||||
</Helmet>
|
||||
<ScrollbarLessBox height='100%' overflowY='scroll'>
|
||||
<ScrollbarLessBox height='100%' overflowY='scroll' display="flex" flexDirection="column">
|
||||
<Welcome firstTime={props.launch.firstTime} api={props.api} />
|
||||
<Box
|
||||
mx='2'
|
||||
@ -53,7 +78,7 @@ export default function LaunchApp(props) {
|
||||
color="black"
|
||||
icon="Mail"
|
||||
/>
|
||||
<Text ml="1" mt='1px' color="black">DMs + Drafts</Text>
|
||||
<Text ml="2" mt='1px' color="black">DMs + Drafts</Text>
|
||||
</Row>
|
||||
</Box>
|
||||
</Tile>
|
||||
@ -77,36 +102,15 @@ export default function LaunchApp(props) {
|
||||
icon="CreateGroup"
|
||||
bg="green"
|
||||
color="#fff"
|
||||
text="Create a Group"
|
||||
text="Create Group"
|
||||
>
|
||||
<NewGroup {...props} />
|
||||
</ModalButton>
|
||||
<Groups unreads={props.unreads} groups={props.groups} associations={props.associations} />
|
||||
</Box>
|
||||
<Box alignSelf="flex-start" display={["block", "none"]}>{hashBox}</Box>
|
||||
</ScrollbarLessBox>
|
||||
<Box
|
||||
position="absolute"
|
||||
fontFamily="mono"
|
||||
left="0"
|
||||
bottom="0"
|
||||
color="gray"
|
||||
bg="white"
|
||||
ml={3}
|
||||
mb={3}
|
||||
borderRadius={2}
|
||||
fontSize={0}
|
||||
p={2}
|
||||
cursor="pointer"
|
||||
onClick={() => {
|
||||
writeText(props.baseHash);
|
||||
setHashText('copied');
|
||||
setTimeout(() => {
|
||||
setHashText(props.baseHash);
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
{hashText || props.baseHash}
|
||||
</Box>
|
||||
<Box display={["none", "block"]}>{hashBox}</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -171,7 +171,7 @@ export default class WeatherTile extends React.Component {
|
||||
onClick={() => this.setState({ manualEntry: !this.state.manualEntry })}
|
||||
>
|
||||
<Box>
|
||||
<Icon icon='Weather' display='inline-block' verticalAlign='top' pt='3px' pr='2px' />
|
||||
<Icon icon='Weather' display='inline-block' verticalAlign='top' mt='3px' mr='2' />
|
||||
<Text>Weather</Text>
|
||||
</Box>
|
||||
<Text style={{ cursor: 'pointer' }}>
|
||||
@ -217,15 +217,14 @@ export default class WeatherTile extends React.Component {
|
||||
title={`${locationName} Weather`}
|
||||
>
|
||||
<Text>
|
||||
<Icon icon='Weather' display='inline' style={{ position: 'relative', top: '.3em' }} />
|
||||
Weather
|
||||
<Icon icon='Weather' display='inline' mr='2' style={{ position: 'relative', top: '.3em' }} />
|
||||
<Text
|
||||
cursor='pointer'
|
||||
onClick={() =>
|
||||
this.setState({ manualEntry: !this.state.manualEntry })
|
||||
}
|
||||
>
|
||||
->
|
||||
Weather ->
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
@ -268,7 +267,7 @@ export default class WeatherTile extends React.Component {
|
||||
flexDirection="column"
|
||||
justifyContent="flex-start"
|
||||
>
|
||||
<Text><Icon icon='Weather' color='black' display='inline' style={{ position: 'relative', top: '.3em' }} /> Weather</Text>
|
||||
<Text><Icon icon='Weather' color='black' display='inline' mr='2' style={{ position: 'relative', top: '.3em' }} /> Weather</Text>
|
||||
<Text width='100%' display='flex' flexDirection='column' mt={1}>
|
||||
Loading, please check again later...
|
||||
</Text>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 <Center width='100%' height='100%'><LoadingSpinner/></Center>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Col alignItems="center" height="100%" width="100%" overflowY="auto">
|
||||
<Col alignItems="center" height="100%" width="100%" overflowY="hidden">
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={relativePath("")}
|
||||
render={(props) => {
|
||||
return (
|
||||
<Col width="100%" p={4} alignItems="center" maxWidth="768px">
|
||||
<Col width="100%" flexShrink='0'>
|
||||
<LinkSubmit s3={s3} name={name} ship={ship.slice(1)} api={api} />
|
||||
</Col>
|
||||
{Array.from(graph).map(([date, node]) => {
|
||||
const contact = contactDetails[node.post.author];
|
||||
return (
|
||||
<LinkItem
|
||||
association={resource}
|
||||
contacts={contacts}
|
||||
key={date.toString()}
|
||||
resource={resourcePath}
|
||||
node={node}
|
||||
contacts={contactDetails}
|
||||
unreads={unreads}
|
||||
nickname={contact?.nickname}
|
||||
baseUrl={resourceUrl}
|
||||
group={group}
|
||||
path={resource["group-path"]}
|
||||
api={api}
|
||||
mb={3}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Col>
|
||||
<LinkWindow
|
||||
s3={s3}
|
||||
association={resource}
|
||||
contacts={contacts}
|
||||
resource={resourcePath}
|
||||
graph={graph}
|
||||
unreads={unreads}
|
||||
baseUrl={resourceUrl}
|
||||
group={group}
|
||||
path={resource["group-path"]}
|
||||
api={api}
|
||||
mb={3}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@ -112,6 +105,7 @@ export function LinkResource(props: LinkResourceProps) {
|
||||
const contact = contactDetails[node.post.author];
|
||||
|
||||
return (
|
||||
<Col alignItems="center" overflowY="auto" width="100%">
|
||||
<Col width="100%" p={3} maxWidth="768px">
|
||||
<Link to={resourceUrl}><Text bold>{"<- Back"}</Text></Link>
|
||||
<LinkItem
|
||||
@ -125,6 +119,7 @@ export function LinkResource(props: LinkResourceProps) {
|
||||
path={resource["group-path"]}
|
||||
api={api}
|
||||
mt={3}
|
||||
measure={emptyMeasure}
|
||||
/>
|
||||
<Comments
|
||||
ship={ship}
|
||||
@ -141,6 +136,7 @@ export function LinkResource(props: LinkResourceProps) {
|
||||
group={group}
|
||||
/>
|
||||
</Col>
|
||||
</Col>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
104
pkg/interface/src/views/apps/links/LinkWindow.tsx
Normal file
104
pkg/interface/src/views/apps/links/LinkWindow.tsx
Normal file
@ -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<VirtualScroller>();
|
||||
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 (
|
||||
<Col key={0} mx="auto" mt="4" maxWidth="768px" width="100%" flexShrink={0} px={3}>
|
||||
<LinkSubmit s3={props.s3} name={name} ship={ship.slice(1)} api={api} />
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VirtualScroller
|
||||
ref={(l) => (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 (
|
||||
<>
|
||||
<Col key={index.toString()} mx="auto" mt="4" maxWidth="768px" width="100%" flexShrink={0} px={3}>
|
||||
<LinkSubmit s3={props.s3} name={name} ship={ship.slice(1)} api={api} />
|
||||
</Col>
|
||||
<LinkItem {...linkProps} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
return <LinkItem {...linkProps} />;
|
||||
}}
|
||||
loadRows={fetchLinks}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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<HTMLDivElement | null>(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 (
|
||||
<Box width="100%" {...rest}>
|
||||
|
||||
<Box mx="auto" px={3} maxWidth="768px" ref={ref} width="100%" {...rest}>
|
||||
<Box
|
||||
lineHeight="tall"
|
||||
display='flex'
|
||||
@ -90,6 +103,7 @@ export const LinkItem = (props: LinkItemProps) => {
|
||||
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) => {
|
||||
</Anchor>
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
|
||||
<Row minWidth='0' flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white">
|
||||
|
||||
|
||||
<Author
|
||||
showImage
|
||||
contacts={contacts}
|
||||
@ -136,9 +150,9 @@ export const LinkItem = (props: LinkItemProps) => {
|
||||
</Box>
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
|
||||
<Dropdown
|
||||
width="200px"
|
||||
dropWidth="200px"
|
||||
alignX="right"
|
||||
alignY="top"
|
||||
options={
|
||||
@ -156,7 +170,7 @@ export const LinkItem = (props: LinkItemProps) => {
|
||||
>
|
||||
<Icon ml="2" display="block" icon="Ellipsis" color="gray" />
|
||||
</Dropdown>
|
||||
|
||||
|
||||
</Row>
|
||||
</Box>);
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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 (
|
||||
<Col onClick={onClick} flexGrow="1" p="2">
|
||||
<Header
|
||||
chat
|
||||
associations={props.associations}
|
||||
read={read}
|
||||
archived={props.archived}
|
||||
time={time}
|
||||
authors={authors}
|
||||
moduleIcon="Chat"
|
||||
channel={chat}
|
||||
contacts={props.contacts}
|
||||
group={groupPath}
|
||||
description={desc}
|
||||
/>
|
||||
<Col pb="3" pl="5">
|
||||
{_.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 (
|
||||
<Link key={idx} to={to}>
|
||||
<ChatMessage
|
||||
measure={() => {}}
|
||||
msg={content}
|
||||
isLastRead={false}
|
||||
group={group}
|
||||
contacts={groupContacts}
|
||||
fontSize='0'
|
||||
pt='2'
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{contents.length > 5 && (
|
||||
<Box ml="4" mt="3" mb="2" color="gray" fontSize="14px">
|
||||
and {contents.length - 5} other message
|
||||
{contents.length > 6 ? "s" : ""}
|
||||
</Box>
|
||||
)}
|
||||
</Col>
|
||||
</Col>
|
||||
);
|
||||
}
|
@ -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 ? (
|
||||
<Sigil
|
||||
ship={`~${author}`}
|
||||
size={16}
|
||||
@ -178,7 +181,7 @@ const GraphNode = ({
|
||||
color={`#000000`}
|
||||
classes="mix-blend-diff"
|
||||
/>
|
||||
);
|
||||
) : <Box style={{ width: '16px' }}></Box>;
|
||||
|
||||
const groupContacts = contacts[groupPath] ?? {};
|
||||
|
||||
@ -192,10 +195,10 @@ const GraphNode = ({
|
||||
}, [read, onRead]);
|
||||
|
||||
return (
|
||||
<Row onClick={onClick} gapX="2" pt="2">
|
||||
<Row onClick={onClick} gapX="2" pt={showContact ? 2 : 0}>
|
||||
<Col>{img}</Col>
|
||||
<Col flexGrow={1} alignItems="flex-start">
|
||||
<Row
|
||||
{showContact && <Row
|
||||
mb="2"
|
||||
height="16px"
|
||||
alignItems="center"
|
||||
@ -208,8 +211,8 @@ const GraphNode = ({
|
||||
<Text ml="2" gray>
|
||||
{moment(time).format("HH:mm")}
|
||||
</Text>
|
||||
</Row>
|
||||
<Row width="100%" p="1">
|
||||
</Row>}
|
||||
<Row width="100%" p="1" flexDirection="column">
|
||||
<GraphNodeContent
|
||||
contacts={groupContacts}
|
||||
post={post}
|
||||
@ -244,16 +247,15 @@ export function GraphNotification(props: {
|
||||
const desc = describeNotification(index.description, contents.length !== 1);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
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 (
|
||||
<Col flexGrow={1} width="100%" p="2">
|
||||
<>
|
||||
<Header
|
||||
onClick={onClick}
|
||||
archived={props.archived}
|
||||
@ -267,7 +269,7 @@ return (
|
||||
description={desc}
|
||||
associations={props.associations}
|
||||
/>
|
||||
<Col flexGrow={1} width="100%" pl="5">
|
||||
<Box flexGrow={1} width="100%" pl={5} gridArea="main">
|
||||
{_.map(contents, (content, idx) => (
|
||||
<GraphNode
|
||||
post={content}
|
||||
@ -282,9 +284,10 @@ return (
|
||||
groupPath={group}
|
||||
read={read}
|
||||
onRead={onClick}
|
||||
showContact={idx === 0}
|
||||
/>
|
||||
))}
|
||||
</Col>
|
||||
</Col>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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 (
|
||||
<Row onClick={props.onClick} p="2" flexWrap="wrap" gapX="1" alignItems="center">
|
||||
<Row onClick={props.onClick} p="2" flexWrap="wrap" alignItems="center" gridArea="header">
|
||||
{!props.archived && (
|
||||
<Icon
|
||||
display="block"
|
||||
mr="1"
|
||||
icon={read ? "Circle" : "Bullet"}
|
||||
opacity={read ? 0 : 1}
|
||||
mr={2}
|
||||
icon="Bullet"
|
||||
color="blue"
|
||||
/>
|
||||
)}
|
||||
@ -84,13 +85,13 @@ export function Header(props: {
|
||||
{authorDesc}
|
||||
</Text>
|
||||
<Text mr="1">{description}</Text>
|
||||
{!!moduleIcon && <Icon icon={moduleIcon as any} />}
|
||||
{!!channel && <Text fontWeight="500">{channelTitle}</Text>}
|
||||
<Rule vertical height="12px" />
|
||||
{!!moduleIcon && <Icon icon={moduleIcon as any} mr={1} />}
|
||||
{!!channel && <Text fontWeight="500" mr={1}>{channelTitle}</Text>}
|
||||
<Rule vertical height="12px" mr={1} />
|
||||
{groupTitle &&
|
||||
<>
|
||||
<Text fontWeight="500">{groupTitle}</Text>
|
||||
<Rule vertical height="12px"/>
|
||||
<Text fontWeight="500" mr={1}>{groupTitle}</Text>
|
||||
<Rule vertical height="12px" mr={1} />
|
||||
</>
|
||||
}
|
||||
<Text fontWeight="regular" color="lightGray">
|
||||
|
@ -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<DatedTimebox>(([date, nots]) => [
|
||||
let notificationsByDay = f.flow(
|
||||
f.map<DatedTimebox, DatedTimebox>(([date, nots]) => [
|
||||
date,
|
||||
nots.filter(filterNotification(associations, props.filter)),
|
||||
]),
|
||||
f.groupBy<DatedTimebox>(([date]) =>
|
||||
moment(daToUnix(date)).format("DDMMYYYY")
|
||||
),
|
||||
f.values,
|
||||
f.reverse
|
||||
f.groupBy<DatedTimebox>(([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<string, DatedTimebox[]>(
|
||||
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 =
|
||||
<InviteItem
|
||||
key={uid}
|
||||
invite={invite}
|
||||
onAccept={acceptInvite(appKey, uid)}
|
||||
onDecline={() => api.invite.decline(appKey, uid)}
|
||||
/>;
|
||||
returned.push(inviteItem);
|
||||
});
|
||||
});
|
||||
return returned;
|
||||
};
|
||||
|
||||
return (
|
||||
<Col position="relative" height="100%" overflowY="auto" onScroll={onScroll} >
|
||||
<Col zIndex={4} gapY={2} bg="white" top="0px" position="sticky">
|
||||
{inviteItems(invites, api)}
|
||||
</Col>
|
||||
{newNotifications && (
|
||||
<DaySection
|
||||
latest
|
||||
timeboxes={[newNotifications]}
|
||||
contacts={props.contacts}
|
||||
archive={!!props.showArchive}
|
||||
associations={props.associations}
|
||||
groups={props.groups}
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
groupConfig={props.notificationsGroupConfig}
|
||||
chatConfig={props.notificationsChatConfig}
|
||||
api={api}
|
||||
/>
|
||||
)}
|
||||
{_.map(
|
||||
notificationsByDay,
|
||||
(timeboxes, idx) =>
|
||||
timeboxes.length > 0 && (
|
||||
<DaySection
|
||||
key={idx}
|
||||
timeboxes={timeboxes}
|
||||
contacts={props.contacts}
|
||||
archive={!!props.showArchive}
|
||||
associations={props.associations}
|
||||
api={api}
|
||||
groups={props.groups}
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
groupConfig={props.notificationsGroupConfig}
|
||||
chatConfig={props.notificationsChatConfig}
|
||||
/>
|
||||
)
|
||||
<Col ref={scrollRef} position="relative" height="100%" overflowY="auto">
|
||||
<Invites invites={invites} api={api} associations={associations} />
|
||||
{[...notificationsByDayMap.keys()].sort().reverse().map((day, index) => {
|
||||
const timeboxes = notificationsByDayMap.get(day)!;
|
||||
return timeboxes.length > 0 && (
|
||||
<DaySection
|
||||
key={day}
|
||||
label={day === 'latest' ? 'Today' : moment(day).calendar(null, calendar)}
|
||||
timeboxes={timeboxes}
|
||||
contacts={props.contacts}
|
||||
archive={!!props.showArchive}
|
||||
associations={props.associations}
|
||||
api={api}
|
||||
groups={props.groups}
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
groupConfig={props.notificationsGroupConfig}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{loadedAll && (
|
||||
<Center mt="2" borderTop={notifications.length !== 0 ? 1 : 0} borderTopColor="washedGray" width="100%" height="96px">
|
||||
<Text gray fontSize="1">No more notifications</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Col>
|
||||
);
|
||||
@ -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 (
|
||||
<>
|
||||
<Box position="sticky" zIndex="3" top="-1px" bg="white">
|
||||
<Box position="sticky" zIndex={3} top="-1px" bg="white">
|
||||
<Box p="2" bg="scales.black05">
|
||||
<Text>
|
||||
{moment(daToUnix(timeboxes[0][0])).calendar(null, calendar)}
|
||||
{label}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
{_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i) =>
|
||||
{_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i: number) =>
|
||||
_.map(nots.sort(sortIndexedNotification), (not, j: number) => (
|
||||
<React.Fragment key={j}>
|
||||
{(i !== 0 || j !== 0) && (
|
||||
<Box flexShrink="0" height="4px" bg="scales.black05" />
|
||||
<Box flexShrink={0} height="4px" bg="scales.black05" />
|
||||
)}
|
||||
<Notification
|
||||
graphConfig={graphConfig}
|
||||
groupConfig={groupConfig}
|
||||
chatConfig={chatConfig}
|
||||
api={api}
|
||||
associations={associations}
|
||||
notification={not}
|
||||
|
74
pkg/interface/src/views/apps/notifications/invites.tsx
Normal file
74
pkg/interface/src/views/apps/notifications/invites.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { Box, Row, Col } from "@tlon/indigo-react";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { Invites as IInvites, Associations, Invite } from "~/types";
|
||||
import { resourceAsPath } from "~/logic/lib/util";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
|
||||
import InviteItem from "~/views/components/Invite";
|
||||
|
||||
interface InvitesProps {
|
||||
api: GlobalApi;
|
||||
invites: IInvites;
|
||||
associations: Associations;
|
||||
}
|
||||
|
||||
export function Invites(props: InvitesProps) {
|
||||
const { api, invites } = props;
|
||||
const history = useHistory();
|
||||
const waiter = useWaitForProps(props);
|
||||
|
||||
const acceptInvite = (
|
||||
app: string,
|
||||
uid: string,
|
||||
invite: Invite
|
||||
) => 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 (
|
||||
<Col
|
||||
zIndex={4}
|
||||
gapY={2}
|
||||
bg="white"
|
||||
top="0px"
|
||||
position="sticky"
|
||||
flexShrink={0}
|
||||
>
|
||||
{Object.keys(invites).reduce((items, appKey) => {
|
||||
const app = invites[appKey];
|
||||
let appItems = Object.keys(app).map((uid) => {
|
||||
const invite = app[uid];
|
||||
return (
|
||||
<InviteItem
|
||||
key={uid}
|
||||
invite={invite}
|
||||
onAccept={acceptInvite(appKey, uid, invite)}
|
||||
onDecline={declineInvite(appKey, uid)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return [...items, ...appItems];
|
||||
}, [] as JSX.Element[])}
|
||||
</Col>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<Row width="100%" flexShrink={0} alignItems="top" justifyContent="space-between">
|
||||
<Box
|
||||
width="100%"
|
||||
display="grid"
|
||||
gridTemplateColumns="1fr 200px"
|
||||
gridTemplateRows="auto"
|
||||
gridTemplateAreas="'header actions' 'main main'"
|
||||
pb={2}
|
||||
{...bind}
|
||||
>
|
||||
{children}
|
||||
<Row gapX="2" p="2" pt='3' alignItems="top">
|
||||
<Row gapX="2" p="2" pt='3' gridArea="actions" justifyContent="flex-end" opacity={[1, hovering ? 1 : 0]}>
|
||||
<StatelessAsyncAction name={changeMuteDesc} onClick={onChangeMute} backgroundColor="transparent">
|
||||
{changeMuteDesc}
|
||||
</StatelessAsyncAction>
|
||||
@ -103,7 +108,7 @@ function NotificationWrapper(props: {
|
||||
</StatelessAsyncAction>
|
||||
)}
|
||||
</Row>
|
||||
</Row>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@ -166,26 +171,6 @@ export function Notification(props: NotificationProps) {
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
if ("chat" in notification.index) {
|
||||
const index = notification.index.chat;
|
||||
const c: ChatNotificationContents = (contents as any).chat;
|
||||
return (
|
||||
<Wrapper>
|
||||
<ChatNotification
|
||||
api={props.api}
|
||||
index={index}
|
||||
contents={c}
|
||||
contacts={props.contacts}
|
||||
read={read}
|
||||
archived={archived}
|
||||
groups={props.groups}
|
||||
timebox={props.time}
|
||||
time={time}
|
||||
associations={associations}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
@ -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 (
|
||||
<Body>
|
||||
<Col overflowY="hidden" height="100%">
|
||||
<Row
|
||||
p="3"
|
||||
alignItems="center"
|
||||
height="48px"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
borderBottom="1"
|
||||
borderBottomColor="washedGray"
|
||||
>
|
||||
<Text>Updates</Text>
|
||||
<Row>
|
||||
<Box>
|
||||
<HeaderLink current={view} view="">
|
||||
Inbox
|
||||
</HeaderLink>
|
||||
</Box>
|
||||
<Box>
|
||||
<HeaderLink current={view} view="preferences">
|
||||
Preferences
|
||||
</HeaderLink>
|
||||
</Box>
|
||||
</Row>
|
||||
<Dropdown
|
||||
alignX="right"
|
||||
alignY="top"
|
||||
options={
|
||||
<Col
|
||||
p="2"
|
||||
backgroundColor="white"
|
||||
border={1}
|
||||
borderRadius={1}
|
||||
borderColor="lightGray"
|
||||
gapY="2"
|
||||
>
|
||||
<FormikOnBlur
|
||||
initialValues={filter}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<GroupSearch
|
||||
id="groups"
|
||||
label="Filter Groups"
|
||||
caption="Only show notifications from this group"
|
||||
associations={props.associations}
|
||||
/>
|
||||
</FormikOnBlur>
|
||||
</Col>
|
||||
}
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Notifications</title>
|
||||
</Helmet>
|
||||
<Body>
|
||||
<Col overflowY="hidden" height="100%">
|
||||
<Row
|
||||
p="3"
|
||||
alignItems="center"
|
||||
height="48px"
|
||||
justifyContent="space-between"
|
||||
width="100%"
|
||||
borderBottom="1"
|
||||
borderBottomColor="washedGray"
|
||||
>
|
||||
<Box>
|
||||
<Text mr="1" gray>
|
||||
Filter:
|
||||
</Text>
|
||||
<Text>{groupFilterDesc}</Text>
|
||||
</Box>
|
||||
</Dropdown>
|
||||
</Row>
|
||||
{view === "preferences" && (
|
||||
<NotificationPreferences
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
api={props.api}
|
||||
dnd={props.doNotDisturb}
|
||||
/>
|
||||
)}
|
||||
{!view && <Inbox {...props} filter={filter.groups} />}
|
||||
</Col>
|
||||
</Body>
|
||||
<Text>Updates</Text>
|
||||
<Row>
|
||||
<Box>
|
||||
<HeaderLink current={view} view="">
|
||||
Inbox
|
||||
</HeaderLink>
|
||||
</Box>
|
||||
<Box>
|
||||
<HeaderLink current={view} view="preferences">
|
||||
Preferences
|
||||
</HeaderLink>
|
||||
</Box>
|
||||
</Row>
|
||||
<Dropdown
|
||||
alignX="right"
|
||||
alignY="top"
|
||||
options={
|
||||
<Col
|
||||
p="2"
|
||||
backgroundColor="white"
|
||||
border={1}
|
||||
borderRadius={1}
|
||||
borderColor="lightGray"
|
||||
gapY="2"
|
||||
>
|
||||
<FormikOnBlur
|
||||
initialValues={filter}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<GroupSearch
|
||||
id="groups"
|
||||
label="Filter Groups"
|
||||
caption="Only show notifications from this group"
|
||||
associations={props.associations}
|
||||
/>
|
||||
</FormikOnBlur>
|
||||
</Col>
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<Text mr="1" gray>
|
||||
Filter:
|
||||
</Text>
|
||||
<Text>{groupFilterDesc}</Text>
|
||||
</Box>
|
||||
</Dropdown>
|
||||
</Row>
|
||||
{view === "preferences" && (
|
||||
<NotificationPreferences
|
||||
graphConfig={props.notificationsGraphConfig}
|
||||
api={props.api}
|
||||
dnd={props.doNotDisturb}
|
||||
/>
|
||||
)}
|
||||
{!view && <Inbox {...props} filter={filter.groups} />}
|
||||
</Col>
|
||||
</Body>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
@ -29,7 +29,7 @@ export function Profile(props: any) {
|
||||
|
||||
const image = (!hideAvatars && contact?.avatar)
|
||||
? <BaseImage src={contact.avatar} width='100%' height='100%' style={{ objectFit: 'cover' }} />
|
||||
: <Sigil ship={`~${ship}`} size={96} color={hexColor} />;
|
||||
: <Sigil ship={`${ship}`} size={96} color={hexColor} />;
|
||||
|
||||
return (
|
||||
<Center
|
||||
|
@ -35,7 +35,7 @@ export function ViewProfile(props: any) {
|
||||
width="100%">
|
||||
<Center width="100%">
|
||||
<Text mono color="darkGray">
|
||||
{`~${ship}`}
|
||||
{`${ship}`}
|
||||
</Text>
|
||||
</Center>
|
||||
</Row>
|
||||
@ -63,7 +63,7 @@ export function ViewProfile(props: any) {
|
||||
</Button>
|
||||
</Center>
|
||||
</Row>
|
||||
) : null
|
||||
) : null
|
||||
}
|
||||
<Box
|
||||
height="200px"
|
||||
@ -72,7 +72,7 @@ export function ViewProfile(props: any) {
|
||||
border={1}
|
||||
borderColor="washedGray">
|
||||
<Center height="100%">
|
||||
<Text mono pr={1} color="gray">{`~${ship} `}</Text>
|
||||
<Text mono pr={1} color="gray">{`${ship} `}</Text>
|
||||
<Text color="gray">remains private</Text>
|
||||
</Center>
|
||||
</Box>
|
||||
|
@ -15,7 +15,7 @@ export default function ProfileScreen(props: any) {
|
||||
return (
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>OS1 - Profile</title>
|
||||
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Profile</title>
|
||||
</Helmet>
|
||||
<Route
|
||||
path={"/~profile/:ship/:edit?"}
|
||||
|
@ -10,6 +10,7 @@ import CodeMirror from "codemirror";
|
||||
|
||||
import "codemirror/mode/markdown/markdown";
|
||||
import "codemirror/addon/display/placeholder";
|
||||
import "codemirror/addon/edit/continuelist";
|
||||
|
||||
import "codemirror/lib/codemirror.css";
|
||||
import { Box } from "@tlon/indigo-react";
|
||||
@ -54,6 +55,7 @@ export function MarkdownEditor(
|
||||
scrollbarStyle: "native",
|
||||
// cursorHeight: 0.85,
|
||||
placeholder: placeholder || "",
|
||||
extraKeys: { 'Enter': 'newlineAndIndentContinueMarkdownList' }
|
||||
};
|
||||
|
||||
const editor: React.RefObject<any> = useRef();
|
||||
|
@ -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
|
||||
</Text>
|
||||
@ -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 (
|
||||
<Box
|
||||
my={3}
|
||||
@ -86,6 +93,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
width="100%"
|
||||
gridRowGap={4}
|
||||
mx="auto"
|
||||
ref={windowRef}
|
||||
>
|
||||
<Link to={rootUrl}>
|
||||
<Text>{"<- Notebook Index"}</Text>
|
||||
|
@ -61,9 +61,9 @@ export function NotePreview(props: NotePreviewProps) {
|
||||
overflow='hidden'
|
||||
p='2'
|
||||
>
|
||||
<WrappedBox mb={2}><Text bold fontSize='0'>{title}</Text></WrappedBox>
|
||||
<WrappedBox mb={2}><Text bold>{title}</Text></WrappedBox>
|
||||
<WrappedBox>
|
||||
<Text fontSize='14px'>
|
||||
<Text fontSize='14px' lineHeight='tall'>
|
||||
<ReactMarkdown
|
||||
unwrapDisallowed
|
||||
allowedTypes={['text', 'root', 'break', 'paragraph', 'image']}
|
||||
|
@ -1,10 +1,9 @@
|
||||
import React from "react";
|
||||
import { Link, RouteComponentProps } from "react-router-dom";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { NotebookPosts } from "./NotebookPosts";
|
||||
import { Box, Button, Text, Row, Col } from "@tlon/indigo-react";
|
||||
import { Col } from "@tlon/indigo-react";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { Contacts, Rolodex, Groups, Associations, Graph, Association, Unreads } from "~/types";
|
||||
import { useShowNickname } from "~/logic/lib/util";
|
||||
|
||||
interface NotebookProps {
|
||||
api: GlobalApi;
|
||||
@ -30,44 +29,14 @@ export function Notebook(props: NotebookProps & RouteComponentProps) {
|
||||
association,
|
||||
graph
|
||||
} = props;
|
||||
const { metadata } = association;
|
||||
|
||||
const group = groups[association?.['group-path']];
|
||||
if (!group) {
|
||||
return null; // Waiting on groups to populate
|
||||
}
|
||||
|
||||
const relativePath = (p: string) => 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 (
|
||||
<Col gapY="4" pt={4} mx="auto" px={3} maxWidth="768px">
|
||||
<Row justifyContent="space-between">
|
||||
<Box>
|
||||
<Text display='block'>{metadata?.title}</Text>
|
||||
<Text color="lightGray">by </Text>
|
||||
<Text fontFamily={showNickname ? 'sans' : 'mono'}>
|
||||
{showNickname ? contact?.nickname : ship}
|
||||
</Text>
|
||||
</Box>
|
||||
{isWriter && (
|
||||
<Link to={relativePath('/new')}>
|
||||
<Button primary style={{ cursor: 'pointer' }}>
|
||||
New Post
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</Row>
|
||||
<Box borderBottom="1" borderBottomColor="washedGray" />
|
||||
<NotebookPosts
|
||||
graph={graph}
|
||||
host={ship}
|
||||
|
@ -206,9 +206,6 @@
|
||||
}
|
||||
|
||||
@media all and (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #333;
|
||||
}
|
||||
.bg-black-d {
|
||||
background-color: black;
|
||||
}
|
||||
@ -267,7 +264,7 @@
|
||||
background-color: #333;
|
||||
}
|
||||
.publish .cm-s-tlon.CodeMirror {
|
||||
background: #333;
|
||||
background: unset;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
@ -46,8 +46,8 @@ export default class TermApp extends Component {
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>OS1 - Terminal</title>
|
||||
<Helmet defer={false}>
|
||||
<title>{ this.props.notificationsCount ? `(${String(this.props.notificationsCount) }) `: '' }Landscape</title>
|
||||
</Helmet>
|
||||
<Box
|
||||
height='100%'
|
||||
|
@ -29,10 +29,15 @@ export function AsyncButton({
|
||||
}, [status]);
|
||||
|
||||
return (
|
||||
<Button disabled={!isValid} type="submit" {...rest}>
|
||||
<Button
|
||||
hideDisabled={isSubmitting}
|
||||
disabled={!isValid || isSubmitting}
|
||||
type="submit"
|
||||
{...rest}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<LoadingSpinner
|
||||
foreground={rest.primary ? "white" : 'black'}
|
||||
foreground={rest.primary ? "white" : "black"}
|
||||
background="gray"
|
||||
/>
|
||||
) : success === true ? (
|
||||
|
@ -52,13 +52,14 @@ export default function Author(props: AuthorProps) {
|
||||
<Box
|
||||
ml={showImage ? 2 : 0}
|
||||
color="black"
|
||||
fontSize='1'
|
||||
lineHeight='tall'
|
||||
fontFamily={showNickname ? "sans" : "mono"}
|
||||
fontWeight={showNickname ? '500' : '400'}
|
||||
>
|
||||
{name}
|
||||
</Box>
|
||||
<Box ml={2} color={props.unread ? "blue" : "gray"}>
|
||||
<Box fontSize='1' ml={2} color={props.unread ? "blue" : "gray"}>
|
||||
{dateFmt}
|
||||
</Box>
|
||||
{props.children}
|
||||
|
@ -77,7 +77,7 @@ export function Comments(props: CommentsProps) {
|
||||
if ('text' in curr) {
|
||||
val = val + curr.text;
|
||||
} else if ('mention' in curr) {
|
||||
val = val + curr.mention;
|
||||
val = val + `~${curr.mention}`;
|
||||
} else if ('url' in curr) {
|
||||
val = val + curr.url;
|
||||
} else if ('code' in curr) {
|
||||
|
@ -21,6 +21,7 @@ interface DropdownProps {
|
||||
alignY: AlignY | AlignY[];
|
||||
alignX: AlignX | AlignX[];
|
||||
width?: string;
|
||||
dropWidth?: string;
|
||||
}
|
||||
|
||||
const ClickBox = styled(Box)`
|
||||
@ -111,14 +112,14 @@ export function Dropdown(props: DropdownProps) {
|
||||
});
|
||||
|
||||
return (
|
||||
<Box flexShrink={1} position={open ? "relative" : "static"} minWidth='0'>
|
||||
<Box flexShrink={1} position={open ? "relative" : "static"} minWidth='0' width={props?.width ? props.width : 'auto'}>
|
||||
<ClickBox width='100%' ref={anchorRef} onClick={onOpen}>
|
||||
{children}
|
||||
</ClickBox>
|
||||
{open && (
|
||||
<Portal>
|
||||
<DropdownOptions
|
||||
width={props.width || "max-content"}
|
||||
width={props?.dropWidth || "max-content"}
|
||||
{...coords}
|
||||
ref={dropdownRef}
|
||||
>
|
||||
|
@ -19,28 +19,19 @@ interface MentionTextProps {
|
||||
group: Group;
|
||||
}
|
||||
export function MentionText(props: MentionTextProps) {
|
||||
const { content, contacts, contact, group } = props;
|
||||
const { content, contacts, contact, group, ...rest } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{_.map(content, (c, idx) => {
|
||||
<RichText contacts={contacts} contact={contact} group={group} {...rest}>
|
||||
{content.reduce((accum, c) => {
|
||||
if ("text" in c) {
|
||||
return (
|
||||
<RichText
|
||||
inline
|
||||
key={idx}
|
||||
>
|
||||
{c.text}
|
||||
</RichText>
|
||||
);
|
||||
return accum + c.text;
|
||||
} else if ("mention" in c) {
|
||||
return (
|
||||
<Mention key={idx} contacts={contacts || {}} contact={contact || {}} group={group} ship={c.mention} />
|
||||
);
|
||||
return accum + `[~${c.mention}]`;
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</>
|
||||
return accum;
|
||||
}, '')}
|
||||
</RichText>
|
||||
);
|
||||
}
|
||||
|
||||
@ -53,7 +44,7 @@ export function Mention(props: {
|
||||
const { contacts, ship } = props;
|
||||
let { contact } = props;
|
||||
|
||||
contact = (contact?.nickname) ? contact : contacts?.[ship];
|
||||
contact = (contact?.color) ? contact : contacts?.[ship];
|
||||
|
||||
const showNickname = useShowNickname(contact);
|
||||
|
||||
|
@ -90,6 +90,8 @@ class OverlaySigil extends PureComponent<OverlaySigilProps, OverlaySigilState> {
|
||||
api,
|
||||
sigilClass,
|
||||
hideAvatars,
|
||||
pr = 0,
|
||||
pl = 0,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
@ -113,6 +115,8 @@ class OverlaySigil extends PureComponent<OverlaySigilProps, OverlaySigilState> {
|
||||
onClick={this.profileShow}
|
||||
ref={this.containerRef}
|
||||
className={className}
|
||||
pr={pr}
|
||||
pl={pl}
|
||||
>
|
||||
{state.clicked && (
|
||||
<ProfileOverlay
|
||||
|
@ -18,6 +18,7 @@ type ProfileOverlayProps = ColProps & {
|
||||
group?: Group;
|
||||
onDismiss(): void;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
history: any;
|
||||
api: any;
|
||||
}
|
||||
@ -61,6 +62,7 @@ class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> {
|
||||
bottomSpace,
|
||||
group = false,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
history,
|
||||
onDismiss,
|
||||
...rest
|
||||
@ -89,7 +91,7 @@ class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> {
|
||||
classes="brt2"
|
||||
svgClass="brt2"
|
||||
/>;
|
||||
const showNickname = useShowNickname(contact);
|
||||
const showNickname = useShowNickname(contact, hideNicknames);
|
||||
|
||||
// TODO: we need to rethink this "top-level profile view" of other ships
|
||||
/* if (!group.hidden) {
|
||||
@ -127,7 +129,7 @@ class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> {
|
||||
)}
|
||||
<Text mono gray>{cite(`~${ship}`)}</Text>
|
||||
{!isOwn && (
|
||||
<Button mt={2} width="100%" style={{ cursor: 'pointer' }} onClick={() => history.push(`/~landscape/dm/${ship}`)}>
|
||||
<Button mt={2} fontSize='0' width="100%" style={{ cursor: 'pointer' }} onClick={() => history.push(`/~landscape/dm/${ship}`)}>
|
||||
Send Message
|
||||
</Button>
|
||||
)}
|
||||
@ -147,4 +149,4 @@ class ProfileOverlay extends PureComponent<ProfileOverlayProps, {}> {
|
||||
}
|
||||
}
|
||||
|
||||
export default withLocalState(ProfileOverlay, ['hideAvatars']);
|
||||
export default withLocalState(ProfileOverlay, ['hideAvatars', 'hideNicknames']);
|
@ -8,14 +8,14 @@ const ReconnectButton = ({ connection, subscription }) => {
|
||||
if (connectedStatus === "disconnected") {
|
||||
return (
|
||||
<Button onClick={reconnect} borderColor='red' px='2'>
|
||||
<Text display={['none', 'inline']} textAlign='middle' color='red'>Reconnect </Text>
|
||||
<Text color='red'>↻</Text>
|
||||
<Text display={['none', 'inline']} textAlign='middle' color='red'>Reconnect</Text>
|
||||
<Text color='red'> ↻</Text>
|
||||
</Button>
|
||||
);
|
||||
} else if (connectedStatus === "reconnecting") {
|
||||
return (
|
||||
<Button borderColor='yellow' px='2' onClick={() => {}} cursor='default'>
|
||||
<LoadingSpinner pr='2' foreground='scales.yellow60' background='scales.yellow30'/>
|
||||
<LoadingSpinner pr={['0','2']} foreground='scales.yellow60' background='scales.yellow30'/>
|
||||
<Text display={['none', 'inline']} textAlign='middle' color='yellow'>Reconnecting</Text>
|
||||
</Button>
|
||||
)
|
||||
|
@ -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 }) => (
|
||||
<ReactMarkdown
|
||||
{...props}
|
||||
renderers={{
|
||||
link: (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 <RemoteContent className="mw-100" url={linkProps.href} />;
|
||||
}
|
||||
if (hasProvider(props.href)) {
|
||||
return <RemoteContent className="mw-100" url={props.href} />;
|
||||
|
||||
return <BaseAnchor target='_blank' rel='noreferrer noopener' borderBottom='1px solid' remoteContentPolicy={remoteContentPolicy} {...linkProps}>{linkProps.children}</BaseAnchor>;
|
||||
},
|
||||
linkReference: (linkProps) => {
|
||||
const linkText = String(linkProps.children[0].props.children);
|
||||
if (isValidPatp(linkText)) {
|
||||
return <Mention contacts={props.contacts || {}} contact={props.contact || {}} group={props.group} ship={deSig(linkText)} />;
|
||||
}
|
||||
return <BaseAnchor target='_blank' rel='noreferrer noopener' borderBottom='1px solid' {...props}>{props.children}</BaseAnchor>;
|
||||
return linkText;
|
||||
},
|
||||
paragraph: (paraProps) => {
|
||||
return <Text display={props.inline ? 'inline' : 'block'} mb='2' {...props}>{paraProps.children}</Text>;
|
||||
|
@ -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}
|
||||
|
@ -8,6 +8,7 @@ import { useFormikContext } from "formik";
|
||||
interface AsyncActionProps {
|
||||
children: ReactNode;
|
||||
name: string;
|
||||
disabled?: boolean;
|
||||
onClick: (e: React.MouseEvent) => Promise<void>;
|
||||
}
|
||||
|
||||
@ -15,6 +16,7 @@ export function StatelessAsyncAction({
|
||||
children,
|
||||
onClick,
|
||||
name = '',
|
||||
disabled = false,
|
||||
...rest
|
||||
}: AsyncActionProps & Parameters<typeof Action>[0]) {
|
||||
const {
|
||||
@ -23,7 +25,10 @@ export function StatelessAsyncAction({
|
||||
} = useStatelessAsyncClickable(onClick, name);
|
||||
|
||||
return (
|
||||
<Action onClick={handleClick} {...rest}>
|
||||
<Action
|
||||
hideDisabled={!disabled}
|
||||
disabled={disabled || state === 'loading'}
|
||||
onClick={handleClick} {...rest}>
|
||||
{state === "error" ? (
|
||||
"Error"
|
||||
) : state === "loading" ? (
|
||||
|
@ -7,7 +7,7 @@ import { useStatelessAsyncClickable } from "~/logic/lib/useStatelessAsyncClickab
|
||||
|
||||
interface AsyncButtonProps {
|
||||
children: ReactNode;
|
||||
name: string;
|
||||
name?: string;
|
||||
onClick: (e: React.MouseEvent) => Promise<void>;
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ export function StatelessAsyncButton({
|
||||
children,
|
||||
onClick,
|
||||
name = "",
|
||||
disabled = false,
|
||||
...rest
|
||||
}: AsyncButtonProps & Parameters<typeof Button>[0]) {
|
||||
const {
|
||||
@ -23,7 +24,12 @@ export function StatelessAsyncButton({
|
||||
} = useStatelessAsyncClickable(onClick, name);
|
||||
|
||||
return (
|
||||
<Button onClick={handleClick} {...rest}>
|
||||
<Button
|
||||
hideDisabled={!disabled}
|
||||
disabled={disabled || state === 'loading'}
|
||||
onClick={handleClick}
|
||||
{...rest}
|
||||
>
|
||||
{state === "error" ? (
|
||||
"Error"
|
||||
) : state === "loading" ? (
|
||||
|
@ -24,7 +24,7 @@ const StatusBar = (props) => {
|
||||
pb='3'
|
||||
>
|
||||
<Row collapse>
|
||||
<Button borderColor='washedGray' mr='2' px='2' onClick={() => props.history.push('/')} {...props}>
|
||||
<Button width="32px" borderColor='washedGray' mr='2' px='2' onClick={() => props.history.push('/')} {...props}>
|
||||
<Icon icon='Spaces' color='black'/>
|
||||
</Button>
|
||||
<StatusBarItem mr={2} onClick={() => 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}`
|
||||
)}
|
||||
>
|
||||
<Text color='#000000'>Submit <Text color='#000000' display={['none', 'inline']}>an</Text> issue</Text>
|
||||
@ -65,28 +65,37 @@ const StatusBar = (props) => {
|
||||
<Icon icon='Gear' color='black'/>
|
||||
</StatusBarItem>
|
||||
<Dropdown
|
||||
width="200px"
|
||||
dropWidth="150px"
|
||||
width="auto"
|
||||
alignY="top"
|
||||
alignX="right"
|
||||
options={
|
||||
<Box backgroundColor="white">
|
||||
<Col mt='6' p='1' backgroundColor="white" color="washedGray" border={1} borderRadius={2} borderColor="lightGray" boxShadow="0px 0px 0px 3px">
|
||||
<Row
|
||||
p={1}
|
||||
color='black'
|
||||
onClick={() => props.history.push('/~profile/' + window.ship)}>
|
||||
cursor='pointer'
|
||||
fontSize={0}
|
||||
onClick={() => props.history.push('/~profile/~' + window.ship)}>
|
||||
View Profile
|
||||
</Row>
|
||||
<Row
|
||||
p={1}
|
||||
color='black'
|
||||
onClick={() => props.history.push('/~profile/' + window.ship)}>
|
||||
cursor='pointer'
|
||||
fontSize={0}
|
||||
onClick={() => props.history.push('/~profile/~' + window.ship)}>
|
||||
Set Status
|
||||
</Row>
|
||||
<Row
|
||||
p={1}
|
||||
color='black'
|
||||
onClick={() => props.history.push('/~profile/' + window.ship)}>
|
||||
cursor='pointer'
|
||||
fontSize={0}
|
||||
onClick={() => props.history.push('/~profile/~' + window.ship)}>
|
||||
System Settings
|
||||
</Row>
|
||||
</Box>
|
||||
</Col>
|
||||
}>
|
||||
<StatusBarItem
|
||||
px={'2'}
|
||||
|
@ -44,6 +44,8 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
|
||||
timeout: ReturnType<typeof setTimeout>;
|
||||
} | 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<VirtualScrollerProps, Vir
|
||||
visibleItems: new BigIntOrderedMap(),
|
||||
endgap: props.origin === 'bottom' ? 0 : undefined,
|
||||
totalHeight: 0,
|
||||
averageHeight: 64,
|
||||
averageHeight: 130,
|
||||
scrollTop: props.origin === 'top' ? 0 : undefined
|
||||
};
|
||||
|
||||
@ -61,8 +63,8 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
|
||||
this.window = null;
|
||||
this.cache = new BigIntOrderedMap();
|
||||
|
||||
this.recalculateTotalHeight = this.recalculateTotalHeight.bind(this);
|
||||
this.calculateVisibleItems = this.calculateVisibleItems.bind(this);
|
||||
this.recalculateTotalHeight = _.throttle(this.recalculateTotalHeight.bind(this), 200);
|
||||
this.calculateVisibleItems = _.throttle(this.calculateVisibleItems.bind(this), 200);
|
||||
this.estimateIndexFromScrollTop = this.estimateIndexFromScrollTop.bind(this);
|
||||
this.invertedKeyHandler = this.invertedKeyHandler.bind(this);
|
||||
this.heightOf = this.heightOf.bind(this);
|
||||
@ -74,6 +76,8 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
|
||||
|
||||
componentDidMount() {
|
||||
this.calculateVisibleItems();
|
||||
|
||||
this.recalculateTotalHeight();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: VirtualScrollerProps, prevState: VirtualScrollerState) {
|
||||
@ -107,7 +111,7 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
|
||||
let { averageHeight } = this.state;
|
||||
let totalHeight = 0;
|
||||
this.props.data.forEach((datum, index) => {
|
||||
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<VirtualScrollerProps, Vir
|
||||
let startgap = 0, heightShown = 0, endgap = 0;
|
||||
let startGapFilled = false;
|
||||
let visibleItems = new BigIntOrderedMap<any>();
|
||||
let startBuffer = new BigIntOrderedMap<any>();
|
||||
let endBuffer = new BigIntOrderedMap<any>();
|
||||
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<VirtualScrollerProps, Vir
|
||||
onCalculateVisibleItems ? onCalculateVisibleItems(visibleItems) : null;
|
||||
this.setState({
|
||||
startgap: Number(startgap.toFixed()),
|
||||
visibleItems: new BigIntOrderedMap([...startBuffer, ...visibleItems, ...endBuffer]),
|
||||
visibleItems,
|
||||
endgap: Number(endgap.toFixed()),
|
||||
});
|
||||
}
|
||||
@ -238,6 +224,8 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
|
||||
}
|
||||
}
|
||||
|
||||
this.overscan = Math.max(element.offsetHeight * 3, 500);
|
||||
|
||||
this.window = element;
|
||||
if (this.props.origin === 'bottom') {
|
||||
element.addEventListener('wheel', (event) => {
|
||||
@ -303,7 +291,7 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
|
||||
data
|
||||
} = this.props;
|
||||
|
||||
const indexesToRender = visibleItems.keys().reverse();
|
||||
const indexesToRender = origin === 'top' ? visibleItems.keys() : visibleItems.keys().reverse();
|
||||
|
||||
const transform = origin === 'top' ? 'scale3d(1, 1, 1)' : 'scale3d(1, -1, 1)';
|
||||
|
||||
@ -314,7 +302,7 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
|
||||
height: element.offsetHeight,
|
||||
element
|
||||
});
|
||||
_.debounce(this.recalculateTotalHeight, 500)();
|
||||
this.recalculateTotalHeight();
|
||||
}
|
||||
};
|
||||
return renderer({ index, measure, scrollWindow: this.window });
|
||||
@ -322,7 +310,7 @@ export default class VirtualScroller extends Component<VirtualScrollerProps, Vir
|
||||
|
||||
return (
|
||||
<Box overflowY='scroll' ref={this.setWindow.bind(this)} onScroll={this.onScroll.bind(this)} style={{ ...style, ...{ transform } }}>
|
||||
<Box ref={this.scrollContainer} style={{ transform }}>
|
||||
<Box ref={this.scrollContainer} style={{ transform, width: '100%' }}>
|
||||
<Box style={{ height: `${origin === 'top' ? startgap : endgap}px` }}></Box>
|
||||
{indexesToRender.map(render)}
|
||||
<Box style={{ height: `${origin === 'top' ? endgap : startgap}px` }}></Box>
|
||||
|
@ -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 : <Text gray ml={2}>{category.charAt(0).toUpperCase() + category.slice(1)}</Text>;
|
||||
? null : <Row pl='2' height='5' alignItems='center' bg='washedGray'><Text gray bold>{category.charAt(0).toUpperCase() + category.slice(1)}</Text></Row>;
|
||||
const selected = this.state.selected?.length ? this.state.selected[1] : '';
|
||||
return (<Box key={i} width='max(50vw, 300px)' maxWidth='600px'>
|
||||
<Rule borderTopWidth="0.5px" color="washedGray" />
|
||||
{categoryTitle}
|
||||
{categoryResults.map((result, i2) => (
|
||||
<OmniboxResult
|
||||
@ -250,6 +249,7 @@ export class Omnibox extends Component {
|
||||
selected={selected}
|
||||
invites={props.invites}
|
||||
notifications={props.notifications}
|
||||
contacts={props.contacts}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
@ -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 (
|
||||
<Box
|
||||
backgroundColor='scales.black30'
|
||||
|
@ -22,7 +22,7 @@ export class OmniboxInput extends Component {
|
||||
border='1px solid transparent'
|
||||
borderRadius='2'
|
||||
maxWidth='calc(600px - 1.15rem)'
|
||||
fontSize='0'
|
||||
fontSize='1'
|
||||
style={{ boxSizing: 'border-box' }}
|
||||
placeholder='Search...'
|
||||
onKeyDown={props.control}
|
||||
|
@ -2,6 +2,7 @@ import React, { Component } from 'react';
|
||||
import { Box, Row, Icon, Text } from '@tlon/indigo-react';
|
||||
import defaultApps from '~/logic/lib/default-apps';
|
||||
import Sigil from '~/logic/lib/sigil';
|
||||
import { uxToHex } from '~/logic/lib/util';
|
||||
|
||||
export class OmniboxResult extends Component {
|
||||
constructor(props) {
|
||||
@ -25,9 +26,8 @@ export class OmniboxResult extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
getIcon(icon, selected, link, invites, notifications) {
|
||||
getIcon(icon, selected, link, invites, notifications, text, color) {
|
||||
const iconFill = (this.state.hovered || (selected === link)) ? 'white' : 'black';
|
||||
const sigilFill = (this.state.hovered || (selected === link)) ? '#3a8ff7' : '#ffffff';
|
||||
const bulletFill = (this.state.hovered || (selected === link)) ? 'white' : 'blue';
|
||||
|
||||
const inviteCount = [].concat(...Object.values(invites).map(obj => 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 = <Icon display="inline-block" verticalAlign="middle" icon={icon} mr='2' size='16px' color={iconFill} />;
|
||||
graphic = <Icon display="inline-block" verticalAlign="middle" icon={icon} mr='2' size='18px' color={iconFill} />;
|
||||
} else if (icon === 'inbox') {
|
||||
graphic = <Box display='flex' verticalAlign='middle' position="relative">
|
||||
<Icon display='inline-block' verticalAlign='middle' icon='Inbox' mr='2' size='16px' color={iconFill} />
|
||||
<Icon display='inline-block' verticalAlign='middle' icon='Inbox' mr='2' size='18px' color={iconFill} />
|
||||
{(notifications > 0 || inviteCount.length > 0) && (
|
||||
<Icon display='inline-block' icon='Bullet' style={{ position: 'absolute', top: -5, left: 5 }} color={bulletFill} />
|
||||
)}
|
||||
</Box>;
|
||||
} else if (icon === 'logout') {
|
||||
graphic = <Icon display="inline-block" verticalAlign="middle" icon='SignOut' mr='2' size='16px' color={iconFill} />;
|
||||
graphic = <Icon display="inline-block" verticalAlign="middle" icon='SignOut' mr='2' size='18px' color={iconFill} />;
|
||||
} else if (icon === 'profile') {
|
||||
graphic = <Sigil color={sigilFill} classes='dib flex-shrink-0 v-mid mr2' ship={window.ship} size={16} icon padded />;
|
||||
text = text.startsWith('Profile') ? window.ship : text;
|
||||
graphic = <Sigil color={color} classes='dib flex-shrink-0 v-mid mr2' ship={text} size={18} icon padded />;
|
||||
} else if (icon === 'home') {
|
||||
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Mail' mr='2' size='16px' color={iconFill} />;
|
||||
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Mail' mr='2' size='18px' color={iconFill} />;
|
||||
} else if (icon === 'notifications') {
|
||||
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Inbox' mr='2' size='16px' color={iconFill} />;
|
||||
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Inbox' mr='2' size='18px' color={iconFill} />;
|
||||
} else {
|
||||
graphic = <Icon display='inline-block' icon='NullIcon' verticalAlign="middle" mr='2' size="16px" color={iconFill} />;
|
||||
}
|
||||
@ -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 (
|
||||
<Row
|
||||
|
@ -48,7 +48,7 @@ export function ChannelMenu(props: ChannelMenuProps) {
|
||||
|
||||
const isOurs = ship.slice(1) === window.ship;
|
||||
|
||||
const isMuted =
|
||||
const isMuted =
|
||||
props.graphNotificationConfig.watching.findIndex(
|
||||
(a) => 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) {
|
||||
</ChannelMenuItem>
|
||||
<ChannelMenuItem bottom icon="Gear" color="black">
|
||||
<Link to={`${baseUrl}/settings`}>
|
||||
<Box fontSize={0} p="2">
|
||||
<Box fontSize={1} p="2">
|
||||
Channel Settings
|
||||
</Box>
|
||||
</Link>
|
||||
@ -117,9 +119,9 @@ export function ChannelMenu(props: ChannelMenuProps) {
|
||||
}
|
||||
alignX="right"
|
||||
alignY="top"
|
||||
width="250px"
|
||||
dropWidth="250px"
|
||||
>
|
||||
<Icon display="block" icon="Menu" color="gray" />
|
||||
<Icon display="block" icon="Menu" color="gray" pr='2' />
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
@ -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 }) => (
|
||||
<Link to={to}>
|
||||
@ -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 (
|
||||
<Link key={g} style={{ minWidth: 0 }} to={`/~landscape${g}`}>
|
||||
<Row px={1} pb={2} alignItems="center">
|
||||
@ -60,7 +57,7 @@ function RecentGroups(props: { recent: string[]; associations: Associations }) {
|
||||
bg={`#${color}`}
|
||||
mr={2}
|
||||
display="block"
|
||||
flexShrink='0'
|
||||
flexShrink={0}
|
||||
/>
|
||||
<Text verticalAlign='top' maxWidth='100%' overflow='hidden' display='inline-block' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}>{assoc?.metadata?.title}</Text>
|
||||
</Row>
|
||||
@ -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 (
|
||||
<Box zIndex="2" position="sticky" top="0px" p={2}>
|
||||
<Box height='48px' backgroundColor="white" zIndex="2" position="sticky" top="0px" py={3} pl='3' borderBottom='1px solid' borderColor='washedGray'>
|
||||
<Col
|
||||
justifyContent="center"
|
||||
bg="white"
|
||||
borderRadius={1}
|
||||
border={1}
|
||||
borderColor="washedGray"
|
||||
>
|
||||
<Row alignItems="center" justifyContent="space-between">
|
||||
<Row justifyContent="space-between">
|
||||
<Dropdown
|
||||
width="231px"
|
||||
width="100%"
|
||||
dropWidth="231px"
|
||||
alignY="top"
|
||||
options={
|
||||
<Col
|
||||
@ -134,9 +129,9 @@ export function GroupSwitcher(props: {
|
||||
<Icon mr="2" color="gray" icon="Plus" />
|
||||
<Text> Join Group</Text>
|
||||
</GroupSwitcherItem>
|
||||
{workspace.type === "group" && (
|
||||
{workspace.type === 'group' && (
|
||||
<>
|
||||
<GroupSwitcherItem to={navTo("/popover/participants")}>
|
||||
<GroupSwitcherItem to={navTo('/popover/participants')}>
|
||||
<Icon
|
||||
mr={2}
|
||||
color="gray"
|
||||
@ -144,7 +139,7 @@ export function GroupSwitcher(props: {
|
||||
/>
|
||||
<Text> Participants</Text>
|
||||
</GroupSwitcherItem>
|
||||
<GroupSwitcherItem to={navTo("/popover/settings")}>
|
||||
<GroupSwitcherItem to={navTo('/popover/settings')}>
|
||||
<Icon
|
||||
mr={2}
|
||||
color="gray"
|
||||
@ -152,7 +147,7 @@ export function GroupSwitcher(props: {
|
||||
/>
|
||||
<Text> Group Settings</Text>
|
||||
</GroupSwitcherItem>
|
||||
{isAdmin && (<GroupSwitcherItem bottom to={navTo("/invites")}>
|
||||
{isAdmin && (<GroupSwitcherItem bottom to={navTo('/invites')}>
|
||||
<Icon
|
||||
mr={2}
|
||||
color="blue"
|
||||
@ -165,25 +160,25 @@ export function GroupSwitcher(props: {
|
||||
</Col>
|
||||
}
|
||||
>
|
||||
<Row p={2} alignItems="center" width='100%' minWidth='0'>
|
||||
<Row alignItems="center" mr={1} flex='1' width='100%' minWidth='0'>
|
||||
<Text overflow='hidden' display='inline-block' flexShrink='1' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre'}}>{title}</Text>
|
||||
<Icon size='12px' ml='1' mt="0px" display="inline-block" icon="ChevronSouth" />
|
||||
<Row width='100%' minWidth='0' flexShrink={0}>
|
||||
<Row justifyContent="space-between" mr={1} flexShrink={0} width='100%' minWidth='0'>
|
||||
<Text lineHeight="1.1" fontSize='2' fontWeight="700" overflow='hidden' display='inline-block' flexShrink='1' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}>{title}</Text>
|
||||
</Row>
|
||||
</Row>
|
||||
</Dropdown>
|
||||
<Row pr={1} justifyContent="flex-end" alignItems="center">
|
||||
{(workspace.type === "group") && (
|
||||
<Row pr='3' verticalAlign="middle">
|
||||
{(workspace.type === 'group') && (
|
||||
<>
|
||||
{isAdmin && (<Link to={navTo("/invites")}>
|
||||
{isAdmin && (<Link to={navTo('/invites')}>
|
||||
<Icon
|
||||
display="block"
|
||||
display="inline-block"
|
||||
color='blue'
|
||||
icon="Users"
|
||||
ml='12px'
|
||||
/>
|
||||
</Link>)}
|
||||
<Link to={navTo("/popover/settings")}>
|
||||
<Icon color='gray' display="block" m={2} icon="Gear" />
|
||||
<Link to={navTo('/popover/settings')}>
|
||||
<Icon color='gray' display="inline-block" ml={'12px'} icon="Gear" />
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
@ -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 <Loading />;
|
||||
}
|
||||
|
||||
title += ` - ${association.metadata.title}`;
|
||||
return (
|
||||
<Skeleton
|
||||
recentGroups={recentGroups}
|
||||
mobileHide
|
||||
selected={appPath}
|
||||
{...props}
|
||||
baseUrl={baseUrl}
|
||||
>
|
||||
<UnjoinedResource
|
||||
graphKeys={props.graphKeys}
|
||||
notebooks={props.notebooks}
|
||||
inbox={props.inbox}
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>{props.notificationsCount ? `(${String(props.notificationsCount)}) ` : ''}{ title }</title>
|
||||
</Helmet>
|
||||
<Skeleton
|
||||
recentGroups={recentGroups}
|
||||
mobileHide
|
||||
selected={appPath}
|
||||
{...props}
|
||||
baseUrl={baseUrl}
|
||||
api={api}
|
||||
association={association}
|
||||
/>
|
||||
{popovers(routeProps, resourceUrl)}
|
||||
</Skeleton>
|
||||
>
|
||||
<UnjoinedResource
|
||||
graphKeys={props.graphKeys}
|
||||
notebooks={props.notebooks}
|
||||
inbox={props.inbox}
|
||||
baseUrl={baseUrl}
|
||||
api={api}
|
||||
association={association}
|
||||
/>
|
||||
{popovers(routeProps, resourceUrl)}
|
||||
</Skeleton>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@ -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 (
|
||||
<Skeleton recentGroups={recentGroups} {...props} baseUrl={baseUrl}>
|
||||
<Col
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
display={["none", "flex"]}
|
||||
p='4'
|
||||
>
|
||||
<Box p="4"><Text fontSize="0" color='gray'>
|
||||
{description}
|
||||
</Text></Box>
|
||||
</Col>
|
||||
{popovers(routeProps, baseUrl)}
|
||||
</Skeleton>
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>{props.notificationsCount ? `(${String(props.notificationsCount)}) ` : ''}{ title }</title>
|
||||
</Helmet>
|
||||
<Skeleton recentGroups={recentGroups} {...props} baseUrl={baseUrl}>
|
||||
<Col
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
display={["none", "flex"]}
|
||||
p='4'
|
||||
>
|
||||
<Box p="4"><Text fontSize="0" color='gray'>
|
||||
{description}
|
||||
</Text></Box>
|
||||
</Col>
|
||||
{popovers(routeProps, baseUrl)}
|
||||
</Skeleton>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user