mirror of
https://github.com/ilyakooo0/urbit.git
synced 2025-01-05 22:03:50 +03:00
Merge branch 'master' into release/next-vere
This commit is contained in:
commit
55ff6eeede
4
.github/actions/glob/Dockerfile
vendored
Normal file
4
.github/actions/glob/Dockerfile
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
FROM jaredtobin/janeway:v0.13.3
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
EXPOSE 22/tcp
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
25
.github/actions/glob/action.yml
vendored
Normal file
25
.github/actions/glob/action.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: 'glob'
|
||||
description: 'Create a glob and deploy it to a moon'
|
||||
inputs:
|
||||
ship:
|
||||
description: "Ship to deploy to"
|
||||
required: true
|
||||
credentials:
|
||||
description: "base64-encoded GCP Service Account credentials"
|
||||
required: true
|
||||
ssh-sec-key:
|
||||
description: "A base64-encoded SSH secret key for the container to use"
|
||||
required: true
|
||||
ssh-pub-key:
|
||||
description: "The corresponding base64-encoded SSH public key"
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'Dockerfile'
|
||||
args:
|
||||
- ${{ inputs.ship }}
|
||||
- ${{ inputs.credentials }}
|
||||
- ${{ inputs.ssh-sec-key }}
|
||||
- ${{ inputs.ssh-pub-key }}
|
||||
|
32
.github/actions/glob/entrypoint.sh
vendored
Executable file
32
.github/actions/glob/entrypoint.sh
vendored
Executable file
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cd "$GITHUB_WORKSPACE" || exit
|
||||
|
||||
echo "$2" | base64 -d > service-account
|
||||
echo "$3" | base64 -d > id_ssh
|
||||
echo "$4" | base64 -d > id_ssh.pub
|
||||
|
||||
chmod 600 service-account
|
||||
chmod 600 id_ssh
|
||||
chmod 600 id_ssh.pub
|
||||
|
||||
janeway release glob --dev --no-pill \
|
||||
--credentials service-account \
|
||||
--ssh-key id_ssh \
|
||||
--do-it-live \
|
||||
| bash
|
||||
|
||||
SHORTHASH=$(git rev-parse --short HEAD)
|
||||
|
||||
janeway release prepare-ota arvo-glob-"$SHORTHASH" "$1" \
|
||||
--credentials service-account \
|
||||
--ssh-key id_ssh \
|
||||
--do-it-live \
|
||||
| bash
|
||||
|
||||
janeway release perform-ota "$1" \
|
||||
--credentials service-account \
|
||||
--ssh-key id_ssh \
|
||||
--do-it-live \
|
||||
| bash
|
||||
|
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
|
||||
|
20
.github/workflows/glob.yml
vendored
Normal file
20
.github/workflows/glob.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
name: glob
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'release/next-js'
|
||||
jobs:
|
||||
glob:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Create and deploy a glob to ~lomlyx-lopsem-nidsut-tomdun"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
lfs: true
|
||||
- uses: ./.github/actions/glob
|
||||
with:
|
||||
ship: 'lomlyx-lopsem-nidsut-tomdun'
|
||||
credentials: ${{ secrets.JANEWAY_SERVICE_KEY }}
|
||||
ssh-sec-key: ${{ secrets.JANEWAY_SSH_SEC_KEY }}
|
||||
ssh-pub-key: ${{ secrets.JANEWAY_SSH_PUB_KEY }}
|
||||
|
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:9e169f1822997ece960527045b17790caf1490a79fd7bec86b0fb4c1eaf2cfc7
|
||||
size 8319616
|
||||
oid sha256:271d575a87373f4ed73b195780973ed41cb72be21b428a645c42a49ab5f786ee
|
||||
size 8873583
|
||||
|
@ -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" ];
|
||||
};
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -5,7 +5,7 @@
|
||||
/- glob
|
||||
/+ default-agent, verb, dbug
|
||||
|%
|
||||
++ hash 0v5.bo337.25di9.mg5d5.i9vun.5qaqe
|
||||
++ hash 0v7.ttn7o.50403.rf6oh.63hnc.hgpc9
|
||||
+$ 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
|
||||
@ -593,7 +636,9 @@
|
||||
?< (~(has by archive) resource)
|
||||
?> (~(has by graphs) resource)
|
||||
=/ updates=(list [=time upd=logged-update:store])
|
||||
(tap:orm-log update-log)
|
||||
:: updates are time-ordered with most recent first
|
||||
:: process with earliest first
|
||||
(bap:orm-log update-log)
|
||||
=| cards=(list card)
|
||||
|- ^- (quip card _state)
|
||||
?~ updates
|
||||
@ -603,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)
|
||||
@ -866,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)
|
||||
|
@ -81,15 +81,6 @@
|
||||
==
|
||||
--
|
||||
::
|
||||
++ resource-for-update
|
||||
|= =vase
|
||||
^- (unit resource)
|
||||
=/ =update:store
|
||||
!<(update:store vase)
|
||||
?: ?=(%initial -.update)
|
||||
~
|
||||
`resource.update
|
||||
::
|
||||
++ take-update
|
||||
|= =vase
|
||||
^- [(list card) agent]
|
||||
|
@ -1,7 +1,7 @@
|
||||
:: hark-graph-hook: notifications for graph-store [landscape]
|
||||
::
|
||||
/- store=hark-store, post, group-store, metadata-store, hook=hark-graph-hook
|
||||
/+ resource, metadata, default-agent, dbug, graph-store, graph, grouplib=group
|
||||
/- post, group-store, metadata-store, hook=hark-graph-hook, store=hark-store
|
||||
/+ resource, metadata, default-agent, dbug, graph-store, graph, grouplib=group, store=hark-store
|
||||
::
|
||||
::
|
||||
~% %hark-graph-hook-top ..part ~
|
||||
@ -9,41 +9,43 @@
|
||||
+$ 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=_&
|
||||
==
|
||||
::
|
||||
+$ notif-kind
|
||||
[name=@t parent-lent=@ud mode=?(%each %count) watch=?]
|
||||
[name=@t parent-lent=@ud mode=?(%each %count %none) watch=?]
|
||||
::
|
||||
++ scry
|
||||
|* [[our=@p now=@da] =mold p=path]
|
||||
?> ?=(^ p)
|
||||
?> ?=(^ t.p)
|
||||
.^(mold i.p (scot %p our) i.t.p (scot %da now) t.t.p)
|
||||
::
|
||||
++ scry-conversion
|
||||
|= [[our=@p now=@da] desk=term =mark]
|
||||
~+
|
||||
%^ scry [our now]
|
||||
tube:clay
|
||||
/cc/[desk]/[mark]/notification-kind
|
||||
::
|
||||
--
|
||||
::
|
||||
=| state-0
|
||||
=| state-1
|
||||
=* state -
|
||||
::
|
||||
=>
|
||||
|_ =bowl:gall
|
||||
::
|
||||
++ scry
|
||||
|* [=mold p=path]
|
||||
?> ?=(^ p)
|
||||
?> ?=(^ t.p)
|
||||
.^(mold i.p (scot %p our.bowl) i.t.p (scot %da now.bowl) t.t.p)
|
||||
::
|
||||
++ give
|
||||
|= [paths=(list path) =update:hook]
|
||||
^- (list card)
|
||||
[%give %fact paths hark-graph-hook-update+!>(update)]~
|
||||
::
|
||||
++ watch-graph
|
||||
^- card
|
||||
[%pass /graph %agent [our.bowl %graph-store] %watch /updates]
|
||||
--
|
||||
=<
|
||||
%- agent:dbug
|
||||
^- agent:gall
|
||||
~% %hark-graph-hook-agent ..card ~
|
||||
@ -61,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
|
||||
@ -107,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)
|
||||
@ -162,160 +190,96 @@
|
||||
(graph-update !<(update:graph-store q.cage.sign))
|
||||
[cards this]
|
||||
==
|
||||
++ add-graph
|
||||
|= [rid=resource =graph:graph-store]
|
||||
^- (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
|
||||
:- (give:ha ~[/updates] %listen [rid ~])
|
||||
state(watching (~(put in watching) [rid ~]))
|
||||
::
|
||||
++ graph-update
|
||||
|= =update:graph-store
|
||||
^- (quip card _state)
|
||||
?: ?=(%add-graph -.q.update)
|
||||
(add-graph resource.q.update graph.q.update)
|
||||
?. ?=(%add-nodes -.q.update)
|
||||
[~ state]
|
||||
=* rid resource.q.update
|
||||
(check-nodes ~(tap by nodes.q.update) rid)
|
||||
?+ -.q.update `state
|
||||
%add-graph (add-graph resource.q.update)
|
||||
::
|
||||
?(%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 *(set [resource index:graph-store]))
|
||||
%+ skim ~(tap in watching)
|
||||
|= [r=resource idx=index:graph-store]
|
||||
=(r rid)
|
||||
:_ state(watching (~(dif in watching) unwatched))
|
||||
^- (list card)
|
||||
:- (poke-hark:ha %remove-graph rid)
|
||||
%- zing
|
||||
%+ turn ~(tap in unwatched)
|
||||
|= [r=resource =index:graph-store]
|
||||
(give:ha ~[/updates] %ignore r index)
|
||||
::
|
||||
++ add-graph
|
||||
|= rid=resource
|
||||
^- (quip card _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 ~]))
|
||||
::
|
||||
::
|
||||
++ check-nodes
|
||||
|= $: nodes=(list [p=index:graph-store q=node:graph-store])
|
||||
|= $: nodes=(list node:graph-store)
|
||||
rid=resource
|
||||
==
|
||||
=/ group=resource
|
||||
(need (group-from-app-resource:met %graph rid))
|
||||
=/ =metadata:metadata-store
|
||||
(need (peek-metadata:met %graph group rid))
|
||||
=+ %+ scry:ha
|
||||
,mark=(unit mark)
|
||||
/gx/graph-store/graph-mark/(scot %p entity.rid)/[name.rid]/noun
|
||||
=+ %+ scry:ha
|
||||
,=tube:clay
|
||||
/cc/[q.byk.bowl]/[(fall mark %graph-validator-link)]/notification-kind
|
||||
=| cards=(list card)
|
||||
|^
|
||||
?~ nodes
|
||||
[cards state]
|
||||
=* index p.i.nodes
|
||||
=* node q.i.nodes
|
||||
=^ node-cards state
|
||||
(check-node node tube)
|
||||
%_ $
|
||||
nodes t.nodes
|
||||
cards (weld node-cards cards)
|
||||
==
|
||||
::
|
||||
++ check-node-children
|
||||
|= [=node:graph-store =tube:clay]
|
||||
^- (quip card _state)
|
||||
?: ?=(%empty -.children.node)
|
||||
[~ state]
|
||||
=/ children=(list [=atom =node:graph-store])
|
||||
(tap:orm:graph-store p.children.node)
|
||||
=| cards=(list card)
|
||||
|- ^- (quip card _state)
|
||||
?~ children
|
||||
[cards state]
|
||||
=^ new-cards state
|
||||
(check-node node.i.children tube)
|
||||
%_ $
|
||||
cards (weld cards new-cards)
|
||||
children t.children
|
||||
==
|
||||
::
|
||||
++ check-node
|
||||
|= [=node:graph-store =tube:clay]
|
||||
^- (quip card _state)
|
||||
=^ child-cards state
|
||||
(check-node-children node tube)
|
||||
=+ !< notif-kind=(unit notif-kind)
|
||||
(tube !>([0 post.node]))
|
||||
?~ notif-kind
|
||||
[child-cards state]
|
||||
=/ desc=@t
|
||||
?: (is-mention contents.post.node)
|
||||
%mention
|
||||
name.u.notif-kind
|
||||
=/ parent=index:post
|
||||
(scag parent-lent.u.notif-kind index.post.node)
|
||||
=/ notif-index=index:store
|
||||
[%graph group rid module.metadata desc parent]
|
||||
?: =(our.bowl author.post.node)
|
||||
=^ self-cards state
|
||||
(self-post node notif-index [mode watch]:u.notif-kind)
|
||||
:_ state
|
||||
(weld child-cards self-cards)
|
||||
:_ state
|
||||
%+ weld child-cards
|
||||
%+ weld
|
||||
%^ update-unread-count
|
||||
mode.u.notif-kind notif-index
|
||||
[time-sent index]:post.node
|
||||
?. ?| =(desc %mention)
|
||||
(~(has in watching) [rid parent])
|
||||
==
|
||||
~
|
||||
=/ =contents:store
|
||||
[%graph (limo post.node ~)]
|
||||
~[(add-unread notif-index [time-sent.post.node %.n contents])]
|
||||
::
|
||||
++ is-mention
|
||||
|= contents=(list content:post)
|
||||
^- ?
|
||||
?. mentions %.n
|
||||
?~ contents %.n
|
||||
?. ?=(%mention -.i.contents)
|
||||
$(contents t.contents)
|
||||
?: =(our.bowl ship.i.contents)
|
||||
%.y
|
||||
$(contents t.contents)
|
||||
::
|
||||
++ self-post
|
||||
|= $: =node:graph-store
|
||||
=index:store
|
||||
mode=?(%count %each)
|
||||
watch=?
|
||||
==
|
||||
^- (quip card _state)
|
||||
=| cards=(list card)
|
||||
=? cards ?=(%count mode)
|
||||
:_ cards
|
||||
(poke-hark %read-count index)
|
||||
?. &(watch watch-on-self)
|
||||
[cards state]
|
||||
:- cards
|
||||
state(watching (~(put in watching) [rid index.post.node]))
|
||||
::
|
||||
++ poke-hark
|
||||
|= =action:store
|
||||
^- card
|
||||
=- [%pass / %agent [our.bowl %hark-store] %poke -]
|
||||
hark-action+!>(action)
|
||||
::
|
||||
++ update-unread-count
|
||||
|= [mode=?(%count %each %none) =index:store time=@da ref=index:graph-store]
|
||||
?- mode
|
||||
%count ~[(poke-hark %unread-count index time)]
|
||||
%each ~[(poke-hark %unread-each index ref time)]
|
||||
%none ~
|
||||
==
|
||||
::
|
||||
++ add-unread
|
||||
|= [=index:store =notification:store]
|
||||
(poke-hark %add-note index notification)
|
||||
::
|
||||
--
|
||||
=/ group=(unit resource)
|
||||
(group-from-app-resource:met %graph rid)
|
||||
?~ group
|
||||
~& no-group+rid
|
||||
`state
|
||||
=/ metadata=(unit metadata:metadata-store)
|
||||
(peek-metadata:met %graph u.group rid)
|
||||
?~ metadata `state
|
||||
abet:check:(abed:handle-update:ha rid nodes u.group module.u.metadata)
|
||||
--
|
||||
::
|
||||
++ on-peek on-peek:def
|
||||
@ -334,4 +298,176 @@
|
||||
==
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
|
||||
::
|
||||
|_ =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]
|
||||
^- (list card)
|
||||
[%give %fact paths hark-graph-hook-update+!>(update)]~
|
||||
::
|
||||
++ watch-graph
|
||||
^- card
|
||||
[%pass /graph %agent [our.bowl %graph-store] %watch /updates]
|
||||
::
|
||||
++ poke-hark
|
||||
|= =action:store
|
||||
^- card
|
||||
=- [%pass / %agent [our.bowl %hark-store] %poke -]
|
||||
hark-action+!>(action)
|
||||
::
|
||||
++ is-mention
|
||||
|= contents=(list content:post)
|
||||
^- ?
|
||||
?. mentions %.n
|
||||
?~ contents %.n
|
||||
?. ?=(%mention -.i.contents)
|
||||
$(contents t.contents)
|
||||
?: =(our.bowl ship.i.contents)
|
||||
%.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)
|
||||
group=resource
|
||||
module=term
|
||||
hark-pokes=(list action:store) :: output
|
||||
new-watches=(list index:graph-store)
|
||||
==
|
||||
++ update-core .
|
||||
::
|
||||
++ abed
|
||||
|= [r=resource upds=(list node:graph-store) grp=resource mod=term]
|
||||
update-core(rid r, updates upds, group grp, module mod)
|
||||
::
|
||||
++ get-conversion
|
||||
(^get-conversion rid)
|
||||
::
|
||||
++ abet
|
||||
^- (quip card _state)
|
||||
:_ state(watching (~(uni in watching) (silt (turn new-watches (lead rid)))))
|
||||
^- (list card)
|
||||
%+ welp (turn (flop hark-pokes) poke-hark)
|
||||
%- zing
|
||||
%+ turn (flop new-watches)
|
||||
|=(=index:graph-store (give ~[/updates] [%listen rid index]))
|
||||
::
|
||||
++ hark
|
||||
|= =action:store
|
||||
^+ update-core
|
||||
update-core(hark-pokes [action hark-pokes])
|
||||
::
|
||||
++ new-watch
|
||||
|= =index:graph-store
|
||||
update-core(new-watches [index new-watches])
|
||||
::
|
||||
++ check
|
||||
|- ^+ update-core
|
||||
?~ updates
|
||||
update-core
|
||||
=/ core=_update-core
|
||||
(check-node i.updates)
|
||||
=. updates.core t.updates
|
||||
$(update-core core)
|
||||
::
|
||||
++ check-node-children
|
||||
|= =node:graph-store
|
||||
^+ update-core
|
||||
?: ?=(%empty -.children.node)
|
||||
update-core
|
||||
=/ children=(list [=atom =node:graph-store])
|
||||
(tap:orm:graph-store p.children.node)
|
||||
|- ^+ update-core
|
||||
?~ children
|
||||
update-core
|
||||
=. update-core (check-node node.i.children)
|
||||
$(children t.children)
|
||||
::
|
||||
++ check-node
|
||||
|= =node:graph-store
|
||||
^+ update-core
|
||||
=. update-core (check-node-children node)
|
||||
=+ !< notif-kind=(unit notif-kind)
|
||||
(get-conversion !>([0 post.node]))
|
||||
?~ notif-kind
|
||||
update-core
|
||||
=/ desc=@t
|
||||
?: (is-mention contents.post.node)
|
||||
%mention
|
||||
name.u.notif-kind
|
||||
=* not-kind u.notif-kind
|
||||
=/ parent=index:post
|
||||
(scag parent-lent.not-kind index.post.node)
|
||||
=/ notif-index=index:store
|
||||
[%graph group rid module desc parent]
|
||||
?: =(our.bowl author.post.node)
|
||||
(self-post node notif-index [mode watch]:not-kind)
|
||||
=. update-core
|
||||
(update-unread-count not-kind notif-index [time-sent index]:post.node)
|
||||
=? update-core
|
||||
?| =(desc %mention)
|
||||
(~(has in watching) [rid parent])
|
||||
==
|
||||
=/ =contents:store
|
||||
[%graph (limo post.node ~)]
|
||||
(add-unread notif-index [time-sent.post.node %.n contents])
|
||||
update-core
|
||||
::
|
||||
++ update-unread-count
|
||||
|= [=notif-kind =index:store time=@da ref=index:graph-store]
|
||||
=/ =stats-index:store
|
||||
(to-stats-index:store index)
|
||||
?- mode.notif-kind
|
||||
%count (hark %unread-count stats-index time)
|
||||
%each (hark %unread-each stats-index ref time)
|
||||
%none update-core
|
||||
==
|
||||
::
|
||||
++ self-post
|
||||
|= $: =node:graph-store
|
||||
=index:store
|
||||
mode=?(%count %each %none)
|
||||
watch=?
|
||||
==
|
||||
^+ update-core
|
||||
?: ?=(%none mode) update-core
|
||||
=/ =stats-index:store
|
||||
(to-stats-index:store index)
|
||||
=. update-core
|
||||
(hark %seen-index time-sent.post.node stats-index)
|
||||
=? update-core ?=(%count mode)
|
||||
(hark %read-count stats-index)
|
||||
=? update-core &(watch watch-on-self)
|
||||
(new-watch index.post.node)
|
||||
update-core
|
||||
::
|
||||
++ add-unread
|
||||
|= [=index:store =notification:store]
|
||||
(hark %add-note index notification)
|
||||
::
|
||||
--
|
||||
--
|
||||
|
@ -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
|
||||
|
@ -10,7 +10,7 @@
|
||||
:: Usefull for non-linear, low-volume applications, i.e. blogs,
|
||||
:: collections
|
||||
::
|
||||
/- post, group-store, metadata-store
|
||||
/- post, group-store, metadata-store, store=hark-store
|
||||
/+ resource, metadata, default-agent, dbug, graph-store, graphl=graph, verb, store=hark-store
|
||||
::
|
||||
::
|
||||
@ -19,29 +19,37 @@
|
||||
+$ card card:agent:gall
|
||||
+$ versioned-state
|
||||
$% state:state-zero:store
|
||||
state-1
|
||||
state:state-one:store
|
||||
state-2
|
||||
state-3
|
||||
==
|
||||
+$ unread-stats
|
||||
[indices=(set index:graph-store) last=@da]
|
||||
::
|
||||
+$ state-1
|
||||
$: %1
|
||||
unreads-each=(jug index:store index:graph-store)
|
||||
unreads-count=(map index:store @ud)
|
||||
last-seen=(map index:store @da)
|
||||
+$ 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
|
||||
archive=notifications:store
|
||||
current-timebox=@da
|
||||
dnd=_|
|
||||
==
|
||||
::
|
||||
+$ state-2
|
||||
[%2 base-state]
|
||||
::
|
||||
+$ state-3
|
||||
[%3 base-state]
|
||||
::
|
||||
+$ inflated-state
|
||||
$: state-1
|
||||
$: state-3
|
||||
cache
|
||||
==
|
||||
:: $cache: useful to have precalculated, but can be derived from state
|
||||
:: albeit expensively
|
||||
+$ cache
|
||||
$: by-index=(jug index:store @da)
|
||||
$: by-index=(jug stats-index:store [time=@da =index:store])
|
||||
~
|
||||
==
|
||||
::
|
||||
@ -76,15 +84,40 @@
|
||||
=| cards=(list card)
|
||||
|^
|
||||
?- -.old
|
||||
%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
|
||||
[cards this(+.state (inflate-cache:ha old), -.state old)]
|
||||
%_ $
|
||||
::
|
||||
old
|
||||
%* . *state-2
|
||||
unreads-each ((convert-unread ,(set index:graph-store)) uni-by unreads-each.old)
|
||||
unreads-count ((convert-unread ,@ud) add unreads-count.old)
|
||||
last-seen ((convert-unread ,@da) max last-seen.old)
|
||||
notifications notifications.old
|
||||
archive archive.old
|
||||
current-timebox current-timebox.old
|
||||
dnd dnd.old
|
||||
==
|
||||
==
|
||||
::
|
||||
%0
|
||||
|
||||
%_ $
|
||||
::
|
||||
old
|
||||
%* . *state-1
|
||||
%* . *state:state-one:store
|
||||
notifications (convert-notifications-1 notifications.old)
|
||||
archive (convert-notifications-1 archive.old)
|
||||
current-timebox current-timebox.old
|
||||
@ -92,6 +125,28 @@
|
||||
==
|
||||
==
|
||||
==
|
||||
:: discard publish edits
|
||||
++ uni-by
|
||||
|= [a=(set index:graph-store) b=(set index:graph-store)]
|
||||
=/ merged
|
||||
(~(uni in a) b)
|
||||
%- ~(gas in *(set index:graph-store))
|
||||
%+ skip ~(tap in merged)
|
||||
|=(=index:graph-store &(=((lent index) 3) !=(-:(flop index) 1)))
|
||||
::
|
||||
++ convert-unread
|
||||
|* value=mold
|
||||
|= [combine=$-([value value] value) unreads=(map index:store value)]
|
||||
^- (map stats-index:store value)
|
||||
%+ roll
|
||||
~(tap in unreads)
|
||||
|= [[=index:store val=value] out=(map stats-index:store value)]
|
||||
=/ old=value
|
||||
(~(gut by unreads) index (combine))
|
||||
=/ =stats-index:store
|
||||
(to-stats-index:store index)
|
||||
(~(put by out) stats-index (combine old val))
|
||||
::
|
||||
++ convert-notifications-1
|
||||
|= old=notifications:state-zero:store
|
||||
%+ gas:orm *notifications:store
|
||||
@ -159,48 +214,36 @@
|
||||
^- update:store
|
||||
:- %more
|
||||
^- (list update:store)
|
||||
:+ give-unreads
|
||||
[%set-dnd dnd]
|
||||
%+ weld
|
||||
%+ turn
|
||||
(tap-nonempty:ha archive)
|
||||
(timebox-update &)
|
||||
%+ turn
|
||||
(tap-nonempty:ha notifications)
|
||||
(timebox-update |)
|
||||
:- give-unreads
|
||||
[%set-dnd dnd]~
|
||||
::
|
||||
++ give-since-unreads
|
||||
^- (list [index:store index-stats:store])
|
||||
^- (list [stats-index:store stats:store])
|
||||
%+ turn
|
||||
~(tap by unreads-count)
|
||||
|= [=index:store count=@ud]
|
||||
?> ?=(%graph -.index)
|
||||
:* index
|
||||
~(wyt in (~(gut by by-index) index ~))
|
||||
|= [=stats-index:store count=@ud]
|
||||
?> ?=(%graph -.stats-index)
|
||||
:* stats-index
|
||||
~(wyt in (~(gut by by-index) stats-index ~))
|
||||
[%count count]
|
||||
(~(gut by last-seen) index *time)
|
||||
(~(gut by last-seen) stats-index *time)
|
||||
==
|
||||
::
|
||||
++ give-each-unreads
|
||||
^- (list [index:store index-stats:store])
|
||||
^- (list [stats-index:store stats:store])
|
||||
%+ turn
|
||||
~(tap by unreads-each)
|
||||
|= [=index:store indices=(set index:graph-store)]
|
||||
:* index
|
||||
~(wyt in (~(gut by by-index) index ~))
|
||||
|= [=stats-index:store indices=(set index:graph-store)]
|
||||
:* stats-index
|
||||
~(wyt in (~(gut by by-index) stats-index ~))
|
||||
[%each indices]
|
||||
(~(gut by last-seen) index *time)
|
||||
(~(gut by last-seen) stats-index *time)
|
||||
==
|
||||
::
|
||||
++ give-unreads
|
||||
^- update:store
|
||||
:- %unreads
|
||||
(weld give-each-unreads give-since-unreads)
|
||||
::
|
||||
++ timebox-update
|
||||
|= archived=?
|
||||
|= [time=@da =timebox:store]
|
||||
^- update:store
|
||||
[%timebox time archived ~(tap by timebox)]
|
||||
--
|
||||
::
|
||||
++ on-peek
|
||||
@ -238,209 +281,45 @@
|
||||
=^ 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)
|
||||
|^
|
||||
?- -.action
|
||||
%add-note (add-note +.action)
|
||||
%archive (do-archive +.action)
|
||||
::
|
||||
%read-each (read-each +.action)
|
||||
%unread-each (unread-each +.action)
|
||||
::
|
||||
%read-count (read-count +.action)
|
||||
%unread-count (unread-count +.action)
|
||||
::
|
||||
%read-note (read-note +.action)
|
||||
%unread-note (unread-note +.action)
|
||||
::
|
||||
%read-all read-all
|
||||
::
|
||||
%set-dnd (set-dnd +.action)
|
||||
%seen seen
|
||||
==
|
||||
::
|
||||
++ add-note
|
||||
|= [=index:store =notification:store]
|
||||
^- (quip card _state)
|
||||
=/ =timebox:store
|
||||
(gut-orm:ha notifications current-timebox)
|
||||
=/ existing-notif
|
||||
(~(get by timebox) index)
|
||||
=/ new=notification:store
|
||||
?~ existing-notif
|
||||
notification
|
||||
(merge-notification:ha u.existing-notif notification)
|
||||
=/ new-read=?
|
||||
?~ existing-notif
|
||||
%.y
|
||||
read.u.existing-notif
|
||||
=. read.new %.n
|
||||
=/ new-timebox=timebox:store
|
||||
(~(put by timebox) index new)
|
||||
:- (give:ha [/updates]~ %added current-timebox index new)
|
||||
%_ state
|
||||
+ ?.(new-read +.state (upd-unreads:ha index current-timebox %.n))
|
||||
notifications (put:orm notifications current-timebox new-timebox)
|
||||
==
|
||||
::
|
||||
++ do-archive
|
||||
|= [time=@da =index:store]
|
||||
^- (quip card _state)
|
||||
=/ =timebox:store
|
||||
(gut-orm:ha notifications time)
|
||||
=/ =notification:store
|
||||
(~(got by timebox) index)
|
||||
=/ new-timebox=timebox:store
|
||||
(~(del by timebox) index)
|
||||
:- (give:ha [/updates]~ %archive time index)
|
||||
%_ state
|
||||
+ ?.(read.notification (upd-unreads:ha index time %.y) +.state)
|
||||
::
|
||||
notifications
|
||||
(put:orm notifications time new-timebox)
|
||||
::
|
||||
archive
|
||||
%^ jub-orm:ha archive time
|
||||
|= archive-box=timebox:store
|
||||
^- timebox:store
|
||||
(~(put by archive-box) index notification(read %.y))
|
||||
==
|
||||
::
|
||||
++ unread-each
|
||||
|= [=index:store unread=index:graph-store time=@da]
|
||||
:- (give:ha ~[/updates] %unread-each index unread time)
|
||||
%_ state
|
||||
unreads-each
|
||||
%+ jub index
|
||||
|= indices=(set index:graph-store)
|
||||
(~(put in indices) unread)
|
||||
::
|
||||
last-seen
|
||||
(~(put by last-seen) index time)
|
||||
==
|
||||
::
|
||||
++ jub
|
||||
|= [=index:store f=$-((set index:graph-store) (set index:graph-store))]
|
||||
^- (jug index:store index:graph-store)
|
||||
=/ val=(set index:graph-store)
|
||||
(~(gut by unreads-each) index ~)
|
||||
(~(put by unreads-each) index (f val))
|
||||
::
|
||||
++ read-each
|
||||
|= [=index:store ref=index:graph-store]
|
||||
=/ to-dismiss=(list @da)
|
||||
%+ skim
|
||||
~(tap in (~(get ju by-index) index))
|
||||
|= time=@da
|
||||
=/ =timebox:store
|
||||
(gut-orm notifications time)
|
||||
=/ not=(unit notification:store)
|
||||
(~(get by timebox) index)
|
||||
?~ not %.n
|
||||
?> ?=(%graph -.contents.u.not)
|
||||
(lien list.contents.u.not |=(p=post:post =(index.p ref)))
|
||||
=| cards=(list card)
|
||||
|-
|
||||
?^ to-dismiss
|
||||
=^ crds state
|
||||
(read-note i.to-dismiss index)
|
||||
$(cards (weld cards crds), to-dismiss t.to-dismiss)
|
||||
:- (weld cards (give:ha ~[/updates] %read-each index ref))
|
||||
%_ state
|
||||
::
|
||||
unreads-each
|
||||
%+ jub index
|
||||
|= indices=(set index:graph-store)
|
||||
(~(del in indices) ref)
|
||||
==
|
||||
::
|
||||
++ read-note
|
||||
|= [time=@da =index:store]
|
||||
^- (quip card _state)
|
||||
:- (give:ha [/updates]~ %read-note time index)
|
||||
%_ state
|
||||
+ (upd-unreads:ha index time %.y)
|
||||
notifications (change-read-status:ha time index %.y)
|
||||
==
|
||||
::
|
||||
++ unread-note
|
||||
|= [time=@da =index:store]
|
||||
^- (quip card _state)
|
||||
:- (give:ha [/updates]~ %unread-note time index)
|
||||
%_ state
|
||||
+ (upd-unreads:ha index time %.n)
|
||||
notifications (change-read-status:ha time index %.n)
|
||||
==
|
||||
::
|
||||
++ read-count
|
||||
|= =index:store
|
||||
^- (quip card _state)
|
||||
=^ cards state
|
||||
(read-index index)
|
||||
:- %+ weld cards
|
||||
(give:ha [/updates]~ %read-count index)
|
||||
%_ state
|
||||
unreads-count (~(put by unreads-count) index 0)
|
||||
==
|
||||
::
|
||||
++ read-boxes
|
||||
|= [boxes=(set @da) =index:store]
|
||||
^- (quip card _state)
|
||||
=/ boxes=(list @da)
|
||||
~(tap in boxes)
|
||||
=| crds=(list card)
|
||||
|-
|
||||
?~ boxes [crds state]
|
||||
=* box i.boxes
|
||||
=^ cards state
|
||||
(read-note box index)
|
||||
$(boxes t.boxes, crds (welp crds cards))
|
||||
::
|
||||
++ read-index
|
||||
|= =index:store
|
||||
^- (quip card _state)
|
||||
=/ boxes=(set @da)
|
||||
(~(get ju by-index) index)
|
||||
=^ cards state
|
||||
(read-boxes boxes index)
|
||||
:_ state
|
||||
%+ welp cards
|
||||
(give:ha ~[/updates] %read-index index)
|
||||
::
|
||||
++ read-all
|
||||
^- (quip card _state)
|
||||
`state
|
||||
::
|
||||
++ unread-count
|
||||
|= [=index:store time=@da]
|
||||
^- (quip card _state)
|
||||
:- (give:ha [/updates]~ %unread-count index time)
|
||||
=/ curr=@ud
|
||||
(~(gut by unreads-count) index 0)
|
||||
%_ state
|
||||
last-seen (~(put by last-seen) index time)
|
||||
unreads-count (~(put by unreads-count) index +(curr))
|
||||
==
|
||||
::
|
||||
++ seen
|
||||
^- (quip card _state)
|
||||
:_ state(current-timebox now.bowl)
|
||||
:~ cancel-autoseen:ha
|
||||
autoseen-timer:ha
|
||||
==
|
||||
::
|
||||
++ set-dnd
|
||||
|= d=?
|
||||
^- (quip card _state)
|
||||
:_ state(dnd d)
|
||||
(give:ha [/updates]~ %set-dnd d)
|
||||
--
|
||||
abet:translate:(abed:poke-engine:ha action)
|
||||
--
|
||||
::
|
||||
++ on-agent on-agent:def
|
||||
@ -459,35 +338,289 @@
|
||||
--
|
||||
|_ =bowl:gall
|
||||
+* met ~(. metadata bowl)
|
||||
++ poke-engine
|
||||
|_ [in=action:store out=(list update:store) cards=(list card)]
|
||||
++ poke-core .
|
||||
::
|
||||
++ abed
|
||||
|= =action:store poke-core(in action)
|
||||
::
|
||||
++ abet
|
||||
^- (quip card _state)
|
||||
:_ state
|
||||
%+ snoc (flop cards)
|
||||
[%give %fact ~[/updates] %hark-update !>([%more (flop out)])]
|
||||
::
|
||||
++ give
|
||||
|= =update:store poke-core(out [update out])
|
||||
::
|
||||
++ emit
|
||||
|= =card poke-core(cards [card cards])
|
||||
::
|
||||
++ translate
|
||||
^+ poke-core
|
||||
?+ -.in poke-core
|
||||
::
|
||||
%add-note (add-note +.in)
|
||||
%archive (do-archive +.in)
|
||||
::
|
||||
%unread-count (unread-count +.in)
|
||||
%read-count (read-count +.in)
|
||||
::
|
||||
%read-each (read-each +.in)
|
||||
%unread-each (unread-each +.in)
|
||||
::
|
||||
%read-note (read-note +.in)
|
||||
%unread-note (unread-note +.in)
|
||||
::
|
||||
%seen-index (seen-index +.in)
|
||||
%remove-graph (remove-graph +.in)
|
||||
%set-dnd (set-dnd +.in)
|
||||
%seen seen
|
||||
==
|
||||
::
|
||||
:: +| %note
|
||||
::
|
||||
:: notification tracking
|
||||
++ upd-cache
|
||||
|= [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))
|
||||
::
|
||||
++ add-note
|
||||
|= [=index:store =notification:store]
|
||||
^+ poke-core
|
||||
=/ =timebox:store
|
||||
(gut-orm notifications current-timebox)
|
||||
=/ existing-notif
|
||||
(~(get by timebox) index)
|
||||
=/ new=notification:store
|
||||
(merge-notification existing-notif notification)
|
||||
=/ new-read=?
|
||||
?~ existing-notif %.y
|
||||
read.u.existing-notif
|
||||
=/ new-timebox=timebox:store
|
||||
(~(put by timebox) index new)
|
||||
=. poke-core (put-notifs current-timebox new-timebox)
|
||||
=? poke-core new-read
|
||||
(upd-cache %.n current-timebox index)
|
||||
(give %added current-timebox index new)
|
||||
::
|
||||
++ do-archive
|
||||
|= [time=@da =index:store]
|
||||
^+ poke-core
|
||||
=/ =timebox:store
|
||||
(gut-orm notifications time)
|
||||
=/ =notification:store
|
||||
(~(got by timebox) index)
|
||||
=/ new-timebox=timebox:store
|
||||
(~(del by timebox) index)
|
||||
=? poke-core !read.notification
|
||||
(upd-cache %.y time index)
|
||||
=. poke-core
|
||||
(put-notifs time new-timebox)
|
||||
=. archive
|
||||
%^ jub-orm archive time
|
||||
|= archive-box=timebox:store
|
||||
(~(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)
|
||||
=/ 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]
|
||||
%. [%read-note time index]
|
||||
give:(change-read-status time index %.y)
|
||||
::
|
||||
++ unread-note
|
||||
|= [time=@da =index:store]
|
||||
%. [%unread-note time index]
|
||||
give:(change-read-status time index %.n)
|
||||
::
|
||||
:: +| %each
|
||||
::
|
||||
:: each unread tracking
|
||||
::
|
||||
++ unread-each
|
||||
|= [=stats-index:store unread=index:graph-store time=@da]
|
||||
=. poke-core (seen-index time stats-index)
|
||||
%+ jub-unreads-each:(give %unread-each stats-index unread time)
|
||||
stats-index
|
||||
|= indices=(set index:graph-store)
|
||||
(~(put ^in indices) unread)
|
||||
::
|
||||
++ read-index-each
|
||||
|= [=stats-index:store ref=index:graph-store]
|
||||
%- read-indices
|
||||
%+ skim
|
||||
~(tap ^in (~(get ju by-index) stats-index))
|
||||
|= [time=@da =index:store]
|
||||
=/ =timebox:store
|
||||
(gut-orm notifications time)
|
||||
=/ 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
|
||||
|= [=stats-index:store ref=index:graph-store]
|
||||
=. poke-core (read-index-each stats-index ref)
|
||||
%+ jub-unreads-each:(give %read-each stats-index ref)
|
||||
stats-index
|
||||
|= indices=(set index:graph-store)
|
||||
(~(del ^in indices) ref)
|
||||
::
|
||||
++ jub-unreads-each
|
||||
|= $: =stats-index:store
|
||||
f=$-((set index:graph-store) (set index:graph-store))
|
||||
==
|
||||
poke-core(unreads-each (jub stats-index f))
|
||||
::
|
||||
++ unread-count
|
||||
|= [=stats-index:store time=@da]
|
||||
=/ new-count
|
||||
+((~(gut by unreads-count) stats-index 0))
|
||||
=. unreads-count
|
||||
(~(put by unreads-count) stats-index new-count)
|
||||
(seen-index:(give %unread-count stats-index time) time stats-index)
|
||||
::
|
||||
++ read-count
|
||||
|= =stats-index:store
|
||||
=. unreads-count (~(put by unreads-count) stats-index 0)
|
||||
=/ times=(list [@da index:store])
|
||||
~(tap ^in (~(get ju by-index) stats-index))
|
||||
(give:(read-indices times) %read-count stats-index)
|
||||
::
|
||||
++ read-indices
|
||||
|= times=(list [time=@da =index:store])
|
||||
|-
|
||||
?~ times poke-core
|
||||
=/ core
|
||||
(read-note i.times)
|
||||
$(poke-core core, times t.times)
|
||||
::
|
||||
++ seen-index
|
||||
|= [time=@da =stats-index:store]
|
||||
=/ new-time=@da
|
||||
(max time (~(gut by last-seen) stats-index 0))
|
||||
=. last-seen
|
||||
(~(put by last-seen) stats-index new-time)
|
||||
(give %seen-index new-time stats-index)
|
||||
::
|
||||
++ remove-graph
|
||||
|= rid=resource
|
||||
|^
|
||||
=/ indices get-stats-indices
|
||||
=. poke-core
|
||||
(give %remove-graph rid)
|
||||
=. poke-core
|
||||
(remove-notifications indices)
|
||||
=. unreads-count
|
||||
((dif-map-by-key ,@ud) unreads-count indices)
|
||||
=. unreads-each
|
||||
%+ (dif-map-by-key ,(set index:graph-store))
|
||||
unreads-each indices
|
||||
=. last-seen
|
||||
((dif-map-by-key ,@da) last-seen indices)
|
||||
=. by-index
|
||||
((dif-map-by-key ,(set [@da =index:store])) by-index indices)
|
||||
poke-core
|
||||
::
|
||||
++ get-stats-indices
|
||||
%- ~(gas ^in *(set stats-index:store))
|
||||
%+ skim
|
||||
;: weld
|
||||
~(tap ^in ~(key by unreads-count))
|
||||
~(tap ^in ~(key by last-seen))
|
||||
~(tap ^in ~(key by unreads-each))
|
||||
~(tap ^in ~(key by by-index))
|
||||
==
|
||||
|= =stats-index:store
|
||||
?. ?=(%graph -.stats-index) %.n
|
||||
=(graph.stats-index rid)
|
||||
::
|
||||
++ dif-map-by-key
|
||||
|* value=mold
|
||||
|= [=(map stats-index:store value) =(set stats-index:store)]
|
||||
=/ to-remove ~(tap ^in set)
|
||||
|-
|
||||
?~ to-remove map
|
||||
=. map
|
||||
(~(del by map) i.to-remove)
|
||||
$(to-remove t.to-remove)
|
||||
::
|
||||
++ remove-notifications
|
||||
|= =(set stats-index:store)
|
||||
^+ poke-core
|
||||
=/ indices
|
||||
~(tap ^in set)
|
||||
|-
|
||||
?~ indices poke-core
|
||||
=/ times=(list [time=@da =index:store])
|
||||
~(tap ^in (~(get ju by-index) i.indices))
|
||||
=. poke-core
|
||||
(read-indices times)
|
||||
$(indices t.indices)
|
||||
--
|
||||
::
|
||||
++ seen
|
||||
=> (emit cancel-autoseen)
|
||||
=> (emit autoseen-timer)
|
||||
poke-core(current-timebox now.bowl)
|
||||
::
|
||||
++ set-dnd
|
||||
|= d=?
|
||||
(give:poke-core(dnd d) %set-dnd d)
|
||||
--
|
||||
::
|
||||
++ merge-notification
|
||||
|= [existing=notification:store new=notification:store]
|
||||
|= [existing=(unit notification:store) new=notification:store]
|
||||
^- notification:store
|
||||
?- -.contents.existing
|
||||
?~ existing new
|
||||
?- -.contents.u.existing
|
||||
::
|
||||
%graph
|
||||
?> ?=(%graph -.contents.new)
|
||||
existing(read %.n, list.contents (weld list.contents.existing list.contents.new))
|
||||
u.existing(read %.n, list.contents (weld list.contents.u.existing list.contents.new))
|
||||
::
|
||||
%group
|
||||
?> ?=(%group -.contents.new)
|
||||
existing(read %.n, list.contents (weld list.contents.existing list.contents.new))
|
||||
u.existing(read %.n, list.contents (weld list.contents.u.existing list.contents.new))
|
||||
==
|
||||
::
|
||||
++ change-read-status
|
||||
|= [time=@da =index:store read=?]
|
||||
^+ notifications
|
||||
%^ jub-orm notifications time
|
||||
|= =timebox:store
|
||||
%+ ~(jab by timebox) index
|
||||
|= =notification:store
|
||||
?> !=(read read.notification)
|
||||
notification(read read)
|
||||
:: +key-orm: +key:by for ordered maps
|
||||
++ key-orm
|
||||
|= =notifications:store
|
||||
^- (list @da)
|
||||
(turn (tap:orm notifications) |=([key=@da =timebox:store] key))
|
||||
(turn (tap:orm notifications) |=([@da *] +<-))
|
||||
:: +jub-orm: combo +jab/+gut for ordered maps
|
||||
:: TODO: move to zuse.hoon
|
||||
++ jub-orm
|
||||
@ -496,6 +629,12 @@
|
||||
=/ =timebox:store
|
||||
(fun (gut-orm notifications time))
|
||||
(put:orm notifications time timebox)
|
||||
++ jub
|
||||
|= [=stats-index:store f=$-((set index:graph-store) (set index:graph-store))]
|
||||
^- (jug stats-index:store index:graph-store)
|
||||
=/ val=(set index:graph-store)
|
||||
(~(gut by unreads-each) stats-index ~)
|
||||
(~(put by unreads-each) stats-index (f val))
|
||||
:: +gut-orm: +gut:by for ordered maps
|
||||
:: TODO: move to zuse.hoon
|
||||
++ gut-orm
|
||||
@ -523,47 +662,32 @@
|
||||
^- (list card)
|
||||
[%give %fact paths [%hark-update !>(update)]]~
|
||||
::
|
||||
++ upd-unreads
|
||||
|= [=index:store time=@da read=?]
|
||||
^+ +.state
|
||||
%_ +.state
|
||||
::
|
||||
by-index
|
||||
%. [index time]
|
||||
?: read
|
||||
~(del ju by-index)
|
||||
~(put ju by-index)
|
||||
==
|
||||
::
|
||||
++ group-for-index
|
||||
|= =index:store
|
||||
^- (unit resource)
|
||||
?. ?=(%graph -.index)
|
||||
~
|
||||
`group.index
|
||||
::
|
||||
++ give-dirtied-unreads
|
||||
|= [=index:store =update:store]
|
||||
^- (list card)
|
||||
=/ group
|
||||
(group-for-index index)
|
||||
?~ group ~
|
||||
(give ~[group+(en-path:resource u.group)] update)
|
||||
::
|
||||
++ tap-nonempty
|
||||
|= =notifications:store
|
||||
^- (list [@da timebox:store])
|
||||
%+ skim (tap:orm notifications)
|
||||
|=([@da =timebox:store] !=(~(wyt by timebox) 0))
|
||||
|
||||
::
|
||||
++ upd-cache
|
||||
|= [read=? time=@da =index:store]
|
||||
^+ +.state
|
||||
%_ +.state
|
||||
::
|
||||
by-index
|
||||
%. [(to-stats-index:store index) time index]
|
||||
?: read
|
||||
~(del ju by-index)
|
||||
~(put ju by-index)
|
||||
==
|
||||
::
|
||||
++ inflate-cache
|
||||
|= state-1
|
||||
|= state-3
|
||||
^+ +.state
|
||||
=/ nots=(list [p=@da =timebox:store])
|
||||
(tap:orm notifications)
|
||||
|- =* outer $
|
||||
?~ nots
|
||||
+.state
|
||||
?~ nots +.state
|
||||
=/ unreads ~(tap by timebox.i.nots)
|
||||
|- =* inner $
|
||||
?~ unreads
|
||||
@ -573,6 +697,6 @@
|
||||
?: read.notification
|
||||
inner(unreads t.unreads)
|
||||
=. +.state
|
||||
(upd-unreads index p.i.nots %.n)
|
||||
(upd-cache %.n p.i.nots index)
|
||||
inner(unreads t.unreads)
|
||||
--
|
||||
|
@ -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.794cd750ed8a20a02046.js"></script>
|
||||
<script src="/~landscape/js/bundle/index.7d4248944fe1255cb74b.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);
|
||||
|
@ -392,17 +392,20 @@
|
||||
++ handle-stop-thread
|
||||
|= [=tid nice=?]
|
||||
^- (quip card ^state)
|
||||
=/ =yarn (~(got by tid.state) tid)
|
||||
?: (has-yarn running.state yarn)
|
||||
=/ yarn=(unit yarn) (~(get by tid.state) tid)
|
||||
?~ yarn
|
||||
~& %stopping-nonexistent-thread
|
||||
[~ state]
|
||||
?: (has-yarn running.state u.yarn)
|
||||
?: nice
|
||||
(thread-done yarn *vase)
|
||||
(thread-fail yarn %cancelled ~)
|
||||
?: (~(has by starting.state) yarn)
|
||||
(thread-done u.yarn *vase)
|
||||
(thread-fail u.yarn %cancelled ~)
|
||||
?: (~(has by starting.state) u.yarn)
|
||||
(thread-fail-not-running tid %stopped-before-started ~)
|
||||
~& [%thread-not-started yarn]
|
||||
~& [%thread-not-started u.yarn]
|
||||
?: nice
|
||||
(thread-done yarn *vase)
|
||||
(thread-fail yarn %cancelled ~)
|
||||
(thread-done u.yarn *vase)
|
||||
(thread-fail u.yarn %cancelled ~)
|
||||
::
|
||||
++ take-input
|
||||
|= [=yarn input=(unit input:strand)]
|
||||
|
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))
|
||||
--
|
@ -17,6 +17,15 @@
|
||||
%+ scry-for update:store
|
||||
/graph/(scot %p entity.res)/[name.res]
|
||||
::
|
||||
++ get-graph-mop
|
||||
|= res=resource
|
||||
^- graph:store
|
||||
=/ =update:store
|
||||
(get-graph res)
|
||||
?> ?=(%0 -.update)
|
||||
?> ?=(%add-graph -.q.update)
|
||||
graph.q.update
|
||||
::
|
||||
++ gut-younger-node-siblings
|
||||
|= [res=resource =index:store]
|
||||
^- (map index:store node:store)
|
||||
@ -40,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
|
||||
|
@ -27,6 +27,17 @@
|
||||
description+so
|
||||
index+(su ;~(pfix fas (more fas dem)))
|
||||
==
|
||||
::
|
||||
++ stats-index
|
||||
%- of
|
||||
:~ graph+graph-stats-index
|
||||
group+dejs-path:resource
|
||||
==
|
||||
++ graph-stats-index
|
||||
%- ot
|
||||
:~ graph+dejs-path:resource
|
||||
index+graph-store-index
|
||||
==
|
||||
:: parse date as @ud
|
||||
:: TODO: move to zuse
|
||||
++ sd
|
||||
@ -41,6 +52,8 @@
|
||||
:~ time+sd
|
||||
index+index
|
||||
==
|
||||
++ graph-store-index
|
||||
(su ;~(pfix fas (more fas dem)))
|
||||
::
|
||||
++ add
|
||||
|= jon=json
|
||||
@ -48,8 +61,8 @@
|
||||
::
|
||||
++ read-graph-index
|
||||
%- ot
|
||||
:~ index+index
|
||||
target+(su ;~(pfix fas (more fas dem)))
|
||||
:~ index+stats-index
|
||||
target+graph-store-index
|
||||
==
|
||||
::
|
||||
++ action
|
||||
@ -61,7 +74,7 @@
|
||||
read-note+notif-ref
|
||||
add-note+add
|
||||
set-dnd+bo
|
||||
read-count+index
|
||||
read-count+stats-index
|
||||
read-each+read-graph-index
|
||||
==
|
||||
--
|
||||
@ -81,25 +94,44 @@
|
||||
%count (numb count.upd)
|
||||
%more (more +.upd)
|
||||
%read-each (read-each +.upd)
|
||||
%read-count (index +.upd)
|
||||
%read-count (stats-index +.upd)
|
||||
%unread-each (unread-each +.upd)
|
||||
%unread-count (unread-count +.upd)
|
||||
%remove-graph s+(enjs-path:resource +.upd)
|
||||
%seen-index (seen-index +.upd)
|
||||
%unreads (unreads +.upd)
|
||||
::
|
||||
?(%archive %read-note %unread-note)
|
||||
(notif-ref +.upd)
|
||||
==
|
||||
::
|
||||
++ stats-index
|
||||
|= s=^stats-index
|
||||
%+ frond -.s
|
||||
|^
|
||||
?- -.s
|
||||
%graph (graph-stats-index +.s)
|
||||
%group s+(enjs-path:resource +.s)
|
||||
==
|
||||
::
|
||||
++ graph-stats-index
|
||||
|= [graph=resource =index:graph-store]
|
||||
%- pairs
|
||||
:~ graph+s+(enjs-path:resource graph)
|
||||
index+(index:enjs:graph-store index)
|
||||
==
|
||||
--
|
||||
::
|
||||
++ unreads
|
||||
|= l=(list [^index ^index-stats])
|
||||
|= l=(list [^stats-index ^stats])
|
||||
^- json
|
||||
:- %a
|
||||
^- (list json)
|
||||
%+ turn l
|
||||
|= [idx=^index stats=^index-stats]
|
||||
|= [idx=^stats-index s=^stats]
|
||||
%- pairs
|
||||
:~ stats+(index-stats stats)
|
||||
index+(index idx)
|
||||
:~ stats+(stats s)
|
||||
index+(stats-index idx)
|
||||
==
|
||||
::
|
||||
++ unread
|
||||
@ -113,13 +145,13 @@
|
||||
(numb num.unreads)
|
||||
==
|
||||
::
|
||||
++ index-stats
|
||||
|= stats=^index-stats
|
||||
++ stats
|
||||
|= s=^stats
|
||||
^- json
|
||||
%- pairs
|
||||
:~ unreads+(unread unreads.stats)
|
||||
notifications+(numb notifications.stats)
|
||||
last+(time last-seen.stats)
|
||||
:~ unreads+(unread unreads.s)
|
||||
notifications+(numb notifications.s)
|
||||
last+(time last-seen.s)
|
||||
==
|
||||
++ added
|
||||
|= [tim=@da idx=^index not=^notification]
|
||||
@ -137,6 +169,13 @@
|
||||
:~ time+s+(scot %ud tim)
|
||||
index+(index idx)
|
||||
==
|
||||
++ seen-index
|
||||
|= [tim=@da idx=^stats-index]
|
||||
^- json
|
||||
%- pairs
|
||||
:~ time+(time tim)
|
||||
index+(stats-index idx)
|
||||
==
|
||||
::
|
||||
++ more
|
||||
|= upds=(list ^update)
|
||||
@ -236,26 +275,45 @@
|
||||
==
|
||||
::
|
||||
++ read-each
|
||||
|= [=^index target=index:graph-store]
|
||||
|= [s=^stats-index target=index:graph-store]
|
||||
%- pairs
|
||||
:~ index+(^index index)
|
||||
:~ index+(stats-index s)
|
||||
target+(index:enjs:graph-store target)
|
||||
==
|
||||
::
|
||||
++ unread-each
|
||||
|= [=^index target=index:graph-store tim=@da]
|
||||
|= [s=^stats-index target=index:graph-store tim=@da]
|
||||
%- pairs
|
||||
:~ index+(^index index)
|
||||
:~ index+(stats-index s)
|
||||
target+(index:enjs:graph-store target)
|
||||
last+(time tim)
|
||||
==
|
||||
::
|
||||
++ unread-count
|
||||
|= [=^index tim=@da]
|
||||
|= [s=^stats-index tim=@da]
|
||||
%- pairs
|
||||
:~ index+(^index index)
|
||||
:~ index+(stats-index s)
|
||||
last+(time tim)
|
||||
==
|
||||
--
|
||||
--
|
||||
::
|
||||
++ to-stats-index
|
||||
|= =index
|
||||
^- stats-index
|
||||
?- -.index
|
||||
%graph [%graph graph.index index.index]
|
||||
%group [%group group.index]
|
||||
==
|
||||
++ stats-index-is-index
|
||||
|= [=stats-index =index]
|
||||
?- -.index
|
||||
%graph
|
||||
?. ?=(%graph -.stats-index) %.n
|
||||
=([graph index]:index [graph index]:stats-index)
|
||||
::
|
||||
%group
|
||||
?. ?=(%group -.stats-index) %.n
|
||||
=(group:index group:stats-index)
|
||||
==
|
||||
--
|
||||
|
@ -564,7 +564,12 @@
|
||||
::
|
||||
++ se-klin :: disconnect app
|
||||
|= gyl=gill:gall
|
||||
+>(eel (~(del in eel) gyl))
|
||||
=/ gil=(unit gill:gall) se-agon
|
||||
=. eel (~(del in eel) gyl)
|
||||
?~ gil +>.$
|
||||
?: =(gyl u.gil)
|
||||
+>.$(inx 0)
|
||||
(se-alas u.gil)
|
||||
::
|
||||
++ se-link :: connect to app
|
||||
|= gyl=gill:gall
|
||||
@ -788,6 +793,7 @@
|
||||
?- fec
|
||||
[%bel *] ta-bel
|
||||
[%blk *] +>
|
||||
[%bye *] +>(..ta (se-klin gyl))
|
||||
[%clr *] +>(..ta (se-blit fec))
|
||||
[%det *] (ta-got +.fec)
|
||||
[%err *] (ta-err p.fec)
|
||||
|
@ -285,7 +285,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
|
||||
@ -424,15 +424,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
|
||||
@ -368,7 +367,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)
|
||||
@ -385,7 +384,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)
|
||||
@ -394,5 +393,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))]
|
||||
--
|
||||
::
|
||||
|
@ -7,7 +7,7 @@
|
||||
?+ index.p.i ~
|
||||
[@ ~] `[%link 0 %each %.y]
|
||||
[@ @ %1 ~] `[%comment 1 %count %.n]
|
||||
[@ @ @ ~] `[%edit-comment 1 %count %.n]
|
||||
[@ @ @ ~] `[%edit-comment 1 %none %.n]
|
||||
==
|
||||
--
|
||||
++ grab
|
||||
|
@ -9,9 +9,9 @@
|
||||
++ notification-kind
|
||||
?+ index.p.i ~
|
||||
[@ %1 %1 ~] `[%note 0 %each %.n]
|
||||
[@ %1 @ ~] `[%edit-note 0 %each %.n]
|
||||
[@ %1 @ ~] `[%edit-note 0 %none %.n]
|
||||
[@ %2 @ %1 ~] `[%comment 1 %count %.n]
|
||||
[@ %2 @ @ ~] `[%edit-comment 1 %count %.n]
|
||||
[@ %2 @ @ ~] `[%edit-comment 1 %none %.n]
|
||||
==
|
||||
--
|
||||
++ grab
|
||||
|
@ -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
|
||||
--
|
||||
--
|
@ -75,7 +75,7 @@
|
||||
info+(tape ~(ram re tank))
|
||||
==
|
||||
::
|
||||
?(%bel %clr %nex)
|
||||
?(%bel %clr %nex %bye)
|
||||
(frond %act %s -.sef)
|
||||
==
|
||||
--
|
||||
|
@ -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]
|
||||
|
@ -35,6 +35,20 @@
|
||||
[date=@da read=? =contents]
|
||||
--
|
||||
::
|
||||
++ state-one
|
||||
|%
|
||||
+$ state
|
||||
$: %1
|
||||
unreads-each=(jug index index:graph-store)
|
||||
unreads-count=(map index @ud)
|
||||
last-seen=(map index @da)
|
||||
=notifications
|
||||
archive=notifications
|
||||
current-timebox=@da
|
||||
dnd=_|
|
||||
==
|
||||
--
|
||||
::
|
||||
+$ index
|
||||
$% $: %graph
|
||||
group=resource
|
||||
@ -70,24 +84,33 @@
|
||||
$% [%add-note =index =notification]
|
||||
[%archive time=@da index]
|
||||
::
|
||||
[%unread-count =index =time]
|
||||
[%read-count =index]
|
||||
[%unread-count =stats-index =time]
|
||||
[%read-count =stats-index]
|
||||
::
|
||||
[%unread-each =index ref=index:graph-store time=@da]
|
||||
[%read-each index ref=index:graph-store]
|
||||
::
|
||||
[%unread-each =stats-index ref=index:graph-store time=@da]
|
||||
[%read-each =stats-index ref=index:graph-store]
|
||||
::
|
||||
[%read-note time=@da index]
|
||||
[%unread-note time=@da index]
|
||||
::
|
||||
[%seen-index time=@da =stats-index]
|
||||
[%remove-graph =resource]
|
||||
::
|
||||
[%read-all ~]
|
||||
[%set-dnd dnd=?]
|
||||
[%seen ~]
|
||||
==
|
||||
::
|
||||
++ stats-index
|
||||
$% [%graph graph=resource =index:graph-store]
|
||||
[%group group=resource]
|
||||
==
|
||||
::
|
||||
+$ indexed-notification
|
||||
[index notification]
|
||||
::
|
||||
+$ index-stats
|
||||
+$ stats
|
||||
[notifications=@ud =unreads last-seen=@da]
|
||||
::
|
||||
+$ unreads
|
||||
@ -99,10 +122,9 @@
|
||||
$% action
|
||||
[%more more=(list update)]
|
||||
[%added time=@da =index =notification]
|
||||
[%read-index =index]
|
||||
[%read time=@da =index]
|
||||
[%timebox time=@da archived=? =(list [index notification])]
|
||||
[%count count=@ud]
|
||||
[%unreads unreads=(list [index index-stats])]
|
||||
[%clear =stats-index]
|
||||
[%unreads unreads=(list [stats-index stats])]
|
||||
==
|
||||
--
|
||||
|
@ -30,6 +30,7 @@
|
||||
+$ sole-effect :: app to sole
|
||||
$% [%bel ~] :: beep
|
||||
[%blk p=@ud q=@c] :: blink+match char at
|
||||
[%bye ~] :: close session
|
||||
[%clr ~] :: clear screen
|
||||
[%det sole-change] :: edit command
|
||||
[%err p=@ud] :: error point
|
||||
|
@ -8363,7 +8363,7 @@
|
||||
[%bust *] ~(example ax %base p.gen)
|
||||
[%ktcl *] ~(factory ax p.gen)
|
||||
[%dbug *] q.gen
|
||||
[%eror *] ~>(%slog.[0 leaf=p.gen] !!)
|
||||
[%eror *] ~_((crip p.gen) !!)
|
||||
::
|
||||
[%knit *] ::
|
||||
:+ %tsgr [%ktts %v %$ 1] :: => v=.
|
||||
@ -10679,6 +10679,7 @@
|
||||
==
|
||||
:: ::
|
||||
++ redo :: refurbish faces
|
||||
~/ %redo
|
||||
|= $: :: ref: raw payload
|
||||
::
|
||||
ref=type
|
||||
|
@ -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
|
||||
@ -871,12 +874,12 @@
|
||||
?^ dud
|
||||
?+ -.task
|
||||
(on-crud:event-core -.task tang.u.dud)
|
||||
%hear (on-hole:event-core [lane blob]:task)
|
||||
%hear (on-hear:event-core lane.task blob.task dud)
|
||||
==
|
||||
::
|
||||
?- -.task
|
||||
%born on-born:event-core
|
||||
%hear (on-hear:event-core [lane blob]:task)
|
||||
%hear (on-hear:event-core [lane blob ~]:task)
|
||||
%heed (on-heed:event-core ship.task)
|
||||
%init on-init:event-core
|
||||
%jilt (on-jilt:event-core ship.task)
|
||||
@ -1194,15 +1197,15 @@
|
||||
=/ =channel [[our ship] now channel-state -.peer-state]
|
||||
abet:on-jilt:(make-peer-core peer-state channel)
|
||||
:: +on-hear: handle raw packet receipt
|
||||
:: +on-hole: handle packet crash notification
|
||||
::
|
||||
++ on-hear |=([l=lane b=blob] (on-hear-packet l (decode-packet b) ok=&))
|
||||
++ on-hole |=([l=lane b=blob] (on-hear-packet l (decode-packet b) ok=|))
|
||||
++ on-hear
|
||||
|= [l=lane b=blob d=(unit goof)]
|
||||
(on-hear-packet l (decode-packet b) d)
|
||||
:: +on-hear-packet: handle mildly processed packet receipt
|
||||
::
|
||||
++ on-hear-packet
|
||||
~/ %on-hear-packet
|
||||
|= [=lane =packet ok=?]
|
||||
|= [=lane =packet dud=(unit goof)]
|
||||
^+ event-core
|
||||
::
|
||||
?: =(our sndr.packet)
|
||||
@ -1226,7 +1229,7 @@
|
||||
::
|
||||
++ on-hear-forward
|
||||
~/ %on-hear-forward
|
||||
|= [=lane =packet ok=?]
|
||||
|= [=lane =packet dud=(unit goof)]
|
||||
^+ event-core
|
||||
%- %^ trace for.veb sndr.packet
|
||||
|.("forward: {<sndr.packet>} -> {<rcvr.packet>}")
|
||||
@ -1246,7 +1249,7 @@
|
||||
::
|
||||
++ on-hear-open
|
||||
~/ %on-hear-open
|
||||
|= [=lane =packet ok=?]
|
||||
|= [=lane =packet dud=(unit goof)]
|
||||
^+ event-core
|
||||
:: assert the comet can't pretend to be a moon or other address
|
||||
::
|
||||
@ -1283,7 +1286,7 @@
|
||||
::
|
||||
++ on-hear-shut
|
||||
~/ %on-hear-shut
|
||||
|= [=lane =packet ok=?]
|
||||
|= [=lane =packet dud=(unit goof)]
|
||||
^+ event-core
|
||||
=/ sndr-state (~(get by peers.ames-state) sndr.packet)
|
||||
:: if we don't know them, maybe enqueue a jael %public-keys request
|
||||
@ -1338,7 +1341,7 @@
|
||||
:: perform peer-specific handling of packet
|
||||
::
|
||||
=/ peer-core (make-peer-core peer-state channel)
|
||||
abet:(on-hear-shut-packet:peer-core lane shut-packet ok)
|
||||
abet:(on-hear-shut-packet:peer-core lane shut-packet dud)
|
||||
:: +on-take-boon: receive request to give message to peer
|
||||
::
|
||||
++ on-take-boon
|
||||
@ -1373,7 +1376,7 @@
|
||||
|. ^- tape
|
||||
=/ sndr [our our-life.channel]
|
||||
=/ rcvr [ship her-life.channel]
|
||||
"plea {<sndr^rcvr^bone^vane.plea^path.plea>}"
|
||||
"plea {<sndr^rcvr^bone=bone^vane.plea^path.plea>}"
|
||||
::
|
||||
abet:(on-memo:(make-peer-core peer-state channel) bone plea %plea)
|
||||
:: +on-take-wake: receive wakeup or error notification from behn
|
||||
@ -1897,7 +1900,7 @@
|
||||
:: +on-hear-shut-packet: handle receipt of ack or message fragment
|
||||
::
|
||||
++ on-hear-shut-packet
|
||||
|= [=lane =shut-packet ok=?]
|
||||
|= [=lane =shut-packet dud=(unit goof)]
|
||||
^+ peer-core
|
||||
:: update and print connection status
|
||||
::
|
||||
@ -1906,12 +1909,15 @@
|
||||
=/ =bone bone.shut-packet
|
||||
::
|
||||
?: ?=(%& -.meat.shut-packet)
|
||||
(run-message-sink bone %hear lane shut-packet ok)
|
||||
:: ignore .ok for |message-pump; just try again on error
|
||||
(run-message-sink bone %hear lane shut-packet ?=(~ dud))
|
||||
:: Just try again on error, printing trace
|
||||
::
|
||||
:: Note this implies that vanes should never crash on %done,
|
||||
:: since we have no way to continue using the flow if they do.
|
||||
::
|
||||
=+ ?~ dud ~
|
||||
%. ~
|
||||
(slog leaf+"ames: crashed on message ack" >mote.u.dud< tang.u.dud)
|
||||
(run-message-pump bone %hear [message-num +.meat]:shut-packet)
|
||||
:: +on-memo: handle request to send message
|
||||
::
|
||||
@ -1928,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))
|
||||
@ -2197,11 +2203,12 @@
|
||||
?. ?=([%hear * * ok=%.n] task)
|
||||
:: fresh boon; give message to client vane
|
||||
::
|
||||
%- (trace msg.veb |.("boon {<her.channel^bone -.task>}"))
|
||||
%- (trace msg.veb |.("boon {<her.channel^bone=bone -.task>}"))
|
||||
peer-core
|
||||
:: we previously crashed on this message; notify client vane
|
||||
::
|
||||
%- (trace msg.veb |.("crashed on boon {<her.channel^bone -.task>}"))
|
||||
%- %+ trace msg.veb
|
||||
|.("crashed on boon {<her.channel^bone=bone -.task>}")
|
||||
boon-to-lost
|
||||
:: +boon-to-lost: convert all boons to losts
|
||||
::
|
||||
@ -2219,7 +2226,7 @@
|
||||
++ on-sink-nack-trace
|
||||
|= [=message-num message=*]
|
||||
^+ peer-core
|
||||
%- (trace msg.veb |.("nack trace {<her.channel^bone>}"))
|
||||
%- (trace msg.veb |.("nack trace {<her.channel^bone=bone>}"))
|
||||
::
|
||||
=+ ;; =naxplanation message
|
||||
:: ack nack-trace message (only applied if we don't later crash)
|
||||
@ -2236,7 +2243,7 @@
|
||||
++ on-sink-plea
|
||||
|= [=message-num message=*]
|
||||
^+ peer-core
|
||||
%- (trace msg.veb |.("plea {<her.channel^bone>}"))
|
||||
%- (trace msg.veb |.("plea {<her.channel^bone=bone>}"))
|
||||
:: is this the first time we're trying to process this message?
|
||||
::
|
||||
?. ?=([%hear * * ok=%.n] task)
|
||||
@ -2346,7 +2353,8 @@
|
||||
:: ignore duplicate message acks
|
||||
::
|
||||
?: (lth message-num current.state)
|
||||
%- (trace snd.veb |.("duplicate done {<current.state message-num>}"))
|
||||
%- %+ trace snd.veb
|
||||
|.("duplicate done {<current=current.state message-num=message-num>}")
|
||||
message-pump
|
||||
:: ignore duplicate and future acks
|
||||
::
|
||||
@ -2381,6 +2389,19 @@
|
||||
::
|
||||
=. queued-message-acks.state
|
||||
(~(del by queued-message-acks.state) current.state)
|
||||
:: clear all packets from this message from the packet pump
|
||||
::
|
||||
:: Note we did this when the original packet came in, a few lines
|
||||
:: above. It's not clear why, but it doesn't always clear the
|
||||
:: packets when it's not the current message. As a workaround,
|
||||
:: we clear the packets again when we catch up to this packet.
|
||||
::
|
||||
:: This is slightly inefficient because we run this twice for
|
||||
:: each packet and it may emit a few unnecessary packets, but
|
||||
:: but it's not incorrect. pump-metrics are updated only once,
|
||||
:: at the time when we actually delete the packet.
|
||||
::
|
||||
=. message-pump (run-packet-pump %done current.state lag=*@dr)
|
||||
:: give %done to vane if we're ready
|
||||
::
|
||||
?- -.u.cur
|
||||
@ -2646,7 +2667,7 @@
|
||||
=(0 (mod counter.metrics.state 20))
|
||||
==
|
||||
same
|
||||
(trace snd.veb |.("{<[fragment-num show:gauge]>}"))
|
||||
(trace snd.veb |.("send: {<[fragment=fragment-num show:gauge]>}"))
|
||||
:: .resends is backward, so fold backward and emit
|
||||
::
|
||||
=. packet-pump
|
||||
@ -2705,7 +2726,7 @@
|
||||
=- =. metrics.state metrics.-
|
||||
=. live.state live.-
|
||||
::
|
||||
%- (trace snd.veb |.("done {<message-num^show:gauge>}"))
|
||||
%- (trace snd.veb |.("done {<message-num=message-num^show:gauge>}"))
|
||||
(fast-resend-after-ack message-num `fragment-num`0)
|
||||
::
|
||||
^+ [metrics=metrics.state live=live.state]
|
||||
@ -2936,7 +2957,8 @@
|
||||
:: ignore messages from far future; limit to 10 in progress
|
||||
::
|
||||
?: (gte seq (add 10 last-acked.state))
|
||||
%- (trace odd.veb |.("future %hear {<seq^last-acked.state>}"))
|
||||
%- %+ trace odd.veb
|
||||
|.("future %hear {<seq=seq^last-acked=last-acked.state>}")
|
||||
message-sink
|
||||
::
|
||||
=/ is-last-fragment=? =(+(fragment-num) num-fragments)
|
||||
@ -2946,12 +2968,13 @@
|
||||
?. is-last-fragment
|
||||
:: single packet ack
|
||||
::
|
||||
%- (trace rcv.veb |.("send dupe ack {<seq^fragment-num>}"))
|
||||
%- %+ trace rcv.veb
|
||||
|.("send dupe ack {<seq=seq^fragment-num=fragment-num>}")
|
||||
(give %send seq %& fragment-num)
|
||||
:: whole message (n)ack
|
||||
::
|
||||
=/ ok=? !(~(has in nax.state) seq)
|
||||
%- (trace rcv.veb |.("send dupe message ack {<seq>} ok={<ok>}"))
|
||||
%- (trace rcv.veb |.("send dupe message ack {<seq=seq>} ok={<ok>}"))
|
||||
(give %send seq %| ok lag=`@dr`0)
|
||||
:: last-acked<seq<=last-heard; heard message, unprocessed
|
||||
::
|
||||
@ -2965,15 +2988,18 @@
|
||||
%- %+ trace rcv.veb
|
||||
|. ^- tape
|
||||
=/ data
|
||||
:* her.channel seq
|
||||
fragment-num num-fragments
|
||||
:* her.channel seq=seq
|
||||
fragment-num=fragment-num num-fragments=num-fragments
|
||||
la=last-acked.state lh=last-heard.state
|
||||
==
|
||||
"hear last in-progress {<data>}"
|
||||
message-sink
|
||||
:: ack all other packets
|
||||
::
|
||||
%- (trace rcv.veb |.("send ack-1 {<seq^fragment-num^num-fragments>}"))
|
||||
%- %+ trace rcv.veb |.
|
||||
=/ data
|
||||
[seq=seq fragment-num=fragment-num num-fragments=num-fragments]
|
||||
"send ack-1 {<data>}"
|
||||
(give %send seq %& fragment-num)
|
||||
:: last-heard<seq<10+last-heard; this is a packet in a live message
|
||||
::
|
||||
@ -2996,10 +3022,12 @@
|
||||
?: already-heard-fragment
|
||||
?: is-last-fragment
|
||||
%- %+ trace rcv.veb |.
|
||||
=/ data [her.channel seq last-heard.state last-acked.state]
|
||||
=/ data
|
||||
[her.channel seq=seq lh=last-heard.state la=last-acked.state]
|
||||
"hear last dupe {<data>}"
|
||||
message-sink
|
||||
%- (trace rcv.veb |.("send dupe ack {<her.channel^seq^fragment-num>}"))
|
||||
%- %+ trace rcv.veb
|
||||
|.("send dupe ack {<her.channel^seq=seq^fragment-num=fragment-num>}")
|
||||
(give %send seq %& fragment-num)
|
||||
:: new fragment; store in state and check if message is done
|
||||
::
|
||||
@ -3014,7 +3042,10 @@
|
||||
:: ack any packet other than the last one, and continue either way
|
||||
::
|
||||
=? message-sink !is-last-fragment
|
||||
%- (trace rcv.veb |.("send ack-2 {<seq^fragment-num^num-fragments>}"))
|
||||
%- %+ trace rcv.veb |.
|
||||
=/ data
|
||||
[seq=seq fragment-num=fragment-num num-fragments=num-fragments]
|
||||
"send ack-2 {<data>}"
|
||||
(give %send seq %& fragment-num)
|
||||
:: enqueue all completed messages starting at +(last-heard.state)
|
||||
::
|
||||
@ -3037,7 +3068,7 @@
|
||||
=. live-messages.state (~(del by live-messages.state) seq)
|
||||
::
|
||||
%- %+ trace msg.veb
|
||||
|.("hear {<her.channel>} {<seq>} {<num-fragments.u.live>}kb")
|
||||
|.("hear {<her.channel>} {<seq=seq>} {<num-fragments.u.live>}kb")
|
||||
=/ message=* (assemble-fragments [num-fragments fragments]:u.live)
|
||||
=. message-sink (enqueue-to-vane seq message)
|
||||
::
|
||||
|
@ -206,6 +206,8 @@
|
||||
~> %slog.[0 leaf+"gall: pupa call dud"]
|
||||
(mean >mote.u.dud< tang.u.dud)
|
||||
=/ task ((harden task:gall) wrapped-task)
|
||||
?: ?=(%vega -.task)
|
||||
[~ pupal-gate]
|
||||
(molt duct `[duct %slip %g task])
|
||||
::
|
||||
++ scry scry:adult-core
|
||||
@ -367,6 +369,7 @@
|
||||
%- ~(gas in *(set [care:clay path]))
|
||||
:* [%z /sys/hoon/hoon]
|
||||
[%z /sys/arvo/hoon]
|
||||
[%z /sys/lull/hoon]
|
||||
[%z /sys/zuse/hoon]
|
||||
[%z /sys/vane/gall/hoon]
|
||||
%+ murn ~(tap by yokes.state)
|
||||
@ -1064,9 +1067,17 @@
|
||||
:: We convert from cards to duct-indexed moves when resolving
|
||||
:: them in Arvo.
|
||||
::
|
||||
:: We accept %huck to "fake" being a message to a ship but
|
||||
:: actually send it to a vane.
|
||||
::
|
||||
+$ neet
|
||||
$% neat
|
||||
[%huck [=ship name=term] =note-arvo]
|
||||
==
|
||||
::
|
||||
++ ap-from-internal
|
||||
~/ %ap-from-internal
|
||||
|= card=(wind neat gift:agent)
|
||||
|= card=(wind neet gift:agent)
|
||||
^- (list move)
|
||||
::
|
||||
?- -.card
|
||||
@ -1122,16 +1133,19 @@
|
||||
%pass
|
||||
=/ =duct system-duct.state
|
||||
=/ =wire p.card
|
||||
=/ =neat q.card
|
||||
=/ =neet q.card
|
||||
=. wire
|
||||
?: ?=(%agent -.neat)
|
||||
[%out (scot %p ship.neat) name.neat wire]
|
||||
[(scot %p attributing.agent-routes) wire]
|
||||
?- -.neet
|
||||
%agent [%out (scot %p ship.neet) name.neet wire]
|
||||
%huck [%out (scot %p ship.neet) name.neet wire]
|
||||
%arvo [(scot %p attributing.agent-routes) wire]
|
||||
==
|
||||
=. wire [%use agent-name nonce.current-agent wire]
|
||||
=/ =note-arvo
|
||||
?- -.neat
|
||||
%arvo note-arvo.neat
|
||||
%agent [%g %deal [our ship.neat] [name deal]:neat]
|
||||
?- -.neet
|
||||
%arvo note-arvo.neet
|
||||
%huck note-arvo.neet
|
||||
%agent [%g %deal [our ship.neet] [name deal]:neet]
|
||||
==
|
||||
[duct %pass wire note-arvo]~
|
||||
==
|
||||
@ -1308,10 +1322,10 @@
|
||||
:: +ap-pass: request action.
|
||||
::
|
||||
++ ap-pass
|
||||
|= [=path =neat]
|
||||
|= [=path =neet]
|
||||
^+ ap-core
|
||||
=/ internal-moves
|
||||
(ap-from-internal %pass path neat)
|
||||
(ap-from-internal %pass path neet)
|
||||
ap-core(agent-moves (weld internal-moves agent-moves))
|
||||
:: +ap-reinstall: reinstall.
|
||||
::
|
||||
@ -1525,8 +1539,7 @@
|
||||
::
|
||||
=. ap-core
|
||||
(ap-pass wire %agent dock %leave ~)
|
||||
=/ way [%out (scot %p p.dock) q.dock wire]
|
||||
(ap-pass way %arvo %b %huck `sign-arvo`[%gall %unto %kick ~])
|
||||
(ap-pass wire %huck dock %b %huck `sign-arvo`[%gall %unto %kick ~])
|
||||
:: +ap-mule: run virtualized with intercepted scry, preserving type
|
||||
::
|
||||
:: Compare +mute and +mule. Those pass through scry, which
|
||||
|
@ -681,7 +681,7 @@
|
||||
|
|
||||
?. ?=([[@ *] *] b)
|
||||
&
|
||||
(lth i.i.a i.i.b)
|
||||
(lth (end 3 i.i.a) (end 3 i.i.b))
|
||||
--
|
||||
::
|
||||
++ get-source
|
||||
@ -1043,7 +1043,8 @@
|
||||
=/ who (slaw %p i.tyl)
|
||||
?~ who [~ ~]
|
||||
=/ sec (~(got by jaw.own.pki.lex) lyf.own.pki.lex)
|
||||
``[%noun !>((end 6 (shaf %pass (shax sec))))]
|
||||
=/ sal (add %pass step.own.pki.lex)
|
||||
``[%noun !>((end 6 (shaf sal (shax sec))))]
|
||||
::
|
||||
%life
|
||||
?. ?=([@ ~] tyl) [~ ~]
|
||||
|
@ -5293,6 +5293,17 @@
|
||||
?~ a b
|
||||
::
|
||||
$(a l.a, b [n.a $(a r.a)])
|
||||
:: +bap: convert to list, largest to smallest
|
||||
::
|
||||
++ bap
|
||||
|= a=(tree item)
|
||||
^- (list item)
|
||||
::
|
||||
=| b=(list item)
|
||||
|- ^+ b
|
||||
?~ a b
|
||||
::
|
||||
$(a r.a, b [n.a $(a l.a)])
|
||||
:: +gas: put a list of items
|
||||
::
|
||||
++ gas
|
||||
|
@ -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,
|
||||
|
@ -2,6 +2,7 @@ const path = require('path');
|
||||
// const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
const MomentLocalesPlugin = require('moment-locales-webpack-plugin');
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
@ -25,7 +26,7 @@ module.exports = {
|
||||
]
|
||||
}
|
||||
},
|
||||
exclude: /node_modules/
|
||||
exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light)\/).*/
|
||||
},
|
||||
{
|
||||
test: /\.css$/i,
|
||||
@ -53,6 +54,10 @@ module.exports = {
|
||||
plugins: [
|
||||
new MomentLocalesPlugin(),
|
||||
new CleanWebpackPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.LANDSCAPE_STREAM': JSON.stringify(process.env.LANDSCAPE_STREAM),
|
||||
'process.env.LANDSCAPE_SHORTHASH': JSON.stringify(process.env.LANDSCAPE_SHORTHASH)
|
||||
})
|
||||
// new HtmlWebpackPlugin({
|
||||
// title: 'Hot Module Replacement',
|
||||
// template: './public/index.html',
|
||||
@ -61,7 +66,7 @@ module.exports = {
|
||||
output: {
|
||||
filename: 'index.[contenthash].js',
|
||||
path: path.resolve(__dirname, '../../arvo/app/landscape/js/bundle'),
|
||||
publicPath: '/',
|
||||
publicPath: '/'
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
|
10920
pkg/interface/dbug/package-lock.json
generated
10920
pkg/interface/dbug/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -121,7 +121,7 @@ export class Eyre extends Component {
|
||||
</>);
|
||||
const subscriptionItems = c.subscriptions.map(s => {
|
||||
//NOTE jsx sorta copied from /components/subscriptions
|
||||
return {key: `${s.id} ${s.app} ${s.ship} ${s.path}`, jsx: (
|
||||
return {key: `${s.id} ${s.ship} ${s.app} ${s.path}`, jsx: (
|
||||
<div class="flex">
|
||||
<div class="flex-auto" style={{maxWidth: '15%'}}>
|
||||
{s.id}
|
||||
@ -144,7 +144,7 @@ export class Eyre extends Component {
|
||||
return {key: c.session, jsx: (
|
||||
<Summary summary={summary} details={(
|
||||
<SearchableList
|
||||
placeholder="id, app, ship, path"
|
||||
placeholder="id, ship, app, path"
|
||||
items={subscriptionItems}
|
||||
/>
|
||||
)} />
|
||||
|
6050
pkg/interface/package-lock.json
generated
6050
pkg/interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -4,81 +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",
|
||||
"lodash": "^4.17.15",
|
||||
"codemirror": "^5.59.2",
|
||||
"css-loader": "^3.6.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"formik": "^2.2.6",
|
||||
"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",
|
||||
"yup": "^0.29.3"
|
||||
"urbit-ob": "^5.0.1",
|
||||
"yup": "^0.29.3",
|
||||
"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,12 +196,10 @@ export class HarkApi extends BaseApi<StoreState> {
|
||||
});
|
||||
}
|
||||
|
||||
getMore(archive = false) {
|
||||
const offset = this.store.state[
|
||||
archive ? 'archivedNotifications' : 'notifications'
|
||||
].size;
|
||||
getMore() {
|
||||
const offset = this.store.state['notifications']?.size || 0;
|
||||
const count = 3;
|
||||
return this.getSubset(offset,count, archive);
|
||||
return this.getSubset(offset, count, false);
|
||||
}
|
||||
|
||||
async getSubset(offset:number, count:number, isArchive: boolean) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import BaseApi from "./base";
|
||||
import { StoreState } from "../store/type";
|
||||
import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from "../types/local-update";
|
||||
|
||||
export default class LocalApi extends BaseApi<StoreState> {
|
||||
getBaseHash() {
|
||||
@ -9,76 +8,6 @@ export default class LocalApi extends BaseApi<StoreState> {
|
||||
});
|
||||
}
|
||||
|
||||
sidebarToggle() {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
sidebarToggle: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setDark(isDark: boolean) {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
setDark: isDark
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setOmnibox() {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
omniboxShown: true
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setBackground(backgroundConfig: BackgroundConfig) {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
backgroundConfig
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hideAvatars(hideAvatars: boolean) {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
hideAvatars
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hideNicknames(hideNicknames: boolean) {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
hideNicknames
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setRemoteContentPolicy(policy: LocalUpdateRemoteContentPolicy) {
|
||||
this.store.handleEvent({
|
||||
data: {
|
||||
local: {
|
||||
remoteContentPolicy: policy
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dehydrate() {
|
||||
this.store.dehydrate();
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,7 +1,9 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import _ from "lodash";
|
||||
import f from "lodash/fp";
|
||||
import f, { memoize } from "lodash/fp";
|
||||
import bigInt, { BigInteger } from "big-integer";
|
||||
import { Contact } from '~/types';
|
||||
import useLocalState from '../state/local';
|
||||
|
||||
export const MOBILE_BROWSER_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i;
|
||||
|
||||
@ -11,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) {
|
||||
@ -353,4 +355,19 @@ export function usePreventWindowUnload(shouldPreventDefault: boolean, message =
|
||||
|
||||
export function pluralize(text: string, isPlural = false, vowel = false) {
|
||||
return isPlural ? `${text}s`: `${vowel ? 'an' : 'a'} ${text}`;
|
||||
}
|
||||
|
||||
// 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 };
|
||||
}
|
@ -8,6 +8,7 @@ import {
|
||||
import { makePatDa } from "~/logic/lib/util";
|
||||
import _ from "lodash";
|
||||
import {StoreState} from "../store/type";
|
||||
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
||||
|
||||
type HarkState = Pick<StoreState, "notifications" | "notificationsGraphConfig" | "notificationsGroupConfig" | "unreads" | "notificationsChatConfig">;
|
||||
|
||||
@ -140,6 +141,22 @@ function reduce(data: any, state: HarkState) {
|
||||
readSince(data, state);
|
||||
unreadSince(data, state);
|
||||
unreadEach(data, state);
|
||||
seenIndex(data, state);
|
||||
removeGraph(data, state);
|
||||
}
|
||||
|
||||
function removeGraph(json: any, state: HarkState) {
|
||||
const data = _.get(json, 'remove-graph');
|
||||
if(data) {
|
||||
delete state.unreads.graph[data];
|
||||
}
|
||||
}
|
||||
|
||||
function seenIndex(json: any, state: HarkState) {
|
||||
const data = _.get(json, 'seen-index');
|
||||
if(data) {
|
||||
updateNotificationStats(state, data.index, 'last', () => data.time);
|
||||
}
|
||||
}
|
||||
|
||||
function readEach(json: any, state: HarkState) {
|
||||
@ -152,16 +169,13 @@ function readEach(json: any, state: HarkState) {
|
||||
function readSince(json: any, state: HarkState) {
|
||||
const data = _.get(json, 'read-count');
|
||||
if(data) {
|
||||
console.log(data);
|
||||
updateUnreadCount(state, data, () => 0)
|
||||
}
|
||||
}
|
||||
|
||||
function unreadSince(json: any, state: HarkState) {
|
||||
const data = _.get(json, 'unread-count');
|
||||
console.log(data);
|
||||
if(data) {
|
||||
updateNotificationStats(state, data.index, 'last', () => data.last)
|
||||
updateUnreadCount(state, data.index, u => u + 1);
|
||||
}
|
||||
}
|
||||
@ -169,7 +183,6 @@ function unreadSince(json: any, state: HarkState) {
|
||||
function unreadEach(json: any, state: HarkState) {
|
||||
const data = _.get(json, 'unread-each');
|
||||
if(data) {
|
||||
updateNotificationStats(state, data.index, 'last', () => data.last);
|
||||
updateUnreads(state, data.index, us => us.add(data.target))
|
||||
}
|
||||
}
|
||||
@ -177,7 +190,7 @@ function unreadEach(json: any, state: HarkState) {
|
||||
function unreads(json: any, state: HarkState) {
|
||||
const data = _.get(json, 'unreads');
|
||||
if(data) {
|
||||
console.log(data);
|
||||
clearState(state);
|
||||
data.forEach(({ index, stats }) => {
|
||||
const { unreads, notifications, last } = stats;
|
||||
updateNotificationStats(state, index, 'notifications', x => x + notifications);
|
||||
@ -193,21 +206,47 @@ function unreads(json: any, state: HarkState) {
|
||||
}
|
||||
}
|
||||
|
||||
function clearState(state){
|
||||
let initialState = {
|
||||
notifications: new BigIntOrderedMap<Timebox>(),
|
||||
archivedNotifications: new BigIntOrderedMap<Timebox>(),
|
||||
notificationsGroupConfig: [],
|
||||
notificationsChatConfig: [],
|
||||
notificationsGraphConfig: {
|
||||
watchOnSelf: false,
|
||||
mentions: false,
|
||||
watching: [],
|
||||
},
|
||||
unreads: {
|
||||
graph: {},
|
||||
group: {}
|
||||
},
|
||||
notificationsCount: 0
|
||||
};
|
||||
|
||||
Object.keys(initialState).forEach(key => {
|
||||
state[key] = initialState[key];
|
||||
});
|
||||
}
|
||||
|
||||
function updateUnreadCount(state: HarkState, index: NotifIndex, count: (c: number) => number) {
|
||||
if(!('graph' in index)) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
const property = [index.graph.graph, index.graph.index, 'unreads'];
|
||||
const curr = _.get(state.unreads.graph, property, 0);
|
||||
_.set(state.unreads.graph, property, count(curr));
|
||||
const newCount = count(curr)
|
||||
_.set(state.unreads.graph, property, newCount);
|
||||
}
|
||||
|
||||
function updateUnreads(state: HarkState, index: NotifIndex, f: (us: Set<string>) => void) {
|
||||
if(!('graph' in index)) {
|
||||
return;
|
||||
return;
|
||||
}
|
||||
const unreads = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set<string>());
|
||||
const oldSize = unreads.size;
|
||||
f(unreads);
|
||||
const newSize = unreads.size;
|
||||
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], unreads);
|
||||
}
|
||||
|
||||
@ -222,10 +261,7 @@ function updateNotificationStats(state: HarkState, index: NotifIndex, statField:
|
||||
} else if('group' in index) {
|
||||
const curr = _.get(state.unreads.group, [index.group.group, statField], 0);
|
||||
_.set(state.unreads.group, [index.group.group, statField], f(curr));
|
||||
} else if('chat' in index) {
|
||||
const curr = _.get(state.unreads.chat, [index.chat.chat, statField], 0);
|
||||
_.set(state.unreads.chat, [index.chat.chat, statField], f(curr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function added(json: any, state: HarkState) {
|
||||
@ -350,17 +386,7 @@ function archive(json: any, state: HarkState) {
|
||||
notifIdxEqual(index, idxNotif.index)
|
||||
);
|
||||
state.notifications.set(time, unarchived);
|
||||
const archiveBox = state.archivedNotifications.get(time) || [];
|
||||
const readCount = archived.filter(
|
||||
({ notification }) => !notification.read
|
||||
).length;
|
||||
updateNotificationStats(state, index, 'notifications', x => x - readCount);
|
||||
state.archivedNotifications.set(time, [
|
||||
...archiveBox,
|
||||
...archived.map(({ notification, index }) => ({
|
||||
notification: { ...notification, read: true },
|
||||
index,
|
||||
})),
|
||||
]);
|
||||
const newlyRead = archived.filter(x => !x.notification.read).length;
|
||||
updateNotificationStats(state, index, 'notifications', (x) => x - newlyRead);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
import { StoreState } from '~/store/type';
|
||||
import { Cage } from '~/types/cage';
|
||||
import { LocalUpdate, BackgroundConfig } from '~/types/local-update';
|
||||
import { LocalUpdate } from '~/types/local-update';
|
||||
|
||||
type LocalState = Pick<StoreState, 'sidebarShown' | 'omniboxShown' | 'baseHash' | 'hideAvatars' | 'hideNicknames' | 'background' | 'dark' | 'suspendedFocus' | 'remoteContentPolicy'>;
|
||||
type LocalState = Pick<StoreState, 'baseHash'>;
|
||||
|
||||
export default class LocalReducer<S extends LocalState> {
|
||||
rehydrate(state: S) {
|
||||
@ -18,20 +18,11 @@ export default class LocalReducer<S extends LocalState> {
|
||||
}
|
||||
|
||||
dehydrate(state: S) {
|
||||
const json = _.pick(state, ['hideNicknames' , 'hideAvatars' , 'background', 'remoteContentPolicy']);
|
||||
localStorage.setItem('localReducer', JSON.stringify(json));
|
||||
}
|
||||
reduce(json: Cage, state: S) {
|
||||
const data = json['local'];
|
||||
if (data) {
|
||||
this.sidebarToggle(data, state);
|
||||
this.setDark(data, state);
|
||||
this.baseHash(data, state);
|
||||
this.backgroundConfig(data, state)
|
||||
this.hideAvatars(data, state)
|
||||
this.hideNicknames(data, state)
|
||||
this.omniboxShown(data, state);
|
||||
this.remoteContentPolicy(data, state);
|
||||
}
|
||||
}
|
||||
baseHash(obj: LocalUpdate, state: S) {
|
||||
@ -39,53 +30,4 @@ export default class LocalReducer<S extends LocalState> {
|
||||
state.baseHash = obj.baseHash;
|
||||
}
|
||||
}
|
||||
|
||||
omniboxShown(obj: LocalUpdate, state: S) {
|
||||
if ('omniboxShown' in obj) {
|
||||
state.omniboxShown = !state.omniboxShown;
|
||||
if (state.suspendedFocus) {
|
||||
state.suspendedFocus.focus();
|
||||
state.suspendedFocus = null;
|
||||
} else {
|
||||
state.suspendedFocus = document.activeElement;
|
||||
document.activeElement?.blur();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sidebarToggle(obj: LocalUpdate, state: S) {
|
||||
if ('sidebarToggle' in obj) {
|
||||
state.sidebarShown = !state.sidebarShown;
|
||||
}
|
||||
}
|
||||
|
||||
setDark(obj: LocalUpdate, state: S) {
|
||||
if('setDark' in obj) {
|
||||
state.dark = obj.setDark;
|
||||
}
|
||||
}
|
||||
|
||||
backgroundConfig(obj: LocalUpdate, state: S) {
|
||||
if('backgroundConfig' in obj) {
|
||||
state.background = obj.backgroundConfig;
|
||||
}
|
||||
}
|
||||
|
||||
remoteContentPolicy(obj: LocalUpdate, state: S) {
|
||||
if('remoteContentPolicy' in obj) {
|
||||
state.remoteContentPolicy = obj.remoteContentPolicy;
|
||||
}
|
||||
}
|
||||
|
||||
hideAvatars(obj: LocalUpdate, state: S) {
|
||||
if('hideAvatars' in obj) {
|
||||
state.hideAvatars = obj.hideAvatars;
|
||||
}
|
||||
}
|
||||
|
||||
hideNicknames(obj: LocalUpdate, state: S) {
|
||||
if( 'hideNicknames' in obj) {
|
||||
state.hideNicknames = obj.hideNicknames;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
58
pkg/interface/src/logic/state/local.tsx
Normal file
58
pkg/interface/src/logic/state/local.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import create, { State } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import produce from 'immer';
|
||||
import { BackgroundConfig, RemoteContentPolicy } from "~/types/local-update";
|
||||
|
||||
export interface LocalState extends State {
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
remoteContentPolicy: RemoteContentPolicy;
|
||||
dark: boolean;
|
||||
background: BackgroundConfig;
|
||||
omniboxShown: boolean;
|
||||
suspendedFocus?: HTMLElement;
|
||||
toggleOmnibox: () => void;
|
||||
set: (fn: (state: LocalState) => void) => void
|
||||
};
|
||||
|
||||
const useLocalState = create<LocalState>(persist((set, get) => ({
|
||||
dark: false,
|
||||
background: undefined,
|
||||
hideAvatars: false,
|
||||
hideNicknames: false,
|
||||
remoteContentPolicy: {
|
||||
imageShown: true,
|
||||
audioShown: true,
|
||||
videoShown: true,
|
||||
oembedShown: true,
|
||||
},
|
||||
omniboxShown: false,
|
||||
suspendedFocus: undefined,
|
||||
toggleOmnibox: () => set(produce(state => {
|
||||
state.omniboxShown = !state.omniboxShown;
|
||||
if (typeof state.suspendedFocus?.focus === 'function') {
|
||||
state.suspendedFocus.focus();
|
||||
state.suspendedFocus = undefined;
|
||||
} else {
|
||||
state.suspendedFocus = document.activeElement;
|
||||
state.suspendedFocus.blur();
|
||||
}
|
||||
})),
|
||||
set: fn => set(produce(fn))
|
||||
}), {
|
||||
name: 'localReducer'
|
||||
}));
|
||||
|
||||
function withLocalState<P, S extends keyof LocalState>(Component: any, stateMemberKeys?: S[]) {
|
||||
return React.forwardRef((props: Omit<P, S>, ref) => {
|
||||
const localState = stateMemberKeys ? useLocalState(
|
||||
state => stateMemberKeys.reduce(
|
||||
(object, key) => ({ ...object, [key]: state[key] }), {}
|
||||
)
|
||||
): useLocalState();
|
||||
return <Component ref={ref} {...localState} {...props} />
|
||||
});
|
||||
}
|
||||
|
||||
export { useLocalState as default, withLocalState };
|
@ -51,19 +51,7 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
initialState(): StoreState {
|
||||
return {
|
||||
connection: 'connected',
|
||||
sidebarShown: true,
|
||||
omniboxShown: false,
|
||||
suspendedFocus: null,
|
||||
baseHash: null,
|
||||
background: undefined,
|
||||
remoteContentPolicy: {
|
||||
imageShown: true,
|
||||
audioShown: true,
|
||||
videoShown: true,
|
||||
oembedShown: true,
|
||||
},
|
||||
hideAvatars: false,
|
||||
hideNicknames: false,
|
||||
invites: {},
|
||||
associations: {
|
||||
contacts: {},
|
||||
@ -88,7 +76,6 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
credentials: null
|
||||
},
|
||||
contacts: {},
|
||||
dark: false,
|
||||
notifications: new BigIntOrderedMap<Timebox>(),
|
||||
archivedNotifications: new BigIntOrderedMap<Timebox>(),
|
||||
notificationsGroupConfig: [],
|
||||
|
@ -11,23 +11,13 @@ import {
|
||||
Notifications,
|
||||
NotificationGraphConfig,
|
||||
GroupNotificationsConfig,
|
||||
LocalUpdateRemoteContentPolicy,
|
||||
BackgroundConfig,
|
||||
Unreads
|
||||
} from "~/types";
|
||||
|
||||
export interface StoreState {
|
||||
// local state
|
||||
sidebarShown: boolean;
|
||||
omniboxShown: boolean;
|
||||
suspendedFocus: HTMLInputElement | null;
|
||||
dark: boolean;
|
||||
connection: ConnectionStatus;
|
||||
baseHash: string | null;
|
||||
background: BackgroundConfig;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
|
||||
// invite state
|
||||
invites: Invites;
|
||||
|
@ -1,7 +1,3 @@
|
||||
interface LocalUpdateSidebarToggle {
|
||||
sidebarToggle: boolean;
|
||||
}
|
||||
|
||||
interface LocalUpdateSetDark {
|
||||
setDark: boolean;
|
||||
}
|
||||
@ -26,7 +22,7 @@ interface LocalUpdateSetOmniboxShown {
|
||||
omniboxShown: boolean;
|
||||
}
|
||||
|
||||
export interface LocalUpdateRemoteContentPolicy {
|
||||
export interface RemoteContentPolicy {
|
||||
imageShown: boolean;
|
||||
audioShown: boolean;
|
||||
videoShown: boolean;
|
||||
@ -46,11 +42,10 @@ interface BackgroundConfigColor {
|
||||
export type BackgroundConfig = BackgroundConfigUrl | BackgroundConfigColor | undefined;
|
||||
|
||||
export type LocalUpdate =
|
||||
LocalUpdateSidebarToggle
|
||||
| LocalUpdateSetDark
|
||||
| LocalUpdateBaseHash
|
||||
| LocalUpdateBackgroundConfig
|
||||
| LocalUpdateHideAvatars
|
||||
| LocalUpdateHideNicknames
|
||||
| LocalUpdateSetOmniboxShown
|
||||
| LocalUpdateRemoteContentPolicy;
|
||||
| RemoteContentPolicy;
|
@ -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';
|
||||
|
||||
@ -26,6 +26,7 @@ import GlobalSubscription from '~/logic/subscription/global';
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { uxToHex } from '~/logic/lib/util';
|
||||
import { foregroundFromBackground } from '~/logic/lib/sigil';
|
||||
import { withLocalState } from '~/logic/state/local';
|
||||
|
||||
|
||||
const Root = styled.div`
|
||||
@ -39,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;
|
||||
@ -86,23 +87,29 @@ class App extends React.Component {
|
||||
componentDidMount() {
|
||||
this.subscription.start();
|
||||
this.themeWatcher = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
this.api.local.setDark(this.themeWatcher.matches);
|
||||
this.themeWatcher.addListener(this.updateTheme);
|
||||
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.
|
||||
this.updateTheme(this.themeWatcher);
|
||||
}, 500);
|
||||
this.api.local.getBaseHash();
|
||||
this.store.rehydrate();
|
||||
Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
this.api.local.setOmnibox();
|
||||
this.props.toggleOmnibox();
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.themeWatcher.removeListener(this.updateTheme);
|
||||
this.themeWatcher.onchange = undefined;
|
||||
}
|
||||
|
||||
updateTheme(e) {
|
||||
this.api.local.setDark(e.matches);
|
||||
this.props.set(state => {
|
||||
state.dark = e.matches;
|
||||
});
|
||||
}
|
||||
|
||||
faviconString() {
|
||||
@ -122,11 +129,11 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { state } = this;
|
||||
const { state, props } = this;
|
||||
const associations = state.associations ?
|
||||
state.associations : { contacts: {} };
|
||||
const theme = state.dark ? dark : light;
|
||||
const { background } = state;
|
||||
const theme = props.dark ? dark : light;
|
||||
const background = this.props.background;
|
||||
|
||||
const notificationsCount = state.notificationsCount || 0;
|
||||
const doNotDisturb = state.doNotDisturb || false;
|
||||
@ -164,7 +171,7 @@ class App extends React.Component {
|
||||
notifications={state.notificationsCount}
|
||||
invites={state.invites}
|
||||
groups={state.groups}
|
||||
show={state.omniboxShown}
|
||||
show={this.props.omniboxShown}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary>
|
||||
@ -183,5 +190,5 @@ class App extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default process.env.NODE_ENV === 'production' ? App : hot(App);
|
||||
export default withLocalState(process.env.NODE_ENV === 'production' ? App : hot(App));
|
||||
|
||||
|
@ -87,7 +87,6 @@ export function ChatResource(props: ChatResourceProps) {
|
||||
<Col {...bind} height="100%" overflow="hidden" position="relative">
|
||||
{dragging && <SubmitDragger />}
|
||||
<ChatWindow
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
mailboxSize={5}
|
||||
match={props.match as any}
|
||||
stationPendingMessages={[]}
|
||||
@ -105,8 +104,6 @@ export function ChatResource(props: ChatResourceProps) {
|
||||
ship={owner}
|
||||
station={station}
|
||||
api={props.api}
|
||||
hideNicknames={props.hideNicknames}
|
||||
hideAvatars={props.hideAvatars}
|
||||
location={props.location}
|
||||
scrollTo={scrollTo ? parseInt(scrollTo, 10) : undefined}
|
||||
/>
|
||||
@ -119,7 +116,6 @@ export function ChatResource(props: ChatResourceProps) {
|
||||
contacts={contacts}
|
||||
onUnmount={appendUnsent}
|
||||
s3={props.s3}
|
||||
hideAvatars={props.hideAvatars}
|
||||
placeholder="Message..."
|
||||
message={unsent[station] || ''}
|
||||
deleteMessage={clearUnsent}
|
||||
|
@ -10,6 +10,7 @@ import { Envelope } from '~/types/chat-update';
|
||||
import { Contacts, Content } from '~/types';
|
||||
import { Row, BaseImage, Box, Icon, LoadingSpinner } from '@tlon/indigo-react';
|
||||
import withS3 from '~/views/components/withS3';
|
||||
import { withLocalState } from '~/logic/state/local';
|
||||
|
||||
type ChatInputProps = IuseS3 & {
|
||||
api: GlobalApi;
|
||||
@ -66,7 +67,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
||||
inCodeMode: false
|
||||
}, async () => {
|
||||
const output = await props.api.graph.eval(text);
|
||||
const contents: Content[] = [{ code: { output, expression: text }}];
|
||||
const contents: Content[] = [{ code: { output, expression: text }}];
|
||||
const post = createPost(contents);
|
||||
props.api.graph.addPost(ship, name, post);
|
||||
});
|
||||
@ -199,4 +200,4 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
||||
}
|
||||
}
|
||||
|
||||
export default withS3(ChatInput, {accept: 'image/*'});
|
||||
export default withLocalState(withS3(ChatInput, {accept: 'image/*'}), ['hideAvatars']);
|
||||
|
@ -4,14 +4,14 @@ import _ from "lodash";
|
||||
import { Box, Row, Text, Rule } from "@tlon/indigo-react";
|
||||
|
||||
import OverlaySigil from '~/views/components/OverlaySigil';
|
||||
import { uxToHex, cite, writeText } from '~/logic/lib/util';
|
||||
import { Envelope, IMessage } from "~/types/chat-update";
|
||||
import { Group, Association, Contacts, LocalUpdateRemoteContentPolicy, Post } from "~/types";
|
||||
import { uxToHex, cite, writeText, useShowNickname } from '~/logic/lib/util';
|
||||
import { Group, Association, Contacts, Post } from "~/types";
|
||||
import TextContent from './content/text';
|
||||
import CodeContent from './content/code';
|
||||
import RemoteContent from '~/views/components/RemoteContent';
|
||||
import { Mention } from "~/views/components/MentionText";
|
||||
import styled from "styled-components";
|
||||
import useLocalState from "~/logic/state/local";
|
||||
|
||||
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
|
||||
|
||||
@ -39,9 +39,6 @@ interface ChatMessageProps {
|
||||
group: Group;
|
||||
association: Association;
|
||||
contacts: Contacts;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
className?: string;
|
||||
isPending: boolean;
|
||||
style?: any;
|
||||
@ -76,9 +73,6 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
||||
group,
|
||||
association,
|
||||
contacts,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
remoteContentPolicy,
|
||||
className = '',
|
||||
isPending,
|
||||
style,
|
||||
@ -109,11 +103,8 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
||||
msg,
|
||||
timestamp,
|
||||
contacts,
|
||||
hideNicknames,
|
||||
association,
|
||||
group,
|
||||
hideAvatars,
|
||||
remoteContentPolicy,
|
||||
measure: reboundMeasure.bind(this),
|
||||
style,
|
||||
containerClass,
|
||||
@ -162,9 +153,6 @@ interface MessageProps {
|
||||
group: Group;
|
||||
association: Association;
|
||||
contacts: Contacts;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
containerClass: string;
|
||||
isPending: boolean;
|
||||
style: any;
|
||||
@ -172,105 +160,96 @@ interface MessageProps {
|
||||
scrollWindow: HTMLDivElement;
|
||||
};
|
||||
|
||||
export class MessageWithSigil extends PureComponent<MessageProps> {
|
||||
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
export const MessageWithSigil = (props) => {
|
||||
const {
|
||||
msg,
|
||||
timestamp,
|
||||
contacts,
|
||||
association,
|
||||
group,
|
||||
measure,
|
||||
api,
|
||||
history,
|
||||
scrollWindow,
|
||||
fontSize
|
||||
} = props;
|
||||
|
||||
render() {
|
||||
const {
|
||||
msg,
|
||||
timestamp,
|
||||
contacts,
|
||||
hideNicknames,
|
||||
association,
|
||||
group,
|
||||
hideAvatars,
|
||||
remoteContentPolicy,
|
||||
measure,
|
||||
api,
|
||||
history,
|
||||
scrollWindow,
|
||||
fontSize
|
||||
} = this.props;
|
||||
const dark = useLocalState(state => state.dark);
|
||||
|
||||
const datestamp = moment.unix(msg['time-sent'] / 1000).format(DATESTAMP_FORMAT);
|
||||
const contact = msg.author in contacts ? contacts[msg.author] : false;
|
||||
const showNickname = !hideNicknames && contact && contact.nickname;
|
||||
const name = showNickname ? contact!.nickname : cite(msg.author);
|
||||
const color = contact ? `#${uxToHex(contact.color)}` : this.isDark ? '#000000' :'#FFFFFF'
|
||||
const sigilClass = contact ? '' : this.isDark ? 'mix-blend-diff' : 'mix-blend-darken';
|
||||
const datestamp = moment.unix(msg['time-sent'] / 1000).format(DATESTAMP_FORMAT);
|
||||
const contact = msg.author in contacts ? contacts[msg.author] : false;
|
||||
const showNickname = useShowNickname(contact);
|
||||
const name = showNickname ? contact.nickname : cite(msg.author);
|
||||
const color = contact ? `#${uxToHex(contact.color)}` : dark ? '#000000' :'#FFFFFF'
|
||||
const sigilClass = contact ? '' : dark ? 'mix-blend-diff' : 'mix-blend-darken';
|
||||
|
||||
let nameSpan = null;
|
||||
let nameSpan = null;
|
||||
|
||||
const copyNotice = (saveName) => {
|
||||
if (nameSpan !== null) {
|
||||
nameSpan.innerText = 'Copied';
|
||||
setTimeout(() => {
|
||||
nameSpan.innerText = saveName;
|
||||
}, 800);
|
||||
}
|
||||
};
|
||||
const copyNotice = (saveName) => {
|
||||
if (nameSpan !== null) {
|
||||
nameSpan.innerText = 'Copied';
|
||||
setTimeout(() => {
|
||||
nameSpan.innerText = saveName;
|
||||
}, 800);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<OverlaySigil
|
||||
ship={msg.author}
|
||||
contact={contact}
|
||||
color={color}
|
||||
sigilClass={sigilClass}
|
||||
group={group}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
scrollWindow={scrollWindow}
|
||||
history={history}
|
||||
api={api}
|
||||
bg="white"
|
||||
className="fl pr3 v-top pt1"
|
||||
/>
|
||||
<Box flexGrow={1} display='block' className="clamp-message">
|
||||
<Box
|
||||
return (
|
||||
<>
|
||||
<OverlaySigil
|
||||
ship={msg.author}
|
||||
contact={contact}
|
||||
color={color}
|
||||
sigilClass={sigilClass}
|
||||
group={group}
|
||||
scrollWindow={scrollWindow}
|
||||
history={history}
|
||||
api={api}
|
||||
bg="white"
|
||||
className="fl pr3 v-top pt1"
|
||||
/>
|
||||
<Box flexGrow={1} display='block' className="clamp-message">
|
||||
<Box
|
||||
flexShrink={0}
|
||||
className="hide-child"
|
||||
pt={1}
|
||||
pb={1}
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
>
|
||||
<Text
|
||||
fontSize={0}
|
||||
mr={3}
|
||||
flexShrink={0}
|
||||
className="hide-child"
|
||||
pt={1}
|
||||
pb={1}
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
>
|
||||
<Text
|
||||
fontSize={0}
|
||||
mr={3}
|
||||
flexShrink={0}
|
||||
mono={!showNickname}
|
||||
fontWeight={(showNickname) ? '500' : '400'}
|
||||
className={`mw5 db truncate pointer`}
|
||||
ref={e => nameSpan = e}
|
||||
onClick={() => {
|
||||
writeText(`~${msg.author}`);
|
||||
copyNotice(name);
|
||||
}}
|
||||
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>
|
||||
</Box>
|
||||
<ContentBox flexShrink={0} fontSize={fontSize ? fontSize : '14px'}>
|
||||
{msg.contents.map(c =>
|
||||
<MessageContent
|
||||
contacts={contacts}
|
||||
content={c}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
measure={measure}
|
||||
fontSize={fontSize}
|
||||
group={group}
|
||||
hideNicknames={hideNicknames}
|
||||
hideAvatars={hideAvatars}
|
||||
/>)}
|
||||
</ContentBox>
|
||||
mono={!showNickname}
|
||||
fontWeight={(showNickname) ? '500' : '400'}
|
||||
className={`mw5 db truncate pointer`}
|
||||
ref={e => nameSpan = e}
|
||||
onClick={() => {
|
||||
writeText(`~${msg.author}`);
|
||||
copyNotice(name);
|
||||
}}
|
||||
title={`~${msg.author}`}
|
||||
>{name}</Text>
|
||||
<Text flexShrink='0' fontSize='0' gray mono className="v-mid">{timestamp}</Text>
|
||||
<Text flexShrink={0} gray mono ml={2} className="v-mid child dn-s">{datestamp}</Text>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
<ContentBox flexShrink={0} fontSize={fontSize ? fontSize : '14px'}>
|
||||
{msg.contents.map(c =>
|
||||
<MessageContent
|
||||
contacts={contacts}
|
||||
content={c}
|
||||
measure={measure}
|
||||
fontSize={fontSize}
|
||||
group={group}
|
||||
/>)}
|
||||
</ContentBox>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const ContentBox = styled(Box)`
|
||||
& > :first-child {
|
||||
margin-left: 0px;
|
||||
@ -278,9 +257,9 @@ const ContentBox = styled(Box)`
|
||||
|
||||
`;
|
||||
|
||||
export const MessageWithoutSigil = ({ timestamp, contacts, msg, remoteContentPolicy, measure, group, hideNicknames, hideAvatars }) => (
|
||||
export const MessageWithoutSigil = ({ timestamp, contacts, msg, measure, group }) => (
|
||||
<>
|
||||
<Text flexShrink={0} mono gray display='inline-block' pt='2px' lineHeight='tall' className="child">{timestamp}</Text>
|
||||
<Text flexShrink={0} mono gray display='inline-block' pt='2px' lineHeight='tall' className="child" fontSize='0'>{timestamp}</Text>
|
||||
<ContentBox flexShrink={0} fontSize='14px' className="clamp-message" style={{ flexGrow: 1 }}>
|
||||
{msg.contents.map((c, i) => (
|
||||
<MessageContent
|
||||
@ -288,15 +267,12 @@ export const MessageWithoutSigil = ({ timestamp, contacts, msg, remoteContentPol
|
||||
contacts={contacts}
|
||||
content={c}
|
||||
group={group}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
measure={measure}
|
||||
hideNicknames={hideNicknames}
|
||||
hideAvatars={hideAvatars}/>))}
|
||||
measure={measure}/>))}
|
||||
</ContentBox>
|
||||
</>
|
||||
);
|
||||
|
||||
export const MessageContent = ({ content, contacts, remoteContentPolicy, measure, fontSize, group, hideNicknames, hideAvatars }) => {
|
||||
export const MessageContent = ({ content, contacts, measure, fontSize, group }) => {
|
||||
if ('code' in content) {
|
||||
return <CodeContent content={content} />;
|
||||
} else if ('url' in content) {
|
||||
@ -304,10 +280,9 @@ export const MessageContent = ({ content, contacts, remoteContentPolicy, measure
|
||||
<Box mx="2px" flexShrink={0} fontSize={fontSize ? fontSize : '14px'} lineHeight="tall" color='black'>
|
||||
<RemoteContent
|
||||
url={content.url}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
onLoad={measure}
|
||||
imageProps={{style: {
|
||||
maxWidth: '18rem',
|
||||
maxWidth: 'min(100%,18rem)',
|
||||
display: 'block'
|
||||
}}}
|
||||
videoProps={{style: {
|
||||
@ -325,7 +300,7 @@ export const MessageContent = ({ content, contacts, remoteContentPolicy, measure
|
||||
} else if ('text' in content) {
|
||||
return <TextContent fontSize={fontSize} content={content} />;
|
||||
} else if ('mention' in content) {
|
||||
return <Mention group={group} ship={content.mention} contact={contacts?.[content.mention]} hideNicknames={hideNicknames} hideAvatars={hideAvatars} />
|
||||
return <Mention group={group} ship={content.mention} contact={contacts?.[content.mention]} />
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -9,8 +9,7 @@ import { Contacts } from "~/types/contact-update";
|
||||
import { Association } from "~/types/metadata-update";
|
||||
import { Group } from "~/types/group-update";
|
||||
import { Envelope, IMessage } from "~/types/chat-update";
|
||||
import { LocalUpdateRemoteContentPolicy, Graph } from "~/types";
|
||||
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
|
||||
import { Graph } from "~/types";
|
||||
|
||||
import VirtualScroller from "~/views/components/VirtualScroller";
|
||||
|
||||
@ -41,9 +40,6 @@ type ChatWindowProps = RouteComponentProps<{
|
||||
ship: Patp;
|
||||
station: any;
|
||||
api: GlobalApi;
|
||||
hideNicknames: boolean;
|
||||
hideAvatars: boolean;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
scrollTo?: number;
|
||||
}
|
||||
|
||||
@ -253,19 +249,16 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
||||
contacts,
|
||||
mailboxSize,
|
||||
graph,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
remoteContentPolicy,
|
||||
history
|
||||
} = this.props;
|
||||
|
||||
const unreadMarkerRef = this.unreadMarkerRef;
|
||||
|
||||
|
||||
const messageProps = { association, group, contacts, hideAvatars, hideNicknames, remoteContentPolicy, unreadMarkerRef, history, api };
|
||||
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'}}>
|
||||
@ -66,6 +68,7 @@ const MessageMarkdown = React.memo(props => (
|
||||
return true;
|
||||
}}
|
||||
plugins={[[
|
||||
RemarkBreaks,
|
||||
RemarkDisableTokenizers,
|
||||
{ block: DISABLED_BLOCK_TOKENS, inline: DISABLED_INLINE_TOKENS }
|
||||
]]} />
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -14,10 +14,7 @@ export default class GraphApp extends PureComponent {
|
||||
const graphKeys = props.graphKeys || new Set([]);
|
||||
const graphs = props.graphs || {};
|
||||
|
||||
const {
|
||||
api, sidebarShown, s3,
|
||||
hideAvatars, hideNicknames, remoteContentPolicy
|
||||
} = this.props;
|
||||
const { api } = this.props;
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
|
@ -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}
|
||||
|
@ -6,6 +6,7 @@ import ErrorBoundary from '~/views/components/ErrorBoundary';
|
||||
import Tile from './tile';
|
||||
|
||||
export const weatherStyleMap = {
|
||||
Clear: 'rgba(67, 169, 255, 0.4)',
|
||||
Sunny: 'rgba(67, 169, 255, 0.4)',
|
||||
PartlyCloudy: 'rgba(178, 211, 255, 0.33)',
|
||||
Cloudy: 'rgba(136, 153, 176, 0.43)',
|
||||
@ -170,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' }}>
|
||||
@ -216,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>
|
||||
|
||||
@ -267,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';
|
||||
@ -11,10 +11,13 @@ 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;
|
||||
@ -33,9 +36,6 @@ export function LinkResource(props: LinkResourceProps) {
|
||||
graphKeys,
|
||||
unreads,
|
||||
s3,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
remoteContentPolicy,
|
||||
history
|
||||
} = props;
|
||||
|
||||
@ -61,42 +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}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@ -119,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
|
||||
@ -126,15 +113,13 @@ export function LinkResource(props: LinkResourceProps) {
|
||||
key={node.post.index}
|
||||
resource={resourcePath}
|
||||
node={node}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
baseUrl={resourceUrl}
|
||||
unreads={unreads}
|
||||
group={group}
|
||||
path={resource["group-path"]}
|
||||
api={api}
|
||||
mt={3}
|
||||
measure={emptyMeasure}
|
||||
/>
|
||||
<Comments
|
||||
ship={ship}
|
||||
@ -145,15 +130,13 @@ export function LinkResource(props: LinkResourceProps) {
|
||||
unreads={unreads}
|
||||
contacts={contactDetails}
|
||||
api={api}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
editCommentId={editCommentId}
|
||||
history={props.history}
|
||||
baseUrl={`${resourceUrl}/${props.match.params.index}`}
|
||||
group={group}
|
||||
/>
|
||||
</Col>
|
||||
</Col>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
97
pkg/interface/src/views/apps/links/LinkWindow.tsx
Normal file
97
pkg/interface/src/views/apps/links/LinkWindow.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
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'
|
||||
}), []);
|
||||
|
||||
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,13 +1,12 @@
|
||||
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, BaseImage, Icon, Action } from '@tlon/indigo-react';
|
||||
import { Row, Col, Anchor, Box, Text, Icon, Action } from '@tlon/indigo-react';
|
||||
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
import { writeText } from '~/logic/lib/util';
|
||||
import Author from '~/views/components/Author';
|
||||
|
||||
import { roleForShip } from '~/logic/lib/group';
|
||||
import { Contacts, GraphNode, Group, LocalUpdateRemoteContentPolicy, Rolodex, Unreads } from '~/types';
|
||||
import { Contacts, GraphNode, Group, Rolodex, Unreads } from '~/types';
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { Dropdown } from '~/views/components/Dropdown';
|
||||
import RemoteContent from '~/views/components/RemoteContent';
|
||||
@ -15,30 +14,28 @@ import RemoteContent from '~/views/components/RemoteContent';
|
||||
interface LinkItemProps {
|
||||
node: GraphNode;
|
||||
resource: string;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
api: GlobalApi;
|
||||
group: Group;
|
||||
path: string;
|
||||
contacts: Rolodex[];
|
||||
contacts: Rolodex;
|
||||
unreads: Unreads;
|
||||
measure: (el: any) => void;
|
||||
}
|
||||
|
||||
export const LinkItem = (props: LinkItemProps) => {
|
||||
const {
|
||||
node,
|
||||
resource,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
remoteContentPolicy,
|
||||
api,
|
||||
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}/
|
||||
);
|
||||
@ -76,11 +73,19 @@ export const LinkItem = (props: LinkItemProps) => {
|
||||
|
||||
const markRead = () => {
|
||||
api.hark.markEachAsRead(props.association, '/', `/${index}`, 'link', 'link');
|
||||
console.log('mark read');
|
||||
}
|
||||
|
||||
|
||||
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'
|
||||
@ -97,8 +102,8 @@ export const LinkItem = (props: LinkItemProps) => {
|
||||
<RemoteContent
|
||||
url={contents[1].url}
|
||||
text={contents[0].text}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
unfold={true}
|
||||
onLoad={onMeasure}
|
||||
style={{ alignSelf: 'center' }}
|
||||
oembedProps={{
|
||||
p: 2,
|
||||
@ -125,17 +130,14 @@ 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[path]}
|
||||
contacts={contacts}
|
||||
ship={author}
|
||||
date={node.post['time-sent']}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
group={group}
|
||||
api={api}
|
||||
></Author>
|
||||
@ -148,9 +150,9 @@ export const LinkItem = (props: LinkItemProps) => {
|
||||
</Box>
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
|
||||
<Dropdown
|
||||
width="200px"
|
||||
dropWidth="200px"
|
||||
alignX="right"
|
||||
alignY="top"
|
||||
options={
|
||||
@ -168,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,84 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Row, Col, Anchor, Box, Text, BaseImage } from '@tlon/indigo-react';
|
||||
|
||||
import { Sigil } from '~/logic/lib/sigil';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { cite } from '~/logic/lib/util';
|
||||
import { Author } from "~/views/apps/publish/components/Author";
|
||||
|
||||
import { roleForShip } from '~/logic/lib/group';
|
||||
|
||||
export const LinkItem = (props) => {
|
||||
const {
|
||||
node,
|
||||
nickname,
|
||||
avatar,
|
||||
contacts,
|
||||
unread,
|
||||
resource,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
api,
|
||||
group
|
||||
} = props;
|
||||
|
||||
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}/
|
||||
);
|
||||
|
||||
const author = node.post.author;
|
||||
const index = node.post.index.split('/')[1];
|
||||
const size = node.children ? node.children.size : 0;
|
||||
const date = node.post['time-sent'];
|
||||
const contents = node.post.contents;
|
||||
const hostname = URLparser.exec(contents[1].url) ? URLparser.exec(contents[1].url)[4] : null;
|
||||
|
||||
const showAvatar = avatar && !hideAvatars;
|
||||
const showNickname = nickname && !hideNicknames;
|
||||
|
||||
const img = showAvatar
|
||||
? <BaseImage display='inline-block' src={props.avatar} height={36} width={36} />
|
||||
: <Sigil ship={`~${author}`} size={36} color={'#' + props.color} />;
|
||||
|
||||
const baseUrl = props.baseUrl || `/~404/${resource}`;
|
||||
|
||||
const ourRole = group ? roleForShip(group, window.ship) : undefined;
|
||||
const [ship, name] = resource.split('/');
|
||||
|
||||
return (
|
||||
<Row minWidth='0' flexShrink='0' width="100%" alignItems="center" py={3} bg="white">
|
||||
{img}
|
||||
<Col minWidth='0' height="100%" width='100%' justifyContent="space-between" ml={2}>
|
||||
<Anchor
|
||||
lineHeight="tall"
|
||||
display='flex'
|
||||
style={{ textDecoration: 'none' }}
|
||||
href={contents[1].url}
|
||||
width="100%"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Text display='inline-block' overflow='hidden' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}>{contents[0].text}</Text>
|
||||
<Text ml="2" color="gray" display='inline-block' flexShrink='0'>{hostname} ↗</Text>
|
||||
</Anchor>
|
||||
<Row alignItems="center" width="100%">
|
||||
<Author
|
||||
contacts={contacts}
|
||||
ship={author}
|
||||
hideAvatars={hideAvatars}
|
||||
hideNicknames={hideNicknames}
|
||||
unread={unread}
|
||||
date={date}
|
||||
>
|
||||
<Link to={`${baseUrl}/${index}`}>
|
||||
<Text ml="2" color="gray">{size} comments</Text>
|
||||
</Link>
|
||||
{(ourRole === 'admin' || node.post.author === window.ship)
|
||||
&& (<Text color='red' ml='2' cursor='pointer' onClick={() => api.graph.removeNodes(`~${ship}`, name, [node.post.index])}>Delete</Text>)}
|
||||
</Author>
|
||||
</Row>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
@ -1,70 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { cite } from '~/logic/lib/util';
|
||||
import RemoteContent from '~/views/components/RemoteContent';
|
||||
|
||||
import { Box, Col, Anchor, Text } from '@tlon/indigo-react';
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
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}/
|
||||
);
|
||||
|
||||
export const LinkPreview = (props) => {
|
||||
const showNickname = props.nickname && !props.hideNicknames;
|
||||
const author = props.post.author;
|
||||
const title = props.post.contents[0].text;
|
||||
const url = props.post.contents[1].url;
|
||||
const hostname = URLparser.exec(url) ? URLparser.exec(url)[4] : null;
|
||||
|
||||
const timeSent =
|
||||
moment.unix(props.post['time-sent'] / 1000).format('hh:mm a');
|
||||
|
||||
useEffect(() => {
|
||||
return () => props.api.hark.markEachAsRead(props.association, '/', props.post.index, 'link', 'link');
|
||||
}, [props.association, props.post.index]);
|
||||
|
||||
const embed = (
|
||||
<RemoteContent
|
||||
unfold={true}
|
||||
renderUrl={false}
|
||||
url={url}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
className="mw-100"
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box pb='6' width='100%'>
|
||||
<Box width='100%' textAlign='center'>{embed}</Box>
|
||||
<Col flex='1 1 auto' minWidth='0' minHeight='0' pt='6'>
|
||||
<Anchor href={url}
|
||||
lineHeight="tall"
|
||||
display='flex'
|
||||
style={{ textDecoration: 'none' }}
|
||||
width='100%'
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<Text
|
||||
display='inline-block'
|
||||
overflow='hidden'
|
||||
style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<Text ml="2" color="gray" display='inline-block' flexShrink='0'>{hostname} ↗</Text>
|
||||
</Anchor>
|
||||
<Box width='100%' pt='1'>
|
||||
<Text fontSize='0' pr='2' display='inline-block' mono={!showNickname} title={author}>
|
||||
{showNickname ? props.nickname : cite(`~${author}`)}
|
||||
</Text>
|
||||
<Text fontSize='0' gray pr='3' display='inline-block'>{timeSent}</Text>
|
||||
<Text gray fontSize='0' display='inline-block'>
|
||||
{props.commentNumber} comments
|
||||
</Text>
|
||||
</Box>
|
||||
</Col>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
@ -1,107 +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;
|
||||
remoteContentPolicy: any;
|
||||
}) {
|
||||
const { index, contents, read, time, api, timebox, remoteContentPolicy } = 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'
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
/>
|
||||
</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);
|
||||
}
|
||||
@ -80,7 +80,6 @@ const GraphNodeContent = ({ group, post, contacts, mod, description, index, remo
|
||||
content={contents}
|
||||
contacts={contacts}
|
||||
group={group}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
/>
|
||||
}
|
||||
return null;
|
||||
@ -91,7 +90,8 @@ const GraphNodeContent = ({ group, post, contacts, mod, description, index, remo
|
||||
content={contents}
|
||||
group={group}
|
||||
contacts={contacts}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
fontSize='14px'
|
||||
lineHeight="tall"
|
||||
/>
|
||||
} else if (idx[1] === "1") {
|
||||
const [{ text: header }, { text: body }] = contents;
|
||||
@ -132,7 +132,6 @@ const GraphNodeContent = ({ group, post, contacts, mod, description, index, remo
|
||||
msg={post}
|
||||
fontSize='0'
|
||||
pt='2'
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
/>
|
||||
</Row>);
|
||||
|
||||
@ -167,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}
|
||||
@ -181,7 +181,7 @@ const GraphNode = ({
|
||||
color={`#000000`}
|
||||
classes="mix-blend-diff"
|
||||
/>
|
||||
);
|
||||
) : <Box style={{ width: '16px' }}></Box>;
|
||||
|
||||
const groupContacts = contacts[groupPath] ?? {};
|
||||
|
||||
@ -195,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"
|
||||
@ -211,15 +211,14 @@ 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}
|
||||
mod={mod}
|
||||
description={description}
|
||||
index={index}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
group={group}
|
||||
/>
|
||||
</Row>
|
||||
@ -239,9 +238,8 @@ export function GraphNotification(props: {
|
||||
groups: Groups;
|
||||
contacts: Rolodex;
|
||||
api: GlobalApi;
|
||||
remoteContentPolicy: any;
|
||||
}) {
|
||||
const { contents, index, read, time, api, timebox, remoteContentPolicy, groups } = props;
|
||||
const { contents, index, read, time, api, timebox, groups } = props;
|
||||
|
||||
const authors = _.map(contents, "author");
|
||||
const { graph, group } = index;
|
||||
@ -249,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}
|
||||
@ -272,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}
|
||||
@ -287,10 +284,10 @@ return (
|
||||
groupPath={group}
|
||||
read={read}
|
||||
onRead={onClick}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
showContact={idx === 0}
|
||||
/>
|
||||
))}
|
||||
</Col>
|
||||
</Col>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
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";
|
||||
import { PropFunc } from "~/types/util";
|
||||
import { getContactDetails } from "~/logic/lib/util";
|
||||
import { getContactDetails, useShowNickname } from "~/logic/lib/util";
|
||||
import { Associations, Contact, Contacts, Rolodex } from "~/types";
|
||||
|
||||
const Text = (props: PropFunc<typeof Text>) => (
|
||||
@ -14,7 +14,7 @@ const Text = (props: PropFunc<typeof Text>) => (
|
||||
function Author(props: { patp: string; contacts: Contacts; last?: boolean }) {
|
||||
const contact: Contact | undefined = props.contacts?.[props.patp];
|
||||
|
||||
const showNickname = !!contact?.nickname;
|
||||
const showNickname = useShowNickname(contact);
|
||||
const name = contact?.nickname || `~${props.patp}`;
|
||||
|
||||
return (
|
||||
@ -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">
|
||||
|
@ -61,20 +61,36 @@ 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(
|
||||
let notificationsByDay = f.flow(
|
||||
f.map<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>(([date]) => {
|
||||
date = moment(daToUnix(date));
|
||||
if (moment().subtract(6, 'hours').isBefore(date)) {
|
||||
return 'latest';
|
||||
} else {
|
||||
return date.format("YYYYMMDD");
|
||||
}
|
||||
}),
|
||||
)(notifications);
|
||||
notificationsByDay = new Map(Object.keys(notificationsByDay).sort().reverse().map(timebox => {
|
||||
return [timebox, notificationsByDay[timebox]];
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
api.hark.getMore(props.showArchive);
|
||||
@ -130,44 +146,27 @@ export default function Inbox(props: {
|
||||
|
||||
return (
|
||||
<Col position="relative" height="100%" overflowY="auto" onScroll={onScroll} >
|
||||
<Col zIndex={4} gapY={2} bg="white" top="0px" position="sticky">
|
||||
<Col zIndex={4} gapY={2} bg="white" top="0px" position="sticky" flexShrink={0}>
|
||||
{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}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
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}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{[...notificationsByDay.keys()].map((day, index) => {
|
||||
const timeboxes = notificationsByDay.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}
|
||||
chatConfig={props.notificationsChatConfig}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
@ -184,21 +183,18 @@ 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;
|
||||
@ -209,7 +205,7 @@ function DaySection({
|
||||
<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>
|
||||
@ -230,7 +226,6 @@ function DaySection({
|
||||
contacts={contacts}
|
||||
groups={groups}
|
||||
time={date}
|
||||
remoteContentPolicy={remoteContentPolicy}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))
|
||||
|
@ -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;
|
||||
@ -31,7 +30,6 @@ interface NotificationProps {
|
||||
graphConfig: NotificationGraphConfig;
|
||||
groupConfig: GroupNotificationsConfig;
|
||||
chatConfig: string[];
|
||||
remoteContentPolicy: any;
|
||||
}
|
||||
|
||||
function getMuted(
|
||||
@ -56,9 +54,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;
|
||||
}
|
||||
|
||||
@ -90,21 +85,31 @@ 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>
|
||||
{!props.archived && (
|
||||
<StatelessAsyncAction name={time.toString()} onClick={onArchive} backgroundColor="transparent">
|
||||
Archive
|
||||
Dismiss
|
||||
</StatelessAsyncAction>
|
||||
)}
|
||||
</Row>
|
||||
</Row>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@ -143,7 +148,6 @@ export function Notification(props: NotificationProps) {
|
||||
timebox={props.time}
|
||||
time={time}
|
||||
associations={associations}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
@ -168,27 +172,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}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
/>
|
||||
</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,87 +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="archive">
|
||||
Archive
|
||||
</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 === "archive" && (
|
||||
<Inbox
|
||||
{...props}
|
||||
archive={props.archivedNotifications}
|
||||
showArchive
|
||||
filter={filter.groups}
|
||||
/>
|
||||
)}
|
||||
{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>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
@ -12,6 +12,7 @@ import GlobalApi from '~/logic/api/global';
|
||||
import { uxToHex } from '~/logic/lib/util';
|
||||
import { S3State, BackgroundConfig } from '~/types';
|
||||
import { BackgroundPicker, BgType } from './BackgroundPicker';
|
||||
import useLocalState, { LocalState } from '~/logic/state/local';
|
||||
|
||||
const formSchema = Yup.object().shape({
|
||||
bgType: Yup.string()
|
||||
@ -33,15 +34,13 @@ interface FormSchema {
|
||||
|
||||
interface DisplayFormProps {
|
||||
api: GlobalApi;
|
||||
dark: boolean;
|
||||
background: BackgroundConfig;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
s3: S3State;
|
||||
}
|
||||
|
||||
export default function DisplayForm(props: DisplayFormProps) {
|
||||
const { api, background, hideAvatars, hideNicknames, s3 } = props;
|
||||
const { api, s3 } = props;
|
||||
|
||||
const { hideAvatars, hideNicknames, background, set: setLocalState } = useLocalState();
|
||||
|
||||
let bgColor, bgUrl;
|
||||
if (background?.type === 'url') {
|
||||
@ -72,10 +71,11 @@ export default function DisplayForm(props: DisplayFormProps) {
|
||||
? { type: 'url', url: values.bgUrl || '' }
|
||||
: undefined;
|
||||
|
||||
api.local.setBackground(bgConfig);
|
||||
api.local.hideAvatars(values.avatars);
|
||||
api.local.hideNicknames(values.nicknames);
|
||||
api.local.dehydrate();
|
||||
setLocalState((state: LocalState) => {
|
||||
state.background = bgConfig;
|
||||
state.hideAvatars = values.avatars;
|
||||
state.hideNicknames = values.nicknames;
|
||||
});
|
||||
actions.setSubmitting(false);
|
||||
}}
|
||||
>
|
||||
|
@ -8,7 +8,7 @@ import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { LocalUpdateRemoteContentPolicy } from "~/types/local-update";
|
||||
import useLocalState from "~/logic/state/local";
|
||||
|
||||
const formSchema = Yup.object().shape({
|
||||
imageShown: Yup.boolean(),
|
||||
@ -26,11 +26,12 @@ interface FormSchema {
|
||||
|
||||
interface RemoteContentFormProps {
|
||||
api: GlobalApi;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
}
|
||||
|
||||
export default function RemoteContentForm(props: RemoteContentFormProps) {
|
||||
const { api, remoteContentPolicy } = props;
|
||||
const { api } = props;
|
||||
const remoteContentPolicy = useLocalState(state => state.remoteContentPolicy);
|
||||
const setRemoteContentPolicy = useLocalState(state => state.set);
|
||||
const imageShown = remoteContentPolicy.imageShown;
|
||||
const audioShown = remoteContentPolicy.audioShown;
|
||||
const videoShown = remoteContentPolicy.videoShown;
|
||||
@ -47,13 +48,9 @@ export default function RemoteContentForm(props: RemoteContentFormProps) {
|
||||
} as FormSchema
|
||||
}
|
||||
onSubmit={(values, actions) => {
|
||||
api.local.setRemoteContentPolicy({
|
||||
imageShown: values.imageShown,
|
||||
audioShown: values.audioShown,
|
||||
videoShown: values.videoShown,
|
||||
oembedShown: values.oembedShown,
|
||||
setRemoteContentPolicy(state => {
|
||||
Object.assign(state.remoteContentPolicy, values);
|
||||
});
|
||||
api.local.dehydrate();
|
||||
actions.setSubmitting(false);
|
||||
}}
|
||||
>
|
||||
|
@ -13,12 +13,7 @@ type ProfileProps = StoreState & { api: GlobalApi; ship: string };
|
||||
|
||||
export default function Settings({
|
||||
api,
|
||||
s3,
|
||||
dark,
|
||||
hideAvatars,
|
||||
hideNicknames,
|
||||
background,
|
||||
remoteContentPolicy
|
||||
s3
|
||||
}: ProfileProps) {
|
||||
return (
|
||||
<Box
|
||||
@ -32,13 +27,9 @@ export default function Settings({
|
||||
>
|
||||
<DisplayForm
|
||||
api={api}
|
||||
dark={dark}
|
||||
hideNicknames={hideNicknames}
|
||||
hideAvatars={hideAvatars}
|
||||
background={background}
|
||||
s3={s3}
|
||||
/>
|
||||
<RemoteContentForm {...{ api, remoteContentPolicy }} />
|
||||
<RemoteContentForm api={api} />
|
||||
<S3Form api={api} s3={s3} />
|
||||
<SecuritySettings api={api} />
|
||||
</Box>
|
||||
|
@ -9,6 +9,7 @@ import { uxToHex, MOBILE_BROWSER_REGEX } from "~/logic/lib/util";
|
||||
|
||||
import Settings from "./components/settings";
|
||||
import { ContactCard } from "~/views/landscape/components/ContactCard";
|
||||
import useLocalState from "~/logic/state/local";
|
||||
|
||||
const SidebarItem = ({ children, view, current }) => {
|
||||
const selected = current === view;
|
||||
@ -32,7 +33,7 @@ const SidebarItem = ({ children, view, current }) => {
|
||||
backgroundColor={selected ? "washedGray" : "white"}
|
||||
>
|
||||
<Icon mr={2} display="inline-block" icon={icon(view)} color='black' />
|
||||
<Text color='black' fontSize={0}>
|
||||
<Text color='black'>
|
||||
{children}
|
||||
</Text>
|
||||
</Row>
|
||||
@ -42,10 +43,11 @@ const SidebarItem = ({ children, view, current }) => {
|
||||
|
||||
export default function ProfileScreen(props: any) {
|
||||
const { ship, dark } = props;
|
||||
const hideAvatars = useLocalState(state => state.hideAvatars);
|
||||
return (
|
||||
<>
|
||||
<Helmet defer={false}>
|
||||
<title>OS1 - Profile</title>
|
||||
<title>{ props.notificationsCount ? `(${String(props.notificationsCount) }) `: '' }Landscape - Profile</title>
|
||||
</Helmet>
|
||||
<Switch>
|
||||
<Route
|
||||
@ -65,7 +67,7 @@ export default function ProfileScreen(props: any) {
|
||||
history.replace("/~profile/identity");
|
||||
}
|
||||
|
||||
const image = (!props?.hideAvatars && contact?.avatar)
|
||||
const image = (!hideAvatars && contact?.avatar)
|
||||
? <BaseImage src={contact.avatar} width='100%' height='100%' style={{ objectFit: 'cover' }} />
|
||||
: <Sigil ship={`~${ship}`} size={80} color={sigilColor} />;
|
||||
return (
|
||||
@ -74,7 +76,7 @@ export default function ProfileScreen(props: any) {
|
||||
height="100%"
|
||||
width="100%"
|
||||
display="grid"
|
||||
gridTemplateColumns={["100%", "200px 1fr"]}
|
||||
gridTemplateColumns={["100%", "250px 1fr"]}
|
||||
gridTemplateRows={["48px 1fr", "1fr"]}
|
||||
borderRadius={1}
|
||||
bg="white"
|
||||
@ -93,8 +95,8 @@ export default function ProfileScreen(props: any) {
|
||||
bg={sigilColor}
|
||||
borderRadius={8}
|
||||
my={4}
|
||||
height={128}
|
||||
width={128}
|
||||
height={160}
|
||||
width={160}
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
@ -132,8 +134,6 @@ export default function ProfileScreen(props: any) {
|
||||
path="/~/default"
|
||||
api={props.api}
|
||||
s3={props.s3}
|
||||
hideAvatars={props.hideAvatars}
|
||||
hideNicknames={props.hideNicknames}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
@ -35,10 +35,7 @@ export function PublishResource(props: PublishResourceProps) {
|
||||
history={props.history}
|
||||
match={props.match}
|
||||
location={props.location}
|
||||
hideAvatars={props.hideAvatars}
|
||||
unreads={props.unreads}
|
||||
hideNicknames={props.hideNicknames}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
graphs={props.graphs}
|
||||
s3={props.s3}
|
||||
/>
|
||||
|
@ -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();
|
||||
|
@ -10,7 +10,7 @@ import { NoteNavigation } from "./NoteNavigation";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { getLatestRevision, getComments } from '~/logic/lib/publish';
|
||||
import Author from "~/views/components/Author";
|
||||
import { Contacts, GraphNode, Graph, LocalUpdateRemoteContentPolicy, Association, Unreads, Group } from "~/types";
|
||||
import { Contacts, GraphNode, Graph, Association, Unreads, Group } from "~/types";
|
||||
|
||||
interface NoteProps {
|
||||
ship: string;
|
||||
@ -21,9 +21,6 @@ interface NoteProps {
|
||||
notebook: Graph;
|
||||
contacts: Contacts;
|
||||
api: GlobalApi;
|
||||
hideAvatars: boolean;
|
||||
hideNicknames: boolean;
|
||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||
rootUrl: string;
|
||||
baseUrl: string;
|
||||
group: Group;
|
||||
@ -45,12 +42,14 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
|
||||
const comments = getComments(note);
|
||||
const [revNum, title, body, post] = getLatestRevision(note);
|
||||
const index = note.post.index.split('/');
|
||||
|
||||
const noteId = bigInt(index[1]);
|
||||
useEffect(() => {
|
||||
api.hark.markEachAsRead(props.association, '/', post.index, 'note', 'publish');
|
||||
}, [props.association]);
|
||||
api.hark.markEachAsRead(props.association, '/',`/${index[1]}/1/1`, 'note', 'publish');
|
||||
}, [props.association, props.note]);
|
||||
|
||||
|
||||
const noteId = bigInt(note.post.index.split('/')[1]);
|
||||
|
||||
let adminLinks: JSX.Element | null = null;
|
||||
if (window.ship === note?.post?.author) {
|
||||
@ -68,7 +67,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
color="red"
|
||||
ml={2}
|
||||
onClick={deletePost}
|
||||
css={{ cursor: "pointer" }}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
Delete
|
||||
</Text>
|
||||
@ -76,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}
|
||||
@ -87,6 +93,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
width="100%"
|
||||
gridRowGap={4}
|
||||
mx="auto"
|
||||
ref={windowRef}
|
||||
>
|
||||
<Link to={rootUrl}>
|
||||
<Text>{"<- Notebook Index"}</Text>
|
||||
@ -95,8 +102,6 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
<Text display="block" mb={2}>{title || ""}</Text>
|
||||
<Box display="flex">
|
||||
<Author
|
||||
hideNicknames={props?.hideNicknames}
|
||||
hideAvatars={props?.hideAvatars}
|
||||
ship={post?.author}
|
||||
contacts={contacts}
|
||||
date={post?.["time-sent"]}
|
||||
@ -121,9 +126,6 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
contacts={props.contacts}
|
||||
association={props.association}
|
||||
api={props.api}
|
||||
hideNicknames={props.hideNicknames}
|
||||
hideAvatars={props.hideAvatars}
|
||||
remoteContentPolicy={props.remoteContentPolicy}
|
||||
baseUrl={baseUrl}
|
||||
editCommentId={editCommentId}
|
||||
history={props.history}
|
||||
|
@ -19,8 +19,6 @@ interface NotePreviewProps {
|
||||
host: string;
|
||||
book: string;
|
||||
node: GraphNode;
|
||||
hideAvatars?: boolean;
|
||||
hideNicknames?: boolean;
|
||||
baseUrl: string;
|
||||
unreads: Unreads;
|
||||
contacts: Contacts;
|
||||
@ -33,7 +31,7 @@ const WrappedBox = styled(Box)`
|
||||
`;
|
||||
|
||||
export function NotePreview(props: NotePreviewProps) {
|
||||
const { node, contacts, hideAvatars, hideNicknames, group } = props;
|
||||
const { node, contacts, group } = props;
|
||||
const { post } = node;
|
||||
if (!post) {
|
||||
return null;
|
||||
@ -45,7 +43,7 @@ export function NotePreview(props: NotePreviewProps) {
|
||||
|
||||
const [rev, title, body, content] = getLatestRevision(node);
|
||||
const appPath = `/ship/${props.host}/${props.book}`;
|
||||
const isUnread = props.unreads.graph?.[appPath]?.['/']?.unreads?.has(content.index);
|
||||
const isUnread = props.unreads.graph?.[appPath]?.['/']?.unreads?.has(`/${noteId}/1/1`);
|
||||
|
||||
const snippet = getSnippet(body);
|
||||
|
||||
@ -63,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']}
|
||||
@ -84,8 +82,6 @@ export function NotePreview(props: NotePreviewProps) {
|
||||
contacts={contacts}
|
||||
ship={post?.author}
|
||||
date={post?.['time-sent']}
|
||||
hideAvatars={hideAvatars || false}
|
||||
hideNicknames={hideNicknames || false}
|
||||
group={group}
|
||||
unread={isUnread}
|
||||
api={props.api}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user