mirror of
https://github.com/urbit/shrub.git
synced 2024-12-19 08:32:39 +03:00
Merge branch 'release/next-js' into release/next-userspace
This commit is contained in:
commit
0a71fb89e2
@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1d56b7351a347a65c06999955114f196523a86c853390d5d1822a90a606619d6
|
oid sha256:f6b5e33e573818120051651c1182163527edbbe0dff0eb6591e12a55cfccb273
|
||||||
size 10357558
|
size 10486101
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{ urbit, libcap, coreutils, bashInteractive, dockerTools, writeScriptBin, amesPort ? 34343 }:
|
{ urbit, curl, libcap, coreutils, bashInteractive, dockerTools, writeScriptBin, amesPort ? 34343 }:
|
||||||
let
|
let
|
||||||
startUrbit = writeScriptBin "start-urbit" ''
|
startUrbit = writeScriptBin "start-urbit" ''
|
||||||
#!${bashInteractive}/bin/bash
|
#!${bashInteractive}/bin/bash
|
||||||
@ -58,12 +58,42 @@ let
|
|||||||
|
|
||||||
exec urbit $ttyflag -p $amesPort $dirname
|
exec urbit $ttyflag -p $amesPort $dirname
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
getUrbitCode = writeScriptBin "get-urbit-code" ''
|
||||||
|
#!${bashInteractive}/bin/bash
|
||||||
|
|
||||||
|
raw=$(curl -s -X POST -H "Content-Type: application/json" \
|
||||||
|
-d '{ "source": { "dojo": "+code" }, "sink": { "stdout": null } }' \
|
||||||
|
http://127.0.0.1:12321)
|
||||||
|
|
||||||
|
# trim \n" from the end
|
||||||
|
trim="''${raw%\\n\"}"
|
||||||
|
|
||||||
|
# trim " from the start
|
||||||
|
code="''${trim#\"}"
|
||||||
|
|
||||||
|
echo "$code"
|
||||||
|
'';
|
||||||
|
|
||||||
|
resetUrbitCode = writeScriptBin "reset-urbit-code" ''
|
||||||
|
#!${bashInteractive}/bin/bash
|
||||||
|
|
||||||
|
curl=$(curl -s -X POST -H "Content-Type: application/json" \
|
||||||
|
-d '{ "source": { "dojo": "+hood/code %reset" }, "sink": { "app": "hood" } }' \
|
||||||
|
http://127.0.0.1:12321)
|
||||||
|
|
||||||
|
if [[ $? -eq 0 ]]
|
||||||
|
then
|
||||||
|
echo "OK"
|
||||||
|
else
|
||||||
|
echo "Curl error: $?"
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
in dockerTools.buildImage {
|
in dockerTools.buildImage {
|
||||||
name = "urbit";
|
name = "urbit";
|
||||||
tag = "v${urbit.version}";
|
tag = "v${urbit.version}";
|
||||||
contents = [ bashInteractive urbit startUrbit coreutils ];
|
contents = [ bashInteractive urbit curl startUrbit getUrbitCode resetUrbitCode coreutils ];
|
||||||
runAsRoot = ''
|
runAsRoot = ''
|
||||||
#!${bashInteractive}
|
#!${bashInteractive}
|
||||||
mkdir -p /urbit
|
mkdir -p /urbit
|
||||||
|
@ -126,6 +126,14 @@
|
|||||||
!=(contact(last-updated *@da) u.old(last-updated *@da))
|
!=(contact(last-updated *@da) u.old(last-updated *@da))
|
||||||
==
|
==
|
||||||
[~ state]
|
[~ state]
|
||||||
|
~| "cannot add a data url to cover!"
|
||||||
|
?> ?| ?=(~ cover.contact)
|
||||||
|
!=('data:' (cut 3 [0 5] u.cover.contact))
|
||||||
|
==
|
||||||
|
~| "cannot add a data url to avatar!"
|
||||||
|
?> ?| ?=(~ avatar.contact)
|
||||||
|
!=('data:' (cut 3 [0 5] u.avatar.contact))
|
||||||
|
==
|
||||||
:- (send-diff [%add ship contact] =(ship our.bowl))
|
:- (send-diff [%add ship contact] =(ship our.bowl))
|
||||||
state(rolodex (~(put by rolodex) ship contact))
|
state(rolodex (~(put by rolodex) ship contact))
|
||||||
::
|
::
|
||||||
@ -149,6 +157,14 @@
|
|||||||
=/ contact (edit-contact old edit-field)
|
=/ contact (edit-contact old edit-field)
|
||||||
?: =(old contact)
|
?: =(old contact)
|
||||||
[~ state]
|
[~ state]
|
||||||
|
~| "cannot add a data url to cover!"
|
||||||
|
?> ?| ?=(~ cover.contact)
|
||||||
|
!=('data:' (cut 3 [0 5] u.cover.contact))
|
||||||
|
==
|
||||||
|
~| "cannot add a data url to avatar!"
|
||||||
|
?> ?| ?=(~ avatar.contact)
|
||||||
|
!=('data:' (cut 3 [0 5] u.avatar.contact))
|
||||||
|
==
|
||||||
=. last-updated.contact timestamp
|
=. last-updated.contact timestamp
|
||||||
:- (send-diff [%edit ship edit-field timestamp] =(ship our.bowl))
|
:- (send-diff [%edit ship edit-field timestamp] =(ship our.bowl))
|
||||||
state(rolodex (~(put by rolodex) ship contact))
|
state(rolodex (~(put by rolodex) ship contact))
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
/- glob
|
/- glob
|
||||||
/+ default-agent, verb, dbug
|
/+ default-agent, verb, dbug
|
||||||
|%
|
|%
|
||||||
++ hash 0v6.qafur.17301.j8obh.vbepn.7tq3l
|
++ hash 0v3.g6u13.haedt.jt4hd.61ek5.6t30q
|
||||||
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
||||||
+$ all-states
|
+$ all-states
|
||||||
$% state-0
|
$% state-0
|
||||||
|
@ -24,6 +24,6 @@
|
|||||||
<div id="portal-root"></div>
|
<div id="portal-root"></div>
|
||||||
<script src="/~landscape/js/channel.js"></script>
|
<script src="/~landscape/js/channel.js"></script>
|
||||||
<script src="/~landscape/js/session.js"></script>
|
<script src="/~landscape/js/session.js"></script>
|
||||||
<script src="/~landscape/js/bundle/index.fd3d400454968e081ca9.js"></script>
|
<script src="/~landscape/js/bundle/index.59e682153138f604d358.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
:: /app-name/%app-name associations for app
|
:: /app-name/%app-name associations for app
|
||||||
:: /group/%path associations for group
|
:: /group/%path associations for group
|
||||||
::
|
::
|
||||||
/- store=metadata-store
|
/- store=metadata-store, pull-hook
|
||||||
/+ default-agent, verb, dbug, resource, *migrate
|
/+ default-agent, verb, dbug, resource, *migrate
|
||||||
|%
|
|%
|
||||||
+$ card card:agent:gall
|
+$ card card:agent:gall
|
||||||
@ -95,16 +95,17 @@
|
|||||||
~
|
~
|
||||||
==
|
==
|
||||||
::
|
::
|
||||||
+$ state-0 [%0 base-state-0]
|
+$ state-0 [%0 base-state-0]
|
||||||
+$ state-1 [%1 base-state-0]
|
+$ state-1 [%1 base-state-0]
|
||||||
+$ state-2 [%2 base-state-0]
|
+$ state-2 [%2 base-state-0]
|
||||||
+$ state-3 [%3 base-state-1]
|
+$ state-3 [%3 base-state-1]
|
||||||
+$ state-4 [%4 base-state-1]
|
+$ state-4 [%4 base-state-1]
|
||||||
+$ state-5 [%5 base-state-1]
|
+$ state-5 [%5 base-state-1]
|
||||||
+$ state-6 [%6 base-state-1]
|
+$ state-6 [%6 base-state-1]
|
||||||
+$ state-7 [%7 base-state-2]
|
+$ state-7 [%7 base-state-2]
|
||||||
+$ state-8 [%8 base-state-3]
|
+$ state-8 [%8 base-state-3]
|
||||||
+$ state-9 [%9 base-state-3]
|
+$ state-9 [%9 base-state-3]
|
||||||
|
+$ state-10 [%10 base-state-3]
|
||||||
+$ versioned-state
|
+$ versioned-state
|
||||||
$% state-0
|
$% state-0
|
||||||
state-1
|
state-1
|
||||||
@ -116,10 +117,11 @@
|
|||||||
state-7
|
state-7
|
||||||
state-8
|
state-8
|
||||||
state-9
|
state-9
|
||||||
|
state-10
|
||||||
==
|
==
|
||||||
::
|
::
|
||||||
+$ inflated-state
|
+$ inflated-state
|
||||||
$: state-9
|
$: state-10
|
||||||
cached-indices
|
cached-indices
|
||||||
==
|
==
|
||||||
--
|
--
|
||||||
@ -232,7 +234,7 @@
|
|||||||
=| cards=(list card)
|
=| cards=(list card)
|
||||||
|^
|
|^
|
||||||
=* loop $
|
=* loop $
|
||||||
?: ?=(%9 -.old)
|
?: ?=(%10 -.old)
|
||||||
:- cards
|
:- cards
|
||||||
%_ state
|
%_ state
|
||||||
associations associations.old
|
associations associations.old
|
||||||
@ -240,7 +242,7 @@
|
|||||||
group-indices (rebuild-group-indices associations.old)
|
group-indices (rebuild-group-indices associations.old)
|
||||||
app-indices (rebuild-app-indices associations.old)
|
app-indices (rebuild-app-indices associations.old)
|
||||||
==
|
==
|
||||||
?: ?=(%8 -.old)
|
?: ?=(%9 -.old)
|
||||||
=/ groups
|
=/ groups
|
||||||
(fall (~(get by (rebuild-app-indices associations.old)) %groups) ~)
|
(fall (~(get by (rebuild-app-indices associations.old)) %groups) ~)
|
||||||
=/ pokes=(list card)
|
=/ pokes=(list card)
|
||||||
@ -252,13 +254,17 @@
|
|||||||
?. ?=([%group [~ [~ [@ [@ @]]]]] config.met)
|
?. ?=([%group [~ [~ [@ [@ @]]]]] config.met)
|
||||||
~
|
~
|
||||||
=* res resource.u.u.feed.config.met
|
=* res resource.u.u.feed.config.met
|
||||||
|
?: =(our.bowl entity.res) ~
|
||||||
=- `[%pass /fix-feed %agent [our.bowl %graph-pull-hook] %poke -]
|
=- `[%pass /fix-feed %agent [our.bowl %graph-pull-hook] %poke -]
|
||||||
:- %pull-hook-action
|
:- %pull-hook-action
|
||||||
!> [%add entity.res name.res]
|
!> ^- action:pull-hook
|
||||||
|
[%add entity.res res]
|
||||||
%_ $
|
%_ $
|
||||||
cards (weld cards pokes)
|
cards (weld cards pokes)
|
||||||
-.old %9
|
-.old %10
|
||||||
==
|
==
|
||||||
|
?: ?=(%8 -.old)
|
||||||
|
$(-.old %9)
|
||||||
?: ?=(%7 -.old)
|
?: ?=(%7 -.old)
|
||||||
$(old [%8 (associations-2-to-3 associations.old) ~])
|
$(old [%8 (associations-2-to-3 associations.old) ~])
|
||||||
?: ?=(%6 -.old)
|
?: ?=(%6 -.old)
|
||||||
|
@ -249,6 +249,7 @@
|
|||||||
font-family: "Source Code Pro";
|
font-family: "Source Code Pro";
|
||||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff");
|
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff");
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
}
|
}
|
||||||
:root {
|
:root {
|
||||||
--red05: rgba(255,65,54,0.05);
|
--red05: rgba(255,65,54,0.05);
|
||||||
|
@ -29,6 +29,51 @@ The image includes `EXPOSE` directives for TCP port 80 and UDP port 34343. Port
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
You should be able to use port mapping for most purposes but you can force Ames to use a custom port.
|
||||||
|
`--port=$AMES_PORT` can be passed as an argument to the `docker start` command. Passing `--port=13436` for example, would use port 13436.
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
Creating a volume for ~sampel=palnet:
|
||||||
|
```
|
||||||
|
docker volume create sampel-palnet
|
||||||
|
```
|
||||||
|
|
||||||
|
Copying key to sampel-palnet's volume (assumes default docker location)
|
||||||
|
```
|
||||||
|
sudo cp ~/sampel-palnet.key /var/lib/docker/volumes/sampel-palnet/_data/sampel-palnet.key
|
||||||
|
```
|
||||||
|
|
||||||
|
Using that volume and launching ~sampel-palnet on host port 8080 with Ames talking on host port 27000:
|
||||||
|
```
|
||||||
|
docker run -d -p 8080:80 -p 27000:34343/udp --name sampel-palnet \
|
||||||
|
--mount type=volume,source=sampel-palnet,destination=/urbit \
|
||||||
|
tloncorp/urbit
|
||||||
|
```
|
||||||
|
|
||||||
|
Using host port 8088 with Ames talking on host port 23232 while forcing Ames to start internally on port 13436:
|
||||||
|
```
|
||||||
|
docker run -d -p 8088:80 -p 23232:13436/udp --name sampel-palnet \
|
||||||
|
--mount type=volume,source=sampel-palnet,destination=/urbit \
|
||||||
|
tloncorp/urbit --port=13436
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting and resetting the Landscape +code
|
||||||
|
This docker image includes tools for retrieving and resetting the Landscape login code belonging to the planet, for programmatic use so the container does not need a tty. These scripts can be called using `docker container exec`.
|
||||||
|
|
||||||
|
Getting the code:
|
||||||
|
```
|
||||||
|
$ docker container exec sampel-palnet /bin/get-urbit-code
|
||||||
|
sampel-sampel-sampel-sampel
|
||||||
|
```
|
||||||
|
|
||||||
|
Resetting the code:
|
||||||
|
```
|
||||||
|
$ docker container exec sampel-palnet /bin/reset-urbit-code
|
||||||
|
OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the code has been reset the new code can be obtained from `/bin/get-urbit-code`.
|
||||||
|
|
||||||
## Extending
|
## 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.
|
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.
|
||||||
|
@ -111,7 +111,7 @@ module.exports = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light)\/).*/
|
exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light|@tlon\/indigo-react)\/).*/
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/i,
|
test: /\.css$/i,
|
||||||
|
@ -30,7 +30,7 @@ module.exports = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light)\/).*/
|
exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light|@tlon\/indigo-react)\/).*/
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/i,
|
test: /\.css$/i,
|
||||||
|
3890
pkg/interface/package-lock.json
generated
3890
pkg/interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,8 +9,8 @@
|
|||||||
"@reach/menu-button": "^0.10.5",
|
"@reach/menu-button": "^0.10.5",
|
||||||
"@reach/tabs": "^0.10.5",
|
"@reach/tabs": "^0.10.5",
|
||||||
"@tlon/indigo-dark": "^1.0.6",
|
"@tlon/indigo-dark": "^1.0.6",
|
||||||
"@tlon/indigo-light": "^1.0.6",
|
"@tlon/indigo-light": "^1.0.7",
|
||||||
"@tlon/indigo-react": "^1.2.19",
|
"@tlon/indigo-react": "^1.2.21",
|
||||||
"@tlon/sigil-js": "^1.4.3",
|
"@tlon/sigil-js": "^1.4.3",
|
||||||
"@urbit/api": "file:../npm/api",
|
"@urbit/api": "file:../npm/api",
|
||||||
"any-ascii": "^0.1.7",
|
"any-ascii": "^0.1.7",
|
||||||
@ -88,7 +88,7 @@
|
|||||||
"react-hot-loader": "^4.13.0",
|
"react-hot-loader": "^4.13.0",
|
||||||
"sass": "^1.32.5",
|
"sass": "^1.32.5",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"typescript": "^3.9.7",
|
"typescript": "^4.2.4",
|
||||||
"webpack": "^4.46.0",
|
"webpack": "^4.46.0",
|
||||||
"webpack-cli": "^3.3.12",
|
"webpack-cli": "^3.3.12",
|
||||||
"webpack-dev-server": "^3.11.2"
|
"webpack-dev-server": "^3.11.2"
|
||||||
|
@ -4,6 +4,11 @@ import { dateToDa, decToUd } from '../lib/util';
|
|||||||
import { NotifIndex, IndexedNotification, Association, GraphNotifDescription } from '@urbit/api';
|
import { NotifIndex, IndexedNotification, Association, GraphNotifDescription } from '@urbit/api';
|
||||||
import { BigInteger } from 'big-integer';
|
import { BigInteger } from 'big-integer';
|
||||||
import { getParentIndex } from '../lib/notification';
|
import { getParentIndex } from '../lib/notification';
|
||||||
|
import useHarkState from '../state/hark';
|
||||||
|
|
||||||
|
function getHarkSize() {
|
||||||
|
return useHarkState.getState().notifications.size ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
export class HarkApi extends BaseApi<StoreState> {
|
export class HarkApi extends BaseApi<StoreState> {
|
||||||
private harkAction(action: any): Promise<any> {
|
private harkAction(action: any): Promise<any> {
|
||||||
@ -172,10 +177,10 @@ export class HarkApi extends BaseApi<StoreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getMore(): Promise<boolean> {
|
async getMore(): Promise<boolean> {
|
||||||
const offset = this.store.state['notifications']?.size || 0;
|
const offset = getHarkSize();
|
||||||
const count = 3;
|
const count = 3;
|
||||||
await this.getSubset(offset, count, false);
|
await this.getSubset(offset, count, false);
|
||||||
return offset === (this.store.state.notifications?.size || 0);
|
return offset === getHarkSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSubset(offset:number, count:number, isArchive: boolean) {
|
async getSubset(offset:number, count:number, isArchive: boolean) {
|
||||||
|
@ -1,234 +0,0 @@
|
|||||||
import bigInt, { BigInteger } from 'big-integer';
|
|
||||||
import { immerable } from 'immer';
|
|
||||||
|
|
||||||
interface NonemptyNode<V> {
|
|
||||||
n: [BigInteger, V];
|
|
||||||
l: MapNode<V>;
|
|
||||||
r: MapNode<V>;
|
|
||||||
}
|
|
||||||
|
|
||||||
type MapNode<V> = NonemptyNode<V> | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An implementation of ordered maps for JS
|
|
||||||
* Plagiarised wholesale from sys/zuse
|
|
||||||
*/
|
|
||||||
export class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
|
||||||
private root: MapNode<V> = null;
|
|
||||||
[immerable] = true;
|
|
||||||
size = 0;
|
|
||||||
|
|
||||||
constructor(initial: [BigInteger, V][] = []) {
|
|
||||||
initial.forEach(([key, val]) => {
|
|
||||||
this.set(key, val);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve an value for a key
|
|
||||||
*/
|
|
||||||
get(key: BigInteger): V | null {
|
|
||||||
const inner = (node: MapNode<V>) => {
|
|
||||||
if (!node) {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
const [k, v] = node.n;
|
|
||||||
if (key.eq(k)) {
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
|
||||||
return inner(node.l);
|
|
||||||
} else {
|
|
||||||
return inner(node.r);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Put an item by a key
|
|
||||||
*/
|
|
||||||
set(key: BigInteger, value: V): void {
|
|
||||||
const inner = (node: MapNode<V>) => {
|
|
||||||
if (!node) {
|
|
||||||
return {
|
|
||||||
n: [key, value],
|
|
||||||
l: null,
|
|
||||||
r: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const [k] = node.n;
|
|
||||||
if (key.eq(k)) {
|
|
||||||
this.size--;
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
n: [k, value]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
|
||||||
const l = inner(node.l);
|
|
||||||
if (!l) {
|
|
||||||
throw new Error('invariant violation');
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
l
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const r = inner(node.r);
|
|
||||||
if (!r) {
|
|
||||||
throw new Error('invariant violation');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...node, r };
|
|
||||||
};
|
|
||||||
this.size++;
|
|
||||||
this.root = inner(this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all entries
|
|
||||||
*/
|
|
||||||
clear() {
|
|
||||||
this.root = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Predicate testing if map contains key
|
|
||||||
*/
|
|
||||||
has(key: BigInteger): boolean {
|
|
||||||
const inner = (node: MapNode<V>) => {
|
|
||||||
if (!node) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const [k] = node.n;
|
|
||||||
|
|
||||||
if (k.eq(key)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
|
||||||
return inner(node.l);
|
|
||||||
}
|
|
||||||
return inner(node.r);
|
|
||||||
};
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove value associated with key, returning whether that key
|
|
||||||
* existed in the first place
|
|
||||||
*/
|
|
||||||
delete(key: BigInteger) {
|
|
||||||
const inner = (node: MapNode<V>): [boolean, MapNode<V>] => {
|
|
||||||
if (!node) {
|
|
||||||
return [false, null];
|
|
||||||
}
|
|
||||||
const [k] = node.n;
|
|
||||||
if (k.eq(key)) {
|
|
||||||
return [true, this.nip(node)];
|
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
|
||||||
const [bool, l] = inner(node.l);
|
|
||||||
return [
|
|
||||||
bool,
|
|
||||||
{
|
|
||||||
...node,
|
|
||||||
l
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const [bool, r] = inner(node.r);
|
|
||||||
return [
|
|
||||||
bool,
|
|
||||||
{
|
|
||||||
...node,
|
|
||||||
r
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
const [ret, newRoot] = inner(this.root);
|
|
||||||
if(ret) {
|
|
||||||
this.size--;
|
|
||||||
}
|
|
||||||
this.root = newRoot;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private nip(nod: NonemptyNode<V>): MapNode<V> {
|
|
||||||
const inner = (node: NonemptyNode<V>) => {
|
|
||||||
if (!node.l) {
|
|
||||||
return node.r;
|
|
||||||
}
|
|
||||||
if (!node.r) {
|
|
||||||
return node.l;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...node.l,
|
|
||||||
r: inner(node.r)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return inner(nod);
|
|
||||||
}
|
|
||||||
|
|
||||||
peekLargest(): [BigInteger, V] | undefined {
|
|
||||||
const inner = (node: MapNode<V>) => {
|
|
||||||
if(!node) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if(node.l) {
|
|
||||||
return inner(node.l);
|
|
||||||
}
|
|
||||||
return node.n;
|
|
||||||
};
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
peekSmallest(): [BigInteger, V] | undefined {
|
|
||||||
const inner = (node: MapNode<V>) => {
|
|
||||||
if(!node) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if(node.r) {
|
|
||||||
return inner(node.r);
|
|
||||||
}
|
|
||||||
return node.n;
|
|
||||||
};
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
keys(): BigInteger[] {
|
|
||||||
const list = Array.from(this);
|
|
||||||
return list.map(([key]) => key);
|
|
||||||
}
|
|
||||||
|
|
||||||
forEach(f: (value: V, key: BigInteger) => void) {
|
|
||||||
const list = Array.from(this);
|
|
||||||
return list.forEach(([k,v]) => f(v,k));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Symbol.iterator](): IterableIterator<[BigInteger, V]> {
|
|
||||||
const result: [BigInteger, V][] = [];
|
|
||||||
const inner = (node: MapNode<V>) => {
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
inner(node.l);
|
|
||||||
result.push(node.n);
|
|
||||||
inner(node.r);
|
|
||||||
};
|
|
||||||
inner(this.root);
|
|
||||||
|
|
||||||
let idx = 0;
|
|
||||||
return {
|
|
||||||
[Symbol.iterator]: this[Symbol.iterator],
|
|
||||||
next: (): IteratorResult<[BigInteger, V]> => {
|
|
||||||
if (idx < result.length) {
|
|
||||||
return { value: result[idx++], done: false };
|
|
||||||
}
|
|
||||||
return { done: true, value: null };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
18
pkg/interface/src/logic/lib/formGroup.ts
Normal file
18
pkg/interface/src/logic/lib/formGroup.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export type SubmitHandler = () => Promise<any>;
|
||||||
|
interface IFormGroupContext {
|
||||||
|
addSubmit: (id: string, submit: SubmitHandler) => void;
|
||||||
|
onDirty: (id: string, touched: boolean) => void;
|
||||||
|
onErrors: (id: string, errors: boolean) => void;
|
||||||
|
submitAll: () => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallback: IFormGroupContext = {
|
||||||
|
addSubmit: () => {},
|
||||||
|
onDirty: () => {},
|
||||||
|
onErrors: () => {},
|
||||||
|
submitAll: () => Promise.resolve(),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FormGroupContext = React.createContext(fallback);
|
@ -1,6 +1,6 @@
|
|||||||
import bigInt, { BigInteger } from 'big-integer';
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
import f from 'lodash/fp';
|
import f from 'lodash/fp';
|
||||||
import { Unreads } from '@urbit/api';
|
import { Unreads, NotificationGraphConfig } from '@urbit/api';
|
||||||
|
|
||||||
export function getLastSeen(
|
export function getLastSeen(
|
||||||
unreads: Unreads,
|
unreads: Unreads,
|
||||||
@ -34,3 +34,13 @@ export function getNotificationCount(
|
|||||||
.map(index => unread[index]?.notifications?.length || 0)
|
.map(index => unread[index]?.notifications?.length || 0)
|
||||||
.reduce(f.add, 0);
|
.reduce(f.add, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isWatching(
|
||||||
|
config: NotificationGraphConfig,
|
||||||
|
graph: string,
|
||||||
|
index = "/"
|
||||||
|
) {
|
||||||
|
return !!config.watching.find(
|
||||||
|
watch => watch.graph === graph && watch.index === index
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -4,3 +4,7 @@ const ua = window.navigator.userAgent;
|
|||||||
export const IS_IOS = ua.includes('iPhone');
|
export const IS_IOS = ua.includes('iPhone');
|
||||||
|
|
||||||
export const IS_SAFARI = ua.includes('Safari') && !ua.includes('Chrome');
|
export const IS_SAFARI = ua.includes('Safari') && !ua.includes('Chrome');
|
||||||
|
|
||||||
|
export const IS_ANDROID = ua.includes('Android');
|
||||||
|
|
||||||
|
export const IS_MOBILE = IS_IOS || IS_ANDROID;
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { Post, GraphNode, TextContent, Graph, NodeMap } from '@urbit/api';
|
import { Post, GraphNode, TextContent } from '@urbit/api';
|
||||||
import { buntPost } from '~/logic/lib/post';
|
import { buntPost } from '~/logic/lib/post';
|
||||||
import { unixToDa } from '~/logic/lib/util';
|
import { unixToDa } from '~/logic/lib/util';
|
||||||
import { BigIntOrderedMap } from './BigIntOrderedMap';
|
import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap";
|
||||||
import bigInt, { BigInteger } from 'big-integer';
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
|
|
||||||
export function newPost(
|
export function newPost(
|
||||||
title: string,
|
title: string,
|
||||||
body: string
|
body: string
|
||||||
): [BigInteger, NodeMap] {
|
): [BigInteger, any] {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const nowDa = unixToDa(now);
|
const nowDa = unixToDa(now);
|
||||||
const root: Post = {
|
const root: Post = {
|
||||||
@ -73,13 +73,16 @@ export function editPost(rev: number, noteId: BigInteger, title: string, body: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getLatestRevision(node: GraphNode): [number, string, string, Post] {
|
export function getLatestRevision(node: GraphNode): [number, string, string, Post] {
|
||||||
const revs = node.children.get(bigInt(1));
|
const revs = node.children?.get(bigInt(1));
|
||||||
const empty = [1, '', '', buntPost()] as [number, string, string, Post];
|
const empty = [1, '', '', buntPost()] as [number, string, string, Post];
|
||||||
if(!revs) {
|
if(!revs) {
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
const [revNum, rev] = [...revs.children][0];
|
let revNum, rev;
|
||||||
if(!rev) {
|
if (revs?.children !== null) {
|
||||||
|
[revNum, rev] = [...revs.children][0];
|
||||||
|
}
|
||||||
|
if (!rev) {
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
const [title, body] = rev.post.contents as TextContent[];
|
const [title, body] = rev.post.contents as TextContent[];
|
||||||
@ -88,18 +91,22 @@ export function getLatestRevision(node: GraphNode): [number, string, string, Pos
|
|||||||
|
|
||||||
export function getLatestCommentRevision(node: GraphNode): [number, Post] {
|
export function getLatestCommentRevision(node: GraphNode): [number, Post] {
|
||||||
const empty = [1, buntPost()] as [number, Post];
|
const empty = [1, buntPost()] as [number, Post];
|
||||||
if (node.children.size <= 0) {
|
const childSize = node?.children?.size ?? 0;
|
||||||
|
if (childSize <= 0) {
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
const [revNum, rev] = [...node.children][0];
|
let revNum, rev;
|
||||||
if(!rev) {
|
if (node?.children !== null) {
|
||||||
|
[revNum, rev] = [...node.children][0];
|
||||||
|
}
|
||||||
|
if (!rev) {
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
return [revNum.toJSNumber(), rev.post];
|
return [revNum.toJSNumber(), rev.post];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getComments(node: GraphNode): GraphNode {
|
export function getComments(node: GraphNode): GraphNode {
|
||||||
const comments = node.children.get(bigInt(2));
|
const comments = node.children?.get(bigInt(2));
|
||||||
if(!comments) {
|
if(!comments) {
|
||||||
return { post: buntPost(), children: new BigIntOrderedMap() };
|
return { post: buntPost(), children: new BigIntOrderedMap() };
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { TutorialProgress, Associations } from '@urbit/api';
|
import { Associations } from '@urbit/api';
|
||||||
|
import { TutorialProgress } from '~/types';
|
||||||
import { AlignX, AlignY } from '~/logic/lib/relativePosition';
|
import { AlignX, AlignY } from '~/logic/lib/relativePosition';
|
||||||
import { Direction } from '~/views/components/Triangle';
|
import { Direction } from '~/views/components/Triangle';
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ interface StepDetail {
|
|||||||
alignY: AlignY | AlignY[];
|
alignY: AlignY | AlignY[];
|
||||||
offsetX: number;
|
offsetX: number;
|
||||||
offsetY: number;
|
offsetY: number;
|
||||||
arrow: Direction;
|
arrow?: Direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasTutorialGroup(props: { associations: Associations }) {
|
export function hasTutorialGroup(props: { associations: Associations }) {
|
||||||
|
@ -15,7 +15,8 @@ function retrieve<T>(key: string, initial: T): T {
|
|||||||
interface SetStateFunc<T> {
|
interface SetStateFunc<T> {
|
||||||
(t: T): T;
|
(t: T): T;
|
||||||
}
|
}
|
||||||
type SetState<T> = T | SetStateFunc<T>;
|
// See microsoft/typescript#37663 for filed bug
|
||||||
|
type SetState<T> = T extends any ? SetStateFunc<T> : never;
|
||||||
export function useLocalStorageState<T>(key: string, initial: T) {
|
export function useLocalStorageState<T>(key: string, initial: T) {
|
||||||
const [state, _setState] = useState(() => retrieve(key, initial));
|
const [state, _setState] = useState(() => retrieve(key, initial));
|
||||||
|
|
||||||
|
@ -2,18 +2,14 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
SyntheticEvent,
|
|
||||||
useMemo,
|
useMemo,
|
||||||
useEffect,
|
|
||||||
useRef
|
useRef
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { Box } from '@tlon/indigo-react';
|
import { Box } from '@tlon/indigo-react';
|
||||||
import { useOutsideClick } from './useOutsideClick';
|
|
||||||
import { ModalOverlay } from '~/views/components/ModalOverlay';
|
import { ModalOverlay } from '~/views/components/ModalOverlay';
|
||||||
import { Portal } from '~/views/components/Portal';
|
import { Portal } from '~/views/components/Portal';
|
||||||
import { ModalPortal } from '~/views/components/ModalPortal';
|
import { PropFunc } from '~/types';
|
||||||
import { PropFunc } from '@urbit/api';
|
|
||||||
|
|
||||||
type ModalFunc = (dismiss: () => void) => JSX.Element;
|
type ModalFunc = (dismiss: () => void) => JSX.Element;
|
||||||
interface UseModalProps {
|
interface UseModalProps {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Primitive } from '@urbit/api';
|
import { Primitive } from '~/types';
|
||||||
|
|
||||||
export default function usePreviousValue<T extends Primitive>(value: T): T {
|
export default function usePreviousValue<T extends Primitive>(value: T): T {
|
||||||
const prev = useRef<T | null>(null);
|
const prev = useRef<T | null>(null);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useWaitForProps } from "./useWaitForProps";
|
|
||||||
import {unstable_batchedUpdates} from "react-dom";
|
import {unstable_batchedUpdates} from "react-dom";
|
||||||
|
|
||||||
export type IOInstance<I, P, O> = (
|
export type IOInstance<I, P, O> = (
|
||||||
@ -10,7 +9,7 @@ export function useRunIO<I, O>(
|
|||||||
io: (i: I) => Promise<O>,
|
io: (i: I) => Promise<O>,
|
||||||
after: (o: O) => void,
|
after: (o: O) => void,
|
||||||
key: string
|
key: string
|
||||||
): () => Promise<void> {
|
): (i: I) => Promise<unknown> {
|
||||||
const [resolve, setResolve] = useState<() => void>(() => () => {});
|
const [resolve, setResolve] = useState<() => void>(() => () => {});
|
||||||
const [reject, setReject] = useState<(e: any) => void>(() => () => {});
|
const [reject, setReject] = useState<(e: any) => void>(() => () => {});
|
||||||
const [output, setOutput] = useState<O | null>(null);
|
const [output, setOutput] = useState<O | null>(null);
|
||||||
|
@ -5,7 +5,7 @@ export function useStatelessAsyncClickable(
|
|||||||
onClick: (e: MouseEvent) => Promise<void>,
|
onClick: (e: MouseEvent) => Promise<void>,
|
||||||
name: string
|
name: string
|
||||||
) {
|
) {
|
||||||
const [state, setState] = useState<ButtonState>('waiting');
|
const [state, setState] = useState<AsyncClickableState>('waiting');
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
async (e: MouseEvent) => {
|
async (e: MouseEvent) => {
|
||||||
try {
|
try {
|
||||||
|
@ -16,7 +16,7 @@ export interface IuseStorage {
|
|||||||
upload: (file: File, bucket: string) => Promise<string>;
|
upload: (file: File, bucket: string) => Promise<string>;
|
||||||
uploadDefault: (file: File) => Promise<string>;
|
uploadDefault: (file: File) => Promise<string>;
|
||||||
uploading: boolean;
|
uploading: boolean;
|
||||||
promptUpload: () => Promise<string | undefined>;
|
promptUpload: () => Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStorage = ({ accept = '*' } = { accept: '*' }): IuseStorage => {
|
const useStorage = ({ accept = '*' } = { accept: '*' }): IuseStorage => {
|
||||||
|
@ -30,7 +30,7 @@ export const getModuleIcon = (mod: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mod === 'post') {
|
if (mod === 'post') {
|
||||||
return 'Spaces';
|
return 'Dashboard';
|
||||||
}
|
}
|
||||||
|
|
||||||
return _.capitalize(mod);
|
return _.capitalize(mod);
|
||||||
@ -192,7 +192,10 @@ export function uxToHex(ux: string) {
|
|||||||
export const hexToUx = (hex) => {
|
export const hexToUx = (hex) => {
|
||||||
const ux = f.flow(
|
const ux = f.flow(
|
||||||
f.chunk(4),
|
f.chunk(4),
|
||||||
f.map(x => _.dropWhile(x, y => y === 0).join('')),
|
// eslint-disable-next-line prefer-arrow-callback
|
||||||
|
f.map(x => _.dropWhile(x, function(y: unknown) {
|
||||||
|
return y === 0;
|
||||||
|
}).join('')),
|
||||||
f.join('.')
|
f.join('.')
|
||||||
)(hex.split(''));
|
)(hex.split(''));
|
||||||
return `0x${ux}`;
|
return `0x${ux}`;
|
||||||
@ -417,7 +420,7 @@ export const useHovering = (): useHoveringInterface => {
|
|||||||
onMouseLeave,
|
onMouseLeave,
|
||||||
}), [onMouseLeave, onMouseOver]);
|
}), [onMouseLeave, onMouseOver]);
|
||||||
|
|
||||||
|
|
||||||
return useMemo(() => ({ hovering, bind }), [hovering, bind]);
|
return useMemo(() => ({ hovering, bind }), [hovering, bind]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
} from "react";
|
} from "react";
|
||||||
import usePreviousValue from "./usePreviousValue";
|
import usePreviousValue from "./usePreviousValue";
|
||||||
|
import {Primitive} from "~/types";
|
||||||
|
|
||||||
export interface VirtualContextProps {
|
export interface VirtualContextProps {
|
||||||
save: () => void;
|
save: () => void;
|
||||||
@ -49,7 +50,7 @@ export function useVirtualResizeState(s: boolean) {
|
|||||||
return [state, setState] as const;
|
return [state, setState] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useVirtualResizeProp<T>(prop: T) {
|
export function useVirtualResizeProp(prop: Primitive) {
|
||||||
const { save, restore } = useVirtual();
|
const { save, restore } = useVirtual();
|
||||||
const oldProp = usePreviousValue(prop)
|
const oldProp = usePreviousValue(prop)
|
||||||
|
|
||||||
@ -58,7 +59,7 @@ export function useVirtualResizeProp<T>(prop: T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
restore();
|
requestAnimationFrame(restore);
|
||||||
}, [prop]);
|
}, [prop]);
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Associations, Workspace } from '@urbit/api';
|
import { Associations } from '@urbit/api';
|
||||||
|
import { Workspace } from '~/types';
|
||||||
|
|
||||||
export function getTitleFromWorkspace(
|
export function getTitleFromWorkspace(
|
||||||
associations: Associations,
|
associations: Associations,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import _ from 'lodash';
|
import { StoreState } from '../store/type';
|
||||||
import { StoreState } from '../../store/type';
|
|
||||||
import { Cage } from '~/types/cage';
|
import { Cage } from '~/types/cage';
|
||||||
|
|
||||||
type LocalState = Pick<StoreState, 'connection'>;
|
type LocalState = Pick<StoreState, 'connection'>;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
|
import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap";
|
||||||
import bigInt, { BigInteger } from "big-integer";
|
import bigInt, { BigInteger } from "big-integer";
|
||||||
import useGraphState, { GraphState } from '../state/graph';
|
import useGraphState, { GraphState } from '../state/graph';
|
||||||
import { reduceState } from '../state/base';
|
import { reduceState } from '../state/base';
|
||||||
|
@ -5,16 +5,14 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
Tags,
|
Tags,
|
||||||
GroupPolicy,
|
GroupPolicy,
|
||||||
GroupPolicyDiff,
|
|
||||||
OpenPolicyDiff,
|
OpenPolicyDiff,
|
||||||
OpenPolicy,
|
OpenPolicy,
|
||||||
InvitePolicyDiff,
|
InvitePolicyDiff,
|
||||||
InvitePolicy
|
InvitePolicy
|
||||||
} from '@urbit/api/groups';
|
} from '@urbit/api/groups';
|
||||||
import { Enc, PatpNoSig } from '@urbit/api';
|
import { Enc } from '@urbit/api';
|
||||||
import { resourceAsPath } from '../lib/util';
|
import { resourceAsPath } from '../lib/util';
|
||||||
import useGroupState, { GroupState } from '../state/group';
|
import useGroupState, { GroupState } from '../state/group';
|
||||||
import { compose } from 'lodash/fp';
|
|
||||||
import { reduceState } from '../state/base';
|
import { reduceState } from '../state/base';
|
||||||
|
|
||||||
function decodeGroup(group: Enc<Group>): Group {
|
function decodeGroup(group: Enc<Group>): Group {
|
||||||
@ -125,9 +123,9 @@ const addMembers = (json: GroupUpdate, state: GroupState): GroupState => {
|
|||||||
state.groups[resourcePath].members.add(member);
|
state.groups[resourcePath].members.add(member);
|
||||||
if (
|
if (
|
||||||
'invite' in state.groups[resourcePath].policy &&
|
'invite' in state.groups[resourcePath].policy &&
|
||||||
state.groups[resourcePath].policy.invite.pending.has(member)
|
state.groups[resourcePath].policy['invite'].pending.has(member)
|
||||||
) {
|
) {
|
||||||
state.groups[resourcePath].policy.invite.pending.delete(member)
|
state.groups[resourcePath].policy['invite'].pending.delete(member);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,7 +157,7 @@ const addTag = (json: GroupUpdate, state: GroupState): GroupState => {
|
|||||||
_.set(tags, tagAccessors, tagged);
|
_.set(tags, tagAccessors, tagged);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
};
|
||||||
|
|
||||||
const removeTag = (json: GroupUpdate, state: GroupState): GroupState => {
|
const removeTag = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||||
if ('removeTag' in json) {
|
if ('removeTag' in json) {
|
||||||
|
@ -1,18 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
Notifications,
|
|
||||||
NotifIndex,
|
NotifIndex,
|
||||||
NotificationGraphConfig,
|
|
||||||
GroupNotificationsConfig,
|
|
||||||
UnreadStats,
|
|
||||||
Timebox
|
Timebox
|
||||||
} from '@urbit/api';
|
} from '@urbit/api';
|
||||||
import { makePatDa } from '~/logic/lib/util';
|
import { makePatDa } from '~/logic/lib/util';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
|
||||||
import useHarkState, { HarkState } from '../state/hark';
|
import useHarkState, { HarkState } from '../state/hark';
|
||||||
import { compose } from 'lodash/fp';
|
import { compose } from 'lodash/fp';
|
||||||
import { reduceState } from '../state/base';
|
import { reduceState } from '../state/base';
|
||||||
import bigInt, {BigInteger} from 'big-integer';
|
import {BigInteger} from 'big-integer';
|
||||||
|
|
||||||
export const HarkReducer = (json: any) => {
|
export const HarkReducer = (json: any) => {
|
||||||
const data = _.get(json, 'harkUpdate', false);
|
const data = _.get(json, 'harkUpdate', false);
|
||||||
@ -151,7 +147,7 @@ function graphWatchSelf(json: any, state: HarkState): HarkState {
|
|||||||
|
|
||||||
function readAll(json: any, state: HarkState): HarkState {
|
function readAll(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'read-all');
|
const data = _.get(json, 'read-all');
|
||||||
if(data) {
|
if(data) {
|
||||||
clearState(state);
|
clearState(state);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
@ -264,7 +260,7 @@ function updateUnreads(state: HarkState, index: NotifIndex, f: (us: Set<string>)
|
|||||||
if(!('graph' in index)) {
|
if(!('graph' in index)) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
let unreads = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set<string>());
|
let unreads: any = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set<string>());
|
||||||
f(unreads);
|
f(unreads);
|
||||||
|
|
||||||
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], unreads);
|
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], unreads);
|
||||||
@ -278,7 +274,7 @@ function addNotificationToUnread(state: HarkState, index: NotifIndex, time: BigI
|
|||||||
_.set(state.unreads.graph, path,
|
_.set(state.unreads.graph, path,
|
||||||
[
|
[
|
||||||
...curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))),
|
...curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))),
|
||||||
{ time, index}
|
{ time, index }
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} else if ('group' in index) {
|
} else if ('group' in index) {
|
||||||
@ -287,7 +283,7 @@ function addNotificationToUnread(state: HarkState, index: NotifIndex, time: BigI
|
|||||||
_.set(state.unreads.group, path,
|
_.set(state.unreads.group, path,
|
||||||
[
|
[
|
||||||
...curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))),
|
...curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))),
|
||||||
{ time, index}
|
{ time, index }
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -312,10 +308,10 @@ function removeNotificationFromUnread(state: HarkState, index: NotifIndex, time:
|
|||||||
function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'unreads' | 'last', f: (x: number) => number) {
|
function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'unreads' | 'last', f: (x: number) => number) {
|
||||||
|
|
||||||
if('graph' in index) {
|
if('graph' in index) {
|
||||||
const curr = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, statField], 0);
|
const curr: any = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, statField], 0);
|
||||||
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, statField], f(curr));
|
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, statField], f(curr));
|
||||||
} else if('group' in index) {
|
} else if('group' in index) {
|
||||||
const curr = _.get(state.unreads.group, [index.group.group, statField], 0);
|
const curr: any = _.get(state.unreads.group, [index.group.group, statField], 0);
|
||||||
_.set(state.unreads.group, [index.group.group, statField], f(curr));
|
_.set(state.unreads.group, [index.group.group, statField], f(curr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ export default class LaunchReducer {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const weatherData: WeatherState = _.get(json, 'weather', false);
|
const weatherData: WeatherState | boolean | Record<string, never> = _.get(json, 'weather', false);
|
||||||
if (weatherData) {
|
if (weatherData) {
|
||||||
useLaunchState.getState().set(state => {
|
useLaunchState.getState().set(state => {
|
||||||
state.weather = weatherData;
|
state.weather = weatherData;
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import useSettingsState, { SettingsState } from "~/logic/state/settings";
|
import useSettingsState, { SettingsState } from '~/logic/state/settings';
|
||||||
import { SettingsUpdate } from '@urbit/api/dist/settings';
|
import { SettingsUpdate } from '@urbit/api/settings';
|
||||||
import { reduceState } from '../state/base';
|
import { reduceState } from '../state/base';
|
||||||
|
import { string } from 'prop-types';
|
||||||
|
|
||||||
export default class SettingsReducer {
|
export default class SettingsReducer {
|
||||||
reduce(json: any) {
|
reduce(json: any) {
|
||||||
@ -40,21 +41,21 @@ export default class SettingsReducer {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
putEntry(json: SettingsUpdate, state: SettingsState): SettingsState {
|
putEntry(json: SettingsUpdate, state: any): SettingsState {
|
||||||
const data = _.get(json, 'put-entry', false);
|
const data: Record<string, string> = _.get(json, 'put-entry', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
if (!state[data["bucket-key"]]) {
|
if (!state[data['bucket-key']]) {
|
||||||
state[data["bucket-key"]] = {};
|
state[data['bucket-key']] = {};
|
||||||
}
|
}
|
||||||
state[data["bucket-key"]][data["entry-key"]] = data.value;
|
state[data['bucket-key']][data['entry-key']] = data.value;
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
delEntry(json: SettingsUpdate, state: SettingsState): SettingsState {
|
delEntry(json: SettingsUpdate, state: any): SettingsState {
|
||||||
const data = _.get(json, 'del-entry', false);
|
const data = _.get(json, 'del-entry', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
delete state[data["bucket-key"]][data["entry-key"]];
|
delete state[data['bucket-key']][data['entry-key']];
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -76,7 +77,7 @@ export default class SettingsReducer {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
getEntry(json: any, state: SettingsState) {
|
getEntry(json: any, state: any) {
|
||||||
const bucketKey = _.get(json, 'bucket-key', false);
|
const bucketKey = _.get(json, 'bucket-key', false);
|
||||||
const entryKey = _.get(json, 'entry-key', false);
|
const entryKey = _.get(json, 'entry-key', false);
|
||||||
const entry = _.get(json, 'entry', false);
|
const entry = _.get(json, 'entry', false);
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import produce from "immer";
|
import produce, { setAutoFreeze } from "immer";
|
||||||
import { compose } from "lodash/fp";
|
import { compose } from "lodash/fp";
|
||||||
import create, { State, UseStore } from "zustand";
|
import create, { State, UseStore } from "zustand";
|
||||||
import { persist, devtools } from "zustand/middleware";
|
import { persist, devtools } from "zustand/middleware";
|
||||||
|
|
||||||
|
setAutoFreeze(false);
|
||||||
|
|
||||||
|
|
||||||
export const stateSetter = <StateType>(
|
export const stateSetter = <StateType>(
|
||||||
fn: (state: StateType) => void,
|
fn: (state: StateType) => void,
|
||||||
|
@ -15,7 +15,7 @@ export interface HarkState extends BaseState<HarkState> {
|
|||||||
notifications: BigIntOrderedMap<Timebox>;
|
notifications: BigIntOrderedMap<Timebox>;
|
||||||
notificationsCount: number;
|
notificationsCount: number;
|
||||||
notificationsGraphConfig: NotificationGraphConfig; // TODO unthread this everywhere
|
notificationsGraphConfig: NotificationGraphConfig; // TODO unthread this everywhere
|
||||||
notificationsGroupConfig: []; // TODO type this
|
notificationsGroupConfig: string[];
|
||||||
unreads: Unreads;
|
unreads: Unreads;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ export interface LaunchState extends BaseState<LaunchState> {
|
|||||||
tiles: {
|
tiles: {
|
||||||
[app: string]: Tile;
|
[app: string]: Tile;
|
||||||
},
|
},
|
||||||
weather: WeatherState | null,
|
weather: WeatherState | null | Record<string, never> | boolean,
|
||||||
userLocation: string | null;
|
userLocation: string | null;
|
||||||
baseHash: string | null;
|
baseHash: string | null;
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
import { MetadataUpdatePreview, Association, Associations } from "@urbit/api";
|
import { MetadataUpdatePreview, Association, Associations } from "@urbit/api";
|
||||||
|
|
||||||
import { BaseState, createState } from "./base";
|
import { BaseState, createState } from "./base";
|
||||||
@ -18,6 +19,11 @@ export function useAssocForGroup(group: string) {
|
|||||||
return useMetadataState(useCallback(s => s.associations.groups[group] as Association | undefined, [group]));
|
return useMetadataState(useCallback(s => s.associations.groups[group] as Association | undefined, [group]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useGraphsForGroup(group: string) {
|
||||||
|
const graphs = useMetadataState(s => s.associations.graph);
|
||||||
|
return _.pickBy(graphs, (a: Association) => a.group === group);
|
||||||
|
}
|
||||||
|
|
||||||
const useMetadataState = createState<MetadataState>('Metadata', {
|
const useMetadataState = createState<MetadataState>('Metadata', {
|
||||||
associations: { groups: {}, graph: {}, contacts: {}, chat: {}, link: {}, publish: {} },
|
associations: { groups: {}, graph: {}, contacts: {}, chat: {}, link: {}, publish: {} },
|
||||||
// preview: async (group): Promise<MetadataUpdatePreview> => {
|
// preview: async (group): Promise<MetadataUpdatePreview> => {
|
||||||
|
@ -58,7 +58,7 @@ const useSettingsState = createState<SettingsState>('Settings', {
|
|||||||
categories: leapCategories,
|
categories: leapCategories,
|
||||||
},
|
},
|
||||||
tutorial: {
|
tutorial: {
|
||||||
seen: false,
|
seen: true,
|
||||||
joined: undefined
|
joined: undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -3,10 +3,8 @@ import _ from 'lodash';
|
|||||||
import BaseStore from './base';
|
import BaseStore from './base';
|
||||||
import InviteReducer from '../reducers/invite-update';
|
import InviteReducer from '../reducers/invite-update';
|
||||||
import MetadataReducer from '../reducers/metadata-update';
|
import MetadataReducer from '../reducers/metadata-update';
|
||||||
import LocalReducer from '../reducers/local';
|
|
||||||
|
|
||||||
import { StoreState } from './type';
|
import { StoreState } from './type';
|
||||||
import { Timebox } from '@urbit/api';
|
|
||||||
import { Cage } from '~/types/cage';
|
import { Cage } from '~/types/cage';
|
||||||
import S3Reducer from '../reducers/s3-update';
|
import S3Reducer from '../reducers/s3-update';
|
||||||
import { GraphReducer } from '../reducers/graph-update';
|
import { GraphReducer } from '../reducers/graph-update';
|
||||||
@ -17,8 +15,6 @@ import LaunchReducer from '../reducers/launch-update';
|
|||||||
import ConnectionReducer from '../reducers/connection';
|
import ConnectionReducer from '../reducers/connection';
|
||||||
import SettingsReducer from '../reducers/settings-update';
|
import SettingsReducer from '../reducers/settings-update';
|
||||||
import GcpReducer from '../reducers/gcp-reducer';
|
import GcpReducer from '../reducers/gcp-reducer';
|
||||||
import { OrderedMap } from '../lib/OrderedMap';
|
|
||||||
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
|
||||||
import { GroupViewReducer } from '../reducers/group-view';
|
import { GroupViewReducer } from '../reducers/group-view';
|
||||||
import { unstable_batchedUpdates } from 'react-dom';
|
import { unstable_batchedUpdates } from 'react-dom';
|
||||||
|
|
||||||
|
@ -53,6 +53,7 @@ const Root = withState(styled.div`
|
|||||||
}
|
}
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
|
touch-action: none;
|
||||||
|
|
||||||
* {
|
* {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
|
@ -188,7 +188,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
) : (
|
) : (
|
||||||
<Icon
|
<Icon
|
||||||
icon='Links'
|
icon='Attachment'
|
||||||
width='16'
|
width='16'
|
||||||
height='16'
|
height='16'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
/* eslint-disable max-lines-per-function */
|
/* eslint-disable max-lines-per-function */
|
||||||
|
import bigInt from 'big-integer';
|
||||||
import React, {
|
import React, {
|
||||||
useState,
|
useState,
|
||||||
useEffect,
|
useEffect,
|
||||||
@ -19,7 +20,8 @@ import {
|
|||||||
writeText,
|
writeText,
|
||||||
useShowNickname,
|
useShowNickname,
|
||||||
useHideAvatar,
|
useHideAvatar,
|
||||||
useHovering
|
useHovering,
|
||||||
|
daToUnix
|
||||||
} from '~/logic/lib/util';
|
} from '~/logic/lib/util';
|
||||||
import {
|
import {
|
||||||
Group,
|
Group,
|
||||||
@ -65,7 +67,7 @@ export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => (
|
|||||||
<Rule borderColor='lightGray' />
|
<Rule borderColor='lightGray' />
|
||||||
<Text
|
<Text
|
||||||
gray
|
gray
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
whiteSpace='nowrap'
|
whiteSpace='nowrap'
|
||||||
textAlign='center'
|
textAlign='center'
|
||||||
fontSize={0}
|
fontSize={0}
|
||||||
@ -107,7 +109,7 @@ export const UnreadMarker = React.forwardRef(
|
|||||||
<Text
|
<Text
|
||||||
color='blue'
|
color='blue'
|
||||||
fontSize={0}
|
fontSize={0}
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
whiteSpace='nowrap'
|
whiteSpace='nowrap'
|
||||||
textAlign='center'
|
textAlign='center'
|
||||||
px={2}
|
px={2}
|
||||||
@ -168,7 +170,7 @@ const MessageActions = ({ api, onReply, association, history, msg, group }) => {
|
|||||||
width='auto'
|
width='auto'
|
||||||
alignY='top'
|
alignY='top'
|
||||||
alignX='right'
|
alignX='right'
|
||||||
flexShrink={'0'}
|
flexShrink={0}
|
||||||
offsetY={8}
|
offsetY={8}
|
||||||
offsetX={-24}
|
offsetX={-24}
|
||||||
options={
|
options={
|
||||||
@ -295,15 +297,20 @@ class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const date = daToUnix(bigInt(msg.index.split('/')[1]));
|
||||||
|
const nextDate = nextMsg ? (
|
||||||
|
daToUnix(bigInt(nextMsg.index.split('/')[1]))
|
||||||
|
) : null;
|
||||||
|
|
||||||
const dayBreak =
|
const dayBreak =
|
||||||
nextMsg &&
|
nextMsg &&
|
||||||
new Date(msg['time-sent']).getDate() !==
|
new Date(date).getDate() !==
|
||||||
new Date(nextMsg['time-sent']).getDate();
|
new Date(nextDate).getDate();
|
||||||
|
|
||||||
const containerClass = `${isPending ? 'o-40' : ''} ${className}`;
|
const containerClass = `${isPending ? 'o-40' : ''} ${className}`;
|
||||||
|
|
||||||
const timestamp = moment
|
const timestamp = moment
|
||||||
.unix(msg['time-sent'] / 1000)
|
.unix(date / 1000)
|
||||||
.format(renderSigil ? 'h:mm A' : 'h:mm');
|
.format(renderSigil ? 'h:mm A' : 'h:mm');
|
||||||
|
|
||||||
const messageProps = {
|
const messageProps = {
|
||||||
@ -339,7 +346,7 @@ class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
{dayBreak && !isLastRead ? (
|
{dayBreak && !isLastRead ? (
|
||||||
<DayBreak when={msg['time-sent']} shimTop={renderSigil} />
|
<DayBreak when={date} shimTop={renderSigil} />
|
||||||
) : null}
|
) : null}
|
||||||
{renderSigil ? (
|
{renderSigil ? (
|
||||||
<MessageWrapper {...messageProps}>
|
<MessageWrapper {...messageProps}>
|
||||||
@ -357,7 +364,7 @@ class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
association={association}
|
association={association}
|
||||||
api={api}
|
api={api}
|
||||||
dayBreak={dayBreak}
|
dayBreak={dayBreak}
|
||||||
when={msg['time-sent']}
|
when={date}
|
||||||
ref={unreadMarkerRef}
|
ref={unreadMarkerRef}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@ -387,8 +394,10 @@ export const MessageAuthor = ({
|
|||||||
const dark = theme === 'dark' || (theme === 'auto' && osDark);
|
const dark = theme === 'dark' || (theme === 'auto' && osDark);
|
||||||
const contacts = useContactState((state) => state.contacts);
|
const contacts = useContactState((state) => state.contacts);
|
||||||
|
|
||||||
|
const date = daToUnix(bigInt(msg.index.split('/')[1]));
|
||||||
|
|
||||||
const datestamp = moment
|
const datestamp = moment
|
||||||
.unix(msg['time-sent'] / 1000)
|
.unix(date / 1000)
|
||||||
.format(DATESTAMP_FORMAT);
|
.format(DATESTAMP_FORMAT);
|
||||||
const contact =
|
const contact =
|
||||||
((msg.author === window.ship && showOurContact) ||
|
((msg.author === window.ship && showOurContact) ||
|
||||||
|
@ -46,7 +46,7 @@ const ScrollbarLessBox = styled(Box)`
|
|||||||
const tutSelector = f.pick(['tutorialProgress', 'nextTutStep', 'hideGroups']);
|
const tutSelector = f.pick(['tutorialProgress', 'nextTutStep', 'hideGroups']);
|
||||||
|
|
||||||
export default function LaunchApp(props) {
|
export default function LaunchApp(props) {
|
||||||
const connection = { props };
|
const { connection } = props;
|
||||||
const baseHash = useLaunchState(state => state.baseHash);
|
const baseHash = useLaunchState(state => state.baseHash);
|
||||||
const [hashText, setHashText] = useState(baseHash);
|
const [hashText, setHashText] = useState(baseHash);
|
||||||
const [exitingTut, setExitingTut] = useState(false);
|
const [exitingTut, setExitingTut] = useState(false);
|
||||||
@ -220,7 +220,7 @@ export default function LaunchApp(props) {
|
|||||||
<NewGroup {...props} />
|
<NewGroup {...props} />
|
||||||
</ModalButton>
|
</ModalButton>
|
||||||
<ModalButton
|
<ModalButton
|
||||||
icon="Boot"
|
icon="BootNode"
|
||||||
bg="washedGray"
|
bg="washedGray"
|
||||||
color="black"
|
color="black"
|
||||||
text="Join Group"
|
text="Join Group"
|
||||||
|
@ -145,8 +145,7 @@ function ContentSummary({ icon, name, author, to }) {
|
|||||||
export const GraphNodeContent = ({ post, mod, index, hidden, association }) => {
|
export const GraphNodeContent = ({ post, mod, index, hidden, association }) => {
|
||||||
const { contents } = post;
|
const { contents } = post;
|
||||||
const idx = index.slice(1).split("/");
|
const idx = index.slice(1).split("/");
|
||||||
const { group, resource } = association;
|
const url = getNodeUrl(mod, hidden, association?.group, association?.resource, index);
|
||||||
const url = getNodeUrl(mod, hidden, group, resource, index);
|
|
||||||
if (mod === "link" && idx.length === 1) {
|
if (mod === "link" && idx.length === 1) {
|
||||||
const [{ text: title }] = contents;
|
const [{ text: title }] = contents;
|
||||||
return (
|
return (
|
||||||
@ -296,7 +295,7 @@ export function GraphNotification(props: {
|
|||||||
dm,
|
dm,
|
||||||
singleAuthor
|
singleAuthor
|
||||||
);
|
);
|
||||||
const groupAssociation = useAssocForGroup(association.group);
|
const groupAssociation = useAssocForGroup(association?.group);
|
||||||
const groups = useGroupState((state) => state.groups);
|
const groups = useGroupState((state) => state.groups);
|
||||||
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
@ -307,13 +306,12 @@ export function GraphNotification(props: {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
const first = contents[0];
|
const first = contents[0];
|
||||||
const { group, resource } = association;
|
|
||||||
history.push(
|
history.push(
|
||||||
getNodeUrl(
|
getNodeUrl(
|
||||||
index.module,
|
index.module,
|
||||||
groups[association.group]?.hidden,
|
groups[association?.group]?.hidden,
|
||||||
group,
|
group,
|
||||||
resource,
|
association?.resource,
|
||||||
first.index
|
first.index
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -328,7 +326,7 @@ export function GraphNotification(props: {
|
|||||||
authorsInHeader ||
|
authorsInHeader ||
|
||||||
index.description === "note" ||
|
index.description === "note" ||
|
||||||
index.description === "link";
|
index.description === "link";
|
||||||
const channelTitle = dm ? undefined : association.metadata.title ?? graph;
|
const channelTitle = dm ? undefined : association?.metadata?.title ?? graph;
|
||||||
const groupTitle = groupAssociation?.metadata?.title;
|
const groupTitle = groupAssociation?.metadata?.title;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -349,7 +347,7 @@ export function GraphNotification(props: {
|
|||||||
description={index.description}
|
description={index.description}
|
||||||
index={contents?.[0].index}
|
index={contents?.[0].index}
|
||||||
association={association}
|
association={association}
|
||||||
hidden={groups[association.group]?.hidden}
|
hidden={groups[association?.group]?.hidden}
|
||||||
/>
|
/>
|
||||||
{contents.length > 4 && (
|
{contents.length > 4 && (
|
||||||
<Text mb="2" gray>
|
<Text mb="2" gray>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { ReactNode, useCallback, useMemo, useState } from "react";
|
import React, { ReactNode, useCallback, useMemo, useState } from "react";
|
||||||
import { Row, Box } from "@tlon/indigo-react";
|
import { Row, Box, Icon } from "@tlon/indigo-react";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import {
|
import {
|
||||||
GraphNotificationContents,
|
GraphNotificationContents,
|
||||||
@ -19,6 +19,7 @@ import { GraphNotification } from "./graph";
|
|||||||
import { BigInteger } from "big-integer";
|
import { BigInteger } from "big-integer";
|
||||||
import { useHovering } from "~/logic/lib/util";
|
import { useHovering } from "~/logic/lib/util";
|
||||||
import useHarkState from "~/logic/state/hark";
|
import useHarkState from "~/logic/state/hark";
|
||||||
|
import {IS_MOBILE} from "~/logic/lib/platform";
|
||||||
|
|
||||||
interface NotificationProps {
|
interface NotificationProps {
|
||||||
notification: IndexedNotification;
|
notification: IndexedNotification;
|
||||||
@ -102,39 +103,30 @@ export function NotificationWrapper(props: {
|
|||||||
}
|
}
|
||||||
borderRadius={2}
|
borderRadius={2}
|
||||||
display="grid"
|
display="grid"
|
||||||
gridTemplateColumns={["1fr", "1fr 200px"]}
|
gridTemplateColumns={["1fr 24px", "1fr 200px"]}
|
||||||
gridTemplateRows="auto"
|
gridTemplateRows="auto"
|
||||||
gridTemplateAreas={["'header' 'main'", "'header actions' 'main main'"]}
|
gridTemplateAreas="'header actions' 'main main'"
|
||||||
p={2}
|
p={2}
|
||||||
m={2}
|
m={2}
|
||||||
{...bind}
|
{...bind}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<Row
|
<Row
|
||||||
display={["none", "flex"]}
|
alignItems="flex-start"
|
||||||
alignItems="center"
|
|
||||||
gapX="2"
|
gapX="2"
|
||||||
gridArea="actions"
|
gridArea="actions"
|
||||||
justifyContent="flex-end"
|
justifyContent="flex-end"
|
||||||
opacity={[1, hovering ? 1 : 0]}
|
opacity={[1, (hovering || IS_MOBILE) ? 1 : 0]}
|
||||||
>
|
>
|
||||||
{time && notification && (
|
{time && notification && (
|
||||||
<>
|
<StatelessAsyncAction
|
||||||
<StatelessAsyncAction
|
name={time.toString()}
|
||||||
name={changeMuteDesc}
|
borderRadius={1}
|
||||||
onClick={onChangeMute}
|
onClick={onArchive}
|
||||||
backgroundColor="transparent"
|
backgroundColor="white"
|
||||||
>
|
>
|
||||||
{changeMuteDesc}
|
<Icon lineHeight="24px" size={16} icon="X" />
|
||||||
</StatelessAsyncAction>
|
</StatelessAsyncAction>
|
||||||
<StatelessAsyncAction
|
|
||||||
name={time.toString()}
|
|
||||||
onClick={onArchive}
|
|
||||||
backgroundColor="transparent"
|
|
||||||
>
|
|
||||||
Dismiss
|
|
||||||
</StatelessAsyncAction>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -3,7 +3,7 @@ import _ from 'lodash';
|
|||||||
import { Link, Switch, Route } from 'react-router-dom';
|
import { Link, Switch, Route } from 'react-router-dom';
|
||||||
import Helmet from 'react-helmet';
|
import Helmet from 'react-helmet';
|
||||||
|
|
||||||
import { Box, Col, Text, Row } from '@tlon/indigo-react';
|
import { Box, Icon, Col, Text, Row } from '@tlon/indigo-react';
|
||||||
|
|
||||||
import { Body } from '~/views/components/Body';
|
import { Body } from '~/views/components/Body';
|
||||||
import { PropFunc } from '~/types/util';
|
import { PropFunc } from '~/types/util';
|
||||||
@ -15,6 +15,7 @@ import { useTutorialModal } from '~/views/components/useTutorialModal';
|
|||||||
import useHarkState from '~/logic/state/hark';
|
import useHarkState from '~/logic/state/hark';
|
||||||
import useMetadataState from '~/logic/state/metadata';
|
import useMetadataState from '~/logic/state/metadata';
|
||||||
import useGroupState from '~/logic/state/group';
|
import useGroupState from '~/logic/state/group';
|
||||||
|
import {StatelessAsyncAction} from '~/views/components/StatelessAsyncAction';
|
||||||
|
|
||||||
const baseUrl = '/~notifications';
|
const baseUrl = '/~notifications';
|
||||||
|
|
||||||
@ -46,8 +47,8 @@ export default function NotificationsScreen(props: any): ReactElement {
|
|||||||
const onSubmit = async ({ groups } : NotificationFilter) => {
|
const onSubmit = async ({ groups } : NotificationFilter) => {
|
||||||
setFilter({ groups });
|
setFilter({ groups });
|
||||||
};
|
};
|
||||||
const onReadAll = useCallback(() => {
|
const onReadAll = useCallback(async () => {
|
||||||
props.api.hark.readAll();
|
await props.api.hark.readAll();
|
||||||
}, []);
|
}, []);
|
||||||
const groupFilterDesc =
|
const groupFilterDesc =
|
||||||
filter.groups.length === 0
|
filter.groups.length === 0
|
||||||
@ -81,59 +82,32 @@ export default function NotificationsScreen(props: any): ReactElement {
|
|||||||
borderBottomColor="lightGray"
|
borderBottomColor="lightGray"
|
||||||
>
|
>
|
||||||
|
|
||||||
<Text ref={anchorRef}>Notifications</Text>
|
<Text fontWeight="bold" fontSize="2" lineHeight="1" ref={anchorRef}>
|
||||||
|
Notifications
|
||||||
|
</Text>
|
||||||
<Row
|
<Row
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
|
gapX="3"
|
||||||
>
|
>
|
||||||
<Box
|
<StatelessAsyncAction
|
||||||
mr="1"
|
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
|
color="black"
|
||||||
|
backgroundColor="white"
|
||||||
onClick={onReadAll}
|
onClick={onReadAll}
|
||||||
cursor="pointer"
|
|
||||||
>
|
|
||||||
<Text mr="1" color="blue">
|
|
||||||
Mark All Read
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
</FormikOnBlur>
|
|
||||||
</Col>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
|
Mark All Read
|
||||||
|
</StatelessAsyncAction>
|
||||||
|
<Link to="/~settings#notifications">
|
||||||
<Box>
|
<Box>
|
||||||
<Text mr="1" gray>
|
<Icon lineHeight="1" icon="Adjust" />
|
||||||
Filter:
|
|
||||||
</Text>
|
|
||||||
<Text>{groupFilterDesc}</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Dropdown>
|
</Link>
|
||||||
</Row>
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
{!view && <Inbox
|
{!view && <Inbox
|
||||||
pendingJoin={pendingJoin}
|
pendingJoin={pendingJoin}
|
||||||
{...props}
|
{...props}
|
||||||
filter={filter.groups}
|
filter={filter.groups}
|
||||||
/>}
|
/>}
|
||||||
</Col>
|
</Col>
|
||||||
</Body>
|
</Body>
|
||||||
|
@ -18,7 +18,7 @@ import { GroupLink } from "~/views/components/GroupLink";
|
|||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from "~/logic/api/global";
|
||||||
import { getModuleIcon } from "~/logic/lib/util";
|
import { getModuleIcon } from "~/logic/lib/util";
|
||||||
import useMetadataState from "~/logic/state/metadata";
|
import useMetadataState from "~/logic/state/metadata";
|
||||||
import { Association, resourceFromPath } from "@urbit/api";
|
import { Association, resourceFromPath, GraphNode } from "@urbit/api";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import useGraphState from "~/logic/state/graph";
|
import useGraphState from "~/logic/state/graph";
|
||||||
import { GraphNodeContent } from "../notifications/graph";
|
import { GraphNodeContent } from "../notifications/graph";
|
||||||
@ -51,7 +51,7 @@ function GraphPermalink(
|
|||||||
const { full = false, showOurContact, pending, link, graph, group, index, api, transcluded } = props;
|
const { full = false, showOurContact, pending, link, graph, group, index, api, transcluded } = props;
|
||||||
const { ship, name } = resourceFromPath(graph);
|
const { ship, name } = resourceFromPath(graph);
|
||||||
const node = useGraphState(
|
const node = useGraphState(
|
||||||
useCallback((s) => s.looseNodes?.[`${ship.slice(1)}/${name}`]?.[index], [
|
useCallback((s) => s.looseNodes?.[`${ship.slice(1)}/${name}`]?.[index] as GraphNode, [
|
||||||
graph,
|
graph,
|
||||||
index,
|
index,
|
||||||
])
|
])
|
||||||
@ -63,7 +63,7 @@ function GraphPermalink(
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
useVirtualResizeProp(node)
|
useVirtualResizeProp(!!node)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (pending || !index) {
|
if (pending || !index) {
|
||||||
|
@ -73,14 +73,14 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
|||||||
if (window.ship === note?.post?.author) {
|
if (window.ship === note?.post?.author) {
|
||||||
adminLinks.push(
|
adminLinks.push(
|
||||||
<Link to={`${baseUrl}/edit`}>
|
<Link to={`${baseUrl}/edit`}>
|
||||||
<Action>Update</Action>
|
<Action backgroundColor="white">Update</Action>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (window.ship === note?.post?.author || ourRole === "admin") {
|
if (window.ship === note?.post?.author || ourRole === "admin") {
|
||||||
adminLinks.push(
|
adminLinks.push(
|
||||||
<Action destructive onClick={deletePost}>
|
<Action backgroundColor="white" destructive onClick={deletePost}>
|
||||||
Delete
|
Delete
|
||||||
</Action>
|
</Action>
|
||||||
)
|
)
|
||||||
|
@ -44,7 +44,7 @@ export function PostForm(props: PostFormProps) {
|
|||||||
validateOnBlur
|
validateOnBlur
|
||||||
>
|
>
|
||||||
<Form style={{ display: 'contents' }}>
|
<Form style={{ display: 'contents' }}>
|
||||||
<Row flexShrink='0' flexDirection={['column-reverse', 'row']} mb={4} gapX={4} justifyContent='space-between'>
|
<Row flexShrink={0} flexDirection={['column-reverse', 'row']} mb={4} gapX={4} justifyContent='space-between'>
|
||||||
<Input maxWidth='40rem' width='100%' flexShrink={[0, 1]} placeholder="Post Title" id="title" />
|
<Input maxWidth='40rem' width='100%' flexShrink={[0, 1]} placeholder="Post Title" id="title" />
|
||||||
<Row flexDirection={['column', 'row']} mb={[4,0]}>
|
<Row flexDirection={['column', 'row']} mb={[4,0]}>
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
|
@ -0,0 +1,135 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Text,
|
||||||
|
Icon,
|
||||||
|
ManagedToggleSwitchField,
|
||||||
|
StatelessToggleSwitchField,
|
||||||
|
Col,
|
||||||
|
Center,
|
||||||
|
} from "@tlon/indigo-react";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
import useMetadataState, { useGraphsForGroup } from "~/logic/state/metadata";
|
||||||
|
import { Association, resourceFromPath } from "@urbit/api";
|
||||||
|
import { MetadataIcon } from "~/views/landscape/components/MetadataIcon";
|
||||||
|
import useGraphState from "~/logic/state/graph";
|
||||||
|
import { useField } from "formik";
|
||||||
|
import useHarkState from "~/logic/state/hark";
|
||||||
|
import { getModuleIcon } from "~/logic/lib/util";
|
||||||
|
import {isWatching} from "~/logic/lib/hark";
|
||||||
|
|
||||||
|
export function GroupChannelPicker(props: {}) {
|
||||||
|
const associations = useMetadataState((s) => s.associations);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col gapY="3">
|
||||||
|
{_.map(associations.groups, (assoc: Association, group: string) => (
|
||||||
|
<GroupWithChannels key={group} association={assoc} />
|
||||||
|
))}
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GroupWithChannels(props: { association: Association }) {
|
||||||
|
const { association } = props;
|
||||||
|
const { metadata } = association;
|
||||||
|
|
||||||
|
const groupWatched = useHarkState((s) =>
|
||||||
|
s.notificationsGroupConfig.includes(association.group)
|
||||||
|
);
|
||||||
|
|
||||||
|
const [{ value }, meta, { setValue }] = useField(
|
||||||
|
`groups["${association.group}"]`
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const onChange = () => {
|
||||||
|
setValue(!value);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(groupWatched);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const graphs = useGraphsForGroup(association.group);
|
||||||
|
const joinedGraphs = useGraphState((s) => s.graphKeys);
|
||||||
|
const joinedGroupGraphs = _.pickBy(graphs, (_, graph: string) => {
|
||||||
|
const { ship, name } = resourceFromPath(graph);
|
||||||
|
return joinedGraphs.has(`${ship.slice(1)}/${name}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
display="grid"
|
||||||
|
gridTemplateColumns="24px 24px 1fr 24px 24px"
|
||||||
|
gridTemplateRows="auto"
|
||||||
|
gridGap="2"
|
||||||
|
gridTemplateAreas="'arrow icon title graphToggle groupToggle'"
|
||||||
|
>
|
||||||
|
{Object.keys(joinedGroupGraphs).length > 0 && (
|
||||||
|
<Center
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={() => setOpen((o) => !o)}
|
||||||
|
gridArea="arrow"
|
||||||
|
>
|
||||||
|
<Icon icon={open ? "ChevronSouth" : "ChevronEast"} />
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
<MetadataIcon
|
||||||
|
size="24px"
|
||||||
|
gridArea="icon"
|
||||||
|
metadata={association.metadata}
|
||||||
|
/>
|
||||||
|
<Box gridArea="title">
|
||||||
|
<Text>{metadata.title}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box gridArea="groupToggle">
|
||||||
|
<StatelessToggleSwitchField selected={value} onChange={onChange} />
|
||||||
|
</Box>
|
||||||
|
{open &&
|
||||||
|
_.map(joinedGroupGraphs, (a: Association, graph: string) => (
|
||||||
|
<Channel key={graph} association={a} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function Channel(props: { association: Association }) {
|
||||||
|
const { association } = props;
|
||||||
|
const { metadata } = association;
|
||||||
|
const watching = useHarkState((s) => {
|
||||||
|
const config = s.notificationsGraphConfig;
|
||||||
|
return isWatching(config, association.resource);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [{ value }, meta, { setValue }] = useField(
|
||||||
|
`graph["${association.resource}"]`
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(watching);
|
||||||
|
}, [watching]);
|
||||||
|
|
||||||
|
const onChange = () => {
|
||||||
|
setValue(!value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const icon = getModuleIcon(metadata.config?.graph);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Center gridColumn="2">
|
||||||
|
<Icon icon={icon} />
|
||||||
|
</Center>
|
||||||
|
<Box gridColumn="3">
|
||||||
|
<Text> {metadata.title}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box gridColumn="4">
|
||||||
|
<StatelessToggleSwitchField selected={value} onChange={onChange} />
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -10,11 +10,19 @@ import GlobalApi from "~/logic/api/global";
|
|||||||
import useHarkState from "~/logic/state/hark";
|
import useHarkState from "~/logic/state/hark";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import {AsyncButton} from "~/views/components/AsyncButton";
|
import {AsyncButton} from "~/views/components/AsyncButton";
|
||||||
|
import {GroupChannelPicker} from "./GroupChannelPicker";
|
||||||
|
import {isWatching} from "~/logic/lib/hark";
|
||||||
|
|
||||||
interface FormSchema {
|
interface FormSchema {
|
||||||
mentions: boolean;
|
mentions: boolean;
|
||||||
dnd: boolean;
|
dnd: boolean;
|
||||||
watchOnSelf: boolean;
|
watchOnSelf: boolean;
|
||||||
|
graph: {
|
||||||
|
[rid: string]: boolean;
|
||||||
|
};
|
||||||
|
groups: {
|
||||||
|
[rid: string]: boolean;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NotificationPreferences(props: {
|
export function NotificationPreferences(props: {
|
||||||
@ -23,6 +31,7 @@ export function NotificationPreferences(props: {
|
|||||||
const { api } = props;
|
const { api } = props;
|
||||||
const dnd = useHarkState(state => state.doNotDisturb);
|
const dnd = useHarkState(state => state.doNotDisturb);
|
||||||
const graphConfig = useHarkState(state => state.notificationsGraphConfig);
|
const graphConfig = useHarkState(state => state.notificationsGraphConfig);
|
||||||
|
const groupConfig = useHarkState(s => s.notificationsGroupConfig);
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
mentions: graphConfig.mentions,
|
mentions: graphConfig.mentions,
|
||||||
dnd: dnd,
|
dnd: dnd,
|
||||||
@ -41,6 +50,16 @@ export function NotificationPreferences(props: {
|
|||||||
if (values.dnd !== dnd && !_.isUndefined(values.dnd)) {
|
if (values.dnd !== dnd && !_.isUndefined(values.dnd)) {
|
||||||
promises.push(api.hark.setDoNotDisturb(values.dnd))
|
promises.push(api.hark.setDoNotDisturb(values.dnd))
|
||||||
}
|
}
|
||||||
|
_.forEach(values.graph, (listen: boolean, graph: string) => {
|
||||||
|
if(listen !== isWatching(graphConfig, graph)) {
|
||||||
|
promises.push(api.hark[listen ? "listenGraph" : "ignoreGraph"](graph, "/"))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_.forEach(values.groups, (listen: boolean, group: string) => {
|
||||||
|
if(listen !== groupConfig.includes(group)) {
|
||||||
|
promises.push(api.hark[listen ? "listenGroup" : "ignoreGroup"](group));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
actions.setStatus({ success: null });
|
actions.setStatus({ success: null });
|
||||||
@ -81,6 +100,15 @@ export function NotificationPreferences(props: {
|
|||||||
id="mentions"
|
id="mentions"
|
||||||
caption="Notify me if someone mentions my @p in a channel I've joined"
|
caption="Notify me if someone mentions my @p in a channel I've joined"
|
||||||
/>
|
/>
|
||||||
|
<Col gapY="3">
|
||||||
|
<Text lineHeight="tall">
|
||||||
|
Activity
|
||||||
|
</Text>
|
||||||
|
<Text gray>
|
||||||
|
Set which groups will send you notifications.
|
||||||
|
</Text>
|
||||||
|
<GroupChannelPicker />
|
||||||
|
</Col>
|
||||||
<AsyncButton primary width="fit-content">
|
<AsyncButton primary width="fit-content">
|
||||||
Save
|
Save
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
|
@ -107,7 +107,7 @@ export default function SettingsScreen(props: any) {
|
|||||||
</Text>
|
</Text>
|
||||||
<Col>
|
<Col>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon='Inbox'
|
icon='Notifications'
|
||||||
text='Notifications'
|
text='Notifications'
|
||||||
hash='notifications'
|
hash='notifications'
|
||||||
/>
|
/>
|
||||||
|
@ -115,8 +115,8 @@ export function ChipInput(props: ChipInputProps): ReactElement {
|
|||||||
<Input
|
<Input
|
||||||
width="auto"
|
width="auto"
|
||||||
height="24px"
|
height="24px"
|
||||||
flexShrink="1"
|
flexShrink={1}
|
||||||
flexGrow="1"
|
flexGrow={1}
|
||||||
pl="0"
|
pl="0"
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
@ -25,6 +25,7 @@ interface DropdownProps {
|
|||||||
offsetY?: number;
|
offsetY?: number;
|
||||||
width?: string;
|
width?: string;
|
||||||
dropWidth?: string;
|
dropWidth?: string;
|
||||||
|
flexShrink?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClickBox = styled(Box)`
|
const ClickBox = styled(Box)`
|
||||||
@ -39,7 +40,7 @@ const DropdownOptions = styled(Box)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function Dropdown(props: DropdownProps): ReactElement {
|
export function Dropdown(props: DropdownProps): ReactElement {
|
||||||
const { children, options, offsetX = 0, offsetY = 0 } = props;
|
const { children, options, offsetX = 0, offsetY = 0, flexShrink = 1 } = props;
|
||||||
const dropdownRef = useRef<HTMLElement>(null);
|
const dropdownRef = useRef<HTMLElement>(null);
|
||||||
const anchorRef = useRef<HTMLElement>(null);
|
const anchorRef = useRef<HTMLElement>(null);
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
@ -47,6 +48,9 @@ export function Dropdown(props: DropdownProps): ReactElement {
|
|||||||
const [coords, setCoords] = useState({});
|
const [coords, setCoords] = useState({});
|
||||||
|
|
||||||
const updatePos = useCallback(() => {
|
const updatePos = useCallback(() => {
|
||||||
|
if(!anchorRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const newCoords = getRelativePosition(anchorRef.current, props.alignX, props.alignY, offsetX, offsetY);
|
const newCoords = getRelativePosition(anchorRef.current, props.alignX, props.alignY, offsetX, offsetY);
|
||||||
if(newCoords) {
|
if(newCoords) {
|
||||||
setCoords(newCoords);
|
setCoords(newCoords);
|
||||||
@ -86,7 +90,7 @@ export function Dropdown(props: DropdownProps): ReactElement {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexShrink={props?.flexShrink ? props.flexShrink : 1} position={open ? 'relative' : 'static'} minWidth='0' width={props?.width ? props.width : 'auto'}>
|
<Box flexShrink={flexShrink} position={open ? 'relative' : 'static'} minWidth='0' width={props?.width ? props.width : 'auto'}>
|
||||||
<ClickBox width='100%' ref={anchorRef} onClick={onOpen}>
|
<ClickBox width='100%' ref={anchorRef} onClick={onOpen}>
|
||||||
{children}
|
{children}
|
||||||
</ClickBox>
|
</ClickBox>
|
||||||
|
175
pkg/interface/src/views/components/FormGroup.tsx
Normal file
175
pkg/interface/src/views/components/FormGroup.tsx
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import React, {
|
||||||
|
ReactNode,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
} from "react";
|
||||||
|
import { Button, Box, Row, Col } from "@tlon/indigo-react";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { useFormikContext } from "formik";
|
||||||
|
import { PropFunc } from "~/types";
|
||||||
|
import { FormGroupContext, SubmitHandler } from "~/logic/lib/formGroup";
|
||||||
|
import { StatelessAsyncButton } from "./StatelessAsyncButton";
|
||||||
|
import { Prompt } from "react-router-dom";
|
||||||
|
import { usePreventWindowUnload } from "~/logic/lib/util";
|
||||||
|
|
||||||
|
export function useFormGroupContext(id: string) {
|
||||||
|
const ctx = React.useContext(FormGroupContext);
|
||||||
|
const addSubmit = useCallback(
|
||||||
|
(submit: SubmitHandler) => {
|
||||||
|
ctx.addSubmit(id, submit);
|
||||||
|
},
|
||||||
|
[ctx.addSubmit, id]
|
||||||
|
);
|
||||||
|
const onDirty = useCallback(
|
||||||
|
(dirty: boolean) => {
|
||||||
|
ctx.onDirty(id, dirty);
|
||||||
|
},
|
||||||
|
[ctx.onDirty, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onErrors = useCallback(
|
||||||
|
(errors: boolean) => {
|
||||||
|
ctx.onErrors(id, errors);
|
||||||
|
},
|
||||||
|
[ctx.onErrors, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addReset = useCallback(
|
||||||
|
(r: () => void) => {
|
||||||
|
ctx.addReset(id, r);
|
||||||
|
},
|
||||||
|
[ctx.addReset, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
onDirty,
|
||||||
|
addSubmit,
|
||||||
|
onErrors,
|
||||||
|
addReset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormGroupChild(props: { id: string }) {
|
||||||
|
const { id } = props;
|
||||||
|
const { addSubmit, onDirty, onErrors, addReset } = useFormGroupContext(id);
|
||||||
|
const {
|
||||||
|
submitForm,
|
||||||
|
dirty,
|
||||||
|
errors,
|
||||||
|
resetForm,
|
||||||
|
initialValues,
|
||||||
|
values
|
||||||
|
} = useFormikContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function submit() {
|
||||||
|
await submitForm();
|
||||||
|
resetForm({ touched: {}, values });
|
||||||
|
}
|
||||||
|
addSubmit(submit);
|
||||||
|
|
||||||
|
}, [submitForm, values]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onDirty(dirty);
|
||||||
|
}, [dirty, onDirty]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onErrors(_.keys(_.pickBy(errors, (s) => !!s)).length > 0);
|
||||||
|
}, [errors, onErrors]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const reset = () => {
|
||||||
|
resetForm({ errors: {}, touched: {}, values: initialValues, status: {} });
|
||||||
|
};
|
||||||
|
addReset(reset);
|
||||||
|
}, [resetForm, initialValues]);
|
||||||
|
|
||||||
|
return <Box display="none" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormGroup(props: { onReset?: () => void; } & PropFunc<typeof Box>) {
|
||||||
|
const { children, onReset, ...rest } = props;
|
||||||
|
const [submits, setSubmits] = useState({} as { [id: string]: SubmitHandler });
|
||||||
|
const [resets, setResets] = useState({} as Record<string, () => void>);
|
||||||
|
const [dirty, setDirty] = useState({} as Record<string, boolean>);
|
||||||
|
const [errors, setErrors] = useState({} as Record<string, boolean>);
|
||||||
|
const addSubmit = useCallback((id: string, s: SubmitHandler) => {
|
||||||
|
setSubmits((ss) => ({ ...ss, [id]: s }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetAll = useCallback(() => {
|
||||||
|
_.map(resets, (r) => r());
|
||||||
|
onReset && onReset();
|
||||||
|
}, [resets, onReset]);
|
||||||
|
|
||||||
|
const submitAll = useCallback(async () => {
|
||||||
|
await Promise.all(
|
||||||
|
_.map(
|
||||||
|
_.pickBy(submits, (_v, k) => dirty[k]),
|
||||||
|
(f) => f()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [submits, dirty]);
|
||||||
|
|
||||||
|
const onDirty = useCallback(
|
||||||
|
(id: string, t: boolean) => {
|
||||||
|
setDirty((ts) => ({ ...ts, [id]: t }));
|
||||||
|
},
|
||||||
|
[setDirty]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onErrors = useCallback((id: string, e: boolean) => {
|
||||||
|
setErrors((es) => ({ ...es, [id]: e }));
|
||||||
|
}, []);
|
||||||
|
const addReset = useCallback((id: string, reset: () => void) => {
|
||||||
|
setResets((rs) => ({ ...rs, [id]: reset }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const context = { addSubmit, submitAll, onErrors, onDirty, addReset };
|
||||||
|
|
||||||
|
const hasErrors = useMemo(
|
||||||
|
() => _.keys(_.pickBy(errors, (s) => !!s)).length > 0,
|
||||||
|
[errors]
|
||||||
|
);
|
||||||
|
const isDirty = useMemo(
|
||||||
|
() => _.keys(_.pickBy(dirty, _.identity)).length > 0,
|
||||||
|
[dirty]
|
||||||
|
);
|
||||||
|
usePreventWindowUnload(isDirty);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box {...rest} position="relative">
|
||||||
|
<Prompt
|
||||||
|
when={isDirty}
|
||||||
|
message="Are you sure you want to leave? You have unsaved changes"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormGroupContext.Provider value={context}>
|
||||||
|
{children}
|
||||||
|
</FormGroupContext.Provider>
|
||||||
|
<Row
|
||||||
|
justifyContent="flex-end"
|
||||||
|
width="100%"
|
||||||
|
position="sticky"
|
||||||
|
bottom="0px"
|
||||||
|
p="3"
|
||||||
|
gapX="2"
|
||||||
|
backgroundColor="white"
|
||||||
|
borderTop="1"
|
||||||
|
borderTopColor="washedGray"
|
||||||
|
>
|
||||||
|
<Button onClick={resetAll}>Cancel</Button>
|
||||||
|
<StatelessAsyncButton
|
||||||
|
onClick={submitAll}
|
||||||
|
disabled={hasErrors || !isDirty}
|
||||||
|
primary
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</StatelessAsyncButton>
|
||||||
|
</Row>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@ -64,7 +64,7 @@ function Elbow(
|
|||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
border="2px solid"
|
border="2px solid"
|
||||||
borderRadius={2}
|
borderRadius={3}
|
||||||
borderColor={color}
|
borderColor={color}
|
||||||
position="absolute"
|
position="absolute"
|
||||||
left="0px"
|
left="0px"
|
||||||
|
@ -83,7 +83,7 @@ const StatusBar = (props) => {
|
|||||||
onClick={() => history.push('/')}
|
onClick={() => history.push('/')}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Icon icon='Spaces' color='black' />
|
<Icon icon='Dashboard' color='black' />
|
||||||
</Button>
|
</Button>
|
||||||
<StatusBarItem float={floatLeap} mr={2} onClick={() => toggleOmnibox()}>
|
<StatusBarItem float={floatLeap} mr={2} onClick={() => toggleOmnibox()}>
|
||||||
{!doNotDisturb && (notificationsCount > 0 || invites.length > 0) && (
|
{!doNotDisturb && (notificationsCount > 0 || invites.length > 0) && (
|
||||||
|
@ -115,6 +115,10 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
* A map of child refs, used to calculate scroll position
|
* A map of child refs, used to calculate scroll position
|
||||||
*/
|
*/
|
||||||
private childRefs = new BigIntOrderedMap<HTMLElement>();
|
private childRefs = new BigIntOrderedMap<HTMLElement>();
|
||||||
|
/**
|
||||||
|
* A set of child refs which have been unmounted
|
||||||
|
*/
|
||||||
|
private orphans = new Set<string>();
|
||||||
/**
|
/**
|
||||||
* If saving, the bottommost visible element that we pin our scroll to
|
* If saving, the bottommost visible element that we pin our scroll to
|
||||||
*/
|
*/
|
||||||
@ -140,6 +144,10 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
|
|
||||||
private scrollRef: HTMLElement | null = null;
|
private scrollRef: HTMLElement | null = null;
|
||||||
|
|
||||||
|
private cleanupRefInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
private initScroll: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
constructor(props: VirtualScrollerProps<T>) {
|
constructor(props: VirtualScrollerProps<T>) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
@ -157,6 +165,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
this.onScroll = IS_IOS ? _.debounce(this.onScroll.bind(this), 400) : this.onScroll.bind(this);
|
this.onScroll = IS_IOS ? _.debounce(this.onScroll.bind(this), 400) : this.onScroll.bind(this);
|
||||||
this.scrollKeyMap = this.scrollKeyMap.bind(this);
|
this.scrollKeyMap = this.scrollKeyMap.bind(this);
|
||||||
this.setWindow = this.setWindow.bind(this);
|
this.setWindow = this.setWindow.bind(this);
|
||||||
|
this.restore = this.restore.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -164,8 +173,27 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
this.resetScroll();
|
this.resetScroll();
|
||||||
this.loadTop();
|
this.loadTop();
|
||||||
this.loadBottom();
|
this.loadBottom();
|
||||||
|
this.cleanupRefInterval = setInterval(this.cleanupRefs, 5000);
|
||||||
|
this.initScroll = setTimeout(() => {
|
||||||
|
log('scroll', 'initialised scroll');
|
||||||
|
this.restore();
|
||||||
|
this.initScroll = null;
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
cleanupRefs = () => {
|
||||||
|
if(this.saveDepth > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[...this.orphans].forEach(o => {
|
||||||
|
const index = bigInt(o);
|
||||||
|
this.childRefs.delete(index);
|
||||||
|
});
|
||||||
|
this.orphans.clear();
|
||||||
|
};
|
||||||
|
|
||||||
// manipulate scrollbar manually, to dodge change detection
|
// manipulate scrollbar manually, to dodge change detection
|
||||||
updateScroll = IS_IOS ? () => {} : _.throttle(() => {
|
updateScroll = IS_IOS ? () => {} : _.throttle(() => {
|
||||||
if(!this.window || !this.scrollRef) {
|
if(!this.window || !this.scrollRef) {
|
||||||
@ -199,6 +227,12 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener('keydown', this.invertedKeyHandler);
|
window.removeEventListener('keydown', this.invertedKeyHandler);
|
||||||
|
if(this.cleanupRefInterval) {
|
||||||
|
clearInterval(this.cleanupRefInterval);
|
||||||
|
}
|
||||||
|
if(this.initScroll) {
|
||||||
|
clearTimeout(this.initScroll);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startOffset() {
|
startOffset() {
|
||||||
@ -237,9 +271,6 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
}, () => {
|
}, () => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this.restore();
|
this.restore();
|
||||||
requestAnimationFrame(() => {
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -339,6 +370,10 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
// bail if we're going to adjust scroll anyway
|
// bail if we're going to adjust scroll anyway
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if(this.initScroll) {
|
||||||
|
clearTimeout(this.initScroll);
|
||||||
|
this.initScroll = null;
|
||||||
|
}
|
||||||
if(this.saveDepth > 0) {
|
if(this.saveDepth > 0) {
|
||||||
log('bail', 'deep scroll queue');
|
log('bail', 'deep scroll queue');
|
||||||
return;
|
return;
|
||||||
@ -394,8 +429,15 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
log('bail', 'Deep restore');
|
log('bail', 'Deep restore');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if(this.initScroll) {
|
||||||
|
log('bail', 'still initialising scroll');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const ref = this.childRefs.get(this.savedIndex)!;
|
let ref = this.childRefs.get(this.savedIndex)
|
||||||
|
if(!ref) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const newScrollTop = this.window.scrollHeight - ref.offsetTop - this.savedDistance;
|
const newScrollTop = this.window.scrollHeight - ref.offsetTop - this.savedDistance;
|
||||||
|
|
||||||
this.window.scrollTo(0, newScrollTop);
|
this.window.scrollTo(0, newScrollTop);
|
||||||
@ -435,11 +477,12 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
if(!this.window || this.savedIndex) {
|
if(!this.window || this.savedIndex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.saveDepth++;
|
if(this.saveDepth !== 0) {
|
||||||
if(this.saveDepth !== 1) {
|
|
||||||
console.log('bail', 'deep save');
|
console.log('bail', 'deep save');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.saveDepth++;
|
||||||
|
|
||||||
let bottomIndex: BigInteger | null = null;
|
let bottomIndex: BigInteger | null = null;
|
||||||
const { scrollTop, scrollHeight } = this.window;
|
const { scrollTop, scrollHeight } = this.window;
|
||||||
@ -472,10 +515,9 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
setRef = (element: HTMLElement | null, index: BigInteger) => {
|
setRef = (element: HTMLElement | null, index: BigInteger) => {
|
||||||
if(element) {
|
if(element) {
|
||||||
this.childRefs.set(index, element);
|
this.childRefs.set(index, element);
|
||||||
|
this.orphans.delete(index.toString());
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => {
|
this.orphans.add(index.toString());
|
||||||
this.childRefs.delete(index);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ export class OmniboxResult extends Component {
|
|||||||
<Icon
|
<Icon
|
||||||
display='inline-block'
|
display='inline-block'
|
||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
icon='Inbox'
|
icon='Notifications'
|
||||||
mr='2'
|
mr='2'
|
||||||
size='18px'
|
size='18px'
|
||||||
color={iconFill}
|
color={iconFill}
|
||||||
@ -85,7 +85,7 @@ export class OmniboxResult extends Component {
|
|||||||
<Icon
|
<Icon
|
||||||
display='inline-block'
|
display='inline-block'
|
||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
icon='SignOut'
|
icon='LogOut'
|
||||||
mr='2'
|
mr='2'
|
||||||
size='18px'
|
size='18px'
|
||||||
color={iconFill}
|
color={iconFill}
|
||||||
@ -119,7 +119,7 @@ export class OmniboxResult extends Component {
|
|||||||
<Icon
|
<Icon
|
||||||
display='inline-block'
|
display='inline-block'
|
||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
icon='Inbox'
|
icon='Notifications'
|
||||||
mr='2'
|
mr='2'
|
||||||
size='18px'
|
size='18px'
|
||||||
color={iconFill}
|
color={iconFill}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
font-family: 'Inter';
|
font-family: 'Inter';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
src: url("/~landscape/fonts/inter-regular.woff2") format("woff2"),
|
src: url("/~landscape/fonts/inter-regular.woff2") format("woff2"),
|
||||||
url("https://media.urbit.org/fonts/Inter-Regular.woff2") format("woff2");
|
url("https://media.urbit.org/fonts/Inter-Regular.woff2") format("woff2");
|
||||||
}
|
}
|
||||||
@ -10,6 +11,7 @@
|
|||||||
font-family: 'Inter';
|
font-family: 'Inter';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
src: url("https://media.urbit.org/fonts/Inter-Medium.woff2") format("woff2");
|
src: url("https://media.urbit.org/fonts/Inter-Medium.woff2") format("woff2");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -17,6 +19,7 @@
|
|||||||
font-family: 'Inter';
|
font-family: 'Inter';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
src: url("https://media.urbit.org/fonts/Inter-SemiBold.woff2") format("woff2");
|
src: url("https://media.urbit.org/fonts/Inter-SemiBold.woff2") format("woff2");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,6 +27,7 @@
|
|||||||
font-family: 'Inter';
|
font-family: 'Inter';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
src: url("/~landscape/fonts/inter-italic.woff2") format("woff2"),
|
src: url("/~landscape/fonts/inter-italic.woff2") format("woff2"),
|
||||||
url("https://media.urbit.org/fonts/Inter-Italic.woff2") format("woff2");
|
url("https://media.urbit.org/fonts/Inter-Italic.woff2") format("woff2");
|
||||||
}
|
}
|
||||||
@ -32,6 +36,7 @@
|
|||||||
font-family: 'Inter';
|
font-family: 'Inter';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
src: url("/~landscape/fonts/inter-bold.woff2") format("woff2"),
|
src: url("/~landscape/fonts/inter-bold.woff2") format("woff2"),
|
||||||
url("https://media.urbit.org/fonts/Inter-Bold.woff2") format("woff2");
|
url("https://media.urbit.org/fonts/Inter-Bold.woff2") format("woff2");
|
||||||
}
|
}
|
||||||
@ -39,6 +44,7 @@
|
|||||||
font-family: 'Inter';
|
font-family: 'Inter';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
src: url("/~landscape/fonts/inter-bolditalic.woff2") format("woff2"),
|
src: url("/~landscape/fonts/inter-bolditalic.woff2") format("woff2"),
|
||||||
url("https://media.urbit.org/fonts/Inter-BoldItalic.woff2") format("woff2");
|
url("https://media.urbit.org/fonts/Inter-BoldItalic.woff2") format("woff2");
|
||||||
}
|
}
|
||||||
@ -48,6 +54,7 @@
|
|||||||
src: url("/~landscape/fonts/sourcecodepro-extralight.woff2"),
|
src: url("/~landscape/fonts/sourcecodepro-extralight.woff2"),
|
||||||
url("https://storage.googleapis.com/media.urbit.org/fonts/scp-extralight.woff");
|
url("https://storage.googleapis.com/media.urbit.org/fonts/scp-extralight.woff");
|
||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@ -55,6 +62,7 @@
|
|||||||
src: url("/~landscape/fonts/sourcecodepro-light.woff2"),
|
src: url("/~landscape/fonts/sourcecodepro-light.woff2"),
|
||||||
url("https://storage.googleapis.com/media.urbit.org/fonts/scp-light.woff");
|
url("https://storage.googleapis.com/media.urbit.org/fonts/scp-light.woff");
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@ -62,6 +70,7 @@
|
|||||||
src: url("/~landscape/fonts/sourcecodepro-regular.woff2"),
|
src: url("/~landscape/fonts/sourcecodepro-regular.woff2"),
|
||||||
url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff");
|
url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff");
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@ -69,6 +78,7 @@
|
|||||||
src: url("(/~landscape/fonts/sourcecodepro-medium.woff2"),
|
src: url("(/~landscape/fonts/sourcecodepro-medium.woff2"),
|
||||||
url("https://storage.googleapis.com/media.urbit.org/fonts/scp-medium.woff");
|
url("https://storage.googleapis.com/media.urbit.org/fonts/scp-medium.woff");
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@ -76,6 +86,7 @@
|
|||||||
src: url("/~landscape/fonts/sourcecodepro-semibold.woff2"),
|
src: url("/~landscape/fonts/sourcecodepro-semibold.woff2"),
|
||||||
url("https://storage.googleapis.com/media.urbit.org/fonts/scp-semibold.woff");
|
url("https://storage.googleapis.com/media.urbit.org/fonts/scp-semibold.woff");
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@ -83,5 +94,6 @@
|
|||||||
src: url("/~landscape/fonts/sourcecodepro-bold.woff2"),
|
src: url("/~landscape/fonts/sourcecodepro-bold.woff2"),
|
||||||
url("https://storage.googleapis.com/media.urbit.org/fonts/scp-bold.woff");
|
url("https://storage.googleapis.com/media.urbit.org/fonts/scp-bold.woff");
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ import GlobalApi from '~/logic/api/global';
|
|||||||
import { resourceFromPath } from '~/logic/lib/group';
|
import { resourceFromPath } from '~/logic/lib/group';
|
||||||
import { FormSubmit } from '~/views/components/FormSubmit';
|
import { FormSubmit } from '~/views/components/FormSubmit';
|
||||||
import { ChannelWritePerms } from '../ChannelWritePerms';
|
import { ChannelWritePerms } from '../ChannelWritePerms';
|
||||||
|
import {FormGroupChild} from '~/views/components/FormGroup';
|
||||||
|
|
||||||
function PermissionsSummary(props: {
|
function PermissionsSummary(props: {
|
||||||
writersSize: number;
|
writersSize: number;
|
||||||
@ -158,7 +159,8 @@ export function GraphPermissions(props: GraphPermissionsProps) {
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
>
|
>
|
||||||
<Form style={{ display: 'contents' }}>
|
<Form style={{ display: 'contents' }}>
|
||||||
<Col mt="4" flexShrink={0} gapY="5">
|
<FormGroupChild id="permissions" />
|
||||||
|
<Col mx="4" mt="4" flexShrink={0} gapY="5">
|
||||||
<Col gapY="1" mt="0">
|
<Col gapY="1" mt="0">
|
||||||
<Text id="permissions" fontWeight="bold" fontSize="2">
|
<Text id="permissions" fontWeight="bold" fontSize="2">
|
||||||
Permissions
|
Permissions
|
||||||
@ -187,7 +189,6 @@ export function GraphPermissions(props: GraphPermissionsProps) {
|
|||||||
caption="If enabled, all members of the group can comment on this channel"
|
caption="If enabled, all members of the group can comment on this channel"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<FormSubmit>Update Permissions</FormSubmit>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { Formik, Form } from 'formik';
|
import { Formik, Form } from "formik";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ManagedTextInputField as Input,
|
ManagedTextInputField as Input,
|
||||||
Col,
|
Col,
|
||||||
Label,
|
Label,
|
||||||
Text
|
Text,
|
||||||
} from '@tlon/indigo-react';
|
} from "@tlon/indigo-react";
|
||||||
import { Association } from '@urbit/api';
|
import { Association } from "@urbit/api";
|
||||||
|
|
||||||
import { FormError } from '~/views/components/FormError';
|
import { FormError } from "~/views/components/FormError";
|
||||||
import { ColorInput } from '~/views/components/ColorInput';
|
import { ColorInput } from "~/views/components/ColorInput";
|
||||||
import { uxToHex } from '~/logic/lib/util';
|
import { uxToHex } from "~/logic/lib/util";
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from "~/logic/api/global";
|
||||||
import { FormSubmit } from '~/views/components/FormSubmit';
|
import { FormSubmit } from "~/views/components/FormSubmit";
|
||||||
|
import { FormGroupChild } from "~/views/components/FormGroup";
|
||||||
|
|
||||||
interface FormSchema {
|
interface FormSchema {
|
||||||
title: string;
|
title: string;
|
||||||
@ -30,9 +31,9 @@ export function ChannelDetails(props: ChannelDetailsProps) {
|
|||||||
const { association, api } = props;
|
const { association, api } = props;
|
||||||
const { metadata } = association;
|
const { metadata } = association;
|
||||||
const initialValues: FormSchema = {
|
const initialValues: FormSchema = {
|
||||||
title: metadata?.title || '',
|
title: metadata?.title || "",
|
||||||
description: metadata?.description || '',
|
description: metadata?.description || "",
|
||||||
color: metadata?.color || '0x0'
|
color: metadata?.color || "0x0",
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (values: FormSchema, actions) => {
|
const onSubmit = async (values: FormSchema, actions) => {
|
||||||
@ -44,8 +45,9 @@ export function ChannelDetails(props: ChannelDetailsProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik initialValues={initialValues} onSubmit={onSubmit}>
|
<Formik initialValues={initialValues} onSubmit={onSubmit}>
|
||||||
<Form style={{ display: 'contents' }}>
|
<Form style={{ display: "contents" }}>
|
||||||
<Col mb="4" flexShrink={0} gapY="4">
|
<FormGroupChild id="details" />
|
||||||
|
<Col mx="4" mb="4" flexShrink={0} gapY="4">
|
||||||
<Col mb={3}>
|
<Col mb={3}>
|
||||||
<Text id="details" fontSize="2" fontWeight="bold">
|
<Text id="details" fontSize="2" fontWeight="bold">
|
||||||
Channel Details
|
Channel Details
|
||||||
@ -69,9 +71,6 @@ export function ChannelDetails(props: ChannelDetailsProps) {
|
|||||||
label="Color"
|
label="Color"
|
||||||
caption="Change the color of this channel"
|
caption="Change the color of this channel"
|
||||||
/>
|
/>
|
||||||
<FormSubmit>
|
|
||||||
Update Details
|
|
||||||
</FormSubmit>
|
|
||||||
<FormError message="Failed to update settings" />
|
<FormError message="Failed to update settings" />
|
||||||
</Col>
|
</Col>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -28,7 +28,7 @@ export function ChannelNotifications(props: ChannelNotificationsProps) {
|
|||||||
const anchorRef = useRef<HTMLElement | null>(null);
|
const anchorRef = useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col mb="6" gapY="4" flexShrink={0}>
|
<Col mx="4" mb="6" gapY="4" flexShrink={0}>
|
||||||
<Text ref={anchorRef} id="notifications" fontSize="2" fontWeight="bold">
|
<Text ref={anchorRef} id="notifications" fontSize="2" fontWeight="bold">
|
||||||
Channel Notifications
|
Channel Notifications
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -13,7 +13,7 @@ export function ChannelPopoverRoutesSidebar(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Col
|
<Col
|
||||||
display={['none', 'flex-column']}
|
display={['none', 'flex']}
|
||||||
minWidth="200px"
|
minWidth="200px"
|
||||||
borderRight="1"
|
borderRight="1"
|
||||||
borderRightColor="washedGray"
|
borderRightColor="washedGray"
|
||||||
@ -27,13 +27,13 @@ export function ChannelPopoverRoutesSidebar(props: {
|
|||||||
Preferences
|
Preferences
|
||||||
</Text>
|
</Text>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="Inbox"
|
icon='Notifications'
|
||||||
text="Notifications"
|
text="Notifications"
|
||||||
to={relativePath('/settings#notifications')}
|
to={relativePath('/settings#notifications')}
|
||||||
/>
|
/>
|
||||||
{!isOwner && (
|
{!isOwner && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="SignOut"
|
icon="LogOut"
|
||||||
text="Unsubscribe"
|
text="Unsubscribe"
|
||||||
color="red"
|
color="red"
|
||||||
to={relativePath('/settings#unsubscribe')}
|
to={relativePath('/settings#unsubscribe')}
|
||||||
@ -45,7 +45,7 @@ export function ChannelPopoverRoutesSidebar(props: {
|
|||||||
Administration
|
Administration
|
||||||
</Text>
|
</Text>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="Boot"
|
icon="BootNode"
|
||||||
text="Channel Details"
|
text="Channel Details"
|
||||||
to={relativePath('/settings#details')}
|
to={relativePath('/settings#details')}
|
||||||
/>
|
/>
|
||||||
@ -56,14 +56,14 @@ export function ChannelPopoverRoutesSidebar(props: {
|
|||||||
/>
|
/>
|
||||||
{ isOwner ? (
|
{ isOwner ? (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="TrashCan"
|
icon="X"
|
||||||
text="Archive Channel"
|
text="Archive Channel"
|
||||||
to={relativePath('/settings#archive')}
|
to={relativePath('/settings#archive')}
|
||||||
color="red"
|
color="red"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="TrashCan"
|
icon="X"
|
||||||
text="Archive Channel"
|
text="Archive Channel"
|
||||||
to={relativePath('/settings#remove')}
|
to={relativePath('/settings#remove')}
|
||||||
color="red"
|
color="red"
|
||||||
|
@ -19,6 +19,7 @@ import { useHistory, Link } from 'react-router-dom';
|
|||||||
import { ChannelNotifications } from './Notifications';
|
import { ChannelNotifications } from './Notifications';
|
||||||
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
|
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
|
||||||
import { isChannelAdmin, isHost } from '~/logic/lib/group';
|
import { isChannelAdmin, isHost } from '~/logic/lib/group';
|
||||||
|
import {FormGroup} from '~/views/components/FormGroup';
|
||||||
|
|
||||||
interface ChannelPopoverRoutesProps {
|
interface ChannelPopoverRoutesProps {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
@ -83,10 +84,10 @@ export function ChannelPopoverRoutes(props: ChannelPopoverRoutesProps) {
|
|||||||
isOwner={isOwner}
|
isOwner={isOwner}
|
||||||
baseUrl={props.baseUrl}
|
baseUrl={props.baseUrl}
|
||||||
/>
|
/>
|
||||||
<Col height="100%" overflowY="auto" p="5" flexGrow={1}>
|
<FormGroup onReset={onDismiss} height="100%" overflowY="auto" pt="5" flexGrow={1}>
|
||||||
<ChannelNotifications {...props} />
|
<ChannelNotifications {...props} />
|
||||||
{!isOwner && (
|
{!isOwner && (
|
||||||
<Col mb="6" flexShrink={0}>
|
<Col mx="4" mb="6" flexShrink={0}>
|
||||||
<Text id="unsubscribe" fontSize="2" fontWeight="bold">
|
<Text id="unsubscribe" fontSize="2" fontWeight="bold">
|
||||||
Unsubscribe from Channel
|
Unsubscribe from Channel
|
||||||
</Text>
|
</Text>
|
||||||
@ -107,7 +108,7 @@ export function ChannelPopoverRoutes(props: ChannelPopoverRoutesProps) {
|
|||||||
<ChannelDetails {...props} />
|
<ChannelDetails {...props} />
|
||||||
<GraphPermissions {...props} />
|
<GraphPermissions {...props} />
|
||||||
{ isOwner ? (
|
{ isOwner ? (
|
||||||
<Col mt="5" mb="6" flexShrink={0}>
|
<Col mx="4" mt="5" mb="6" flexShrink={0}>
|
||||||
<Text id="archive" fontSize="2" fontWeight="bold">
|
<Text id="archive" fontSize="2" fontWeight="bold">
|
||||||
Archive channel
|
Archive channel
|
||||||
</Text>
|
</Text>
|
||||||
@ -124,7 +125,7 @@ export function ChannelPopoverRoutes(props: ChannelPopoverRoutesProps) {
|
|||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
<Col mt="5" mb="6" flexShrink={0}>
|
<Col mx="4" my="6" flexShrink={0}>
|
||||||
<Text id="remove" fontSize="2" fontWeight="bold">
|
<Text id="remove" fontSize="2" fontWeight="bold">
|
||||||
Remove channel from group
|
Remove channel from group
|
||||||
</Text>
|
</Text>
|
||||||
@ -143,7 +144,7 @@ export function ChannelPopoverRoutes(props: ChannelPopoverRoutesProps) {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</FormGroup>
|
||||||
</Row>
|
</Row>
|
||||||
</ModalOverlay>
|
</ModalOverlay>
|
||||||
);
|
);
|
||||||
|
@ -36,7 +36,7 @@ return;
|
|||||||
? 'Permanently delete this group. (All current members will no longer see this group.)'
|
? 'Permanently delete this group. (All current members will no longer see this group.)'
|
||||||
: 'You can rejoin if it is an open group, or if you are reinvited';
|
: 'You can rejoin if it is an open group, or if you are reinvited';
|
||||||
|
|
||||||
const icon = props.owner ? 'X' : 'SignOut';
|
const icon = props.owner ? 'X' : 'LogOut';
|
||||||
const { modal, showModal } = useModal({ modal:
|
const { modal, showModal } = useModal({ modal:
|
||||||
(dismiss: () => void) => {
|
(dismiss: () => void) => {
|
||||||
const onCancel = (e) => {
|
const onCancel = (e) => {
|
||||||
|
@ -29,9 +29,9 @@ export function GroupSummary(props: GroupSummaryProps & PropFunc<typeof Col>): R
|
|||||||
width="40px"
|
width="40px"
|
||||||
height="40px"
|
height="40px"
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
flexShrink="0"
|
flexShrink={0}
|
||||||
/>
|
/>
|
||||||
<Col justifyContent="space-between" flexGrow="1" overflow="hidden">
|
<Col justifyContent="space-between" flexGrow={1} overflow="hidden">
|
||||||
<Text
|
<Text
|
||||||
fontSize="1"
|
fontSize="1"
|
||||||
textOverflow="ellipsis"
|
textOverflow="ellipsis"
|
||||||
|
@ -180,7 +180,7 @@ export function GroupSwitcher(props: {
|
|||||||
>
|
>
|
||||||
<Row flexGrow={1} alignItems="center" width='100%' minWidth='0' flexShrink={0}>
|
<Row flexGrow={1} alignItems="center" width='100%' minWidth='0' flexShrink={0}>
|
||||||
{ metadata && <MetadataIcon flexShrink={0} mr="2" metadata={metadata} height="24px" width="24px" /> }
|
{ metadata && <MetadataIcon flexShrink={0} mr="2" metadata={metadata} height="24px" width="24px" /> }
|
||||||
<Text flexShrink={1} lineHeight="1.1" fontSize='2' fontWeight="600" overflow='hidden' display='inline-block' flexShrink='1' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}>{title}</Text>
|
<Text flexShrink={1} lineHeight="1.1" fontSize='2' fontWeight="600" overflow='hidden' display='inline-block' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}>{title}</Text>
|
||||||
</Row>
|
</Row>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Row pr='3' verticalAlign="middle">
|
<Row pr='3' verticalAlign="middle">
|
||||||
|
@ -69,7 +69,7 @@ export function GroupifyForm(props: GroupifyFormProps) {
|
|||||||
onSubmit={onGroupify}
|
onSubmit={onGroupify}
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<Col flexShrink="0" gapY="4" maxWidth="512px">
|
<Col flexShrink={0} gapY="4" maxWidth="512px">
|
||||||
<Box>
|
<Box>
|
||||||
<Text fontWeight="500">Groupify this channel</Text>
|
<Text fontWeight="500">Groupify this channel</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -24,7 +24,7 @@ function canWrite(props) {
|
|||||||
if(vip === 'host-feed') {
|
if(vip === 'host-feed') {
|
||||||
return isHost(association.group);
|
return isHost(association.group);
|
||||||
}
|
}
|
||||||
|
|
||||||
return isWriter(group, association.resource);
|
return isWriter(group, association.resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ export function PostInput(props) {
|
|||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
) : (
|
) : (
|
||||||
<Icon
|
<Icon
|
||||||
icon='Links'
|
icon='Attachment'
|
||||||
width='16'
|
width='16'
|
||||||
height='16'
|
height='16'
|
||||||
onClick={uploadImage}
|
onClick={uploadImage}
|
||||||
|
@ -79,6 +79,7 @@ export default function PostReplies(props) {
|
|||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
history={history}
|
history={history}
|
||||||
isParent={true}
|
isParent={true}
|
||||||
|
parentPost={parentNode?.post}
|
||||||
vip={vip}
|
vip={vip}
|
||||||
group={group}
|
group={group}
|
||||||
/>
|
/>
|
||||||
|
@ -177,7 +177,7 @@ export function JoinGroup(props: JoinGroupProps): ReactElement {
|
|||||||
<Text gray fontSize="1">
|
<Text gray fontSize="1">
|
||||||
Channels
|
Channels
|
||||||
</Text>
|
</Text>
|
||||||
<Box width="100%" flexShrink="0">
|
<Box width="100%" flexShrink={0}>
|
||||||
{Object.values(preview.channels).map(({ metadata }: any) => (
|
{Object.values(preview.channels).map(({ metadata }: any) => (
|
||||||
<Row width="100%">
|
<Row width="100%">
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -11,7 +11,7 @@ import * as Yup from 'yup';
|
|||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { AsyncButton } from '~/views/components/AsyncButton';
|
import { AsyncButton } from '~/views/components/AsyncButton';
|
||||||
import { FormError } from '~/views/components/FormError';
|
import { FormError } from '~/views/components/FormError';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps, useHistory } from 'react-router-dom';
|
||||||
import { stringToSymbol, parentPath, deSig } from '~/logic/lib/util';
|
import { stringToSymbol, parentPath, deSig } from '~/logic/lib/util';
|
||||||
import { resourceFromPath } from '~/logic/lib/group';
|
import { resourceFromPath } from '~/logic/lib/group';
|
||||||
import { Associations } from '@urbit/api/metadata';
|
import { Associations } from '@urbit/api/metadata';
|
||||||
@ -46,12 +46,13 @@ interface NewChannelProps {
|
|||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NewChannel(props: NewChannelProps & RouteComponentProps): ReactElement {
|
export function NewChannel(props: NewChannelProps): ReactElement {
|
||||||
const { history, api, group, workspace } = props;
|
const history = useHistory();
|
||||||
|
const { api, group, workspace } = props;
|
||||||
|
|
||||||
const groups = useGroupState(state => state.groups);
|
const groups = useGroupState(state => state.groups);
|
||||||
const waiter = useWaitForProps({ groups }, 5000);
|
const waiter = useWaitForProps({ groups }, 5000);
|
||||||
|
|
||||||
const onSubmit = async (values: FormSchema, actions) => {
|
const onSubmit = async (values: FormSchema, actions) => {
|
||||||
const name = (values.name) ? values.name : values.moduleType;
|
const name = (values.name) ? values.name : values.moduleType;
|
||||||
const resId: string = stringToSymbol(values.name)
|
const resId: string = stringToSymbol(values.name)
|
||||||
@ -152,7 +153,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps): ReactE
|
|||||||
name="moduleType"
|
name="moduleType"
|
||||||
/>
|
/>
|
||||||
<IconRadio
|
<IconRadio
|
||||||
icon="Publish"
|
icon="Notebook"
|
||||||
label="Notebook"
|
label="Notebook"
|
||||||
id="publish"
|
id="publish"
|
||||||
name="moduleType"
|
name="moduleType"
|
||||||
|
@ -181,9 +181,9 @@ export function Participants(props: {
|
|||||||
mb={2}
|
mb={2}
|
||||||
px={2}
|
px={2}
|
||||||
zIndex={1}
|
zIndex={1}
|
||||||
flexShrink="0"
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
<Row mr="4" flexShrink="0">
|
<Row mr="4" flexShrink={0}>
|
||||||
<Tab
|
<Tab
|
||||||
selected={filter}
|
selected={filter}
|
||||||
setSelected={setFilter}
|
setSelected={setFilter}
|
||||||
@ -206,9 +206,9 @@ export function Participants(props: {
|
|||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
<Col flexShrink="0" width="100%" height="fit-content">
|
<Col flexShrink={0} width="100%" height="fit-content">
|
||||||
<Row alignItems="center" bg="washedGray" borderRadius="1" px="2" my="2">
|
<Row alignItems="center" bg="washedGray" borderRadius="1" px="2" my="2">
|
||||||
<Icon color="gray" icon="MagnifyingGlass" />
|
<Icon color="gray" icon="Search" />
|
||||||
<Input
|
<Input
|
||||||
maxWidth="256px"
|
maxWidth="256px"
|
||||||
color="gray"
|
color="gray"
|
||||||
|
@ -76,7 +76,7 @@ export function PopoverRoutes(
|
|||||||
<Col gapY="2">
|
<Col gapY="2">
|
||||||
<Text my="1" mx="3" gray>Group</Text>
|
<Text my="1" mx="3" gray>Group</Text>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="Inbox"
|
icon='Notifications'
|
||||||
to={relativeUrl('/settings#notifications')}
|
to={relativeUrl('/settings#notifications')}
|
||||||
text="Notifications"
|
text="Notifications"
|
||||||
/>
|
/>
|
||||||
@ -98,7 +98,7 @@ export function PopoverRoutes(
|
|||||||
text="Group Details"
|
text="Group Details"
|
||||||
/>
|
/>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="Spaces"
|
icon="Dashboard"
|
||||||
to={relativeUrl('/settings#channels')}
|
to={relativeUrl('/settings#channels')}
|
||||||
text="Channel Management"
|
text="Channel Management"
|
||||||
/>
|
/>
|
||||||
|
@ -77,7 +77,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
|||||||
fontSize='1'
|
fontSize='1'
|
||||||
mr='12px'
|
mr='12px'
|
||||||
my='1'
|
my='1'
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
display={['block','none']}
|
display={['block','none']}
|
||||||
>
|
>
|
||||||
<Link to={`/~landscape${workspace}`}>
|
<Link to={`/~landscape${workspace}`}>
|
||||||
@ -98,7 +98,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
|||||||
maxWidth={association?.metadata?.description ? ['100%', '50%'] : 'none'}
|
maxWidth={association?.metadata?.description ? ['100%', '50%'] : 'none'}
|
||||||
mr='2'
|
mr='2'
|
||||||
ml='1'
|
ml='1'
|
||||||
flexShrink={['1', '0']}
|
flexShrink={[1, 0]}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
@ -112,7 +112,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
|||||||
mb='0'
|
mb='0'
|
||||||
minWidth='0'
|
minWidth='0'
|
||||||
maxWidth='50%'
|
maxWidth='50%'
|
||||||
flexShrink='1'
|
flexShrink={1}
|
||||||
disableRemoteContent
|
disableRemoteContent
|
||||||
>
|
>
|
||||||
{workspace === '/messages'
|
{workspace === '/messages'
|
||||||
@ -145,7 +145,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
|||||||
return (
|
return (
|
||||||
<Col width='100%' height='100%' overflow='hidden'>
|
<Col width='100%' height='100%' overflow='hidden'>
|
||||||
<Box
|
<Box
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
height='48px'
|
height='48px'
|
||||||
py='2'
|
py='2'
|
||||||
px='2'
|
px='2'
|
||||||
@ -159,7 +159,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
|||||||
display='flex'
|
display='flex'
|
||||||
alignItems='baseline'
|
alignItems='baseline'
|
||||||
width={`calc(100% - ${actionsWidth}px - 16px)`}
|
width={`calc(100% - ${actionsWidth}px - 16px)`}
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
<BackLink />
|
<BackLink />
|
||||||
<Title />
|
<Title />
|
||||||
@ -169,7 +169,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
|||||||
ml={3}
|
ml={3}
|
||||||
display='flex'
|
display='flex'
|
||||||
alignItems='center'
|
alignItems='center'
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
ref={actionsRef}
|
ref={actionsRef}
|
||||||
>
|
>
|
||||||
{canWrite && <WriterControls />}
|
{canWrite && <WriterControls />}
|
||||||
|
@ -86,7 +86,11 @@ export function SidebarItem(props: {
|
|||||||
let color = 'lightGray';
|
let color = 'lightGray';
|
||||||
|
|
||||||
if (isSynced) {
|
if (isSynced) {
|
||||||
|
if (hasUnread || hasNotification) {
|
||||||
color = 'black';
|
color = 'black';
|
||||||
|
} else {
|
||||||
|
color = 'gray';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fontWeight = (hasUnread || hasNotification) ? '500' : 'normal';
|
const fontWeight = (hasUnread || hasNotification) ? '500' : 'normal';
|
||||||
@ -132,7 +136,7 @@ export function SidebarItem(props: {
|
|||||||
{DM ? img : (
|
{DM ? img : (
|
||||||
<Icon
|
<Icon
|
||||||
display="block"
|
display="block"
|
||||||
color={isSynced ? 'black' : 'gray'}
|
color={isSynced ? 'black' : 'lightGray'}
|
||||||
icon={getModuleIcon(mod) as any}
|
icon={getModuleIcon(mod) as any}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { ReactElement, useCallback } from 'react';
|
import React, { ReactElement, useCallback } from 'react';
|
||||||
import { FormikHelpers } from 'formik';
|
import { FormikHelpers } from 'formik';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Row,
|
Row,
|
||||||
@ -34,6 +34,7 @@ export function SidebarListHeader(props: {
|
|||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
handleSubmit: (c: SidebarListConfig) => void;
|
handleSubmit: (c: SidebarListConfig) => void;
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
|
const history = useHistory();
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(values: SidebarListConfig, actions: FormikHelpers<SidebarListConfig>) => {
|
(values: SidebarListConfig, actions: FormikHelpers<SidebarListConfig>) => {
|
||||||
props.handleSubmit(values);
|
props.handleSubmit(values);
|
||||||
@ -65,7 +66,7 @@ export function SidebarListHeader(props: {
|
|||||||
<Box>
|
<Box>
|
||||||
{( !!feedPath) ? (
|
{( !!feedPath) ? (
|
||||||
<Row
|
<Row
|
||||||
flexShrink="0"
|
flexShrink={0}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
py={2}
|
py={2}
|
||||||
@ -74,18 +75,18 @@ export function SidebarListHeader(props: {
|
|||||||
borderBottom={1}
|
borderBottom={1}
|
||||||
borderColor="lightGray"
|
borderColor="lightGray"
|
||||||
backgroundColor={['transparent',
|
backgroundColor={['transparent',
|
||||||
props.history.location.pathname.includes(`/~landscape${groupPath}/feed`)
|
history.location.pathname.includes(`/~landscape${groupPath}/feed`)
|
||||||
? (
|
? (
|
||||||
'washedGray'
|
'washedGray'
|
||||||
) : (
|
) : (
|
||||||
'transparent'
|
'transparent'
|
||||||
)]}
|
)]}
|
||||||
cursor={['pointer', (
|
cursor={(
|
||||||
props.history.location.pathname === `/~landscape${groupPath}/feed`
|
history.location.pathname === `/~landscape${groupPath}/feed`
|
||||||
? 'default' : 'pointer'
|
? 'default' : 'pointer'
|
||||||
)]}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.history.push(`/~landscape${groupPath}/feed`);
|
history.push(`/~landscape${groupPath}/feed`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text>
|
<Text>
|
||||||
@ -98,14 +99,14 @@ export function SidebarListHeader(props: {
|
|||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
<Row
|
<Row
|
||||||
flexShrink="0"
|
flexShrink={0}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
py={2}
|
py={2}
|
||||||
px={3}
|
px={3}
|
||||||
height='48px'
|
height='48px'
|
||||||
>
|
>
|
||||||
<Box flexShrink='0'>
|
<Box flexShrink={0}>
|
||||||
<Text>
|
<Text>
|
||||||
{props.initialValues.hideUnjoined ? `Joined ${noun}` : `All ${noun}`}
|
{props.initialValues.hideUnjoined ? `Joined ${noun}` : `All ${noun}`}
|
||||||
</Text>
|
</Text>
|
||||||
@ -131,7 +132,6 @@ export function SidebarListHeader(props: {
|
|||||||
>
|
>
|
||||||
<NewChannel
|
<NewChannel
|
||||||
api={props.api}
|
api={props.api}
|
||||||
history={props.history}
|
|
||||||
workspace={props.workspace}
|
workspace={props.workspace}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@ -152,7 +152,7 @@ export function SidebarListHeader(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
<Dropdown
|
<Dropdown
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
width="auto"
|
width="auto"
|
||||||
alignY="top"
|
alignY="top"
|
||||||
alignX={['right', 'left']}
|
alignX={['right', 'left']}
|
||||||
|
@ -1,227 +1,62 @@
|
|||||||
import { BigInteger } from "big-integer";
|
|
||||||
import { immerable } from 'immer';
|
import { immerable } from 'immer';
|
||||||
|
import bigInt, { BigInteger } from "big-integer";
|
||||||
|
|
||||||
interface NonemptyNode<V> {
|
function sortBigInt(a: BigInteger, b: BigInteger) {
|
||||||
n: [BigInteger, V];
|
if (a.lt(b)) {
|
||||||
l: MapNode<V>;
|
return 1;
|
||||||
r: MapNode<V>;
|
} else if (a.eq(b)) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type MapNode<V> = NonemptyNode<V> | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An implementation of ordered maps for JS
|
|
||||||
* Plagiarised wholesale from sys/zuse
|
|
||||||
*/
|
|
||||||
export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
||||||
private root: MapNode<V> = null;
|
private root: Record<string, V> = {}
|
||||||
|
private cachedIter: [BigInteger, V][] | null = null;
|
||||||
[immerable] = true;
|
[immerable] = true;
|
||||||
size: number = 0;
|
|
||||||
|
|
||||||
constructor(initial: [BigInteger, V][] = []) {
|
constructor(items: [BigInteger, V][] = []) {
|
||||||
initial.forEach(([key, val]) => {
|
items.forEach(([key, val]) => {
|
||||||
this.set(key, val);
|
this.set(key, val);
|
||||||
});
|
});
|
||||||
|
this.generateCachedIter();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
get size() {
|
||||||
* Retrieve an value for a key
|
return this.cachedIter?.length ?? Object.keys(this.root).length;
|
||||||
*/
|
|
||||||
get(key: BigInteger): V | null {
|
|
||||||
const inner = (node: MapNode<V>): V | null => {
|
|
||||||
if (!node) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const [k, v] = node.n;
|
|
||||||
if (key.eq(k)) {
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
|
||||||
return inner(node.l);
|
|
||||||
} else {
|
|
||||||
return inner(node.r);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Put an item by a key
|
|
||||||
*/
|
|
||||||
set(key: BigInteger, value: V): void {
|
|
||||||
|
|
||||||
const inner = (node: MapNode<V>): MapNode<V> => {
|
get(key: BigInteger) {
|
||||||
if (!node) {
|
return this.root[key.toString()] ?? null;
|
||||||
return {
|
}
|
||||||
n: [key, value],
|
|
||||||
l: null,
|
set(key: BigInteger, value: V) {
|
||||||
r: null,
|
this.root[key.toString()] = value;
|
||||||
};
|
this.cachedIter = null;
|
||||||
}
|
|
||||||
const [k] = node.n;
|
|
||||||
if (key.eq(k)) {
|
|
||||||
this.size--;
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
n: [k, value],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
|
||||||
const l = inner(node.l);
|
|
||||||
if (!l) {
|
|
||||||
throw new Error("invariant violation");
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
l,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const r = inner(node.r);
|
|
||||||
if (!r) {
|
|
||||||
throw new Error("invariant violation");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...node, r };
|
|
||||||
};
|
|
||||||
this.size++;
|
|
||||||
this.root = inner(this.root);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all entries
|
|
||||||
*/
|
|
||||||
clear() {
|
clear() {
|
||||||
this.root = null;
|
this.cachedIter = null;
|
||||||
|
this.root = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
has(key: BigInteger) {
|
||||||
* Predicate testing if map contains key
|
return key.toString() in this.root;
|
||||||
*/
|
|
||||||
has(key: BigInteger): boolean {
|
|
||||||
const inner = (node: MapNode<V>): boolean => {
|
|
||||||
if (!node) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const [k] = node.n;
|
|
||||||
|
|
||||||
if (k.eq(key)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
|
||||||
return inner(node.l);
|
|
||||||
}
|
|
||||||
return inner(node.r);
|
|
||||||
};
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove value associated with key, returning whether that key
|
|
||||||
* existed in the first place
|
|
||||||
*/
|
|
||||||
delete(key: BigInteger) {
|
delete(key: BigInteger) {
|
||||||
const inner = (node: MapNode<V>): [boolean, MapNode<V>] => {
|
const had = this.has(key);
|
||||||
if (!node) {
|
if(had) {
|
||||||
return [false, null];
|
delete this.root[key.toString()];
|
||||||
}
|
this.cachedIter = null;
|
||||||
const [k] = node.n;
|
|
||||||
if (k.eq(key)) {
|
|
||||||
return [true, this.nip(node)];
|
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
|
||||||
const [bool, l] = inner(node.l);
|
|
||||||
return [
|
|
||||||
bool,
|
|
||||||
{
|
|
||||||
...node,
|
|
||||||
l,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const [bool, r] = inner(node.r);
|
|
||||||
return [
|
|
||||||
bool,
|
|
||||||
{
|
|
||||||
...node,
|
|
||||||
r,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
const [ret, newRoot] = inner(this.root);
|
|
||||||
if(ret) {
|
|
||||||
this.size--;
|
|
||||||
}
|
}
|
||||||
this.root = newRoot;
|
return had;
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private nip(nod: NonemptyNode<V>): MapNode<V> {
|
|
||||||
const inner = (node: NonemptyNode<V>): MapNode<V> => {
|
|
||||||
if (!node.l) {
|
|
||||||
return node.r;
|
|
||||||
}
|
|
||||||
if (!node.r) {
|
|
||||||
return node.l;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...node.l,
|
|
||||||
r: inner(node.r),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return inner(nod);
|
|
||||||
}
|
|
||||||
|
|
||||||
peekLargest(): [BigInteger, V] | undefined {
|
|
||||||
const inner = (node: MapNode<V>): [BigInteger, V] | undefined => {
|
|
||||||
if(!node) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if(node.l) {
|
|
||||||
return inner(node.l);
|
|
||||||
}
|
|
||||||
return node.n;
|
|
||||||
}
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
peekSmallest(): [BigInteger, V] | undefined {
|
|
||||||
const inner = (node: MapNode<V>): [BigInteger, V] | undefined => {
|
|
||||||
if(!node) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if(node.r) {
|
|
||||||
return inner(node.r);
|
|
||||||
}
|
|
||||||
return node.n;
|
|
||||||
}
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
keys(): BigInteger[] {
|
|
||||||
const list = Array.from(this);
|
|
||||||
return list.map(([key]) => key);
|
|
||||||
}
|
|
||||||
|
|
||||||
forEach(f: (value: V, key: BigInteger) => void) {
|
|
||||||
const list = Array.from(this);
|
|
||||||
return list.forEach(([k,v]) => f(v,k));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator](): IterableIterator<[BigInteger, V]> {
|
[Symbol.iterator](): IterableIterator<[BigInteger, V]> {
|
||||||
let result: [BigInteger, V][] = [];
|
|
||||||
const inner = (node: MapNode<V>) => {
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
inner(node.l);
|
|
||||||
result.push(node.n);
|
|
||||||
inner(node.r);
|
|
||||||
};
|
|
||||||
inner(this.root);
|
|
||||||
|
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
|
const result = this.generateCachedIter();
|
||||||
return {
|
return {
|
||||||
[Symbol.iterator]: this[Symbol.iterator],
|
[Symbol.iterator]: this[Symbol.iterator],
|
||||||
next: (): IteratorResult<[BigInteger, V]> => {
|
next: (): IteratorResult<[BigInteger, V]> => {
|
||||||
@ -232,4 +67,31 @@ export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
peekLargest() {
|
||||||
|
const sorted = Array.from(this);
|
||||||
|
return sorted[0] as [BigInteger, V] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
peekSmallest() {
|
||||||
|
const sorted = Array.from(this);
|
||||||
|
return sorted[sorted.length - 1] as [BigInteger, V] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
keys() {
|
||||||
|
return Object.keys(this.root).map(k => bigInt(k)).sort(sortBigInt)
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateCachedIter() {
|
||||||
|
if(this.cachedIter) {
|
||||||
|
return this.cachedIter;
|
||||||
|
}
|
||||||
|
const result = Object.keys(this.root).map(key => {
|
||||||
|
const num = bigInt(key);
|
||||||
|
return [num, this.root[key]] as [BigInteger, V];
|
||||||
|
}).sort(([a], [b]) => sortBigInt(a,b));
|
||||||
|
this.cachedIter = result;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user