mirror of
https://github.com/urbit/shrub.git
synced 2024-12-22 02:11:38 +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
|
||||
oid sha256:1d56b7351a347a65c06999955114f196523a86c853390d5d1822a90a606619d6
|
||||
size 10357558
|
||||
oid sha256:f6b5e33e573818120051651c1182163527edbbe0dff0eb6591e12a55cfccb273
|
||||
size 10486101
|
||||
|
@ -1,4 +1,4 @@
|
||||
{ urbit, libcap, coreutils, bashInteractive, dockerTools, writeScriptBin, amesPort ? 34343 }:
|
||||
{ urbit, curl, libcap, coreutils, bashInteractive, dockerTools, writeScriptBin, amesPort ? 34343 }:
|
||||
let
|
||||
startUrbit = writeScriptBin "start-urbit" ''
|
||||
#!${bashInteractive}/bin/bash
|
||||
@ -59,11 +59,41 @@ let
|
||||
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 {
|
||||
name = "urbit";
|
||||
tag = "v${urbit.version}";
|
||||
contents = [ bashInteractive urbit startUrbit coreutils ];
|
||||
contents = [ bashInteractive urbit curl startUrbit getUrbitCode resetUrbitCode coreutils ];
|
||||
runAsRoot = ''
|
||||
#!${bashInteractive}
|
||||
mkdir -p /urbit
|
||||
|
@ -126,6 +126,14 @@
|
||||
!=(contact(last-updated *@da) u.old(last-updated *@da))
|
||||
==
|
||||
[~ 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))
|
||||
state(rolodex (~(put by rolodex) ship contact))
|
||||
::
|
||||
@ -149,6 +157,14 @@
|
||||
=/ contact (edit-contact old edit-field)
|
||||
?: =(old contact)
|
||||
[~ 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
|
||||
:- (send-diff [%edit ship edit-field timestamp] =(ship our.bowl))
|
||||
state(rolodex (~(put by rolodex) ship contact))
|
||||
|
@ -5,7 +5,7 @@
|
||||
/- glob
|
||||
/+ 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))]
|
||||
+$ all-states
|
||||
$% state-0
|
||||
|
@ -24,6 +24,6 @@
|
||||
<div id="portal-root"></div>
|
||||
<script src="/~landscape/js/channel.js"></script>
|
||||
<script src="/~landscape/js/session.js"></script>
|
||||
<script src="/~landscape/js/bundle/index.fd3d400454968e081ca9.js"></script>
|
||||
<script src="/~landscape/js/bundle/index.59e682153138f604d358.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -23,7 +23,7 @@
|
||||
:: /app-name/%app-name associations for app
|
||||
:: /group/%path associations for group
|
||||
::
|
||||
/- store=metadata-store
|
||||
/- store=metadata-store, pull-hook
|
||||
/+ default-agent, verb, dbug, resource, *migrate
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
@ -95,16 +95,17 @@
|
||||
~
|
||||
==
|
||||
::
|
||||
+$ state-0 [%0 base-state-0]
|
||||
+$ state-1 [%1 base-state-0]
|
||||
+$ state-2 [%2 base-state-0]
|
||||
+$ state-3 [%3 base-state-1]
|
||||
+$ state-4 [%4 base-state-1]
|
||||
+$ state-5 [%5 base-state-1]
|
||||
+$ state-6 [%6 base-state-1]
|
||||
+$ state-7 [%7 base-state-2]
|
||||
+$ state-8 [%8 base-state-3]
|
||||
+$ state-9 [%9 base-state-3]
|
||||
+$ state-0 [%0 base-state-0]
|
||||
+$ state-1 [%1 base-state-0]
|
||||
+$ state-2 [%2 base-state-0]
|
||||
+$ state-3 [%3 base-state-1]
|
||||
+$ state-4 [%4 base-state-1]
|
||||
+$ state-5 [%5 base-state-1]
|
||||
+$ state-6 [%6 base-state-1]
|
||||
+$ state-7 [%7 base-state-2]
|
||||
+$ state-8 [%8 base-state-3]
|
||||
+$ state-9 [%9 base-state-3]
|
||||
+$ state-10 [%10 base-state-3]
|
||||
+$ versioned-state
|
||||
$% state-0
|
||||
state-1
|
||||
@ -116,10 +117,11 @@
|
||||
state-7
|
||||
state-8
|
||||
state-9
|
||||
state-10
|
||||
==
|
||||
::
|
||||
+$ inflated-state
|
||||
$: state-9
|
||||
$: state-10
|
||||
cached-indices
|
||||
==
|
||||
--
|
||||
@ -232,7 +234,7 @@
|
||||
=| cards=(list card)
|
||||
|^
|
||||
=* loop $
|
||||
?: ?=(%9 -.old)
|
||||
?: ?=(%10 -.old)
|
||||
:- cards
|
||||
%_ state
|
||||
associations associations.old
|
||||
@ -240,7 +242,7 @@
|
||||
group-indices (rebuild-group-indices associations.old)
|
||||
app-indices (rebuild-app-indices associations.old)
|
||||
==
|
||||
?: ?=(%8 -.old)
|
||||
?: ?=(%9 -.old)
|
||||
=/ groups
|
||||
(fall (~(get by (rebuild-app-indices associations.old)) %groups) ~)
|
||||
=/ pokes=(list card)
|
||||
@ -252,13 +254,17 @@
|
||||
?. ?=([%group [~ [~ [@ [@ @]]]]] config.met)
|
||||
~
|
||||
=* res resource.u.u.feed.config.met
|
||||
?: =(our.bowl entity.res) ~
|
||||
=- `[%pass /fix-feed %agent [our.bowl %graph-pull-hook] %poke -]
|
||||
:- %pull-hook-action
|
||||
!> [%add entity.res name.res]
|
||||
!> ^- action:pull-hook
|
||||
[%add entity.res res]
|
||||
%_ $
|
||||
cards (weld cards pokes)
|
||||
-.old %9
|
||||
-.old %10
|
||||
==
|
||||
?: ?=(%8 -.old)
|
||||
$(-.old %9)
|
||||
?: ?=(%7 -.old)
|
||||
$(old [%8 (associations-2-to-3 associations.old) ~])
|
||||
?: ?=(%6 -.old)
|
||||
|
@ -249,6 +249,7 @@
|
||||
font-family: "Source Code Pro";
|
||||
src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff");
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
}
|
||||
:root {
|
||||
--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 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
|
||||
|
||||
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,
|
||||
|
@ -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,
|
||||
|
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/tabs": "^0.10.5",
|
||||
"@tlon/indigo-dark": "^1.0.6",
|
||||
"@tlon/indigo-light": "^1.0.6",
|
||||
"@tlon/indigo-react": "^1.2.19",
|
||||
"@tlon/indigo-light": "^1.0.7",
|
||||
"@tlon/indigo-react": "^1.2.21",
|
||||
"@tlon/sigil-js": "^1.4.3",
|
||||
"@urbit/api": "file:../npm/api",
|
||||
"any-ascii": "^0.1.7",
|
||||
@ -88,7 +88,7 @@
|
||||
"react-hot-loader": "^4.13.0",
|
||||
"sass": "^1.32.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"typescript": "^3.9.7",
|
||||
"typescript": "^4.2.4",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"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 { BigInteger } from 'big-integer';
|
||||
import { getParentIndex } from '../lib/notification';
|
||||
import useHarkState from '../state/hark';
|
||||
|
||||
function getHarkSize() {
|
||||
return useHarkState.getState().notifications.size ?? 0;
|
||||
}
|
||||
|
||||
export class HarkApi extends BaseApi<StoreState> {
|
||||
private harkAction(action: any): Promise<any> {
|
||||
@ -172,10 +177,10 @@ export class HarkApi extends BaseApi<StoreState> {
|
||||
}
|
||||
|
||||
async getMore(): Promise<boolean> {
|
||||
const offset = this.store.state['notifications']?.size || 0;
|
||||
const offset = getHarkSize();
|
||||
const count = 3;
|
||||
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) {
|
||||
|
@ -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 f from 'lodash/fp';
|
||||
import { Unreads } from '@urbit/api';
|
||||
import { Unreads, NotificationGraphConfig } from '@urbit/api';
|
||||
|
||||
export function getLastSeen(
|
||||
unreads: Unreads,
|
||||
@ -34,3 +34,13 @@ export function getNotificationCount(
|
||||
.map(index => unread[index]?.notifications?.length || 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_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 { unixToDa } from '~/logic/lib/util';
|
||||
import { BigIntOrderedMap } from './BigIntOrderedMap';
|
||||
import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap";
|
||||
import bigInt, { BigInteger } from 'big-integer';
|
||||
|
||||
export function newPost(
|
||||
title: string,
|
||||
body: string
|
||||
): [BigInteger, NodeMap] {
|
||||
): [BigInteger, any] {
|
||||
const now = Date.now();
|
||||
const nowDa = unixToDa(now);
|
||||
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] {
|
||||
const revs = node.children.get(bigInt(1));
|
||||
const revs = node.children?.get(bigInt(1));
|
||||
const empty = [1, '', '', buntPost()] as [number, string, string, Post];
|
||||
if(!revs) {
|
||||
return empty;
|
||||
}
|
||||
const [revNum, rev] = [...revs.children][0];
|
||||
if(!rev) {
|
||||
let revNum, rev;
|
||||
if (revs?.children !== null) {
|
||||
[revNum, rev] = [...revs.children][0];
|
||||
}
|
||||
if (!rev) {
|
||||
return empty;
|
||||
}
|
||||
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] {
|
||||
const empty = [1, buntPost()] as [number, Post];
|
||||
if (node.children.size <= 0) {
|
||||
const childSize = node?.children?.size ?? 0;
|
||||
if (childSize <= 0) {
|
||||
return empty;
|
||||
}
|
||||
const [revNum, rev] = [...node.children][0];
|
||||
if(!rev) {
|
||||
let revNum, rev;
|
||||
if (node?.children !== null) {
|
||||
[revNum, rev] = [...node.children][0];
|
||||
}
|
||||
if (!rev) {
|
||||
return empty;
|
||||
}
|
||||
return [revNum.toJSNumber(), rev.post];
|
||||
}
|
||||
|
||||
export function getComments(node: GraphNode): GraphNode {
|
||||
const comments = node.children.get(bigInt(2));
|
||||
const comments = node.children?.get(bigInt(2));
|
||||
if(!comments) {
|
||||
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 { Direction } from '~/views/components/Triangle';
|
||||
|
||||
@ -22,7 +23,7 @@ interface StepDetail {
|
||||
alignY: AlignY | AlignY[];
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
arrow: Direction;
|
||||
arrow?: Direction;
|
||||
}
|
||||
|
||||
export function hasTutorialGroup(props: { associations: Associations }) {
|
||||
|
@ -15,7 +15,8 @@ function retrieve<T>(key: string, initial: T): T {
|
||||
interface SetStateFunc<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) {
|
||||
const [state, _setState] = useState(() => retrieve(key, initial));
|
||||
|
||||
|
@ -2,18 +2,14 @@ import React, {
|
||||
useState,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
SyntheticEvent,
|
||||
useMemo,
|
||||
useEffect,
|
||||
useRef
|
||||
} from 'react';
|
||||
|
||||
import { Box } from '@tlon/indigo-react';
|
||||
import { useOutsideClick } from './useOutsideClick';
|
||||
import { ModalOverlay } from '~/views/components/ModalOverlay';
|
||||
import { Portal } from '~/views/components/Portal';
|
||||
import { ModalPortal } from '~/views/components/ModalPortal';
|
||||
import { PropFunc } from '@urbit/api';
|
||||
import { PropFunc } from '~/types';
|
||||
|
||||
type ModalFunc = (dismiss: () => void) => JSX.Element;
|
||||
interface UseModalProps {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useRef } from 'react';
|
||||
import { Primitive } from '@urbit/api';
|
||||
import { Primitive } from '~/types';
|
||||
|
||||
export default function usePreviousValue<T extends Primitive>(value: T): T {
|
||||
const prev = useRef<T | null>(null);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useWaitForProps } from "./useWaitForProps";
|
||||
import {unstable_batchedUpdates} from "react-dom";
|
||||
|
||||
export type IOInstance<I, P, O> = (
|
||||
@ -10,7 +9,7 @@ export function useRunIO<I, O>(
|
||||
io: (i: I) => Promise<O>,
|
||||
after: (o: O) => void,
|
||||
key: string
|
||||
): () => Promise<void> {
|
||||
): (i: I) => Promise<unknown> {
|
||||
const [resolve, setResolve] = useState<() => void>(() => () => {});
|
||||
const [reject, setReject] = useState<(e: any) => void>(() => () => {});
|
||||
const [output, setOutput] = useState<O | null>(null);
|
||||
|
@ -5,7 +5,7 @@ export function useStatelessAsyncClickable(
|
||||
onClick: (e: MouseEvent) => Promise<void>,
|
||||
name: string
|
||||
) {
|
||||
const [state, setState] = useState<ButtonState>('waiting');
|
||||
const [state, setState] = useState<AsyncClickableState>('waiting');
|
||||
const handleClick = useCallback(
|
||||
async (e: MouseEvent) => {
|
||||
try {
|
||||
|
@ -16,7 +16,7 @@ export interface IuseStorage {
|
||||
upload: (file: File, bucket: string) => Promise<string>;
|
||||
uploadDefault: (file: File) => Promise<string>;
|
||||
uploading: boolean;
|
||||
promptUpload: () => Promise<string | undefined>;
|
||||
promptUpload: () => Promise<unknown>;
|
||||
}
|
||||
|
||||
const useStorage = ({ accept = '*' } = { accept: '*' }): IuseStorage => {
|
||||
|
@ -30,7 +30,7 @@ export const getModuleIcon = (mod: string) => {
|
||||
}
|
||||
|
||||
if (mod === 'post') {
|
||||
return 'Spaces';
|
||||
return 'Dashboard';
|
||||
}
|
||||
|
||||
return _.capitalize(mod);
|
||||
@ -192,7 +192,10 @@ export function uxToHex(ux: string) {
|
||||
export const hexToUx = (hex) => {
|
||||
const ux = f.flow(
|
||||
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('.')
|
||||
)(hex.split(''));
|
||||
return `0x${ux}`;
|
||||
|
@ -7,6 +7,7 @@ import React, {
|
||||
useEffect,
|
||||
} from "react";
|
||||
import usePreviousValue from "./usePreviousValue";
|
||||
import {Primitive} from "~/types";
|
||||
|
||||
export interface VirtualContextProps {
|
||||
save: () => void;
|
||||
@ -49,7 +50,7 @@ export function useVirtualResizeState(s: boolean) {
|
||||
return [state, setState] as const;
|
||||
}
|
||||
|
||||
export function useVirtualResizeProp<T>(prop: T) {
|
||||
export function useVirtualResizeProp(prop: Primitive) {
|
||||
const { save, restore } = useVirtual();
|
||||
const oldProp = usePreviousValue(prop)
|
||||
|
||||
@ -58,7 +59,7 @@ export function useVirtualResizeProp<T>(prop: T) {
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
restore();
|
||||
requestAnimationFrame(restore);
|
||||
}, [prop]);
|
||||
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Associations, Workspace } from '@urbit/api';
|
||||
import { Associations } from '@urbit/api';
|
||||
import { Workspace } from '~/types';
|
||||
|
||||
export function getTitleFromWorkspace(
|
||||
associations: Associations,
|
||||
|
@ -1,5 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import { StoreState } from '../../store/type';
|
||||
import { StoreState } from '../store/type';
|
||||
import { Cage } from '~/types/cage';
|
||||
|
||||
type LocalState = Pick<StoreState, 'connection'>;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
|
||||
import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap";
|
||||
import bigInt, { BigInteger } from "big-integer";
|
||||
import useGraphState, { GraphState } from '../state/graph';
|
||||
import { reduceState } from '../state/base';
|
||||
|
@ -5,16 +5,14 @@ import {
|
||||
Group,
|
||||
Tags,
|
||||
GroupPolicy,
|
||||
GroupPolicyDiff,
|
||||
OpenPolicyDiff,
|
||||
OpenPolicy,
|
||||
InvitePolicyDiff,
|
||||
InvitePolicy
|
||||
} from '@urbit/api/groups';
|
||||
import { Enc, PatpNoSig } from '@urbit/api';
|
||||
import { Enc } from '@urbit/api';
|
||||
import { resourceAsPath } from '../lib/util';
|
||||
import useGroupState, { GroupState } from '../state/group';
|
||||
import { compose } from 'lodash/fp';
|
||||
import { reduceState } from '../state/base';
|
||||
|
||||
function decodeGroup(group: Enc<Group>): Group {
|
||||
@ -125,9 +123,9 @@ const addMembers = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||
state.groups[resourcePath].members.add(member);
|
||||
if (
|
||||
'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);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const removeTag = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||
if ('removeTag' in json) {
|
||||
|
@ -1,18 +1,14 @@
|
||||
import {
|
||||
Notifications,
|
||||
NotifIndex,
|
||||
NotificationGraphConfig,
|
||||
GroupNotificationsConfig,
|
||||
UnreadStats,
|
||||
Timebox
|
||||
} from '@urbit/api';
|
||||
import { makePatDa } from '~/logic/lib/util';
|
||||
import _ from 'lodash';
|
||||
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
||||
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
|
||||
import useHarkState, { HarkState } from '../state/hark';
|
||||
import { compose } from 'lodash/fp';
|
||||
import { reduceState } from '../state/base';
|
||||
import bigInt, {BigInteger} from 'big-integer';
|
||||
import {BigInteger} from 'big-integer';
|
||||
|
||||
export const HarkReducer = (json: any) => {
|
||||
const data = _.get(json, 'harkUpdate', false);
|
||||
@ -264,7 +260,7 @@ function updateUnreads(state: HarkState, index: NotifIndex, f: (us: Set<string>)
|
||||
if(!('graph' in index)) {
|
||||
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);
|
||||
|
||||
_.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,
|
||||
[
|
||||
...curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))),
|
||||
{ time, index}
|
||||
{ time, index }
|
||||
]
|
||||
);
|
||||
} else if ('group' in index) {
|
||||
@ -287,7 +283,7 @@ function addNotificationToUnread(state: HarkState, index: NotifIndex, time: BigI
|
||||
_.set(state.unreads.group, path,
|
||||
[
|
||||
...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) {
|
||||
|
||||
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));
|
||||
} 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));
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
useLaunchState.getState().set(state => {
|
||||
state.weather = weatherData;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
import useSettingsState, { SettingsState } from "~/logic/state/settings";
|
||||
import { SettingsUpdate } from '@urbit/api/dist/settings';
|
||||
import useSettingsState, { SettingsState } from '~/logic/state/settings';
|
||||
import { SettingsUpdate } from '@urbit/api/settings';
|
||||
import { reduceState } from '../state/base';
|
||||
import { string } from 'prop-types';
|
||||
|
||||
export default class SettingsReducer {
|
||||
reduce(json: any) {
|
||||
@ -40,21 +41,21 @@ export default class SettingsReducer {
|
||||
return state;
|
||||
}
|
||||
|
||||
putEntry(json: SettingsUpdate, state: SettingsState): SettingsState {
|
||||
const data = _.get(json, 'put-entry', false);
|
||||
putEntry(json: SettingsUpdate, state: any): SettingsState {
|
||||
const data: Record<string, string> = _.get(json, 'put-entry', false);
|
||||
if (data) {
|
||||
if (!state[data["bucket-key"]]) {
|
||||
state[data["bucket-key"]] = {};
|
||||
if (!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;
|
||||
}
|
||||
|
||||
delEntry(json: SettingsUpdate, state: SettingsState): SettingsState {
|
||||
delEntry(json: SettingsUpdate, state: any): SettingsState {
|
||||
const data = _.get(json, 'del-entry', false);
|
||||
if (data) {
|
||||
delete state[data["bucket-key"]][data["entry-key"]];
|
||||
delete state[data['bucket-key']][data['entry-key']];
|
||||
}
|
||||
return state;
|
||||
}
|
||||
@ -76,7 +77,7 @@ export default class SettingsReducer {
|
||||
return state;
|
||||
}
|
||||
|
||||
getEntry(json: any, state: SettingsState) {
|
||||
getEntry(json: any, state: any) {
|
||||
const bucketKey = _.get(json, 'bucket-key', false);
|
||||
const entryKey = _.get(json, 'entry-key', 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 create, { State, UseStore } from "zustand";
|
||||
import { persist, devtools } from "zustand/middleware";
|
||||
|
||||
setAutoFreeze(false);
|
||||
|
||||
|
||||
export const stateSetter = <StateType>(
|
||||
fn: (state: StateType) => void,
|
||||
|
@ -15,7 +15,7 @@ export interface HarkState extends BaseState<HarkState> {
|
||||
notifications: BigIntOrderedMap<Timebox>;
|
||||
notificationsCount: number;
|
||||
notificationsGraphConfig: NotificationGraphConfig; // TODO unthread this everywhere
|
||||
notificationsGroupConfig: []; // TODO type this
|
||||
notificationsGroupConfig: string[];
|
||||
unreads: Unreads;
|
||||
};
|
||||
|
||||
|
@ -9,7 +9,7 @@ export interface LaunchState extends BaseState<LaunchState> {
|
||||
tiles: {
|
||||
[app: string]: Tile;
|
||||
},
|
||||
weather: WeatherState | null,
|
||||
weather: WeatherState | null | Record<string, never> | boolean,
|
||||
userLocation: string | null;
|
||||
baseHash: string | null;
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { MetadataUpdatePreview, Association, Associations } from "@urbit/api";
|
||||
|
||||
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]));
|
||||
}
|
||||
|
||||
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', {
|
||||
associations: { groups: {}, graph: {}, contacts: {}, chat: {}, link: {}, publish: {} },
|
||||
// preview: async (group): Promise<MetadataUpdatePreview> => {
|
||||
|
@ -58,7 +58,7 @@ const useSettingsState = createState<SettingsState>('Settings', {
|
||||
categories: leapCategories,
|
||||
},
|
||||
tutorial: {
|
||||
seen: false,
|
||||
seen: true,
|
||||
joined: undefined
|
||||
}
|
||||
});
|
||||
|
@ -3,10 +3,8 @@ import _ from 'lodash';
|
||||
import BaseStore from './base';
|
||||
import InviteReducer from '../reducers/invite-update';
|
||||
import MetadataReducer from '../reducers/metadata-update';
|
||||
import LocalReducer from '../reducers/local';
|
||||
|
||||
import { StoreState } from './type';
|
||||
import { Timebox } from '@urbit/api';
|
||||
import { Cage } from '~/types/cage';
|
||||
import S3Reducer from '../reducers/s3-update';
|
||||
import { GraphReducer } from '../reducers/graph-update';
|
||||
@ -17,8 +15,6 @@ import LaunchReducer from '../reducers/launch-update';
|
||||
import ConnectionReducer from '../reducers/connection';
|
||||
import SettingsReducer from '../reducers/settings-update';
|
||||
import GcpReducer from '../reducers/gcp-reducer';
|
||||
import { OrderedMap } from '../lib/OrderedMap';
|
||||
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
||||
import { GroupViewReducer } from '../reducers/group-view';
|
||||
import { unstable_batchedUpdates } from 'react-dom';
|
||||
|
||||
|
@ -53,6 +53,7 @@ const Root = withState(styled.div`
|
||||
}
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
touch-action: none;
|
||||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
|
@ -188,7 +188,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<Icon
|
||||
icon='Links'
|
||||
icon='Attachment'
|
||||
width='16'
|
||||
height='16'
|
||||
onClick={() =>
|
||||
|
@ -1,4 +1,5 @@
|
||||
/* eslint-disable max-lines-per-function */
|
||||
import bigInt from 'big-integer';
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
@ -19,7 +20,8 @@ import {
|
||||
writeText,
|
||||
useShowNickname,
|
||||
useHideAvatar,
|
||||
useHovering
|
||||
useHovering,
|
||||
daToUnix
|
||||
} from '~/logic/lib/util';
|
||||
import {
|
||||
Group,
|
||||
@ -65,7 +67,7 @@ export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => (
|
||||
<Rule borderColor='lightGray' />
|
||||
<Text
|
||||
gray
|
||||
flexShrink='0'
|
||||
flexShrink={0}
|
||||
whiteSpace='nowrap'
|
||||
textAlign='center'
|
||||
fontSize={0}
|
||||
@ -107,7 +109,7 @@ export const UnreadMarker = React.forwardRef(
|
||||
<Text
|
||||
color='blue'
|
||||
fontSize={0}
|
||||
flexShrink='0'
|
||||
flexShrink={0}
|
||||
whiteSpace='nowrap'
|
||||
textAlign='center'
|
||||
px={2}
|
||||
@ -168,7 +170,7 @@ const MessageActions = ({ api, onReply, association, history, msg, group }) => {
|
||||
width='auto'
|
||||
alignY='top'
|
||||
alignX='right'
|
||||
flexShrink={'0'}
|
||||
flexShrink={0}
|
||||
offsetY={8}
|
||||
offsetX={-24}
|
||||
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 =
|
||||
nextMsg &&
|
||||
new Date(msg['time-sent']).getDate() !==
|
||||
new Date(nextMsg['time-sent']).getDate();
|
||||
new Date(date).getDate() !==
|
||||
new Date(nextDate).getDate();
|
||||
|
||||
const containerClass = `${isPending ? 'o-40' : ''} ${className}`;
|
||||
|
||||
const timestamp = moment
|
||||
.unix(msg['time-sent'] / 1000)
|
||||
.unix(date / 1000)
|
||||
.format(renderSigil ? 'h:mm A' : 'h:mm');
|
||||
|
||||
const messageProps = {
|
||||
@ -339,7 +346,7 @@ class ChatMessage extends Component<ChatMessageProps> {
|
||||
style={style}
|
||||
>
|
||||
{dayBreak && !isLastRead ? (
|
||||
<DayBreak when={msg['time-sent']} shimTop={renderSigil} />
|
||||
<DayBreak when={date} shimTop={renderSigil} />
|
||||
) : null}
|
||||
{renderSigil ? (
|
||||
<MessageWrapper {...messageProps}>
|
||||
@ -357,7 +364,7 @@ class ChatMessage extends Component<ChatMessageProps> {
|
||||
association={association}
|
||||
api={api}
|
||||
dayBreak={dayBreak}
|
||||
when={msg['time-sent']}
|
||||
when={date}
|
||||
ref={unreadMarkerRef}
|
||||
/>
|
||||
) : null}
|
||||
@ -387,8 +394,10 @@ export const MessageAuthor = ({
|
||||
const dark = theme === 'dark' || (theme === 'auto' && osDark);
|
||||
const contacts = useContactState((state) => state.contacts);
|
||||
|
||||
const date = daToUnix(bigInt(msg.index.split('/')[1]));
|
||||
|
||||
const datestamp = moment
|
||||
.unix(msg['time-sent'] / 1000)
|
||||
.unix(date / 1000)
|
||||
.format(DATESTAMP_FORMAT);
|
||||
const contact =
|
||||
((msg.author === window.ship && showOurContact) ||
|
||||
|
@ -46,7 +46,7 @@ const ScrollbarLessBox = styled(Box)`
|
||||
const tutSelector = f.pick(['tutorialProgress', 'nextTutStep', 'hideGroups']);
|
||||
|
||||
export default function LaunchApp(props) {
|
||||
const connection = { props };
|
||||
const { connection } = props;
|
||||
const baseHash = useLaunchState(state => state.baseHash);
|
||||
const [hashText, setHashText] = useState(baseHash);
|
||||
const [exitingTut, setExitingTut] = useState(false);
|
||||
@ -220,7 +220,7 @@ export default function LaunchApp(props) {
|
||||
<NewGroup {...props} />
|
||||
</ModalButton>
|
||||
<ModalButton
|
||||
icon="Boot"
|
||||
icon="BootNode"
|
||||
bg="washedGray"
|
||||
color="black"
|
||||
text="Join Group"
|
||||
|
@ -145,8 +145,7 @@ function ContentSummary({ icon, name, author, to }) {
|
||||
export const GraphNodeContent = ({ post, mod, index, hidden, association }) => {
|
||||
const { contents } = post;
|
||||
const idx = index.slice(1).split("/");
|
||||
const { group, resource } = association;
|
||||
const url = getNodeUrl(mod, hidden, group, resource, index);
|
||||
const url = getNodeUrl(mod, hidden, association?.group, association?.resource, index);
|
||||
if (mod === "link" && idx.length === 1) {
|
||||
const [{ text: title }] = contents;
|
||||
return (
|
||||
@ -296,7 +295,7 @@ export function GraphNotification(props: {
|
||||
dm,
|
||||
singleAuthor
|
||||
);
|
||||
const groupAssociation = useAssocForGroup(association.group);
|
||||
const groupAssociation = useAssocForGroup(association?.group);
|
||||
const groups = useGroupState((state) => state.groups);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
@ -307,13 +306,12 @@ export function GraphNotification(props: {
|
||||
)
|
||||
) {
|
||||
const first = contents[0];
|
||||
const { group, resource } = association;
|
||||
history.push(
|
||||
getNodeUrl(
|
||||
index.module,
|
||||
groups[association.group]?.hidden,
|
||||
groups[association?.group]?.hidden,
|
||||
group,
|
||||
resource,
|
||||
association?.resource,
|
||||
first.index
|
||||
)
|
||||
);
|
||||
@ -328,7 +326,7 @@ export function GraphNotification(props: {
|
||||
authorsInHeader ||
|
||||
index.description === "note" ||
|
||||
index.description === "link";
|
||||
const channelTitle = dm ? undefined : association.metadata.title ?? graph;
|
||||
const channelTitle = dm ? undefined : association?.metadata?.title ?? graph;
|
||||
const groupTitle = groupAssociation?.metadata?.title;
|
||||
|
||||
return (
|
||||
@ -349,7 +347,7 @@ export function GraphNotification(props: {
|
||||
description={index.description}
|
||||
index={contents?.[0].index}
|
||||
association={association}
|
||||
hidden={groups[association.group]?.hidden}
|
||||
hidden={groups[association?.group]?.hidden}
|
||||
/>
|
||||
{contents.length > 4 && (
|
||||
<Text mb="2" gray>
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 {
|
||||
GraphNotificationContents,
|
||||
@ -19,6 +19,7 @@ import { GraphNotification } from "./graph";
|
||||
import { BigInteger } from "big-integer";
|
||||
import { useHovering } from "~/logic/lib/util";
|
||||
import useHarkState from "~/logic/state/hark";
|
||||
import {IS_MOBILE} from "~/logic/lib/platform";
|
||||
|
||||
interface NotificationProps {
|
||||
notification: IndexedNotification;
|
||||
@ -102,39 +103,30 @@ export function NotificationWrapper(props: {
|
||||
}
|
||||
borderRadius={2}
|
||||
display="grid"
|
||||
gridTemplateColumns={["1fr", "1fr 200px"]}
|
||||
gridTemplateColumns={["1fr 24px", "1fr 200px"]}
|
||||
gridTemplateRows="auto"
|
||||
gridTemplateAreas={["'header' 'main'", "'header actions' 'main main'"]}
|
||||
gridTemplateAreas="'header actions' 'main main'"
|
||||
p={2}
|
||||
m={2}
|
||||
{...bind}
|
||||
>
|
||||
{children}
|
||||
<Row
|
||||
display={["none", "flex"]}
|
||||
alignItems="center"
|
||||
alignItems="flex-start"
|
||||
gapX="2"
|
||||
gridArea="actions"
|
||||
justifyContent="flex-end"
|
||||
opacity={[1, hovering ? 1 : 0]}
|
||||
opacity={[1, (hovering || IS_MOBILE) ? 1 : 0]}
|
||||
>
|
||||
{time && notification && (
|
||||
<>
|
||||
<StatelessAsyncAction
|
||||
name={changeMuteDesc}
|
||||
onClick={onChangeMute}
|
||||
backgroundColor="transparent"
|
||||
>
|
||||
{changeMuteDesc}
|
||||
</StatelessAsyncAction>
|
||||
<StatelessAsyncAction
|
||||
name={time.toString()}
|
||||
onClick={onArchive}
|
||||
backgroundColor="transparent"
|
||||
>
|
||||
Dismiss
|
||||
</StatelessAsyncAction>
|
||||
</>
|
||||
<StatelessAsyncAction
|
||||
name={time.toString()}
|
||||
borderRadius={1}
|
||||
onClick={onArchive}
|
||||
backgroundColor="white"
|
||||
>
|
||||
<Icon lineHeight="24px" size={16} icon="X" />
|
||||
</StatelessAsyncAction>
|
||||
)}
|
||||
</Row>
|
||||
</Box>
|
||||
|
@ -3,7 +3,7 @@ import _ from 'lodash';
|
||||
import { Link, Switch, Route } from 'react-router-dom';
|
||||
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 { PropFunc } from '~/types/util';
|
||||
@ -15,6 +15,7 @@ import { useTutorialModal } from '~/views/components/useTutorialModal';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
import useMetadataState from '~/logic/state/metadata';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
import {StatelessAsyncAction} from '~/views/components/StatelessAsyncAction';
|
||||
|
||||
const baseUrl = '/~notifications';
|
||||
|
||||
@ -46,8 +47,8 @@ export default function NotificationsScreen(props: any): ReactElement {
|
||||
const onSubmit = async ({ groups } : NotificationFilter) => {
|
||||
setFilter({ groups });
|
||||
};
|
||||
const onReadAll = useCallback(() => {
|
||||
props.api.hark.readAll();
|
||||
const onReadAll = useCallback(async () => {
|
||||
await props.api.hark.readAll();
|
||||
}, []);
|
||||
const groupFilterDesc =
|
||||
filter.groups.length === 0
|
||||
@ -81,53 +82,26 @@ export default function NotificationsScreen(props: any): ReactElement {
|
||||
borderBottomColor="lightGray"
|
||||
>
|
||||
|
||||
<Text ref={anchorRef}>Notifications</Text>
|
||||
<Text fontWeight="bold" fontSize="2" lineHeight="1" ref={anchorRef}>
|
||||
Notifications
|
||||
</Text>
|
||||
<Row
|
||||
justifyContent="space-between"
|
||||
gapX="3"
|
||||
>
|
||||
<Box
|
||||
mr="1"
|
||||
<StatelessAsyncAction
|
||||
overflow="hidden"
|
||||
color="black"
|
||||
backgroundColor="white"
|
||||
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>
|
||||
<Text mr="1" gray>
|
||||
Filter:
|
||||
</Text>
|
||||
<Text>{groupFilterDesc}</Text>
|
||||
<Icon lineHeight="1" icon="Adjust" />
|
||||
</Box>
|
||||
</Dropdown>
|
||||
</Link>
|
||||
</Row>
|
||||
</Row>
|
||||
{!view && <Inbox
|
||||
|
@ -18,7 +18,7 @@ import { GroupLink } from "~/views/components/GroupLink";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { getModuleIcon } from "~/logic/lib/util";
|
||||
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 useGraphState from "~/logic/state/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 { ship, name } = resourceFromPath(graph);
|
||||
const node = useGraphState(
|
||||
useCallback((s) => s.looseNodes?.[`${ship.slice(1)}/${name}`]?.[index], [
|
||||
useCallback((s) => s.looseNodes?.[`${ship.slice(1)}/${name}`]?.[index] as GraphNode, [
|
||||
graph,
|
||||
index,
|
||||
])
|
||||
@ -63,7 +63,7 @@ function GraphPermalink(
|
||||
])
|
||||
);
|
||||
|
||||
useVirtualResizeProp(node)
|
||||
useVirtualResizeProp(!!node)
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (pending || !index) {
|
||||
|
@ -73,14 +73,14 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
if (window.ship === note?.post?.author) {
|
||||
adminLinks.push(
|
||||
<Link to={`${baseUrl}/edit`}>
|
||||
<Action>Update</Action>
|
||||
<Action backgroundColor="white">Update</Action>
|
||||
</Link>
|
||||
)
|
||||
};
|
||||
|
||||
if (window.ship === note?.post?.author || ourRole === "admin") {
|
||||
adminLinks.push(
|
||||
<Action destructive onClick={deletePost}>
|
||||
<Action backgroundColor="white" destructive onClick={deletePost}>
|
||||
Delete
|
||||
</Action>
|
||||
)
|
||||
|
@ -44,7 +44,7 @@ export function PostForm(props: PostFormProps) {
|
||||
validateOnBlur
|
||||
>
|
||||
<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" />
|
||||
<Row flexDirection={['column', 'row']} mb={[4,0]}>
|
||||
<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 _ from "lodash";
|
||||
import {AsyncButton} from "~/views/components/AsyncButton";
|
||||
import {GroupChannelPicker} from "./GroupChannelPicker";
|
||||
import {isWatching} from "~/logic/lib/hark";
|
||||
|
||||
interface FormSchema {
|
||||
mentions: boolean;
|
||||
dnd: boolean;
|
||||
watchOnSelf: boolean;
|
||||
graph: {
|
||||
[rid: string]: boolean;
|
||||
};
|
||||
groups: {
|
||||
[rid: string]: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export function NotificationPreferences(props: {
|
||||
@ -23,6 +31,7 @@ export function NotificationPreferences(props: {
|
||||
const { api } = props;
|
||||
const dnd = useHarkState(state => state.doNotDisturb);
|
||||
const graphConfig = useHarkState(state => state.notificationsGraphConfig);
|
||||
const groupConfig = useHarkState(s => s.notificationsGroupConfig);
|
||||
const initialValues = {
|
||||
mentions: graphConfig.mentions,
|
||||
dnd: dnd,
|
||||
@ -41,6 +50,16 @@ export function NotificationPreferences(props: {
|
||||
if (values.dnd !== dnd && !_.isUndefined(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);
|
||||
actions.setStatus({ success: null });
|
||||
@ -81,6 +100,15 @@ export function NotificationPreferences(props: {
|
||||
id="mentions"
|
||||
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">
|
||||
Save
|
||||
</AsyncButton>
|
||||
|
@ -107,7 +107,7 @@ export default function SettingsScreen(props: any) {
|
||||
</Text>
|
||||
<Col>
|
||||
<SidebarItem
|
||||
icon='Inbox'
|
||||
icon='Notifications'
|
||||
text='Notifications'
|
||||
hash='notifications'
|
||||
/>
|
||||
|
@ -115,8 +115,8 @@ export function ChipInput(props: ChipInputProps): ReactElement {
|
||||
<Input
|
||||
width="auto"
|
||||
height="24px"
|
||||
flexShrink="1"
|
||||
flexGrow="1"
|
||||
flexShrink={1}
|
||||
flexGrow={1}
|
||||
pl="0"
|
||||
ref={inputRef}
|
||||
onChange={onChange}
|
||||
|
@ -25,6 +25,7 @@ interface DropdownProps {
|
||||
offsetY?: number;
|
||||
width?: string;
|
||||
dropWidth?: string;
|
||||
flexShrink?: number;
|
||||
}
|
||||
|
||||
const ClickBox = styled(Box)`
|
||||
@ -39,7 +40,7 @@ const DropdownOptions = styled(Box)`
|
||||
`;
|
||||
|
||||
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 anchorRef = useRef<HTMLElement>(null);
|
||||
const { pathname } = useLocation();
|
||||
@ -47,6 +48,9 @@ export function Dropdown(props: DropdownProps): ReactElement {
|
||||
const [coords, setCoords] = useState({});
|
||||
|
||||
const updatePos = useCallback(() => {
|
||||
if(!anchorRef.current) {
|
||||
return;
|
||||
}
|
||||
const newCoords = getRelativePosition(anchorRef.current, props.alignX, props.alignY, offsetX, offsetY);
|
||||
if(newCoords) {
|
||||
setCoords(newCoords);
|
||||
@ -86,7 +90,7 @@ export function Dropdown(props: DropdownProps): ReactElement {
|
||||
}, []);
|
||||
|
||||
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}>
|
||||
{children}
|
||||
</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
|
||||
border="2px solid"
|
||||
borderRadius={2}
|
||||
borderRadius={3}
|
||||
borderColor={color}
|
||||
position="absolute"
|
||||
left="0px"
|
||||
|
@ -83,7 +83,7 @@ const StatusBar = (props) => {
|
||||
onClick={() => history.push('/')}
|
||||
{...props}
|
||||
>
|
||||
<Icon icon='Spaces' color='black' />
|
||||
<Icon icon='Dashboard' color='black' />
|
||||
</Button>
|
||||
<StatusBarItem float={floatLeap} mr={2} onClick={() => toggleOmnibox()}>
|
||||
{!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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@ -140,6 +144,10 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
|
||||
private scrollRef: HTMLElement | null = null;
|
||||
|
||||
private cleanupRefInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
private initScroll: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(props: VirtualScrollerProps<T>) {
|
||||
super(props);
|
||||
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.scrollKeyMap = this.scrollKeyMap.bind(this);
|
||||
this.setWindow = this.setWindow.bind(this);
|
||||
this.restore = this.restore.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@ -164,8 +173,27 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
this.resetScroll();
|
||||
this.loadTop();
|
||||
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
|
||||
updateScroll = IS_IOS ? () => {} : _.throttle(() => {
|
||||
if(!this.window || !this.scrollRef) {
|
||||
@ -199,6 +227,12 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('keydown', this.invertedKeyHandler);
|
||||
if(this.cleanupRefInterval) {
|
||||
clearInterval(this.cleanupRefInterval);
|
||||
}
|
||||
if(this.initScroll) {
|
||||
clearTimeout(this.initScroll);
|
||||
}
|
||||
}
|
||||
|
||||
startOffset() {
|
||||
@ -237,9 +271,6 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
}, () => {
|
||||
requestAnimationFrame(() => {
|
||||
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
|
||||
return;
|
||||
}
|
||||
if(this.initScroll) {
|
||||
clearTimeout(this.initScroll);
|
||||
this.initScroll = null;
|
||||
}
|
||||
if(this.saveDepth > 0) {
|
||||
log('bail', 'deep scroll queue');
|
||||
return;
|
||||
@ -394,8 +429,15 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
log('bail', 'Deep restore');
|
||||
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;
|
||||
|
||||
this.window.scrollTo(0, newScrollTop);
|
||||
@ -435,12 +477,13 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
if(!this.window || this.savedIndex) {
|
||||
return;
|
||||
}
|
||||
this.saveDepth++;
|
||||
if(this.saveDepth !== 1) {
|
||||
if(this.saveDepth !== 0) {
|
||||
console.log('bail', 'deep save');
|
||||
return;
|
||||
}
|
||||
|
||||
this.saveDepth++;
|
||||
|
||||
let bottomIndex: BigInteger | null = null;
|
||||
const { scrollTop, scrollHeight } = this.window;
|
||||
const topSpacing = scrollHeight - scrollTop;
|
||||
@ -472,10 +515,9 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
||||
setRef = (element: HTMLElement | null, index: BigInteger) => {
|
||||
if(element) {
|
||||
this.childRefs.set(index, element);
|
||||
this.orphans.delete(index.toString());
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.childRefs.delete(index);
|
||||
});
|
||||
this.orphans.add(index.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,7 +65,7 @@ export class OmniboxResult extends Component {
|
||||
<Icon
|
||||
display='inline-block'
|
||||
verticalAlign='middle'
|
||||
icon='Inbox'
|
||||
icon='Notifications'
|
||||
mr='2'
|
||||
size='18px'
|
||||
color={iconFill}
|
||||
@ -85,7 +85,7 @@ export class OmniboxResult extends Component {
|
||||
<Icon
|
||||
display='inline-block'
|
||||
verticalAlign='middle'
|
||||
icon='SignOut'
|
||||
icon='LogOut'
|
||||
mr='2'
|
||||
size='18px'
|
||||
color={iconFill}
|
||||
@ -119,7 +119,7 @@ export class OmniboxResult extends Component {
|
||||
<Icon
|
||||
display='inline-block'
|
||||
verticalAlign='middle'
|
||||
icon='Inbox'
|
||||
icon='Notifications'
|
||||
mr='2'
|
||||
size='18px'
|
||||
color={iconFill}
|
||||
|
@ -2,6 +2,7 @@
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("/~landscape/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-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("https://media.urbit.org/fonts/Inter-Medium.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@ -17,6 +19,7 @@
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url("https://media.urbit.org/fonts/Inter-SemiBold.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@ -24,6 +27,7 @@
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("/~landscape/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-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("/~landscape/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-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("/~landscape/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"),
|
||||
url("https://storage.googleapis.com/media.urbit.org/fonts/scp-extralight.woff");
|
||||
font-weight: 200;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@ -55,6 +62,7 @@
|
||||
src: url("/~landscape/fonts/sourcecodepro-light.woff2"),
|
||||
url("https://storage.googleapis.com/media.urbit.org/fonts/scp-light.woff");
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@ -62,6 +70,7 @@
|
||||
src: url("/~landscape/fonts/sourcecodepro-regular.woff2"),
|
||||
url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff");
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@ -69,6 +78,7 @@
|
||||
src: url("(/~landscape/fonts/sourcecodepro-medium.woff2"),
|
||||
url("https://storage.googleapis.com/media.urbit.org/fonts/scp-medium.woff");
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@ -76,6 +86,7 @@
|
||||
src: url("/~landscape/fonts/sourcecodepro-semibold.woff2"),
|
||||
url("https://storage.googleapis.com/media.urbit.org/fonts/scp-semibold.woff");
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
@ -83,5 +94,6 @@
|
||||
src: url("/~landscape/fonts/sourcecodepro-bold.woff2"),
|
||||
url("https://storage.googleapis.com/media.urbit.org/fonts/scp-bold.woff");
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ import GlobalApi from '~/logic/api/global';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import { FormSubmit } from '~/views/components/FormSubmit';
|
||||
import { ChannelWritePerms } from '../ChannelWritePerms';
|
||||
import {FormGroupChild} from '~/views/components/FormGroup';
|
||||
|
||||
function PermissionsSummary(props: {
|
||||
writersSize: number;
|
||||
@ -158,7 +159,8 @@ export function GraphPermissions(props: GraphPermissionsProps) {
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<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">
|
||||
<Text id="permissions" fontWeight="bold" fontSize="2">
|
||||
Permissions
|
||||
@ -187,7 +189,6 @@ export function GraphPermissions(props: GraphPermissionsProps) {
|
||||
caption="If enabled, all members of the group can comment on this channel"
|
||||
/>
|
||||
)}
|
||||
<FormSubmit>Update Permissions</FormSubmit>
|
||||
</Col>
|
||||
</Form>
|
||||
</Formik>
|
||||
|
@ -1,19 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Formik, Form } from 'formik';
|
||||
import React from "react";
|
||||
import { Formik, Form } from "formik";
|
||||
|
||||
import {
|
||||
ManagedTextInputField as Input,
|
||||
Col,
|
||||
Label,
|
||||
Text
|
||||
} from '@tlon/indigo-react';
|
||||
import { Association } from '@urbit/api';
|
||||
Text,
|
||||
} from "@tlon/indigo-react";
|
||||
import { Association } from "@urbit/api";
|
||||
|
||||
import { FormError } from '~/views/components/FormError';
|
||||
import { ColorInput } from '~/views/components/ColorInput';
|
||||
import { uxToHex } from '~/logic/lib/util';
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { FormSubmit } from '~/views/components/FormSubmit';
|
||||
import { FormError } from "~/views/components/FormError";
|
||||
import { ColorInput } from "~/views/components/ColorInput";
|
||||
import { uxToHex } from "~/logic/lib/util";
|
||||
import GlobalApi from "~/logic/api/global";
|
||||
import { FormSubmit } from "~/views/components/FormSubmit";
|
||||
import { FormGroupChild } from "~/views/components/FormGroup";
|
||||
|
||||
interface FormSchema {
|
||||
title: string;
|
||||
@ -30,9 +31,9 @@ export function ChannelDetails(props: ChannelDetailsProps) {
|
||||
const { association, api } = props;
|
||||
const { metadata } = association;
|
||||
const initialValues: FormSchema = {
|
||||
title: metadata?.title || '',
|
||||
description: metadata?.description || '',
|
||||
color: metadata?.color || '0x0'
|
||||
title: metadata?.title || "",
|
||||
description: metadata?.description || "",
|
||||
color: metadata?.color || "0x0",
|
||||
};
|
||||
|
||||
const onSubmit = async (values: FormSchema, actions) => {
|
||||
@ -44,8 +45,9 @@ export function ChannelDetails(props: ChannelDetailsProps) {
|
||||
|
||||
return (
|
||||
<Formik initialValues={initialValues} onSubmit={onSubmit}>
|
||||
<Form style={{ display: 'contents' }}>
|
||||
<Col mb="4" flexShrink={0} gapY="4">
|
||||
<Form style={{ display: "contents" }}>
|
||||
<FormGroupChild id="details" />
|
||||
<Col mx="4" mb="4" flexShrink={0} gapY="4">
|
||||
<Col mb={3}>
|
||||
<Text id="details" fontSize="2" fontWeight="bold">
|
||||
Channel Details
|
||||
@ -69,9 +71,6 @@ export function ChannelDetails(props: ChannelDetailsProps) {
|
||||
label="Color"
|
||||
caption="Change the color of this channel"
|
||||
/>
|
||||
<FormSubmit>
|
||||
Update Details
|
||||
</FormSubmit>
|
||||
<FormError message="Failed to update settings" />
|
||||
</Col>
|
||||
</Form>
|
||||
|
@ -28,7 +28,7 @@ export function ChannelNotifications(props: ChannelNotificationsProps) {
|
||||
const anchorRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
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">
|
||||
Channel Notifications
|
||||
</Text>
|
||||
|
@ -13,7 +13,7 @@ export function ChannelPopoverRoutesSidebar(props: {
|
||||
|
||||
return (
|
||||
<Col
|
||||
display={['none', 'flex-column']}
|
||||
display={['none', 'flex']}
|
||||
minWidth="200px"
|
||||
borderRight="1"
|
||||
borderRightColor="washedGray"
|
||||
@ -27,13 +27,13 @@ export function ChannelPopoverRoutesSidebar(props: {
|
||||
Preferences
|
||||
</Text>
|
||||
<SidebarItem
|
||||
icon="Inbox"
|
||||
icon='Notifications'
|
||||
text="Notifications"
|
||||
to={relativePath('/settings#notifications')}
|
||||
/>
|
||||
{!isOwner && (
|
||||
<SidebarItem
|
||||
icon="SignOut"
|
||||
icon="LogOut"
|
||||
text="Unsubscribe"
|
||||
color="red"
|
||||
to={relativePath('/settings#unsubscribe')}
|
||||
@ -45,7 +45,7 @@ export function ChannelPopoverRoutesSidebar(props: {
|
||||
Administration
|
||||
</Text>
|
||||
<SidebarItem
|
||||
icon="Boot"
|
||||
icon="BootNode"
|
||||
text="Channel Details"
|
||||
to={relativePath('/settings#details')}
|
||||
/>
|
||||
@ -56,14 +56,14 @@ export function ChannelPopoverRoutesSidebar(props: {
|
||||
/>
|
||||
{ isOwner ? (
|
||||
<SidebarItem
|
||||
icon="TrashCan"
|
||||
icon="X"
|
||||
text="Archive Channel"
|
||||
to={relativePath('/settings#archive')}
|
||||
color="red"
|
||||
/>
|
||||
) : (
|
||||
<SidebarItem
|
||||
icon="TrashCan"
|
||||
icon="X"
|
||||
text="Archive Channel"
|
||||
to={relativePath('/settings#remove')}
|
||||
color="red"
|
||||
|
@ -19,6 +19,7 @@ import { useHistory, Link } from 'react-router-dom';
|
||||
import { ChannelNotifications } from './Notifications';
|
||||
import { StatelessAsyncButton } from '~/views/components/StatelessAsyncButton';
|
||||
import { isChannelAdmin, isHost } from '~/logic/lib/group';
|
||||
import {FormGroup} from '~/views/components/FormGroup';
|
||||
|
||||
interface ChannelPopoverRoutesProps {
|
||||
baseUrl: string;
|
||||
@ -83,10 +84,10 @@ export function ChannelPopoverRoutes(props: ChannelPopoverRoutesProps) {
|
||||
isOwner={isOwner}
|
||||
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} />
|
||||
{!isOwner && (
|
||||
<Col mb="6" flexShrink={0}>
|
||||
<Col mx="4" mb="6" flexShrink={0}>
|
||||
<Text id="unsubscribe" fontSize="2" fontWeight="bold">
|
||||
Unsubscribe from Channel
|
||||
</Text>
|
||||
@ -107,7 +108,7 @@ export function ChannelPopoverRoutes(props: ChannelPopoverRoutesProps) {
|
||||
<ChannelDetails {...props} />
|
||||
<GraphPermissions {...props} />
|
||||
{ isOwner ? (
|
||||
<Col mt="5" mb="6" flexShrink={0}>
|
||||
<Col mx="4" mt="5" mb="6" flexShrink={0}>
|
||||
<Text id="archive" fontSize="2" fontWeight="bold">
|
||||
Archive channel
|
||||
</Text>
|
||||
@ -124,7 +125,7 @@ export function ChannelPopoverRoutes(props: ChannelPopoverRoutesProps) {
|
||||
</Col>
|
||||
|
||||
) : (
|
||||
<Col mt="5" mb="6" flexShrink={0}>
|
||||
<Col mx="4" my="6" flexShrink={0}>
|
||||
<Text id="remove" fontSize="2" fontWeight="bold">
|
||||
Remove channel from group
|
||||
</Text>
|
||||
@ -143,7 +144,7 @@ export function ChannelPopoverRoutes(props: ChannelPopoverRoutesProps) {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
</FormGroup>
|
||||
</Row>
|
||||
</ModalOverlay>
|
||||
);
|
||||
|
@ -36,7 +36,7 @@ return;
|
||||
? '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';
|
||||
|
||||
const icon = props.owner ? 'X' : 'SignOut';
|
||||
const icon = props.owner ? 'X' : 'LogOut';
|
||||
const { modal, showModal } = useModal({ modal:
|
||||
(dismiss: () => void) => {
|
||||
const onCancel = (e) => {
|
||||
|
@ -29,9 +29,9 @@ export function GroupSummary(props: GroupSummaryProps & PropFunc<typeof Col>): R
|
||||
width="40px"
|
||||
height="40px"
|
||||
metadata={metadata}
|
||||
flexShrink="0"
|
||||
flexShrink={0}
|
||||
/>
|
||||
<Col justifyContent="space-between" flexGrow="1" overflow="hidden">
|
||||
<Col justifyContent="space-between" flexGrow={1} overflow="hidden">
|
||||
<Text
|
||||
fontSize="1"
|
||||
textOverflow="ellipsis"
|
||||
|
@ -180,7 +180,7 @@ export function GroupSwitcher(props: {
|
||||
>
|
||||
<Row flexGrow={1} alignItems="center" width='100%' minWidth='0' flexShrink={0}>
|
||||
{ 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>
|
||||
</Dropdown>
|
||||
<Row pr='3' verticalAlign="middle">
|
||||
|
@ -69,7 +69,7 @@ export function GroupifyForm(props: GroupifyFormProps) {
|
||||
onSubmit={onGroupify}
|
||||
>
|
||||
<Form>
|
||||
<Col flexShrink="0" gapY="4" maxWidth="512px">
|
||||
<Col flexShrink={0} gapY="4" maxWidth="512px">
|
||||
<Box>
|
||||
<Text fontWeight="500">Groupify this channel</Text>
|
||||
</Box>
|
||||
|
@ -147,7 +147,7 @@ export function PostInput(props) {
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<Icon
|
||||
icon='Links'
|
||||
icon='Attachment'
|
||||
width='16'
|
||||
height='16'
|
||||
onClick={uploadImage}
|
||||
|
@ -79,6 +79,7 @@ export default function PostReplies(props) {
|
||||
baseUrl={baseUrl}
|
||||
history={history}
|
||||
isParent={true}
|
||||
parentPost={parentNode?.post}
|
||||
vip={vip}
|
||||
group={group}
|
||||
/>
|
||||
|
@ -177,7 +177,7 @@ export function JoinGroup(props: JoinGroupProps): ReactElement {
|
||||
<Text gray fontSize="1">
|
||||
Channels
|
||||
</Text>
|
||||
<Box width="100%" flexShrink="0">
|
||||
<Box width="100%" flexShrink={0}>
|
||||
{Object.values(preview.channels).map(({ metadata }: any) => (
|
||||
<Row width="100%">
|
||||
<Icon
|
||||
|
@ -11,7 +11,7 @@ import * as Yup from 'yup';
|
||||
import GlobalApi from '~/logic/api/global';
|
||||
import { AsyncButton } from '~/views/components/AsyncButton';
|
||||
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 { resourceFromPath } from '~/logic/lib/group';
|
||||
import { Associations } from '@urbit/api/metadata';
|
||||
@ -46,8 +46,9 @@ interface NewChannelProps {
|
||||
workspace: Workspace;
|
||||
}
|
||||
|
||||
export function NewChannel(props: NewChannelProps & RouteComponentProps): ReactElement {
|
||||
const { history, api, group, workspace } = props;
|
||||
export function NewChannel(props: NewChannelProps): ReactElement {
|
||||
const history = useHistory();
|
||||
const { api, group, workspace } = props;
|
||||
|
||||
const groups = useGroupState(state => state.groups);
|
||||
const waiter = useWaitForProps({ groups }, 5000);
|
||||
@ -152,7 +153,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps): ReactE
|
||||
name="moduleType"
|
||||
/>
|
||||
<IconRadio
|
||||
icon="Publish"
|
||||
icon="Notebook"
|
||||
label="Notebook"
|
||||
id="publish"
|
||||
name="moduleType"
|
||||
|
@ -181,9 +181,9 @@ export function Participants(props: {
|
||||
mb={2}
|
||||
px={2}
|
||||
zIndex={1}
|
||||
flexShrink="0"
|
||||
flexShrink={0}
|
||||
>
|
||||
<Row mr="4" flexShrink="0">
|
||||
<Row mr="4" flexShrink={0}>
|
||||
<Tab
|
||||
selected={filter}
|
||||
setSelected={setFilter}
|
||||
@ -206,9 +206,9 @@ export function Participants(props: {
|
||||
/>
|
||||
</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">
|
||||
<Icon color="gray" icon="MagnifyingGlass" />
|
||||
<Icon color="gray" icon="Search" />
|
||||
<Input
|
||||
maxWidth="256px"
|
||||
color="gray"
|
||||
|
@ -76,7 +76,7 @@ export function PopoverRoutes(
|
||||
<Col gapY="2">
|
||||
<Text my="1" mx="3" gray>Group</Text>
|
||||
<SidebarItem
|
||||
icon="Inbox"
|
||||
icon='Notifications'
|
||||
to={relativeUrl('/settings#notifications')}
|
||||
text="Notifications"
|
||||
/>
|
||||
@ -98,7 +98,7 @@ export function PopoverRoutes(
|
||||
text="Group Details"
|
||||
/>
|
||||
<SidebarItem
|
||||
icon="Spaces"
|
||||
icon="Dashboard"
|
||||
to={relativeUrl('/settings#channels')}
|
||||
text="Channel Management"
|
||||
/>
|
||||
|
@ -77,7 +77,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
||||
fontSize='1'
|
||||
mr='12px'
|
||||
my='1'
|
||||
flexShrink='0'
|
||||
flexShrink={0}
|
||||
display={['block','none']}
|
||||
>
|
||||
<Link to={`/~landscape${workspace}`}>
|
||||
@ -98,7 +98,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
||||
maxWidth={association?.metadata?.description ? ['100%', '50%'] : 'none'}
|
||||
mr='2'
|
||||
ml='1'
|
||||
flexShrink={['1', '0']}
|
||||
flexShrink={[1, 0]}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
@ -112,7 +112,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
||||
mb='0'
|
||||
minWidth='0'
|
||||
maxWidth='50%'
|
||||
flexShrink='1'
|
||||
flexShrink={1}
|
||||
disableRemoteContent
|
||||
>
|
||||
{workspace === '/messages'
|
||||
@ -145,7 +145,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
||||
return (
|
||||
<Col width='100%' height='100%' overflow='hidden'>
|
||||
<Box
|
||||
flexShrink='0'
|
||||
flexShrink={0}
|
||||
height='48px'
|
||||
py='2'
|
||||
px='2'
|
||||
@ -159,7 +159,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
||||
display='flex'
|
||||
alignItems='baseline'
|
||||
width={`calc(100% - ${actionsWidth}px - 16px)`}
|
||||
flexShrink='0'
|
||||
flexShrink={0}
|
||||
>
|
||||
<BackLink />
|
||||
<Title />
|
||||
@ -169,7 +169,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
||||
ml={3}
|
||||
display='flex'
|
||||
alignItems='center'
|
||||
flexShrink='0'
|
||||
flexShrink={0}
|
||||
ref={actionsRef}
|
||||
>
|
||||
{canWrite && <WriterControls />}
|
||||
|
@ -86,7 +86,11 @@ export function SidebarItem(props: {
|
||||
let color = 'lightGray';
|
||||
|
||||
if (isSynced) {
|
||||
if (hasUnread || hasNotification) {
|
||||
color = 'black';
|
||||
} else {
|
||||
color = 'gray';
|
||||
}
|
||||
}
|
||||
|
||||
const fontWeight = (hasUnread || hasNotification) ? '500' : 'normal';
|
||||
@ -132,7 +136,7 @@ export function SidebarItem(props: {
|
||||
{DM ? img : (
|
||||
<Icon
|
||||
display="block"
|
||||
color={isSynced ? 'black' : 'gray'}
|
||||
color={isSynced ? 'black' : 'lightGray'}
|
||||
icon={getModuleIcon(mod) as any}
|
||||
/>
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { ReactElement, useCallback } from 'react';
|
||||
import { FormikHelpers } from 'formik';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
Row,
|
||||
@ -34,6 +34,7 @@ export function SidebarListHeader(props: {
|
||||
workspace: Workspace;
|
||||
handleSubmit: (c: SidebarListConfig) => void;
|
||||
}): ReactElement {
|
||||
const history = useHistory();
|
||||
const onSubmit = useCallback(
|
||||
(values: SidebarListConfig, actions: FormikHelpers<SidebarListConfig>) => {
|
||||
props.handleSubmit(values);
|
||||
@ -65,7 +66,7 @@ export function SidebarListHeader(props: {
|
||||
<Box>
|
||||
{( !!feedPath) ? (
|
||||
<Row
|
||||
flexShrink="0"
|
||||
flexShrink={0}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
py={2}
|
||||
@ -74,18 +75,18 @@ export function SidebarListHeader(props: {
|
||||
borderBottom={1}
|
||||
borderColor="lightGray"
|
||||
backgroundColor={['transparent',
|
||||
props.history.location.pathname.includes(`/~landscape${groupPath}/feed`)
|
||||
history.location.pathname.includes(`/~landscape${groupPath}/feed`)
|
||||
? (
|
||||
'washedGray'
|
||||
) : (
|
||||
'transparent'
|
||||
)]}
|
||||
cursor={['pointer', (
|
||||
props.history.location.pathname === `/~landscape${groupPath}/feed`
|
||||
cursor={(
|
||||
history.location.pathname === `/~landscape${groupPath}/feed`
|
||||
? 'default' : 'pointer'
|
||||
)]}
|
||||
)}
|
||||
onClick={() => {
|
||||
props.history.push(`/~landscape${groupPath}/feed`);
|
||||
history.push(`/~landscape${groupPath}/feed`);
|
||||
}}
|
||||
>
|
||||
<Text>
|
||||
@ -98,14 +99,14 @@ export function SidebarListHeader(props: {
|
||||
) : null
|
||||
}
|
||||
<Row
|
||||
flexShrink="0"
|
||||
flexShrink={0}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
py={2}
|
||||
px={3}
|
||||
height='48px'
|
||||
>
|
||||
<Box flexShrink='0'>
|
||||
<Box flexShrink={0}>
|
||||
<Text>
|
||||
{props.initialValues.hideUnjoined ? `Joined ${noun}` : `All ${noun}`}
|
||||
</Text>
|
||||
@ -131,7 +132,6 @@ export function SidebarListHeader(props: {
|
||||
>
|
||||
<NewChannel
|
||||
api={props.api}
|
||||
history={props.history}
|
||||
workspace={props.workspace}
|
||||
/>
|
||||
</Col>
|
||||
@ -152,7 +152,7 @@ export function SidebarListHeader(props: {
|
||||
)
|
||||
}
|
||||
<Dropdown
|
||||
flexShrink='0'
|
||||
flexShrink={0}
|
||||
width="auto"
|
||||
alignY="top"
|
||||
alignX={['right', 'left']}
|
||||
|
@ -1,227 +1,62 @@
|
||||
import { BigInteger } from "big-integer";
|
||||
import { immerable } from 'immer';
|
||||
import bigInt, { BigInteger } from "big-integer";
|
||||
|
||||
interface NonemptyNode<V> {
|
||||
n: [BigInteger, V];
|
||||
l: MapNode<V>;
|
||||
r: MapNode<V>;
|
||||
function sortBigInt(a: BigInteger, b: BigInteger) {
|
||||
if (a.lt(b)) {
|
||||
return 1;
|
||||
} 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]> {
|
||||
private root: MapNode<V> = null;
|
||||
private root: Record<string, V> = {}
|
||||
private cachedIter: [BigInteger, V][] | null = null;
|
||||
[immerable] = true;
|
||||
size: number = 0;
|
||||
|
||||
constructor(initial: [BigInteger, V][] = []) {
|
||||
initial.forEach(([key, val]) => {
|
||||
constructor(items: [BigInteger, V][] = []) {
|
||||
items.forEach(([key, val]) => {
|
||||
this.set(key, val);
|
||||
});
|
||||
this.generateCachedIter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve an value for a key
|
||||
*/
|
||||
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);
|
||||
get size() {
|
||||
return this.cachedIter?.length ?? Object.keys(this.root).length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put an item by a key
|
||||
*/
|
||||
set(key: BigInteger, value: V): void {
|
||||
|
||||
const inner = (node: MapNode<V>): 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);
|
||||
get(key: BigInteger) {
|
||||
return this.root[key.toString()] ?? null;
|
||||
}
|
||||
|
||||
set(key: BigInteger, value: V) {
|
||||
this.root[key.toString()] = value;
|
||||
this.cachedIter = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all entries
|
||||
*/
|
||||
clear() {
|
||||
this.root = null;
|
||||
this.cachedIter = null;
|
||||
this.root = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Predicate testing if map contains key
|
||||
*/
|
||||
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);
|
||||
has(key: BigInteger) {
|
||||
return key.toString() in 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--;
|
||||
const had = this.has(key);
|
||||
if(had) {
|
||||
delete this.root[key.toString()];
|
||||
this.cachedIter = null;
|
||||
}
|
||||
this.root = newRoot;
|
||||
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));
|
||||
return had;
|
||||
}
|
||||
|
||||
[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;
|
||||
const result = this.generateCachedIter();
|
||||
return {
|
||||
[Symbol.iterator]: this[Symbol.iterator],
|
||||
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