mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-11-11 04:48:00 +03:00
Merge remote-tracking branch 'origin/release/next-js'
This commit is contained in:
commit
d18630d4e8
@ -5,7 +5,7 @@
|
|||||||
/- glob
|
/- glob
|
||||||
/+ default-agent, verb, dbug
|
/+ default-agent, verb, dbug
|
||||||
|%
|
|%
|
||||||
++ hash 0v3.g6u13.haedt.jt4hd.61ek5.6t30q
|
++ hash 0v3.hls3k.gsbae.rm6pr.p6qve.46dh8
|
||||||
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
||||||
+$ all-states
|
+$ all-states
|
||||||
$% state-0
|
$% state-0
|
||||||
|
@ -24,6 +24,6 @@
|
|||||||
<div id="portal-root"></div>
|
<div id="portal-root"></div>
|
||||||
<script src="/~landscape/js/channel.js"></script>
|
<script src="/~landscape/js/channel.js"></script>
|
||||||
<script src="/~landscape/js/session.js"></script>
|
<script src="/~landscape/js/session.js"></script>
|
||||||
<script src="/~landscape/js/bundle/index.59e682153138f604d358.js"></script>
|
<script src="/~landscape/js/bundle/index.b253f1f3f824fdeb29d3.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -111,7 +111,7 @@ module.exports = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light)\/).*/
|
exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light|@tlon\/indigo-react)\/).*/
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/i,
|
test: /\.css$/i,
|
||||||
|
@ -30,7 +30,7 @@ module.exports = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light)\/).*/
|
exclude: /node_modules\/(?!(@tlon\/indigo-dark|@tlon\/indigo-light|@tlon\/indigo-react)\/).*/
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/i,
|
test: /\.css$/i,
|
||||||
|
3890
pkg/interface/package-lock.json
generated
3890
pkg/interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,8 +9,8 @@
|
|||||||
"@reach/menu-button": "^0.10.5",
|
"@reach/menu-button": "^0.10.5",
|
||||||
"@reach/tabs": "^0.10.5",
|
"@reach/tabs": "^0.10.5",
|
||||||
"@tlon/indigo-dark": "^1.0.6",
|
"@tlon/indigo-dark": "^1.0.6",
|
||||||
"@tlon/indigo-light": "^1.0.6",
|
"@tlon/indigo-light": "^1.0.7",
|
||||||
"@tlon/indigo-react": "^1.2.19",
|
"@tlon/indigo-react": "^1.2.21",
|
||||||
"@tlon/sigil-js": "^1.4.3",
|
"@tlon/sigil-js": "^1.4.3",
|
||||||
"@urbit/api": "file:../npm/api",
|
"@urbit/api": "file:../npm/api",
|
||||||
"any-ascii": "^0.1.7",
|
"any-ascii": "^0.1.7",
|
||||||
@ -88,7 +88,7 @@
|
|||||||
"react-hot-loader": "^4.13.0",
|
"react-hot-loader": "^4.13.0",
|
||||||
"sass": "^1.32.5",
|
"sass": "^1.32.5",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"typescript": "^3.9.7",
|
"typescript": "^4.2.4",
|
||||||
"webpack": "^4.46.0",
|
"webpack": "^4.46.0",
|
||||||
"webpack-cli": "^3.3.12",
|
"webpack-cli": "^3.3.12",
|
||||||
"webpack-dev-server": "^3.11.2"
|
"webpack-dev-server": "^3.11.2"
|
||||||
|
@ -2,6 +2,7 @@ import BaseApi from './base';
|
|||||||
import { StoreState } from '../store/type';
|
import { StoreState } from '../store/type';
|
||||||
import { Patp } from '@urbit/api';
|
import { Patp } from '@urbit/api';
|
||||||
import { ContactEdit } from '@urbit/api/contacts';
|
import { ContactEdit } from '@urbit/api/contacts';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
export default class ContactsApi extends BaseApi<StoreState> {
|
export default class ContactsApi extends BaseApi<StoreState> {
|
||||||
add(ship: Patp, contact: any) {
|
add(ship: Patp, contact: any) {
|
||||||
@ -73,6 +74,28 @@ export default class ContactsApi extends BaseApi<StoreState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async disallowedShipsForOurContact(ships: string[]): Promise<string[]> {
|
||||||
|
return _.compact(
|
||||||
|
await Promise.all(
|
||||||
|
ships.map(
|
||||||
|
async s => {
|
||||||
|
const ship = `~${s}`;
|
||||||
|
if(s === window.ship) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const allowed = await this.fetchIsAllowed(
|
||||||
|
`~${window.ship}`,
|
||||||
|
'personal',
|
||||||
|
ship,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
return allowed ? null : ship;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
retrieve(ship: string) {
|
retrieve(ship: string) {
|
||||||
const resource = { ship, name: '' };
|
const resource = { ship, name: '' };
|
||||||
return this.action('contact-pull-hook', 'pull-hook-action', {
|
return this.action('contact-pull-hook', 'pull-hook-action', {
|
||||||
|
@ -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 };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -99,9 +99,11 @@ export default function index(contacts, associations, apps, currentGroup, groups
|
|||||||
Object.keys(associations).filter((e) => {
|
Object.keys(associations).filter((e) => {
|
||||||
// skip apps with no metadata
|
// skip apps with no metadata
|
||||||
return Object.keys(associations[e]).length > 0;
|
return Object.keys(associations[e]).length > 0;
|
||||||
}).map((e) => {
|
}).map((e) => {
|
||||||
// iterate through each app's metadata object
|
// iterate through each app's metadata object
|
||||||
Object.keys(associations[e]).map((association) => {
|
Object.keys(associations[e])
|
||||||
|
.filter((association) => !associations?.[e]?.[association]?.metadata?.hidden)
|
||||||
|
.map((association) => {
|
||||||
const each = associations[e][association];
|
const each = associations[e][association];
|
||||||
let title = each.resource;
|
let title = each.resource;
|
||||||
if (each.metadata.title !== '') {
|
if (each.metadata.title !== '') {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { Post, GraphNode, TextContent, Graph, NodeMap } from '@urbit/api';
|
import { Post, GraphNode, TextContent } from '@urbit/api';
|
||||||
import { buntPost } from '~/logic/lib/post';
|
import { buntPost } from '~/logic/lib/post';
|
||||||
import { unixToDa } from '~/logic/lib/util';
|
import { unixToDa } from '~/logic/lib/util';
|
||||||
import { BigIntOrderedMap } from './BigIntOrderedMap';
|
import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap";
|
||||||
import bigInt, { BigInteger } from 'big-integer';
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
|
|
||||||
export function newPost(
|
export function newPost(
|
||||||
title: string,
|
title: string,
|
||||||
body: string
|
body: string
|
||||||
): [BigInteger, NodeMap] {
|
): [BigInteger, any] {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const nowDa = unixToDa(now);
|
const nowDa = unixToDa(now);
|
||||||
const root: Post = {
|
const root: Post = {
|
||||||
@ -73,13 +73,16 @@ export function editPost(rev: number, noteId: BigInteger, title: string, body: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getLatestRevision(node: GraphNode): [number, string, string, Post] {
|
export function getLatestRevision(node: GraphNode): [number, string, string, Post] {
|
||||||
const revs = node.children.get(bigInt(1));
|
const revs = node.children?.get(bigInt(1));
|
||||||
const empty = [1, '', '', buntPost()] as [number, string, string, Post];
|
const empty = [1, '', '', buntPost()] as [number, string, string, Post];
|
||||||
if(!revs) {
|
if(!revs) {
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
const [revNum, rev] = [...revs.children][0];
|
let revNum, rev;
|
||||||
if(!rev) {
|
if (revs?.children !== null) {
|
||||||
|
[revNum, rev] = [...revs.children][0];
|
||||||
|
}
|
||||||
|
if (!rev) {
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
const [title, body] = rev.post.contents as TextContent[];
|
const [title, body] = rev.post.contents as TextContent[];
|
||||||
@ -88,18 +91,22 @@ export function getLatestRevision(node: GraphNode): [number, string, string, Pos
|
|||||||
|
|
||||||
export function getLatestCommentRevision(node: GraphNode): [number, Post] {
|
export function getLatestCommentRevision(node: GraphNode): [number, Post] {
|
||||||
const empty = [1, buntPost()] as [number, Post];
|
const empty = [1, buntPost()] as [number, Post];
|
||||||
if (node.children.size <= 0) {
|
const childSize = node?.children?.size ?? 0;
|
||||||
|
if (childSize <= 0) {
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
const [revNum, rev] = [...node.children][0];
|
let revNum, rev;
|
||||||
if(!rev) {
|
if (node?.children !== null) {
|
||||||
|
[revNum, rev] = [...node.children][0];
|
||||||
|
}
|
||||||
|
if (!rev) {
|
||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
return [revNum.toJSNumber(), rev.post];
|
return [revNum.toJSNumber(), rev.post];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getComments(node: GraphNode): GraphNode {
|
export function getComments(node: GraphNode): GraphNode {
|
||||||
const comments = node.children.get(bigInt(2));
|
const comments = node.children?.get(bigInt(2));
|
||||||
if(!comments) {
|
if(!comments) {
|
||||||
return { post: buntPost(), children: new BigIntOrderedMap() };
|
return { post: buntPost(), children: new BigIntOrderedMap() };
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { TutorialProgress, Associations } from '@urbit/api';
|
import { Associations } from '@urbit/api';
|
||||||
|
import { TutorialProgress } from '~/types';
|
||||||
import { AlignX, AlignY } from '~/logic/lib/relativePosition';
|
import { AlignX, AlignY } from '~/logic/lib/relativePosition';
|
||||||
import { Direction } from '~/views/components/Triangle';
|
import { Direction } from '~/views/components/Triangle';
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ interface StepDetail {
|
|||||||
alignY: AlignY | AlignY[];
|
alignY: AlignY | AlignY[];
|
||||||
offsetX: number;
|
offsetX: number;
|
||||||
offsetY: number;
|
offsetY: number;
|
||||||
arrow: Direction;
|
arrow?: Direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasTutorialGroup(props: { associations: Associations }) {
|
export function hasTutorialGroup(props: { associations: Associations }) {
|
||||||
|
@ -15,7 +15,8 @@ function retrieve<T>(key: string, initial: T): T {
|
|||||||
interface SetStateFunc<T> {
|
interface SetStateFunc<T> {
|
||||||
(t: T): T;
|
(t: T): T;
|
||||||
}
|
}
|
||||||
type SetState<T> = T | SetStateFunc<T>;
|
// See microsoft/typescript#37663 for filed bug
|
||||||
|
type SetState<T> = T extends any ? SetStateFunc<T> : never;
|
||||||
export function useLocalStorageState<T>(key: string, initial: T) {
|
export function useLocalStorageState<T>(key: string, initial: T) {
|
||||||
const [state, _setState] = useState(() => retrieve(key, initial));
|
const [state, _setState] = useState(() => retrieve(key, initial));
|
||||||
|
|
||||||
|
@ -2,18 +2,14 @@ import React, {
|
|||||||
useState,
|
useState,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
SyntheticEvent,
|
|
||||||
useMemo,
|
useMemo,
|
||||||
useEffect,
|
|
||||||
useRef
|
useRef
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { Box } from '@tlon/indigo-react';
|
import { Box } from '@tlon/indigo-react';
|
||||||
import { useOutsideClick } from './useOutsideClick';
|
|
||||||
import { ModalOverlay } from '~/views/components/ModalOverlay';
|
import { ModalOverlay } from '~/views/components/ModalOverlay';
|
||||||
import { Portal } from '~/views/components/Portal';
|
import { Portal } from '~/views/components/Portal';
|
||||||
import { ModalPortal } from '~/views/components/ModalPortal';
|
import { PropFunc } from '~/types';
|
||||||
import { PropFunc } from '@urbit/api';
|
|
||||||
|
|
||||||
type ModalFunc = (dismiss: () => void) => JSX.Element;
|
type ModalFunc = (dismiss: () => void) => JSX.Element;
|
||||||
interface UseModalProps {
|
interface UseModalProps {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Primitive } from '@urbit/api';
|
import { Primitive } from '~/types';
|
||||||
|
|
||||||
export default function usePreviousValue<T extends Primitive>(value: T): T {
|
export default function usePreviousValue<T extends Primitive>(value: T): T {
|
||||||
const prev = useRef<T | null>(null);
|
const prev = useRef<T | null>(null);
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useWaitForProps } from "./useWaitForProps";
|
|
||||||
import {unstable_batchedUpdates} from "react-dom";
|
import {unstable_batchedUpdates} from "react-dom";
|
||||||
|
|
||||||
export type IOInstance<I, P, O> = (
|
export type IOInstance<I, P, O> = (
|
||||||
@ -10,7 +9,7 @@ export function useRunIO<I, O>(
|
|||||||
io: (i: I) => Promise<O>,
|
io: (i: I) => Promise<O>,
|
||||||
after: (o: O) => void,
|
after: (o: O) => void,
|
||||||
key: string
|
key: string
|
||||||
): () => Promise<void> {
|
): (i: I) => Promise<unknown> {
|
||||||
const [resolve, setResolve] = useState<() => void>(() => () => {});
|
const [resolve, setResolve] = useState<() => void>(() => () => {});
|
||||||
const [reject, setReject] = useState<(e: any) => void>(() => () => {});
|
const [reject, setReject] = useState<(e: any) => void>(() => () => {});
|
||||||
const [output, setOutput] = useState<O | null>(null);
|
const [output, setOutput] = useState<O | null>(null);
|
||||||
|
@ -5,7 +5,7 @@ export function useStatelessAsyncClickable(
|
|||||||
onClick: (e: MouseEvent) => Promise<void>,
|
onClick: (e: MouseEvent) => Promise<void>,
|
||||||
name: string
|
name: string
|
||||||
) {
|
) {
|
||||||
const [state, setState] = useState<ButtonState>('waiting');
|
const [state, setState] = useState<AsyncClickableState>('waiting');
|
||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
async (e: MouseEvent) => {
|
async (e: MouseEvent) => {
|
||||||
try {
|
try {
|
||||||
|
@ -16,7 +16,7 @@ export interface IuseStorage {
|
|||||||
upload: (file: File, bucket: string) => Promise<string>;
|
upload: (file: File, bucket: string) => Promise<string>;
|
||||||
uploadDefault: (file: File) => Promise<string>;
|
uploadDefault: (file: File) => Promise<string>;
|
||||||
uploading: boolean;
|
uploading: boolean;
|
||||||
promptUpload: () => Promise<string | undefined>;
|
promptUpload: () => Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStorage = ({ accept = '*' } = { accept: '*' }): IuseStorage => {
|
const useStorage = ({ accept = '*' } = { accept: '*' }): IuseStorage => {
|
||||||
|
@ -30,7 +30,7 @@ export const getModuleIcon = (mod: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mod === 'post') {
|
if (mod === 'post') {
|
||||||
return 'Spaces';
|
return 'Dashboard';
|
||||||
}
|
}
|
||||||
|
|
||||||
return _.capitalize(mod);
|
return _.capitalize(mod);
|
||||||
@ -192,7 +192,10 @@ export function uxToHex(ux: string) {
|
|||||||
export const hexToUx = (hex) => {
|
export const hexToUx = (hex) => {
|
||||||
const ux = f.flow(
|
const ux = f.flow(
|
||||||
f.chunk(4),
|
f.chunk(4),
|
||||||
f.map(x => _.dropWhile(x, y => y === 0).join('')),
|
// eslint-disable-next-line prefer-arrow-callback
|
||||||
|
f.map(x => _.dropWhile(x, function(y: unknown) {
|
||||||
|
return y === 0;
|
||||||
|
}).join('')),
|
||||||
f.join('.')
|
f.join('.')
|
||||||
)(hex.split(''));
|
)(hex.split(''));
|
||||||
return `0x${ux}`;
|
return `0x${ux}`;
|
||||||
@ -417,7 +420,7 @@ export const useHovering = (): useHoveringInterface => {
|
|||||||
onMouseLeave,
|
onMouseLeave,
|
||||||
}), [onMouseLeave, onMouseOver]);
|
}), [onMouseLeave, onMouseOver]);
|
||||||
|
|
||||||
|
|
||||||
return useMemo(() => ({ hovering, bind }), [hovering, bind]);
|
return useMemo(() => ({ hovering, bind }), [hovering, bind]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
} from "react";
|
} from "react";
|
||||||
import usePreviousValue from "./usePreviousValue";
|
import usePreviousValue from "./usePreviousValue";
|
||||||
|
import {Primitive} from "~/types";
|
||||||
|
|
||||||
export interface VirtualContextProps {
|
export interface VirtualContextProps {
|
||||||
save: () => void;
|
save: () => void;
|
||||||
@ -42,14 +43,14 @@ export function useVirtualResizeState(s: boolean) {
|
|||||||
[_setState, save]
|
[_setState, save]
|
||||||
);
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useEffect(() => {
|
||||||
restore();
|
requestAnimationFrame(restore);
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
return [state, setState] as const;
|
return [state, setState] as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useVirtualResizeProp<T>(prop: T) {
|
export function useVirtualResizeProp(prop: Primitive) {
|
||||||
const { save, restore } = useVirtual();
|
const { save, restore } = useVirtual();
|
||||||
const oldProp = usePreviousValue(prop)
|
const oldProp = usePreviousValue(prop)
|
||||||
|
|
||||||
@ -57,8 +58,8 @@ export function useVirtualResizeProp<T>(prop: T) {
|
|||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useEffect(() => {
|
||||||
restore();
|
requestAnimationFrame(restore);
|
||||||
}, [prop]);
|
}, [prop]);
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Associations, Workspace } from '@urbit/api';
|
import { Associations } from '@urbit/api';
|
||||||
|
import { Workspace } from '~/types';
|
||||||
|
|
||||||
export function getTitleFromWorkspace(
|
export function getTitleFromWorkspace(
|
||||||
associations: Associations,
|
associations: Associations,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import _ from 'lodash';
|
import { StoreState } from '../store/type';
|
||||||
import { StoreState } from '../../store/type';
|
|
||||||
import { Cage } from '~/types/cage';
|
import { Cage } from '~/types/cage';
|
||||||
|
|
||||||
type LocalState = Pick<StoreState, 'connection'>;
|
type LocalState = Pick<StoreState, 'connection'>;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
|
import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap";
|
||||||
|
import produce from 'immer';
|
||||||
import bigInt, { BigInteger } from "big-integer";
|
import bigInt, { BigInteger } from "big-integer";
|
||||||
import useGraphState, { GraphState } from '../state/graph';
|
import useGraphState, { GraphState } from '../state/graph';
|
||||||
import { reduceState } from '../state/base';
|
import { reduceState } from '../state/base';
|
||||||
@ -51,23 +52,18 @@ const keys = (json, state: GraphState): GraphState => {
|
|||||||
const processNode = (node) => {
|
const processNode = (node) => {
|
||||||
// is empty
|
// is empty
|
||||||
if (!node.children) {
|
if (!node.children) {
|
||||||
node.children = new BigIntOrderedMap();
|
return produce(node, draft => {
|
||||||
return node;
|
draft.children = new BigIntOrderedMap();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// is graph
|
// is graph
|
||||||
let converted = new BigIntOrderedMap();
|
return produce(node, draft => {
|
||||||
for (let idx in node.children) {
|
draft.children = new BigIntOrderedMap()
|
||||||
let item = node.children[idx];
|
.gas(_.map(draft.children, (item, idx) =>
|
||||||
let index = bigInt(idx);
|
[bigInt(idx), processNode(item)] as [BigInteger, any]
|
||||||
|
));
|
||||||
converted.set(
|
});
|
||||||
index,
|
|
||||||
processNode(item)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
node.children = converted;
|
|
||||||
return node;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -85,17 +81,10 @@ const addGraph = (json, state: GraphState): GraphState => {
|
|||||||
state.graphTimesentMap[resource] = {};
|
state.graphTimesentMap[resource] = {};
|
||||||
|
|
||||||
|
|
||||||
for (let idx in data.graph) {
|
state.graphs[resource] = state.graphs[resource].gas(Object.keys(data.graph).map(idx => {
|
||||||
let item = data.graph[idx];
|
return [bigInt(idx), processNode(data.graph[idx])];
|
||||||
let index = bigInt(idx);
|
}));
|
||||||
|
|
||||||
let node = processNode(item);
|
|
||||||
|
|
||||||
state.graphs[resource].set(
|
|
||||||
index,
|
|
||||||
node
|
|
||||||
);
|
|
||||||
}
|
|
||||||
state.graphKeys.add(resource);
|
state.graphKeys.add(resource);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
@ -116,7 +105,7 @@ const removeGraph = (json, state: GraphState): GraphState => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mapifyChildren = (children) => {
|
const mapifyChildren = (children) => {
|
||||||
return new BigIntOrderedMap(
|
return new BigIntOrderedMap().gas(
|
||||||
_.map(children, (node, idx) => {
|
_.map(children, (node, idx) => {
|
||||||
idx = idx && idx.startsWith('/') ? idx.slice(1) : idx;
|
idx = idx && idx.startsWith('/') ? idx.slice(1) : idx;
|
||||||
const nd = {...node, children: mapifyChildren(node.children || {}) };
|
const nd = {...node, children: mapifyChildren(node.children || {}) };
|
||||||
@ -128,8 +117,7 @@ const addNodes = (json, state) => {
|
|||||||
const _addNode = (graph, index, node) => {
|
const _addNode = (graph, index, node) => {
|
||||||
// set child of graph
|
// set child of graph
|
||||||
if (index.length === 1) {
|
if (index.length === 1) {
|
||||||
graph.set(index[0], node);
|
return graph.set(index[0], node);
|
||||||
return graph;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// set parent of graph
|
// set parent of graph
|
||||||
@ -138,19 +126,20 @@ const addNodes = (json, state) => {
|
|||||||
console.error('parent node does not exist, cannot add child');
|
console.error('parent node does not exist, cannot add child');
|
||||||
return graph;
|
return graph;
|
||||||
}
|
}
|
||||||
parNode.children = _addNode(parNode.children, index.slice(1), node);
|
return graph.set(index[0], produce(parNode, draft => {
|
||||||
graph.set(index[0], parNode);
|
draft.children = _addNode(draft.children, index.slice(1), node);
|
||||||
return graph;
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const _remove = (graph, index) => {
|
const _remove = (graph, index) => {
|
||||||
if (index.length === 1) {
|
if (index.length === 1) {
|
||||||
graph.delete(index[0]);
|
return graph.delete(index[0]);
|
||||||
} else {
|
} else {
|
||||||
const child = graph.get(index[0]);
|
const child = graph.get(index[0]);
|
||||||
if (child) {
|
if (child) {
|
||||||
child.children = _remove(child.children, index.slice(1));
|
return graph.set(index[0], produce(child, draft => {
|
||||||
graph.set(index[0], child);
|
draft.children = _remove(draft.children, index.slice(1));
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,10 +155,9 @@ const addNodes = (json, state) => {
|
|||||||
return bigInt(ind);
|
return bigInt(ind);
|
||||||
});
|
});
|
||||||
|
|
||||||
graph = _remove(graph, indexArr);
|
|
||||||
delete state.graphTimesentMap[resource][timestamp];
|
delete state.graphTimesentMap[resource][timestamp];
|
||||||
|
return _remove(graph, indexArr);
|
||||||
}
|
}
|
||||||
|
|
||||||
return graph;
|
return graph;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -208,11 +196,12 @@ const addNodes = (json, state) => {
|
|||||||
return aArr.length - bArr.length;
|
return aArr.length - bArr.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
let graph = state.graphs[resource];
|
|
||||||
|
|
||||||
indices.forEach((index) => {
|
indices.forEach((index) => {
|
||||||
let node = data.nodes[index];
|
let node = data.nodes[index];
|
||||||
graph = _removePending(graph, node.post, resource);
|
const old = state.graphs[resource].size;
|
||||||
|
state.graphs[resource] = _removePending(state.graphs[resource], node.post, resource);
|
||||||
|
const newSize = state.graphs[resource].size;
|
||||||
|
|
||||||
|
|
||||||
if (index.split('/').length === 0) { return; }
|
if (index.split('/').length === 0) { return; }
|
||||||
let indexArr = index.split('/').slice(1).map((ind) => {
|
let indexArr = index.split('/').slice(1).map((ind) => {
|
||||||
@ -225,17 +214,21 @@ const addNodes = (json, state) => {
|
|||||||
state.graphTimesentMap[resource][node.post['time-sent']] = index;
|
state.graphTimesentMap[resource][node.post['time-sent']] = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
node.children = mapifyChildren(node?.children || {});
|
|
||||||
|
|
||||||
graph = _addNode(
|
state.graphs[resource] = _addNode(
|
||||||
graph,
|
state.graphs[resource],
|
||||||
indexArr,
|
indexArr,
|
||||||
node
|
produce(node, draft => {
|
||||||
|
draft.children = mapifyChildren(draft?.children || {});
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
if(newSize !== old) {
|
||||||
|
console.log(`${resource}, (${old}, ${newSize}, ${state.graphs[resource].size})`);
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
state.graphs[resource] = graph;
|
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
@ -243,13 +236,15 @@ const addNodes = (json, state) => {
|
|||||||
const removeNodes = (json, state: GraphState): GraphState => {
|
const removeNodes = (json, state: GraphState): GraphState => {
|
||||||
const _remove = (graph, index) => {
|
const _remove = (graph, index) => {
|
||||||
if (index.length === 1) {
|
if (index.length === 1) {
|
||||||
graph.delete(index[0]);
|
return graph.delete(index[0]);
|
||||||
} else {
|
} else {
|
||||||
const child = graph.get(index[0]);
|
const child = graph.get(index[0]);
|
||||||
if (child) {
|
if (child) {
|
||||||
_remove(child.children, index.slice(1));
|
return graph.set(index[0], produce(draft => {
|
||||||
graph.set(index[0], child);
|
draft.children = _remove(draft.children, index.slice(1))
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
return graph;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -264,7 +259,7 @@ const removeNodes = (json, state: GraphState): GraphState => {
|
|||||||
let indexArr = index.split('/').slice(1).map((ind) => {
|
let indexArr = index.split('/').slice(1).map((ind) => {
|
||||||
return bigInt(ind);
|
return bigInt(ind);
|
||||||
});
|
});
|
||||||
_remove(state.graphs[res], indexArr);
|
state.graphs[res] = _remove(state.graphs[res], indexArr);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
|
@ -5,16 +5,14 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
Tags,
|
Tags,
|
||||||
GroupPolicy,
|
GroupPolicy,
|
||||||
GroupPolicyDiff,
|
|
||||||
OpenPolicyDiff,
|
OpenPolicyDiff,
|
||||||
OpenPolicy,
|
OpenPolicy,
|
||||||
InvitePolicyDiff,
|
InvitePolicyDiff,
|
||||||
InvitePolicy
|
InvitePolicy
|
||||||
} from '@urbit/api/groups';
|
} from '@urbit/api/groups';
|
||||||
import { Enc, PatpNoSig } from '@urbit/api';
|
import { Enc } from '@urbit/api';
|
||||||
import { resourceAsPath } from '../lib/util';
|
import { resourceAsPath } from '../lib/util';
|
||||||
import useGroupState, { GroupState } from '../state/group';
|
import useGroupState, { GroupState } from '../state/group';
|
||||||
import { compose } from 'lodash/fp';
|
|
||||||
import { reduceState } from '../state/base';
|
import { reduceState } from '../state/base';
|
||||||
|
|
||||||
function decodeGroup(group: Enc<Group>): Group {
|
function decodeGroup(group: Enc<Group>): Group {
|
||||||
@ -125,9 +123,9 @@ const addMembers = (json: GroupUpdate, state: GroupState): GroupState => {
|
|||||||
state.groups[resourcePath].members.add(member);
|
state.groups[resourcePath].members.add(member);
|
||||||
if (
|
if (
|
||||||
'invite' in state.groups[resourcePath].policy &&
|
'invite' in state.groups[resourcePath].policy &&
|
||||||
state.groups[resourcePath].policy.invite.pending.has(member)
|
state.groups[resourcePath].policy['invite'].pending.has(member)
|
||||||
) {
|
) {
|
||||||
state.groups[resourcePath].policy.invite.pending.delete(member)
|
state.groups[resourcePath].policy['invite'].pending.delete(member);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,7 +157,7 @@ const addTag = (json: GroupUpdate, state: GroupState): GroupState => {
|
|||||||
_.set(tags, tagAccessors, tagged);
|
_.set(tags, tagAccessors, tagged);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
};
|
||||||
|
|
||||||
const removeTag = (json: GroupUpdate, state: GroupState): GroupState => {
|
const removeTag = (json: GroupUpdate, state: GroupState): GroupState => {
|
||||||
if ('removeTag' in json) {
|
if ('removeTag' in json) {
|
||||||
|
@ -1,18 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
Notifications,
|
|
||||||
NotifIndex,
|
NotifIndex,
|
||||||
NotificationGraphConfig,
|
|
||||||
GroupNotificationsConfig,
|
|
||||||
UnreadStats,
|
|
||||||
Timebox
|
Timebox
|
||||||
} from '@urbit/api';
|
} from '@urbit/api';
|
||||||
import { makePatDa } from '~/logic/lib/util';
|
import { makePatDa } from '~/logic/lib/util';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
|
||||||
import useHarkState, { HarkState } from '../state/hark';
|
import useHarkState, { HarkState } from '../state/hark';
|
||||||
import { compose } from 'lodash/fp';
|
import { compose } from 'lodash/fp';
|
||||||
import { reduceState } from '../state/base';
|
import { reduceState } from '../state/base';
|
||||||
import bigInt, {BigInteger} from 'big-integer';
|
import {BigInteger} from 'big-integer';
|
||||||
|
|
||||||
export const HarkReducer = (json: any) => {
|
export const HarkReducer = (json: any) => {
|
||||||
const data = _.get(json, 'harkUpdate', false);
|
const data = _.get(json, 'harkUpdate', false);
|
||||||
@ -151,7 +147,7 @@ function graphWatchSelf(json: any, state: HarkState): HarkState {
|
|||||||
|
|
||||||
function readAll(json: any, state: HarkState): HarkState {
|
function readAll(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'read-all');
|
const data = _.get(json, 'read-all');
|
||||||
if(data) {
|
if(data) {
|
||||||
clearState(state);
|
clearState(state);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
@ -264,7 +260,7 @@ function updateUnreads(state: HarkState, index: NotifIndex, f: (us: Set<string>)
|
|||||||
if(!('graph' in index)) {
|
if(!('graph' in index)) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
let unreads = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set<string>());
|
let unreads: any = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set<string>());
|
||||||
f(unreads);
|
f(unreads);
|
||||||
|
|
||||||
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], unreads);
|
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], unreads);
|
||||||
@ -278,7 +274,7 @@ function addNotificationToUnread(state: HarkState, index: NotifIndex, time: BigI
|
|||||||
_.set(state.unreads.graph, path,
|
_.set(state.unreads.graph, path,
|
||||||
[
|
[
|
||||||
...curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))),
|
...curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))),
|
||||||
{ time, index}
|
{ time, index }
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} else if ('group' in index) {
|
} else if ('group' in index) {
|
||||||
@ -287,7 +283,7 @@ function addNotificationToUnread(state: HarkState, index: NotifIndex, time: BigI
|
|||||||
_.set(state.unreads.group, path,
|
_.set(state.unreads.group, path,
|
||||||
[
|
[
|
||||||
...curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))),
|
...curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))),
|
||||||
{ time, index}
|
{ time, index }
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -312,10 +308,10 @@ function removeNotificationFromUnread(state: HarkState, index: NotifIndex, time:
|
|||||||
function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'unreads' | 'last', f: (x: number) => number) {
|
function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'unreads' | 'last', f: (x: number) => number) {
|
||||||
|
|
||||||
if('graph' in index) {
|
if('graph' in index) {
|
||||||
const curr = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, statField], 0);
|
const curr: any = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, statField], 0);
|
||||||
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, statField], f(curr));
|
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, statField], f(curr));
|
||||||
} else if('group' in index) {
|
} else if('group' in index) {
|
||||||
const curr = _.get(state.unreads.group, [index.group.group, statField], 0);
|
const curr: any = _.get(state.unreads.group, [index.group.group, statField], 0);
|
||||||
_.set(state.unreads.group, [index.group.group, statField], f(curr));
|
_.set(state.unreads.group, [index.group.group, statField], f(curr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -333,9 +329,9 @@ function added(json: any, state: HarkState): HarkState {
|
|||||||
);
|
);
|
||||||
if (arrIdx !== -1) {
|
if (arrIdx !== -1) {
|
||||||
timebox[arrIdx] = { index, notification };
|
timebox[arrIdx] = { index, notification };
|
||||||
state.notifications.set(time, timebox);
|
state.notifications = state.notifications.set(time, timebox);
|
||||||
} else {
|
} else {
|
||||||
state.notifications.set(time, [...timebox, { index, notification }]);
|
state.notifications = state.notifications.set(time, [...timebox, { index, notification }]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
@ -354,7 +350,7 @@ const timebox = (json: any, state: HarkState): HarkState => {
|
|||||||
if (data) {
|
if (data) {
|
||||||
const time = makePatDa(data.time);
|
const time = makePatDa(data.time);
|
||||||
if (!data.archive) {
|
if (!data.archive) {
|
||||||
state.notifications.set(time, data.notifications);
|
state.notifications = state.notifications.set(time, data.notifications);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
@ -407,7 +403,7 @@ function setRead(
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
timebox[arrIdx].notification.read = read;
|
timebox[arrIdx].notification.read = read;
|
||||||
state.notifications.set(patDa, timebox);
|
state.notifications = state.notifications.set(patDa, timebox);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ export default class LaunchReducer {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const weatherData: WeatherState = _.get(json, 'weather', false);
|
const weatherData: WeatherState | boolean | Record<string, never> = _.get(json, 'weather', false);
|
||||||
if (weatherData) {
|
if (weatherData) {
|
||||||
useLaunchState.getState().set(state => {
|
useLaunchState.getState().set(state => {
|
||||||
state.weather = weatherData;
|
state.weather = weatherData;
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import useSettingsState, { SettingsState } from "~/logic/state/settings";
|
import useSettingsState, { SettingsState } from '~/logic/state/settings';
|
||||||
import { SettingsUpdate } from '@urbit/api/dist/settings';
|
import { SettingsUpdate } from '@urbit/api/settings';
|
||||||
import { reduceState } from '../state/base';
|
import { reduceState } from '../state/base';
|
||||||
|
import { string } from 'prop-types';
|
||||||
|
|
||||||
export default class SettingsReducer {
|
export default class SettingsReducer {
|
||||||
reduce(json: any) {
|
reduce(json: any) {
|
||||||
@ -40,21 +41,21 @@ export default class SettingsReducer {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
putEntry(json: SettingsUpdate, state: SettingsState): SettingsState {
|
putEntry(json: SettingsUpdate, state: any): SettingsState {
|
||||||
const data = _.get(json, 'put-entry', false);
|
const data: Record<string, string> = _.get(json, 'put-entry', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
if (!state[data["bucket-key"]]) {
|
if (!state[data['bucket-key']]) {
|
||||||
state[data["bucket-key"]] = {};
|
state[data['bucket-key']] = {};
|
||||||
}
|
}
|
||||||
state[data["bucket-key"]][data["entry-key"]] = data.value;
|
state[data['bucket-key']][data['entry-key']] = data.value;
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
delEntry(json: SettingsUpdate, state: SettingsState): SettingsState {
|
delEntry(json: SettingsUpdate, state: any): SettingsState {
|
||||||
const data = _.get(json, 'del-entry', false);
|
const data = _.get(json, 'del-entry', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
delete state[data["bucket-key"]][data["entry-key"]];
|
delete state[data['bucket-key']][data['entry-key']];
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -76,7 +77,7 @@ export default class SettingsReducer {
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
getEntry(json: any, state: SettingsState) {
|
getEntry(json: any, state: any) {
|
||||||
const bucketKey = _.get(json, 'bucket-key', false);
|
const bucketKey = _.get(json, 'bucket-key', false);
|
||||||
const entryKey = _.get(json, 'entry-key', false);
|
const entryKey = _.get(json, 'entry-key', false);
|
||||||
const entry = _.get(json, 'entry', false);
|
const entry = _.get(json, 'entry', false);
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import produce from "immer";
|
import produce, { setAutoFreeze } from "immer";
|
||||||
import { compose } from "lodash/fp";
|
import { compose } from "lodash/fp";
|
||||||
import create, { State, UseStore } from "zustand";
|
import create, { State, UseStore } from "zustand";
|
||||||
import { persist, devtools } from "zustand/middleware";
|
import { persist, devtools } from "zustand/middleware";
|
||||||
|
|
||||||
|
setAutoFreeze(false);
|
||||||
|
|
||||||
|
|
||||||
export const stateSetter = <StateType>(
|
export const stateSetter = <StateType>(
|
||||||
fn: (state: StateType) => void,
|
fn: (state: StateType) => void,
|
||||||
|
@ -35,4 +35,8 @@ export function useContact(ship: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useOurContact() {
|
||||||
|
return useContact(`~${window.ship}`)
|
||||||
|
}
|
||||||
|
|
||||||
export default useContactState;
|
export default useContactState;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Graphs, decToUd, numToUd, GraphNode } from "@urbit/api";
|
import { Graphs, decToUd, numToUd, GraphNode, deSig, Association, resourceFromPath } from "@urbit/api";
|
||||||
|
import {useCallback} from "react";
|
||||||
|
|
||||||
import { BaseState, createState } from "./base";
|
import { BaseState, createState } from "./base";
|
||||||
|
|
||||||
@ -128,6 +129,20 @@ const useGraphState = createState<GraphState>('Graph', {
|
|||||||
// });
|
// });
|
||||||
// graphReducer(node);
|
// graphReducer(node);
|
||||||
// },
|
// },
|
||||||
}, ['graphs', 'graphKeys', 'looseNodes']);
|
}, ['graphs', 'graphKeys', 'looseNodes', 'graphTimesentMap']);
|
||||||
|
|
||||||
|
export function useGraph(ship: string, name: string) {
|
||||||
|
return useGraphState(
|
||||||
|
useCallback(s => s.graphs[`${deSig(ship)}/${name}`], [ship, name])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGraphForAssoc(association: Association) {
|
||||||
|
const { resource } = association;
|
||||||
|
const { ship, name } = resourceFromPath(resource);
|
||||||
|
return useGraph(ship, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.useGraphState = useGraphState;
|
||||||
|
|
||||||
export default useGraphState;
|
export default useGraphState;
|
||||||
|
@ -9,7 +9,7 @@ export interface LaunchState extends BaseState<LaunchState> {
|
|||||||
tiles: {
|
tiles: {
|
||||||
[app: string]: Tile;
|
[app: string]: Tile;
|
||||||
},
|
},
|
||||||
weather: WeatherState | null,
|
weather: WeatherState | null | Record<string, never> | boolean,
|
||||||
userLocation: string | null;
|
userLocation: string | null;
|
||||||
baseHash: string | null;
|
baseHash: string | null;
|
||||||
};
|
};
|
||||||
|
@ -90,8 +90,8 @@ const useLocalState = create<LocalStateZus>(persist((set, get) => ({
|
|||||||
name: 'localReducer'
|
name: 'localReducer'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function withLocalState<P, S extends keyof LocalState>(Component: any, stateMemberKeys?: S[]) {
|
function withLocalState<P, S extends keyof LocalState, C extends React.ComponentType<P>>(Component: C, stateMemberKeys?: S[]) {
|
||||||
return React.forwardRef((props: Omit<P, S>, ref) => {
|
return React.forwardRef<C, Omit<P, S>>((props, ref) => {
|
||||||
const localState = stateMemberKeys ? useLocalState(
|
const localState = stateMemberKeys ? useLocalState(
|
||||||
state => stateMemberKeys.reduce(
|
state => stateMemberKeys.reduce(
|
||||||
(object, key) => ({ ...object, [key]: state[key] }), {}
|
(object, key) => ({ ...object, [key]: state[key] }), {}
|
||||||
|
@ -3,10 +3,8 @@ import _ from 'lodash';
|
|||||||
import BaseStore from './base';
|
import BaseStore from './base';
|
||||||
import InviteReducer from '../reducers/invite-update';
|
import InviteReducer from '../reducers/invite-update';
|
||||||
import MetadataReducer from '../reducers/metadata-update';
|
import MetadataReducer from '../reducers/metadata-update';
|
||||||
import LocalReducer from '../reducers/local';
|
|
||||||
|
|
||||||
import { StoreState } from './type';
|
import { StoreState } from './type';
|
||||||
import { Timebox } from '@urbit/api';
|
|
||||||
import { Cage } from '~/types/cage';
|
import { Cage } from '~/types/cage';
|
||||||
import S3Reducer from '../reducers/s3-update';
|
import S3Reducer from '../reducers/s3-update';
|
||||||
import { GraphReducer } from '../reducers/graph-update';
|
import { GraphReducer } from '../reducers/graph-update';
|
||||||
@ -17,8 +15,6 @@ import LaunchReducer from '../reducers/launch-update';
|
|||||||
import ConnectionReducer from '../reducers/connection';
|
import ConnectionReducer from '../reducers/connection';
|
||||||
import SettingsReducer from '../reducers/settings-update';
|
import SettingsReducer from '../reducers/settings-update';
|
||||||
import GcpReducer from '../reducers/gcp-reducer';
|
import GcpReducer from '../reducers/gcp-reducer';
|
||||||
import { OrderedMap } from '../lib/OrderedMap';
|
|
||||||
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
|
||||||
import { GroupViewReducer } from '../reducers/group-view';
|
import { GroupViewReducer } from '../reducers/group-view';
|
||||||
import { unstable_batchedUpdates } from 'react-dom';
|
import { unstable_batchedUpdates } from 'react-dom';
|
||||||
|
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
import React, { useRef, useCallback, useEffect, useState } from 'react';
|
import React, {
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
} from 'react';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import { Col } from '@tlon/indigo-react';
|
import { Col } from '@tlon/indigo-react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import bigInt from 'big-integer';
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
|
|
||||||
import { Association } from '@urbit/api/metadata';
|
import { Association } from '@urbit/api/metadata';
|
||||||
import { StoreState } from '~/logic/store/type';
|
import { StoreState } from '~/logic/store/type';
|
||||||
@ -16,176 +22,148 @@ import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
|
|||||||
import { Loading } from '~/views/components/Loading';
|
import { Loading } from '~/views/components/Loading';
|
||||||
import { isWriter, resourceFromPath } from '~/logic/lib/group';
|
import { isWriter, resourceFromPath } from '~/logic/lib/group';
|
||||||
|
|
||||||
import './css/custom.css';
|
|
||||||
import useContactState from '~/logic/state/contact';
|
import useContactState from '~/logic/state/contact';
|
||||||
import useGraphState from '~/logic/state/graph';
|
import useGraphState, { useGraphForAssoc } from '~/logic/state/graph';
|
||||||
import useGroupState from '~/logic/state/group';
|
import useGroupState, { useGroupForAssoc } from '~/logic/state/group';
|
||||||
import useHarkState from '~/logic/state/hark';
|
import useHarkState from '~/logic/state/hark';
|
||||||
import {Post} from '@urbit/api';
|
import { Content, createPost, Post } from '@urbit/api';
|
||||||
import {getPermalinkForGraph} from '~/logic/lib/permalinks';
|
import { getPermalinkForGraph } from '~/logic/lib/permalinks';
|
||||||
|
import { ChatPane } from './components/ChatPane';
|
||||||
|
|
||||||
|
const getCurrGraphSize = (ship: string, name: string) => {
|
||||||
|
const { graphs } = useGraphState.getState();
|
||||||
|
const graph = graphs[`${ship}/${name}`];
|
||||||
|
return graph?.size ?? 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
type ChatResourceProps = StoreState & {
|
type ChatResourceProps = StoreState & {
|
||||||
association: Association;
|
association: Association;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
} & RouteComponentProps;
|
};
|
||||||
|
|
||||||
export function ChatResource(props: ChatResourceProps) {
|
function ChatResource(props: ChatResourceProps) {
|
||||||
const station = props.association.resource;
|
const { association, api } = props;
|
||||||
const groupPath = props.association.group;
|
const { resource } = association;
|
||||||
const groups = useGroupState(state => state.groups);
|
const [toShare, setToShare] = useState<string[] | string | undefined>();
|
||||||
const group = groups[groupPath];
|
const group = useGroupForAssoc(association)!;
|
||||||
const contacts = useContactState(state => state.contacts);
|
const graph = useGraphForAssoc(association);
|
||||||
const graphs = useGraphState(state => state.graphs);
|
const unreads = useHarkState((state) => state.unreads);
|
||||||
const graphPath = station.slice(7);
|
const unreadCount =
|
||||||
const graph = graphs[graphPath];
|
(unreads.graph?.[resource]?.['/']?.unreads as number) || 0;
|
||||||
const unreads = useHarkState(state => state.unreads);
|
const canWrite = group ? isWriter(group, resource) : false;
|
||||||
const unreadCount = unreads.graph?.[station]?.['/']?.unreads || 0;
|
|
||||||
const graphTimesentMap = useGraphState(state => state.graphTimesentMap);
|
|
||||||
const [,, owner, name] = station.split('/');
|
|
||||||
const ourContact = contacts?.[`~${window.ship}`];
|
|
||||||
const chatInput = useRef<ChatInput>();
|
|
||||||
const canWrite = isWriter(group, station);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const count = 100 + unreadCount;
|
const count = Math.min(400, 100 + unreadCount);
|
||||||
props.api.graph.getNewest(owner, name, count);
|
const { ship, name } = resourceFromPath(resource);
|
||||||
}, [station]);
|
props.api.graph.getNewest(ship, name, count);
|
||||||
|
setToShare(undefined);
|
||||||
const onFileDrag = useCallback(
|
(async function() {
|
||||||
(files: FileList | File[]) => {
|
if(group.hidden) {
|
||||||
if (!chatInput.current) {
|
const members = await props.api.contacts.disallowedShipsForOurContact(
|
||||||
return;
|
|
||||||
}
|
|
||||||
chatInput.current?.uploadFiles(files);
|
|
||||||
},
|
|
||||||
[chatInput.current]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { bind, dragging } = useFileDrag(onFileDrag);
|
|
||||||
|
|
||||||
const [unsent, setUnsent] = useLocalStorageState<Record<string, string>>(
|
|
||||||
'chat-unsent',
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
const appendUnsent = useCallback(
|
|
||||||
(u: string) => setUnsent(s => ({ ...s, [station]: u })),
|
|
||||||
[station]
|
|
||||||
);
|
|
||||||
|
|
||||||
const clearUnsent = useCallback(
|
|
||||||
() => setUnsent(s => _.omit(s, station)),
|
|
||||||
[station]
|
|
||||||
);
|
|
||||||
|
|
||||||
const scrollTo = new URLSearchParams(location.search).get('msg');
|
|
||||||
|
|
||||||
const [showBanner, setShowBanner] = useState(false);
|
|
||||||
const [hasLoadedAllowed, setHasLoadedAllowed] = useState(false);
|
|
||||||
const [recipients, setRecipients] = useState([]);
|
|
||||||
|
|
||||||
const res = resourceFromPath(groupPath);
|
|
||||||
const onReply = useCallback((msg: Post) => {
|
|
||||||
const url = getPermalinkForGraph(
|
|
||||||
props.association.group,
|
|
||||||
props.association.resource,
|
|
||||||
msg.index
|
|
||||||
);
|
|
||||||
const message = `${url}\n~${msg.author} : `;
|
|
||||||
setUnsent(s => ({...s, [props.association.resource]: message }));
|
|
||||||
}, [props.association, group, setUnsent]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
if (!res) { return; }
|
|
||||||
if (!group) { return; }
|
|
||||||
if (group.hidden) {
|
|
||||||
const members = _.compact(await Promise.all(
|
|
||||||
Array.from(group.members)
|
Array.from(group.members)
|
||||||
.map(s => {
|
);
|
||||||
const ship = `~${s}`;
|
|
||||||
if(s === window.ship) {
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
return props.api.contacts.fetchIsAllowed(
|
|
||||||
`~${window.ship}`,
|
|
||||||
'personal',
|
|
||||||
ship,
|
|
||||||
true
|
|
||||||
).then(isAllowed => {
|
|
||||||
return isAllowed ? null : ship;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
));
|
|
||||||
|
|
||||||
if(members.length > 0) {
|
if(members.length > 0) {
|
||||||
setShowBanner(true);
|
setToShare(members);
|
||||||
setRecipients(members);
|
|
||||||
} else {
|
|
||||||
setShowBanner(false);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const groupShared = await props.api.contacts.fetchIsAllowed(
|
const { ship: groupHost } = resourceFromPath(association.group);
|
||||||
|
const shared = await props.api.contacts.fetchIsAllowed(
|
||||||
`~${window.ship}`,
|
`~${window.ship}`,
|
||||||
'personal',
|
'personal',
|
||||||
res.ship,
|
groupHost,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
setShowBanner(!groupShared);
|
if(!shared) {
|
||||||
|
setToShare(association.group);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setHasLoadedAllowed(true);
|
|
||||||
})();
|
})();
|
||||||
}, [groupPath, group]);
|
}, [resource]);
|
||||||
|
|
||||||
if(!graph) {
|
const onReply = useCallback(
|
||||||
|
(msg: Post) => {
|
||||||
|
const url = getPermalinkForGraph(
|
||||||
|
props.association.group,
|
||||||
|
props.association.resource,
|
||||||
|
msg.index
|
||||||
|
);
|
||||||
|
return `${url}\n~${msg.author} : `;
|
||||||
|
},
|
||||||
|
[association]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isAdmin = useMemo(
|
||||||
|
() => (group ? group.tags.role.admin.has(`~${window.ship}`) : false),
|
||||||
|
[group]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchMessages = useCallback(async (newer: boolean) => {
|
||||||
|
const { api } = props;
|
||||||
|
const pageSize = 100;
|
||||||
|
|
||||||
|
const [, , ship, name] = resource.split('/');
|
||||||
|
const graphSize = graph?.size ?? 0;
|
||||||
|
const expectedSize = graphSize + pageSize;
|
||||||
|
if (newer) {
|
||||||
|
const index = graph.peekLargest()?.[0];
|
||||||
|
if(!index) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await api.graph.getYoungerSiblings(
|
||||||
|
ship,
|
||||||
|
name,
|
||||||
|
pageSize,
|
||||||
|
`/${index.toString()}`
|
||||||
|
);
|
||||||
|
return expectedSize !== getCurrGraphSize(ship.slice(1), name);
|
||||||
|
} else {
|
||||||
|
const index = graph.peekSmallest()?.[0];
|
||||||
|
if(!index) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
await api.graph.getOlderSiblings(ship, name, pageSize, `/${index.toString()}`);
|
||||||
|
const done = expectedSize !== getCurrGraphSize(ship.slice(1), name);
|
||||||
|
return done;
|
||||||
|
}
|
||||||
|
}, [graph, resource]);
|
||||||
|
|
||||||
|
const onSubmit = useCallback((contents: Content[]) => {
|
||||||
|
const { ship, name } = resourceFromPath(resource);
|
||||||
|
api.graph.addPost(ship, name, createPost(window.ship, contents))
|
||||||
|
}, [resource]);
|
||||||
|
|
||||||
|
const dismissUnread = useCallback(() => {
|
||||||
|
api.hark.markCountAsRead(association, '/', 'message');
|
||||||
|
}, [association]);
|
||||||
|
|
||||||
|
const getPermalink = useCallback(
|
||||||
|
(index: BigInteger) =>
|
||||||
|
getPermalinkForGraph(association.group, resource, `/${index.toString()}`),
|
||||||
|
[association]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!graph) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col {...bind} height="100%" overflow="hidden" position="relative">
|
<ChatPane
|
||||||
<ShareProfile
|
id={resource.slice(7)}
|
||||||
our={ourContact}
|
graph={graph}
|
||||||
api={props.api}
|
unreadCount={unreadCount}
|
||||||
recipient={owner}
|
api={api}
|
||||||
recipients={recipients}
|
canWrite={canWrite}
|
||||||
showBanner={showBanner}
|
onReply={onReply}
|
||||||
setShowBanner={setShowBanner}
|
fetchMessages={fetchMessages}
|
||||||
group={group}
|
dismissUnread={dismissUnread}
|
||||||
groupPath={groupPath}
|
getPermalink={getPermalink}
|
||||||
/>
|
isAdmin={isAdmin}
|
||||||
{dragging && <SubmitDragger />}
|
onSubmit={onSubmit}
|
||||||
<ChatWindow
|
promptShare={toShare}
|
||||||
key={station}
|
/>
|
||||||
history={props.history}
|
|
||||||
graph={graph}
|
|
||||||
graphSize={graph.size}
|
|
||||||
unreadCount={unreadCount}
|
|
||||||
showOurContact={ !showBanner && hasLoadedAllowed }
|
|
||||||
association={props.association}
|
|
||||||
pendingSize={Object.keys(graphTimesentMap[graphPath] || {}).length}
|
|
||||||
group={group}
|
|
||||||
ship={owner}
|
|
||||||
onReply={onReply}
|
|
||||||
station={station}
|
|
||||||
api={props.api}
|
|
||||||
scrollTo={scrollTo ? bigInt(scrollTo) : undefined}
|
|
||||||
/>
|
|
||||||
{ canWrite && (
|
|
||||||
<ChatInput
|
|
||||||
ref={chatInput}
|
|
||||||
api={props.api}
|
|
||||||
station={station}
|
|
||||||
ourContact={
|
|
||||||
(!showBanner && hasLoadedAllowed) ? ourContact : null
|
|
||||||
}
|
|
||||||
envelopes={[]}
|
|
||||||
onUnmount={appendUnsent}
|
|
||||||
placeholder="Message..."
|
|
||||||
message={unsent[station] || ''}
|
|
||||||
deleteMessage={clearUnsent}
|
|
||||||
/> )}
|
|
||||||
</Col>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { ChatResource };
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component, ReactNode } from 'react';
|
||||||
import ChatEditor from './chat-editor';
|
import ChatEditor from './chat-editor';
|
||||||
import { IuseStorage } from '~/logic/lib/useStorage';
|
import { IuseStorage } from '~/logic/lib/useStorage';
|
||||||
import { uxToHex } from '~/logic/lib/util';
|
import { uxToHex } from '~/logic/lib/util';
|
||||||
@ -8,28 +8,29 @@ import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage';
|
|||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { Envelope } from '~/types/chat-update';
|
import { Envelope } from '~/types/chat-update';
|
||||||
import { StorageState } from '~/types';
|
import { StorageState } from '~/types';
|
||||||
import { Contacts, Content } from '@urbit/api';
|
import { Contact, Contacts, Content, Post } from '@urbit/api';
|
||||||
import { Row, BaseImage, Box, Icon, LoadingSpinner } from '@tlon/indigo-react';
|
import { Row, BaseImage, Box, Icon, LoadingSpinner } from '@tlon/indigo-react';
|
||||||
import withStorage from '~/views/components/withStorage';
|
import withStorage from '~/views/components/withStorage';
|
||||||
import { withLocalState } from '~/logic/state/local';
|
import { withLocalState } from '~/logic/state/local';
|
||||||
|
import { MOBILE_BROWSER_REGEX } from "~/logic/lib/util";
|
||||||
|
|
||||||
type ChatInputProps = IuseStorage & {
|
type ChatInputProps = IuseStorage & {
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
numMsgs: number;
|
ourContact?: Contact;
|
||||||
station: unknown;
|
|
||||||
ourContact: unknown;
|
|
||||||
envelopes: Envelope[];
|
|
||||||
onUnmount(msg: string): void;
|
onUnmount(msg: string): void;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
message: string;
|
message: string;
|
||||||
deleteMessage(): void;
|
deleteMessage(): void;
|
||||||
hideAvatars: boolean;
|
hideAvatars: boolean;
|
||||||
|
onSubmit: (contents: Content[]) => void;
|
||||||
|
children?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ChatInputState {
|
interface ChatInputState {
|
||||||
inCodeMode: boolean;
|
inCodeMode: boolean;
|
||||||
submitFocus: boolean;
|
submitFocus: boolean;
|
||||||
uploadingPaste: boolean;
|
uploadingPaste: boolean;
|
||||||
|
currentInput: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
||||||
@ -41,7 +42,8 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
this.state = {
|
this.state = {
|
||||||
inCodeMode: false,
|
inCodeMode: false,
|
||||||
submitFocus: false,
|
submitFocus: false,
|
||||||
uploadingPaste: false
|
uploadingPaste: false,
|
||||||
|
currentInput: props.message
|
||||||
};
|
};
|
||||||
|
|
||||||
this.chatEditor = React.createRef();
|
this.chatEditor = React.createRef();
|
||||||
@ -50,6 +52,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
this.toggleCode = this.toggleCode.bind(this);
|
this.toggleCode = this.toggleCode.bind(this);
|
||||||
this.uploadSuccess = this.uploadSuccess.bind(this);
|
this.uploadSuccess = this.uploadSuccess.bind(this);
|
||||||
this.uploadError = this.uploadError.bind(this);
|
this.uploadError = this.uploadError.bind(this);
|
||||||
|
this.eventHandler = this.eventHandler.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleCode() {
|
toggleCode() {
|
||||||
@ -58,39 +61,28 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
submit(text) {
|
async submit(text) {
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
const [, , ship, name] = props.station.split('/');
|
const { onSubmit, api } = this.props;
|
||||||
if (state.inCodeMode) {
|
this.setState({
|
||||||
this.setState(
|
inCodeMode: false
|
||||||
{
|
});
|
||||||
inCodeMode: false
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
const output = await props.api.graph.eval(text);
|
|
||||||
const contents: Content[] = [{ code: { output, expression: text } }];
|
|
||||||
const post = createPost(contents);
|
|
||||||
props.api.graph.addPost(ship, name, post);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const post = createPost(tokenizeMessage(text));
|
|
||||||
|
|
||||||
props.deleteMessage();
|
props.deleteMessage();
|
||||||
|
if(state.inCodeMode) {
|
||||||
props.api.graph.addPost(ship, name, post);
|
const output = await api.graph.eval(text) as string[];
|
||||||
|
onSubmit([{ code: { output, expression: text } }]);
|
||||||
|
} else {
|
||||||
|
onSubmit(tokenizeMessage(text));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadSuccess(url) {
|
uploadSuccess(url: string) {
|
||||||
const { props } = this;
|
const { props } = this;
|
||||||
if (this.state.uploadingPaste) {
|
if (this.state.uploadingPaste) {
|
||||||
this.chatEditor.current.editor.setValue(url);
|
this.chatEditor.current.editor.setValue(url);
|
||||||
this.setState({ uploadingPaste: false });
|
this.setState({ uploadingPaste: false });
|
||||||
} else {
|
} else {
|
||||||
const [, , ship, name] = props.station.split('/');
|
props.onSubmit([{ url }])
|
||||||
props.api.graph.addPost(ship, name, createPost([{ url }]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,6 +112,10 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventHandler(value) {
|
||||||
|
this.setState({ currentInput: value });
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { props, state } = this;
|
const { props, state } = this;
|
||||||
|
|
||||||
@ -130,6 +126,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
const avatar =
|
const avatar =
|
||||||
props.ourContact && props.ourContact?.avatar && !props.hideAvatars ? (
|
props.ourContact && props.ourContact?.avatar && !props.hideAvatars ? (
|
||||||
<BaseImage
|
<BaseImage
|
||||||
|
flexShrink={0}
|
||||||
src={props.ourContact.avatar}
|
src={props.ourContact.avatar}
|
||||||
height={24}
|
height={24}
|
||||||
width={24}
|
width={24}
|
||||||
@ -170,7 +167,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
className='cf'
|
className='cf'
|
||||||
zIndex={0}
|
zIndex={0}
|
||||||
>
|
>
|
||||||
<Row p='12px 4px 12px 12px' alignItems='center'>
|
<Row p='12px 4px 12px 12px' flexShrink={0} alignItems='center'>
|
||||||
{avatar}
|
{avatar}
|
||||||
</Row>
|
</Row>
|
||||||
<ChatEditor
|
<ChatEditor
|
||||||
@ -180,15 +177,25 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
onUnmount={props.onUnmount}
|
onUnmount={props.onUnmount}
|
||||||
message={props.message}
|
message={props.message}
|
||||||
onPaste={this.onPaste.bind(this)}
|
onPaste={this.onPaste.bind(this)}
|
||||||
|
changeEvent={this.eventHandler}
|
||||||
placeholder='Message...'
|
placeholder='Message...'
|
||||||
/>
|
/>
|
||||||
<Box mx={2} flexShrink={0} height='16px' width='16px' flexBasis='16px'>
|
<Box mx='12px' flexShrink={0} height='16px' width='16px' flexBasis='16px'>
|
||||||
|
<Icon
|
||||||
|
icon='Dojo'
|
||||||
|
cursor='pointer'
|
||||||
|
onClick={this.toggleCode}
|
||||||
|
color={state.inCodeMode ? 'blue' : 'black'}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box ml='12px' mr={3} flexShrink={0} height='16px' width='16px' flexBasis='16px'>
|
||||||
{this.props.canUpload ? (
|
{this.props.canUpload ? (
|
||||||
this.props.uploading ? (
|
this.props.uploading ? (
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
) : (
|
) : (
|
||||||
<Icon
|
<Icon
|
||||||
icon='Links'
|
icon='Attachment'
|
||||||
|
cursor='pointer'
|
||||||
width='16'
|
width='16'
|
||||||
height='16'
|
height='16'
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
@ -198,18 +205,30 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
<Box mr={2} flexShrink={0} height='16px' width='16px' flexBasis='16px'>
|
{MOBILE_BROWSER_REGEX.test(navigator.userAgent) ?
|
||||||
<Icon
|
<Box
|
||||||
icon='Dojo'
|
ml={2}
|
||||||
onClick={this.toggleCode}
|
mr="12px"
|
||||||
color={state.inCodeMode ? 'blue' : 'black'}
|
flexShrink={0}
|
||||||
/>
|
display="flex"
|
||||||
</Box>
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
width="24px"
|
||||||
|
height="24px"
|
||||||
|
borderRadius="50%"
|
||||||
|
backgroundColor={state.currentInput !== '' ? 'blue' : 'gray'}
|
||||||
|
cursor={state.currentInput !== '' ? 'pointer' : 'default'}
|
||||||
|
onClick={() => this.chatEditor.current.submit()}
|
||||||
|
>
|
||||||
|
<Icon icon="ArrowEast" color="white" />
|
||||||
|
</Box>
|
||||||
|
: null}
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withLocalState(withStorage(ChatInput, { accept: 'image/*' }), [
|
export default withLocalState<Omit<ChatInputProps, keyof IuseStorage>, 'hideAvatars', ChatInput>(
|
||||||
'hideAvatars'
|
withStorage<ChatInputProps, ChatInput>(ChatInput, { accept: 'image/*' }),
|
||||||
]);
|
['hideAvatars']
|
||||||
|
)
|
||||||
|
@ -3,6 +3,7 @@ import bigInt from 'big-integer';
|
|||||||
import React, {
|
import React, {
|
||||||
useState,
|
useState,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
Component,
|
Component,
|
||||||
PureComponent,
|
PureComponent,
|
||||||
@ -40,11 +41,13 @@ import styled from 'styled-components';
|
|||||||
import useLocalState from '~/logic/state/local';
|
import useLocalState from '~/logic/state/local';
|
||||||
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
|
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
|
||||||
import Timestamp from '~/views/components/Timestamp';
|
import Timestamp from '~/views/components/Timestamp';
|
||||||
import useContactState from '~/logic/state/contact';
|
import useContactState, {useContact} from '~/logic/state/contact';
|
||||||
import { useIdlingState } from '~/logic/lib/idling';
|
import { useIdlingState } from '~/logic/lib/idling';
|
||||||
import ProfileOverlay from '~/views/components/ProfileOverlay';
|
import ProfileOverlay from '~/views/components/ProfileOverlay';
|
||||||
import {useCopy} from '~/logic/lib/useCopy';
|
import {useCopy} from '~/logic/lib/useCopy';
|
||||||
import {GraphContentWide} from '~/views/landscape/components/Graph/GraphContentWide';
|
import {GraphContentWide} from '~/views/landscape/components/Graph/GraphContentWide';
|
||||||
|
import {Contact} from '@urbit/api';
|
||||||
|
import GlobalApi from '~/logic/api/global';
|
||||||
|
|
||||||
|
|
||||||
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
|
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
|
||||||
@ -67,7 +70,7 @@ export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => (
|
|||||||
<Rule borderColor='lightGray' />
|
<Rule borderColor='lightGray' />
|
||||||
<Text
|
<Text
|
||||||
gray
|
gray
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
whiteSpace='nowrap'
|
whiteSpace='nowrap'
|
||||||
textAlign='center'
|
textAlign='center'
|
||||||
fontSize={0}
|
fontSize={0}
|
||||||
@ -80,16 +83,13 @@ export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const UnreadMarker = React.forwardRef(
|
export const UnreadMarker = React.forwardRef(
|
||||||
({ dayBreak, when, api, association }, ref) => {
|
({ dismissUnread }: any, ref) => {
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const idling = useIdlingState();
|
const idling = useIdlingState();
|
||||||
const dismiss = useCallback(() => {
|
|
||||||
api.hark.markCountAsRead(association, '/', 'message');
|
|
||||||
}, [api, association]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible && !idling) {
|
if (visible && !idling) {
|
||||||
dismiss();
|
dismissUnread();
|
||||||
}
|
}
|
||||||
}, [visible, idling]);
|
}, [visible, idling]);
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ export const UnreadMarker = React.forwardRef(
|
|||||||
<Text
|
<Text
|
||||||
color='blue'
|
color='blue'
|
||||||
fontSize={0}
|
fontSize={0}
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
whiteSpace='nowrap'
|
whiteSpace='nowrap'
|
||||||
textAlign='center'
|
textAlign='center'
|
||||||
px={2}
|
px={2}
|
||||||
@ -141,10 +141,9 @@ const MessageActionItem = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MessageActions = ({ api, onReply, association, history, msg, group }) => {
|
const MessageActions = ({ api, onReply, association, msg, isAdmin, permalink }) => {
|
||||||
const isAdmin = () => group.tags.role.admin.has(window.ship);
|
|
||||||
const isOwn = () => msg.author === window.ship;
|
const isOwn = () => msg.author === window.ship;
|
||||||
const { doCopy, copyDisplay } = useCopy(`web+urbitgraph://group${association.group.slice(5)}/graph${association.resource.slice(5)}${msg.index}`, 'Copy Message Link');
|
const { doCopy, copyDisplay } = useCopy(permalink, 'Copy Message Link');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -170,7 +169,7 @@ const MessageActions = ({ api, onReply, association, history, msg, group }) => {
|
|||||||
width='auto'
|
width='auto'
|
||||||
alignY='top'
|
alignY='top'
|
||||||
alignX='right'
|
alignX='right'
|
||||||
flexShrink={'0'}
|
flexShrink={0}
|
||||||
offsetY={8}
|
offsetY={8}
|
||||||
offsetX={-24}
|
offsetX={-24}
|
||||||
options={
|
options={
|
||||||
@ -235,176 +234,161 @@ interface ChatMessageProps {
|
|||||||
previousMsg?: Post;
|
previousMsg?: Post;
|
||||||
nextMsg?: Post;
|
nextMsg?: Post;
|
||||||
isLastRead: boolean;
|
isLastRead: boolean;
|
||||||
group: Group;
|
permalink: string;
|
||||||
association: Association;
|
|
||||||
transcluded?: number;
|
transcluded?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
style?: unknown;
|
style?: unknown;
|
||||||
scrollWindow: HTMLDivElement;
|
|
||||||
isLastMessage?: boolean;
|
isLastMessage?: boolean;
|
||||||
unreadMarkerRef: React.RefObject<HTMLDivElement>;
|
dismissUnread: () => void;
|
||||||
history: unknown;
|
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
highlighted?: boolean;
|
highlighted?: boolean;
|
||||||
renderSigil?: boolean;
|
renderSigil?: boolean;
|
||||||
hideHover?: boolean;
|
hideHover?: boolean;
|
||||||
innerRef: (el: HTMLDivElement | null) => void;
|
innerRef: (el: HTMLDivElement | null) => void;
|
||||||
onReply?: (msg: Post) => void;
|
onReply?: (msg: Post) => void;
|
||||||
|
showOurContact: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatMessage extends Component<ChatMessageProps> {
|
function ChatMessage(props: ChatMessageProps) {
|
||||||
private divRef: React.RefObject<HTMLDivElement>;
|
let { highlighted } = props;
|
||||||
|
const {
|
||||||
|
msg,
|
||||||
|
previousMsg,
|
||||||
|
nextMsg,
|
||||||
|
isLastRead,
|
||||||
|
group,
|
||||||
|
association,
|
||||||
|
className = '',
|
||||||
|
isPending,
|
||||||
|
style,
|
||||||
|
isLastMessage,
|
||||||
|
api,
|
||||||
|
showOurContact,
|
||||||
|
fontSize,
|
||||||
|
hideHover,
|
||||||
|
dismissUnread,
|
||||||
|
permalink
|
||||||
|
} = props;
|
||||||
|
|
||||||
constructor(props) {
|
let onReply = props?.onReply ?? (() => {});
|
||||||
super(props);
|
const transcluded = props?.transcluded ?? 0;
|
||||||
this.divRef = React.createRef();
|
const renderSigil = props.renderSigil ?? (Boolean(nextMsg && msg.author !== nextMsg.author) ||
|
||||||
}
|
!nextMsg ||
|
||||||
|
msg.number === 1
|
||||||
|
);
|
||||||
|
|
||||||
componentDidMount() {}
|
const ourMention = msg?.contents?.some((e) => {
|
||||||
|
return e?.mention && e?.mention === window.ship;
|
||||||
|
});
|
||||||
|
|
||||||
render() {
|
if (!highlighted) {
|
||||||
const {
|
if (ourMention) {
|
||||||
msg,
|
highlighted = true;
|
||||||
previousMsg,
|
}
|
||||||
nextMsg,
|
|
||||||
isLastRead,
|
|
||||||
group,
|
|
||||||
association,
|
|
||||||
className = '',
|
|
||||||
isPending,
|
|
||||||
style,
|
|
||||||
scrollWindow,
|
|
||||||
isLastMessage,
|
|
||||||
unreadMarkerRef,
|
|
||||||
history,
|
|
||||||
api,
|
|
||||||
highlighted,
|
|
||||||
showOurContact,
|
|
||||||
fontSize,
|
|
||||||
hideHover
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
let onReply = this.props?.onReply ?? (() => {});
|
|
||||||
const transcluded = this.props?.transcluded ?? 0;
|
|
||||||
let { renderSigil } = this.props;
|
|
||||||
|
|
||||||
if (renderSigil === undefined) {
|
|
||||||
renderSigil = Boolean(
|
|
||||||
(nextMsg && msg.author !== nextMsg.author) ||
|
|
||||||
!nextMsg ||
|
|
||||||
msg.number === 1
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = daToUnix(bigInt(msg.index.split('/')[1]));
|
const date = useMemo(() => daToUnix(bigInt(msg.index.split('/')[1])), [msg.index]);
|
||||||
const nextDate = nextMsg ? (
|
const nextDate = useMemo(() => nextMsg ? (
|
||||||
daToUnix(bigInt(nextMsg.index.split('/')[1]))
|
daToUnix(bigInt(nextMsg.index.split('/')[1]))
|
||||||
) : null;
|
) : null,
|
||||||
|
[nextMsg]
|
||||||
|
);
|
||||||
|
|
||||||
const dayBreak =
|
const dayBreak = useMemo(() =>
|
||||||
nextMsg &&
|
nextDate &&
|
||||||
new Date(date).getDate() !==
|
new Date(date).getDate() !==
|
||||||
new Date(nextDate).getDate();
|
new Date(nextDate).getDate()
|
||||||
|
, [nextDate, date])
|
||||||
|
|
||||||
const containerClass = `${isPending ? 'o-40' : ''} ${className}`;
|
const containerClass = `${isPending ? 'o-40' : ''} ${className}`;
|
||||||
|
|
||||||
const timestamp = moment
|
const timestamp = useMemo(() => moment
|
||||||
.unix(date / 1000)
|
.unix(date / 1000)
|
||||||
.format(renderSigil ? 'h:mm A' : 'h:mm');
|
.format(renderSigil ? 'h:mm A' : 'h:mm'),
|
||||||
|
[date, renderSigil]
|
||||||
|
);
|
||||||
|
|
||||||
const messageProps = {
|
const messageProps = {
|
||||||
msg,
|
msg,
|
||||||
timestamp,
|
timestamp,
|
||||||
association,
|
association,
|
||||||
group,
|
isPending,
|
||||||
style,
|
showOurContact,
|
||||||
containerClass,
|
api,
|
||||||
isPending,
|
highlighted,
|
||||||
showOurContact,
|
fontSize,
|
||||||
history,
|
hideHover,
|
||||||
api,
|
transcluded,
|
||||||
scrollWindow,
|
onReply
|
||||||
highlighted,
|
};
|
||||||
fontSize,
|
|
||||||
hideHover,
|
|
||||||
transcluded,
|
|
||||||
onReply
|
|
||||||
};
|
|
||||||
|
|
||||||
const unreadContainerStyle = {
|
const message = useMemo(() => (
|
||||||
height: isLastRead ? '2rem' : '0'
|
<Message
|
||||||
};
|
msg={msg}
|
||||||
|
timestamp={timestamp}
|
||||||
|
timestampHover={!renderSigil}
|
||||||
|
api={api}
|
||||||
|
transcluded={transcluded}
|
||||||
|
showOurContact={showOurContact}
|
||||||
|
/>
|
||||||
|
), [renderSigil, msg, timestamp, api, transcluded, showOurContact]);
|
||||||
|
|
||||||
return (
|
const unreadContainerStyle = {
|
||||||
<Box
|
height: isLastRead ? '2rem' : '0'
|
||||||
ref={this.props.innerRef}
|
};
|
||||||
pt={renderSigil ? 2 : 0}
|
|
||||||
width="100%"
|
return (
|
||||||
pb={isLastMessage ? '20px' : 0}
|
<Box
|
||||||
className={containerClass}
|
ref={props.innerRef}
|
||||||
style={style}
|
pt={renderSigil ? 2 : 0}
|
||||||
>
|
width="100%"
|
||||||
{dayBreak && !isLastRead ? (
|
pb={isLastMessage ? '20px' : 0}
|
||||||
<DayBreak when={date} shimTop={renderSigil} />
|
className={containerClass}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{dayBreak && !isLastRead ? (
|
||||||
|
<DayBreak when={date} shimTop={renderSigil} />
|
||||||
|
) : null}
|
||||||
|
<MessageWrapper permalink={permalink} {...messageProps}>
|
||||||
|
{ renderSigil && <MessageAuthor {...messageProps} />}
|
||||||
|
{message}
|
||||||
|
</MessageWrapper>
|
||||||
|
<Box style={unreadContainerStyle}>
|
||||||
|
{isLastRead ? (
|
||||||
|
<UnreadMarker dismissUnread={dismissUnread} />
|
||||||
) : null}
|
) : null}
|
||||||
{renderSigil ? (
|
|
||||||
<MessageWrapper {...messageProps}>
|
|
||||||
<MessageAuthor pb={1} {...messageProps} />
|
|
||||||
<Message pl={'44px'} pr={4} {...messageProps} />
|
|
||||||
</MessageWrapper>
|
|
||||||
) : (
|
|
||||||
<MessageWrapper {...messageProps}>
|
|
||||||
<Message pl={'44px'} pr={4} timestampHover {...messageProps} />
|
|
||||||
</MessageWrapper>
|
|
||||||
)}
|
|
||||||
<Box style={unreadContainerStyle}>
|
|
||||||
{isLastRead ? (
|
|
||||||
<UnreadMarker
|
|
||||||
association={association}
|
|
||||||
api={api}
|
|
||||||
dayBreak={dayBreak}
|
|
||||||
when={date}
|
|
||||||
ref={unreadMarkerRef}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
</Box>
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default React.forwardRef((props, ref) => (
|
export default React.forwardRef((props: Omit<ChatMessageProps, 'innerRef'>, ref: any) => (
|
||||||
<ChatMessage {...props} innerRef={ref} />
|
<ChatMessage {...props} innerRef={ref} />
|
||||||
));
|
));
|
||||||
|
|
||||||
export const MessageAuthor = ({
|
export const MessageAuthor = ({
|
||||||
timestamp,
|
timestamp,
|
||||||
msg,
|
msg,
|
||||||
group,
|
|
||||||
api,
|
api,
|
||||||
history,
|
|
||||||
scrollWindow,
|
|
||||||
showOurContact,
|
showOurContact,
|
||||||
...rest
|
|
||||||
}) => {
|
}) => {
|
||||||
const osDark = useLocalState((state) => state.dark);
|
const osDark = useLocalState((state) => state.dark);
|
||||||
|
|
||||||
const theme = useSettingsState((s) => s.display.theme);
|
const theme = useSettingsState((s) => s.display.theme);
|
||||||
const dark = theme === 'dark' || (theme === 'auto' && osDark);
|
const dark = theme === 'dark' || (theme === 'auto' && osDark);
|
||||||
const contacts = useContactState((state) => state.contacts);
|
let contact: Contact | null = useContact(`~${msg.author}`);
|
||||||
|
|
||||||
const date = daToUnix(bigInt(msg.index.split('/')[1]));
|
const date = daToUnix(bigInt(msg.index.split('/')[1]));
|
||||||
|
|
||||||
const datestamp = moment
|
const datestamp = moment
|
||||||
.unix(date / 1000)
|
.unix(date / 1000)
|
||||||
.format(DATESTAMP_FORMAT);
|
.format(DATESTAMP_FORMAT);
|
||||||
const contact =
|
contact =
|
||||||
((msg.author === window.ship && showOurContact) ||
|
((msg.author === window.ship && showOurContact) ||
|
||||||
msg.author !== window.ship) &&
|
msg.author !== window.ship)
|
||||||
`~${msg.author}` in contacts
|
? contact
|
||||||
? contacts[`~${msg.author}`]
|
: null;
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const showNickname = useShowNickname(contact);
|
const showNickname = useShowNickname(contact);
|
||||||
const { hideAvatars } = useSettingsState(selectCalmState);
|
const { hideAvatars } = useSettingsState(selectCalmState);
|
||||||
@ -457,7 +441,7 @@ export const MessageAuthor = ({
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Box display='flex' alignItems='flex-start' {...rest}>
|
<Box pb="1" display='flex' alignItems='flex-start'>
|
||||||
<Box
|
<Box
|
||||||
height={24}
|
height={24}
|
||||||
pr={2}
|
pr={2}
|
||||||
@ -509,20 +493,20 @@ export const MessageAuthor = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Message = ({
|
type MessageProps = { timestamp: string; timestampHover: boolean; }
|
||||||
|
& Pick<ChatMessageProps, "msg" | "api" | "transcluded" | "showOurContact">
|
||||||
|
|
||||||
|
export const Message = React.memo(({
|
||||||
timestamp,
|
timestamp,
|
||||||
msg,
|
msg,
|
||||||
group,
|
|
||||||
api,
|
api,
|
||||||
scrollWindow,
|
|
||||||
timestampHover,
|
timestampHover,
|
||||||
transcluded,
|
transcluded,
|
||||||
showOurContact,
|
showOurContact
|
||||||
...rest
|
}: MessageProps) => {
|
||||||
}) => {
|
|
||||||
const { hovering, bind } = useHovering();
|
const { hovering, bind } = useHovering();
|
||||||
return (
|
return (
|
||||||
<Box width="100%" position='relative' {...rest}>
|
<Box pl="44px" width="100%" position='relative'>
|
||||||
{timestampHover ? (
|
{timestampHover ? (
|
||||||
<Text
|
<Text
|
||||||
display={hovering ? 'block' : 'none'}
|
display={hovering ? 'block' : 'none'}
|
||||||
@ -549,7 +533,9 @@ export const Message = ({
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
Message.displayName = 'Message';
|
||||||
|
|
||||||
export const MessagePlaceholder = ({
|
export const MessagePlaceholder = ({
|
||||||
height,
|
height,
|
||||||
@ -578,7 +564,7 @@ export const MessagePlaceholder = ({
|
|||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
display='block'
|
display='block'
|
||||||
background='gray'
|
background='washedGray'
|
||||||
width='24px'
|
width='24px'
|
||||||
height='24px'
|
height='24px'
|
||||||
borderRadius='50%'
|
borderRadius='50%'
|
||||||
@ -601,12 +587,13 @@ export const MessagePlaceholder = ({
|
|||||||
display='inline-block'
|
display='inline-block'
|
||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
fontSize='0'
|
fontSize='0'
|
||||||
gray
|
washedGray
|
||||||
cursor='default'
|
cursor='default'
|
||||||
>
|
>
|
||||||
<Text maxWidth='32rem' display='block'>
|
<Text maxWidth='32rem' display='block'>
|
||||||
<Text
|
<Text
|
||||||
backgroundColor='gray'
|
backgroundColor='washedGray'
|
||||||
|
borderRadius='2'
|
||||||
display='block'
|
display='block'
|
||||||
width='100%'
|
width='100%'
|
||||||
height='100%'
|
height='100%'
|
||||||
@ -618,10 +605,11 @@ export const MessagePlaceholder = ({
|
|||||||
mono
|
mono
|
||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
fontSize='0'
|
fontSize='0'
|
||||||
gray
|
washedGray
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
background='gray'
|
background='washedGray'
|
||||||
|
borderRadius='2'
|
||||||
display='block'
|
display='block'
|
||||||
height='1em'
|
height='1em'
|
||||||
style={{ width: `${((index % 3) + 1) * 3}em` }}
|
style={{ width: `${((index % 3) + 1) * 3}em` }}
|
||||||
@ -632,12 +620,14 @@ export const MessagePlaceholder = ({
|
|||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
fontSize='0'
|
fontSize='0'
|
||||||
ml='2'
|
ml='2'
|
||||||
gray
|
washedGray
|
||||||
|
borderRadius='2'
|
||||||
display={['none', 'inline-block']}
|
display={['none', 'inline-block']}
|
||||||
className='child'
|
className='child'
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
backgroundColor='gray'
|
backgroundColor='washedGray'
|
||||||
|
borderRadius='2'
|
||||||
display='block'
|
display='block'
|
||||||
width='100%'
|
width='100%'
|
||||||
height='100%'
|
height='100%'
|
||||||
@ -646,7 +636,8 @@ export const MessagePlaceholder = ({
|
|||||||
</Box>
|
</Box>
|
||||||
<Text
|
<Text
|
||||||
display='block'
|
display='block'
|
||||||
backgroundColor='gray'
|
backgroundColor='washedGray'
|
||||||
|
borderRadius='2'
|
||||||
height='1em'
|
height='1em'
|
||||||
style={{ width: `${(index % 5) * 20}%` }}
|
style={{ width: `${(index % 5) * 20}%` }}
|
||||||
></Text>
|
></Text>
|
||||||
|
183
pkg/interface/src/views/apps/chat/components/ChatPane.tsx
Normal file
183
pkg/interface/src/views/apps/chat/components/ChatPane.tsx
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import React, { useRef, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
|
import { Col } from '@tlon/indigo-react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
|
|
||||||
|
import { Association } from '@urbit/api/metadata';
|
||||||
|
import { StoreState } from '~/logic/store/type';
|
||||||
|
import { useFileDrag } from '~/logic/lib/useDrag';
|
||||||
|
import ChatWindow from './ChatWindow';
|
||||||
|
import ChatInput from './ChatInput';
|
||||||
|
import GlobalApi from '~/logic/api/global';
|
||||||
|
import { ShareProfile } from '~/views/apps/chat/components/ShareProfile';
|
||||||
|
import SubmitDragger from '~/views/components/SubmitDragger';
|
||||||
|
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
|
||||||
|
import { Loading } from '~/views/components/Loading';
|
||||||
|
import { isWriter, resourceFromPath } from '~/logic/lib/group';
|
||||||
|
|
||||||
|
import useContactState, { useOurContact } from '~/logic/state/contact';
|
||||||
|
import useGraphState from '~/logic/state/graph';
|
||||||
|
import useGroupState from '~/logic/state/group';
|
||||||
|
import useHarkState from '~/logic/state/hark';
|
||||||
|
import { Post, Graph, Content } from '@urbit/api';
|
||||||
|
import { getPermalinkForGraph } from '~/logic/lib/permalinks';
|
||||||
|
|
||||||
|
interface ChatPaneProps {
|
||||||
|
/**
|
||||||
|
* A key to uniquely identify a ChatPane instance. Should be either the
|
||||||
|
* resource for group chats or the @p for DMs
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* The graph of the chat to render
|
||||||
|
*/
|
||||||
|
graph: Graph;
|
||||||
|
unreadCount: number;
|
||||||
|
/**
|
||||||
|
* User able to write to chat
|
||||||
|
*/
|
||||||
|
canWrite: boolean;
|
||||||
|
api: GlobalApi;
|
||||||
|
/**
|
||||||
|
* Get contents of reply message
|
||||||
|
*/
|
||||||
|
onReply: (msg: Post) => string;
|
||||||
|
/**
|
||||||
|
* Fetch more messages
|
||||||
|
*
|
||||||
|
* @param newer Get newer or older backlog
|
||||||
|
* @returns Whether backlog is finished loading in that direction
|
||||||
|
*/
|
||||||
|
fetchMessages: (newer: boolean) => Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* Dismiss unreads for chat
|
||||||
|
*/
|
||||||
|
dismissUnread: () => void;
|
||||||
|
/**
|
||||||
|
* Get permalink for a node
|
||||||
|
*/
|
||||||
|
getPermalink: (idx: BigInteger) => string;
|
||||||
|
isAdmin: boolean;
|
||||||
|
/**
|
||||||
|
* Post message with contents to channel
|
||||||
|
*/
|
||||||
|
onSubmit: (contents: Content[]) => void;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Users or group we haven't shared our contact with yet
|
||||||
|
*
|
||||||
|
* string[] - array of ships
|
||||||
|
* string - path of group
|
||||||
|
*/
|
||||||
|
promptShare?: string[] | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatPane(props: ChatPaneProps) {
|
||||||
|
const {
|
||||||
|
api,
|
||||||
|
graph,
|
||||||
|
unreadCount,
|
||||||
|
canWrite,
|
||||||
|
id,
|
||||||
|
getPermalink,
|
||||||
|
isAdmin,
|
||||||
|
dismissUnread,
|
||||||
|
onSubmit,
|
||||||
|
promptShare = [],
|
||||||
|
fetchMessages
|
||||||
|
} = props;
|
||||||
|
const graphTimesentMap = useGraphState((state) => state.graphTimesentMap);
|
||||||
|
const ourContact = useOurContact();
|
||||||
|
const chatInput = useRef<ChatInput>();
|
||||||
|
|
||||||
|
const onFileDrag = useCallback(
|
||||||
|
(files: FileList | File[]) => {
|
||||||
|
if (!chatInput.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chatInput.current?.uploadFiles(files);
|
||||||
|
},
|
||||||
|
[chatInput.current]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { bind, dragging } = useFileDrag(onFileDrag);
|
||||||
|
|
||||||
|
const [unsent, setUnsent] = useLocalStorageState<Record<string, string>>(
|
||||||
|
'chat-unsent',
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const appendUnsent = useCallback(
|
||||||
|
(u: string) => setUnsent((s) => ({ ...s, [id]: u })),
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearUnsent = useCallback(() => {
|
||||||
|
setUnsent((s) => {
|
||||||
|
if (id in s) {
|
||||||
|
return _.omit(s, id);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
});
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const scrollTo = new URLSearchParams(location.search).get('msg');
|
||||||
|
|
||||||
|
const [showBanner, setShowBanner] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShowBanner(promptShare.length > 0);
|
||||||
|
}, [promptShare]);
|
||||||
|
|
||||||
|
const onReply = useCallback(
|
||||||
|
(msg: Post) => {
|
||||||
|
const message = props.onReply(msg);
|
||||||
|
setUnsent((s) => ({ ...s, [id]: message }));
|
||||||
|
},
|
||||||
|
[id, props.onReply]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!graph) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col {...bind} height="100%" overflow="hidden" position="relative">
|
||||||
|
<ShareProfile
|
||||||
|
our={ourContact}
|
||||||
|
api={api}
|
||||||
|
recipients={showBanner ? promptShare : []}
|
||||||
|
onShare={() => setShowBanner(false)}
|
||||||
|
/>
|
||||||
|
{dragging && <SubmitDragger />}
|
||||||
|
<ChatWindow
|
||||||
|
key={id}
|
||||||
|
graph={graph}
|
||||||
|
graphSize={graph.size}
|
||||||
|
unreadCount={unreadCount}
|
||||||
|
showOurContact={promptShare.length === 0 && !showBanner}
|
||||||
|
pendingSize={Object.keys(graphTimesentMap[id] || {}).length}
|
||||||
|
onReply={onReply}
|
||||||
|
dismissUnread={dismissUnread}
|
||||||
|
fetchMessages={fetchMessages}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
getPermalink={getPermalink}
|
||||||
|
api={api}
|
||||||
|
scrollTo={scrollTo ? bigInt(scrollTo) : undefined}
|
||||||
|
/>
|
||||||
|
{canWrite && (
|
||||||
|
<ChatInput
|
||||||
|
ref={chatInput}
|
||||||
|
api={props.api}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
ourContact={(promptShare.length === 0 && ourContact) || undefined}
|
||||||
|
onUnmount={appendUnsent}
|
||||||
|
placeholder="Message..."
|
||||||
|
message={unsent[id] || ''}
|
||||||
|
deleteMessage={clearUnsent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useEffect, Component, useRef, useState, useCallback } from 'react';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import bigInt, { BigInteger } from 'big-integer';
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
@ -11,7 +11,9 @@ import {
|
|||||||
Associations,
|
Associations,
|
||||||
Group,
|
Group,
|
||||||
Groups,
|
Groups,
|
||||||
Graph
|
Graph,
|
||||||
|
Post,
|
||||||
|
GraphNode
|
||||||
} from '@urbit/api';
|
} from '@urbit/api';
|
||||||
|
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
@ -30,20 +32,21 @@ const DEFAULT_BACKLOG_SIZE = 100;
|
|||||||
const IDLE_THRESHOLD = 64;
|
const IDLE_THRESHOLD = 64;
|
||||||
const MAX_BACKLOG_SIZE = 1000;
|
const MAX_BACKLOG_SIZE = 1000;
|
||||||
|
|
||||||
type ChatWindowProps = RouteComponentProps<{
|
|
||||||
ship: Patp;
|
type ChatWindowProps = {
|
||||||
station: string;
|
|
||||||
}> & {
|
|
||||||
unreadCount: number;
|
unreadCount: number;
|
||||||
graph: Graph;
|
graph: Graph;
|
||||||
graphSize: number;
|
graphSize: number;
|
||||||
association: Association;
|
|
||||||
group: Group;
|
|
||||||
ship: Patp;
|
|
||||||
station: any;
|
station: any;
|
||||||
|
fetchMessages: (newer: boolean) => Promise<boolean>;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
scrollTo?: BigInteger;
|
scrollTo?: BigInteger;
|
||||||
onReply: (msg: Post) => void;
|
onReply: (msg: Post) => void;
|
||||||
|
dismissUnread: () => void;
|
||||||
|
pendingSize?: number;
|
||||||
|
showOurContact: boolean;
|
||||||
|
getPermalink: (index: BigInteger) => string;
|
||||||
|
isAdmin: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ChatWindowState {
|
interface ChatWindowState {
|
||||||
@ -55,12 +58,12 @@ interface ChatWindowState {
|
|||||||
|
|
||||||
const virtScrollerStyle = { height: '100%' };
|
const virtScrollerStyle = { height: '100%' };
|
||||||
|
|
||||||
|
|
||||||
class ChatWindow extends Component<
|
class ChatWindow extends Component<
|
||||||
ChatWindowProps,
|
ChatWindowProps,
|
||||||
ChatWindowState
|
ChatWindowState
|
||||||
> {
|
> {
|
||||||
private virtualList: VirtualScroller | null;
|
private virtualList: VirtualScroller<GraphNode> | null;
|
||||||
private unreadMarkerRef: React.RefObject<HTMLDivElement>;
|
|
||||||
private prevSize = 0;
|
private prevSize = 0;
|
||||||
private unreadSet = false;
|
private unreadSet = false;
|
||||||
|
|
||||||
@ -76,14 +79,12 @@ class ChatWindow extends Component<
|
|||||||
unreadIndex: bigInt.zero
|
unreadIndex: bigInt.zero
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dismissUnread = this.dismissUnread.bind(this);
|
|
||||||
this.scrollToUnread = this.scrollToUnread.bind(this);
|
this.scrollToUnread = this.scrollToUnread.bind(this);
|
||||||
this.handleWindowBlur = this.handleWindowBlur.bind(this);
|
this.handleWindowBlur = this.handleWindowBlur.bind(this);
|
||||||
this.handleWindowFocus = this.handleWindowFocus.bind(this);
|
this.handleWindowFocus = this.handleWindowFocus.bind(this);
|
||||||
this.stayLockedIfActive = this.stayLockedIfActive.bind(this);
|
this.stayLockedIfActive = this.stayLockedIfActive.bind(this);
|
||||||
|
|
||||||
this.virtualList = null;
|
this.virtualList = null;
|
||||||
this.unreadMarkerRef = React.createRef();
|
|
||||||
this.prevSize = props.graph.size;
|
this.prevSize = props.graph.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,10 +93,9 @@ class ChatWindow extends Component<
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.setState({ initialized: true }, () => {
|
this.setState({ initialized: true }, () => {
|
||||||
if(this.props.scrollTo) {
|
if(this.props.scrollTo) {
|
||||||
this.virtualList.scrollToIndex(this.props.scrollTo);
|
this.virtualList!.scrollLocked = false;
|
||||||
|
this.virtualList!.scrollToIndex(this.props.scrollTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
}, this.INITIALIZATION_MAX_TIME);
|
}, this.INITIALIZATION_MAX_TIME);
|
||||||
@ -109,9 +109,11 @@ class ChatWindow extends Component<
|
|||||||
}
|
}
|
||||||
const unreadIndex = graph.keys()[unreadCount];
|
const unreadIndex = graph.keys()[unreadCount];
|
||||||
if (!unreadIndex || unreadCount === 0) {
|
if (!unreadIndex || unreadCount === 0) {
|
||||||
this.setState({
|
if(state.unreadIndex.neq(bigInt.zero)) {
|
||||||
unreadIndex: bigInt.zero
|
this.setState({
|
||||||
});
|
unreadIndex: bigInt.zero
|
||||||
|
});
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -122,8 +124,8 @@ class ChatWindow extends Component<
|
|||||||
dismissedInitialUnread() {
|
dismissedInitialUnread() {
|
||||||
const { unreadCount, graph } = this.props;
|
const { unreadCount, graph } = this.props;
|
||||||
|
|
||||||
return this.state.unreadIndex.neq(bigInt.zero) &&
|
return this.state.unreadIndex.eq(bigInt.zero) ? unreadCount > graph.size :
|
||||||
this.state.unreadIndex.neq(graph.keys()?.[unreadCount]?.[0] ?? bigInt.zero);
|
this.state.unreadIndex.neq(graph.keys()?.[unreadCount]?.[0] ?? bigInt.zero);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWindowBlur() {
|
handleWindowBlur() {
|
||||||
@ -133,12 +135,12 @@ class ChatWindow extends Component<
|
|||||||
handleWindowFocus() {
|
handleWindowFocus() {
|
||||||
this.setState({ idle: false });
|
this.setState({ idle: false });
|
||||||
if (this.virtualList?.window?.scrollTop === 0) {
|
if (this.virtualList?.window?.scrollTop === 0) {
|
||||||
this.dismissUnread();
|
this.props.dismissUnread();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: ChatWindowProps, prevState) {
|
componentDidUpdate(prevProps: ChatWindowProps, prevState) {
|
||||||
const { history, graph, unreadCount, graphSize, station } = this.props;
|
const { graph, unreadCount, graphSize, station } = this.props;
|
||||||
if(unreadCount === 0 && prevProps.unreadCount !== unreadCount) {
|
if(unreadCount === 0 && prevProps.unreadCount !== unreadCount) {
|
||||||
this.unreadSet = true;
|
this.unreadSet = true;
|
||||||
}
|
}
|
||||||
@ -150,8 +152,8 @@ class ChatWindow extends Component<
|
|||||||
}
|
}
|
||||||
if(this.unreadSet &&
|
if(this.unreadSet &&
|
||||||
this.dismissedInitialUnread() &&
|
this.dismissedInitialUnread() &&
|
||||||
this.virtualList?.startOffset() < 5) {
|
this.virtualList!.startOffset() < 5) {
|
||||||
this.dismissUnread();
|
this.props.dismissUnread();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -169,7 +171,7 @@ class ChatWindow extends Component<
|
|||||||
stayLockedIfActive() {
|
stayLockedIfActive() {
|
||||||
if (this.virtualList && !this.state.idle) {
|
if (this.virtualList && !this.state.idle) {
|
||||||
this.virtualList.resetScroll();
|
this.virtualList.resetScroll();
|
||||||
this.dismissUnread();
|
this.props.dismissUnread();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,45 +190,6 @@ class ChatWindow extends Component<
|
|||||||
this.virtualList?.scrollToIndex(this.state.unreadIndex);
|
this.virtualList?.scrollToIndex(this.state.unreadIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissUnread() {
|
|
||||||
const { association } = this.props;
|
|
||||||
if (this.state.fetchPending) return;
|
|
||||||
if (this.props.unreadCount === 0) return;
|
|
||||||
this.props.api.hark.markCountAsRead(association, '/', 'message');
|
|
||||||
}
|
|
||||||
|
|
||||||
setActive = () => {
|
|
||||||
if(this.state.idle) {
|
|
||||||
this.setState({ idle: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchMessages = async (newer: boolean): Promise<boolean> => {
|
|
||||||
const { api, station, graph } = this.props;
|
|
||||||
const pageSize = 100;
|
|
||||||
|
|
||||||
const [, , ship, name] = station.split('/');
|
|
||||||
const expectedSize = graph.size + pageSize;
|
|
||||||
if (newer) {
|
|
||||||
const [index] = graph.peekLargest()!;
|
|
||||||
await api.graph.getYoungerSiblings(
|
|
||||||
ship,
|
|
||||||
name,
|
|
||||||
pageSize,
|
|
||||||
`/${index.toString()}`
|
|
||||||
);
|
|
||||||
return expectedSize !== graph.size;
|
|
||||||
} else {
|
|
||||||
const [index] = graph.peekSmallest()!;
|
|
||||||
await api.graph.getOlderSiblings(ship, name, pageSize, `/${index.toString()}`);
|
|
||||||
const done = expectedSize !== graph.size;
|
|
||||||
if(done) {
|
|
||||||
this.calculateUnreadIndex();
|
|
||||||
}
|
|
||||||
return done;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onScroll = ({ scrollTop, scrollHeight, windowHeight }) => {
|
onScroll = ({ scrollTop, scrollHeight, windowHeight }) => {
|
||||||
if (!this.state.idle && scrollTop > IDLE_THRESHOLD) {
|
if (!this.state.idle && scrollTop > IDLE_THRESHOLD) {
|
||||||
this.setState({ idle: true });
|
this.setState({ idle: true });
|
||||||
@ -237,26 +200,21 @@ class ChatWindow extends Component<
|
|||||||
renderer = React.forwardRef(({ index, scrollWindow }, ref) => {
|
renderer = React.forwardRef(({ index, scrollWindow }, ref) => {
|
||||||
const {
|
const {
|
||||||
api,
|
api,
|
||||||
association,
|
|
||||||
group,
|
|
||||||
showOurContact,
|
showOurContact,
|
||||||
graph,
|
graph,
|
||||||
history,
|
onReply,
|
||||||
groups,
|
getPermalink,
|
||||||
associations,
|
dismissUnread,
|
||||||
onReply
|
isAdmin,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { unreadMarkerRef } = this;
|
const permalink = getPermalink(index);
|
||||||
const messageProps = {
|
const messageProps = {
|
||||||
association,
|
|
||||||
group,
|
|
||||||
showOurContact,
|
showOurContact,
|
||||||
unreadMarkerRef,
|
|
||||||
history,
|
|
||||||
api,
|
api,
|
||||||
groups,
|
onReply,
|
||||||
associations,
|
permalink,
|
||||||
onReply
|
dismissUnread,
|
||||||
|
isAdmin
|
||||||
};
|
};
|
||||||
|
|
||||||
const msg = graph.get(index)?.post;
|
const msg = graph.get(index)?.post;
|
||||||
@ -275,10 +233,10 @@ class ChatWindow extends Component<
|
|||||||
graph.peekLargest()?.[0] ?? bigInt.zero
|
graph.peekLargest()?.[0] ?? bigInt.zero
|
||||||
);
|
);
|
||||||
const highlighted = index.eq(this.props.scrollTo ?? bigInt.zero);
|
const highlighted = index.eq(this.props.scrollTo ?? bigInt.zero);
|
||||||
const keys = graph.keys().reverse();
|
const keys = graph.keys();
|
||||||
const graphIdx = keys.findIndex((idx) => idx.eq(index));
|
const graphIdx = keys.findIndex((idx) => idx.eq(index));
|
||||||
const prevIdx = keys[graphIdx + 1];
|
const prevIdx = keys[graphIdx - 1];
|
||||||
const nextIdx = keys[graphIdx - 1];
|
const nextIdx = keys[graphIdx + 1];
|
||||||
const isLastRead: boolean = this.state.unreadIndex.eq(index);
|
const isLastRead: boolean = this.state.unreadIndex.eq(index);
|
||||||
const props = {
|
const props = {
|
||||||
highlighted,
|
highlighted,
|
||||||
@ -305,32 +263,13 @@ class ChatWindow extends Component<
|
|||||||
const {
|
const {
|
||||||
unreadCount,
|
unreadCount,
|
||||||
api,
|
api,
|
||||||
association,
|
|
||||||
group,
|
|
||||||
graph,
|
graph,
|
||||||
history,
|
|
||||||
groups,
|
|
||||||
associations,
|
|
||||||
showOurContact,
|
showOurContact,
|
||||||
pendingSize,
|
pendingSize = 0,
|
||||||
onReply,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const unreadMarkerRef = this.unreadMarkerRef;
|
|
||||||
const messageProps = {
|
|
||||||
association,
|
|
||||||
group,
|
|
||||||
unreadMarkerRef,
|
|
||||||
history,
|
|
||||||
api,
|
|
||||||
associations
|
|
||||||
};
|
|
||||||
const unreadMsg = graph.get(this.state.unreadIndex);
|
const unreadMsg = graph.get(this.state.unreadIndex);
|
||||||
|
|
||||||
// hack to force a re-render when we toggle showing contact
|
|
||||||
const contactsModified =
|
|
||||||
showOurContact ? 0 : 100;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col height='100%' overflow='hidden' position='relative'>
|
<Col height='100%' overflow='hidden' position='relative'>
|
||||||
{ this.dismissedInitialUnread() &&
|
{ this.dismissedInitialUnread() &&
|
||||||
@ -343,34 +282,29 @@ class ChatWindow extends Component<
|
|||||||
? false
|
? false
|
||||||
: unreadMsg
|
: unreadMsg
|
||||||
}
|
}
|
||||||
dismissUnread={this.dismissUnread}
|
dismissUnread={this.props.dismissUnread}
|
||||||
onClick={this.scrollToUnread}
|
onClick={this.scrollToUnread}
|
||||||
/>)}
|
/>)}
|
||||||
<VirtualScroller
|
<VirtualScroller<GraphNode>
|
||||||
ref={(list) => {
|
ref={(list) => {
|
||||||
this.virtualList = list;
|
this.virtualList = list;
|
||||||
}}
|
}}
|
||||||
offset={unreadCount}
|
offset={unreadCount}
|
||||||
origin='bottom'
|
origin='bottom'
|
||||||
style={virtScrollerStyle}
|
style={virtScrollerStyle}
|
||||||
onStartReached={this.setActive}
|
|
||||||
onBottomLoaded={this.onBottomLoaded}
|
onBottomLoaded={this.onBottomLoaded}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
data={graph}
|
data={graph}
|
||||||
size={graph.size}
|
size={graph.size}
|
||||||
pendingSize={pendingSize + contactsModified}
|
pendingSize={pendingSize}
|
||||||
id={association.resource}
|
|
||||||
averageHeight={22}
|
averageHeight={22}
|
||||||
renderer={this.renderer}
|
renderer={this.renderer}
|
||||||
loadRows={this.fetchMessages}
|
loadRows={this.props.fetchMessages}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withState(ChatWindow, [
|
|
||||||
[useGroupState, ['groups']],
|
export default ChatWindow
|
||||||
[useMetadataState, ['associations']],
|
|
||||||
[useGraphState, ['pendingSize']]
|
|
||||||
]);
|
|
||||||
|
@ -40,27 +40,27 @@ export const ShareProfile = (props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onClick = async () => {
|
const onClick = async () => {
|
||||||
if(group.hidden && recipients.length > 0) {
|
if(typeof recipients === 'string') {
|
||||||
await api.contacts.allowShips(recipients);
|
const [,,ship,name] = recipients.split('/');
|
||||||
await Promise.all(recipients.map(r => api.contacts.share(r)))
|
|
||||||
setShowBanner(false);
|
|
||||||
} else if (!group.hidden) {
|
|
||||||
const [,,ship,name] = groupPath.split('/');
|
|
||||||
await api.contacts.allowGroup(ship,name);
|
await api.contacts.allowGroup(ship,name);
|
||||||
if(ship !== `~${window.ship}`) {
|
if(ship !== `~${window.ship}`) {
|
||||||
await api.contacts.share(ship);
|
await api.contacts.share(ship);
|
||||||
}
|
}
|
||||||
setShowBanner(false);
|
} else if(recipients.length > 0) {
|
||||||
}
|
await api.contacts.allowShips(recipients);
|
||||||
|
await Promise.all(recipients.map(r => api.contacts.share(r)))
|
||||||
|
}
|
||||||
|
props.onShare();
|
||||||
};
|
};
|
||||||
|
|
||||||
return showBanner ? (
|
return props.recipients?.length > 0 ? (
|
||||||
<Row
|
<Row
|
||||||
height="48px"
|
height="48px"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
borderBottom={1}
|
borderBottom={1}
|
||||||
borderColor="lightGray"
|
borderColor="lightGray"
|
||||||
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
<Row pl={3} alignItems="center">
|
<Row pl={3} alignItems="center">
|
||||||
{image}
|
{image}
|
||||||
|
@ -162,6 +162,7 @@ export default class ChatEditor extends Component {
|
|||||||
editor.showHint(['test', 'foo']);
|
editor.showHint(['test', 'foo']);
|
||||||
}
|
}
|
||||||
if (this.state.message !== '' && value == '') {
|
if (this.state.message !== '' && value == '') {
|
||||||
|
this.props.changeEvent(value);
|
||||||
this.setState({
|
this.setState({
|
||||||
message: value
|
message: value
|
||||||
});
|
});
|
||||||
@ -169,6 +170,7 @@ export default class ChatEditor extends Component {
|
|||||||
if (value == this.props.message || value == '' || value == ' ') {
|
if (value == this.props.message || value == '' || value == ' ') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.props.changeEvent(value);
|
||||||
this.setState({
|
this.setState({
|
||||||
message: value
|
message: value
|
||||||
});
|
});
|
||||||
|
@ -8,22 +8,11 @@ import Timestamp from '~/views/components/Timestamp';
|
|||||||
export const UnreadNotice = (props) => {
|
export const UnreadNotice = (props) => {
|
||||||
const { unreadCount, unreadMsg, dismissUnread, onClick } = props;
|
const { unreadCount, unreadMsg, dismissUnread, onClick } = props;
|
||||||
|
|
||||||
if (!unreadMsg || unreadCount === 0) {
|
if (unreadCount === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stamp = moment.unix(unreadMsg.post['time-sent'] / 1000);
|
const stamp = unreadMsg && moment.unix(unreadMsg.post['time-sent'] / 1000);
|
||||||
|
|
||||||
let datestamp = moment
|
|
||||||
.unix(unreadMsg.post['time-sent'] / 1000)
|
|
||||||
.format('YYYY.M.D');
|
|
||||||
const timestamp = moment
|
|
||||||
.unix(unreadMsg.post['time-sent'] / 1000)
|
|
||||||
.format('HH:mm');
|
|
||||||
|
|
||||||
if (datestamp === moment().format('YYYY.M.D')) {
|
|
||||||
datestamp = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -52,15 +41,20 @@ export const UnreadNotice = (props) => {
|
|||||||
whiteSpace='pre'
|
whiteSpace='pre'
|
||||||
overflow='hidden'
|
overflow='hidden'
|
||||||
display='flex'
|
display='flex'
|
||||||
cursor='pointer'
|
cursor={unreadMsg ? 'pointer' : null}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{unreadCount} new message{unreadCount > 1 ? 's' : ''} since{' '}
|
{unreadCount} new message{unreadCount > 1 ? 's' : ''}
|
||||||
<Timestamp stamp={stamp} color='black' date={true} fontSize={1} />
|
{unreadMsg && (
|
||||||
|
<>
|
||||||
|
{' '}since{' '}
|
||||||
|
<Timestamp stamp={stamp} color='black' date={true} fontSize={1} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<Icon
|
<Icon
|
||||||
icon='X'
|
icon='X'
|
||||||
ml='4'
|
ml={unreadMsg ? 4 : 1}
|
||||||
color='black'
|
color='black'
|
||||||
cursor='pointer'
|
cursor='pointer'
|
||||||
textAlign='right'
|
textAlign='right'
|
||||||
|
@ -220,7 +220,7 @@ export default function LaunchApp(props) {
|
|||||||
<NewGroup {...props} />
|
<NewGroup {...props} />
|
||||||
</ModalButton>
|
</ModalButton>
|
||||||
<ModalButton
|
<ModalButton
|
||||||
icon="Boot"
|
icon="BootNode"
|
||||||
bg="washedGray"
|
bg="washedGray"
|
||||||
color="black"
|
color="black"
|
||||||
text="Join Group"
|
text="Join Group"
|
||||||
|
@ -23,7 +23,7 @@ type LinkResourceProps = StoreState & {
|
|||||||
association: Association;
|
association: Association;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
} & RouteComponentProps;
|
};
|
||||||
|
|
||||||
export function LinkResource(props: LinkResourceProps) {
|
export function LinkResource(props: LinkResourceProps) {
|
||||||
const {
|
const {
|
||||||
|
@ -6,7 +6,7 @@ import React, {
|
|||||||
Component,
|
Component,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import { Col, Text } from "@tlon/indigo-react";
|
import { Box, Col, Text } from "@tlon/indigo-react";
|
||||||
import bigInt from "big-integer";
|
import bigInt from "big-integer";
|
||||||
import { Association, Graph, Unreads, Group, Rolodex } from "@urbit/api";
|
import { Association, Graph, Unreads, Group, Rolodex } from "@urbit/api";
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
|
|||||||
return isWriter(group, association.resource);
|
return isWriter(group, association.resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderItem = ({ index, scrollWindow }) => {
|
renderItem = React.forwardRef(({ index, scrollWindow }, ref) => {
|
||||||
const { props } = this;
|
const { props } = this;
|
||||||
const { association, graph, api } = props;
|
const { association, graph, api } = props;
|
||||||
const [, , ship, name] = association.resource.split("/");
|
const [, , ship, name] = association.resource.split("/");
|
||||||
@ -80,12 +80,14 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
|
|||||||
api={api}
|
api={api}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<LinkItem {...linkProps} />
|
<LinkItem ref={ref} {...linkProps} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <LinkItem key={index.toString()} {...linkProps} />;
|
return <Box ref={ref}>
|
||||||
};
|
<LinkItem ref={ref} key={index.toString()} {...linkProps} />;
|
||||||
|
</Box>
|
||||||
|
});
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { graph, api, association } = this.props;
|
const { graph, api, association } = this.props;
|
||||||
@ -136,4 +138,4 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LinkWindow;
|
export default LinkWindow;
|
||||||
|
@ -19,7 +19,7 @@ interface LinkItemProps {
|
|||||||
node: GraphNode;
|
node: GraphNode;
|
||||||
association: Association;
|
association: Association;
|
||||||
resource: string; api: GlobalApi; group: Group; path: string; }
|
resource: string; api: GlobalApi; group: Group; path: string; }
|
||||||
export const LinkItem = (props: LinkItemProps): ReactElement => {
|
export const LinkItem = React.forwardRef((props: LinkItemProps, ref): ReactElement => {
|
||||||
const {
|
const {
|
||||||
association,
|
association,
|
||||||
node,
|
node,
|
||||||
@ -30,7 +30,6 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
|||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
|
||||||
const remoteRef = useRef<typeof RemoteContent | null>(null);
|
const remoteRef = useRef<typeof RemoteContent | null>(null);
|
||||||
const index = node.post.index.split('/')[1];
|
const index = node.post.index.split('/')[1];
|
||||||
|
|
||||||
@ -86,7 +85,7 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
|||||||
permalink,
|
permalink,
|
||||||
'Copy reference'
|
'Copy reference'
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteLink = () => {
|
const deleteLink = () => {
|
||||||
if (confirm('Are you sure you want to delete this link?')) {
|
if (confirm('Are you sure you want to delete this link?')) {
|
||||||
api.graph.removeNodes(`~${ship}`, name, [node.post.index]);
|
api.graph.removeNodes(`~${ship}`, name, [node.post.index]);
|
||||||
@ -167,9 +166,11 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
|||||||
<Row minWidth='0' flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white">
|
<Row minWidth='0' flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white">
|
||||||
<Author
|
<Author
|
||||||
showImage
|
showImage
|
||||||
|
isRelativeTime
|
||||||
ship={author}
|
ship={author}
|
||||||
date={node.post['time-sent']}
|
date={node.post['time-sent']}
|
||||||
group={group}
|
group={group}
|
||||||
|
lineHeight="1"
|
||||||
/>
|
/>
|
||||||
<Box ml="auto">
|
<Box ml="auto">
|
||||||
<Link
|
<Link
|
||||||
@ -208,5 +209,5 @@ export const LinkItem = (props: LinkItemProps): ReactElement => {
|
|||||||
|
|
||||||
</Row>
|
</Row>
|
||||||
</Box>);
|
</Box>);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
@ -92,6 +92,7 @@ export default function NotificationsScreen(props: any): ReactElement {
|
|||||||
<StatelessAsyncAction
|
<StatelessAsyncAction
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
color="black"
|
color="black"
|
||||||
|
backgroundColor="white"
|
||||||
onClick={onReadAll}
|
onClick={onReadAll}
|
||||||
>
|
>
|
||||||
Mark All Read
|
Mark All Read
|
||||||
@ -106,7 +107,7 @@ export default function NotificationsScreen(props: any): ReactElement {
|
|||||||
{!view && <Inbox
|
{!view && <Inbox
|
||||||
pendingJoin={pendingJoin}
|
pendingJoin={pendingJoin}
|
||||||
{...props}
|
{...props}
|
||||||
filter={filter.groups}
|
filter={filter.groups}
|
||||||
/>}
|
/>}
|
||||||
</Col>
|
</Col>
|
||||||
</Body>
|
</Body>
|
||||||
|
@ -4,6 +4,7 @@ import ChatMessage from "../chat/components/ChatMessage";
|
|||||||
import { Association, GraphNode, Post, Group } from "@urbit/api";
|
import { Association, GraphNode, Post, Group } from "@urbit/api";
|
||||||
import { useGroupForAssoc } from "~/logic/state/group";
|
import { useGroupForAssoc } from "~/logic/state/group";
|
||||||
import { MentionText } from "~/views/components/MentionText";
|
import { MentionText } from "~/views/components/MentionText";
|
||||||
|
import { GraphContentWide } from '~/views/landscape/components/Graph/GraphContentWide';
|
||||||
import Author from "~/views/components/Author";
|
import Author from "~/views/components/Author";
|
||||||
import { NoteContent } from "../publish/components/Note";
|
import { NoteContent } from "../publish/components/Note";
|
||||||
import { PostContent } from "~/views/landscape/components/Home/Post/PostContent";
|
import { PostContent } from "~/views/landscape/components/Home/Post/PostContent";
|
||||||
@ -31,7 +32,7 @@ function TranscludedLinkNode(props: {
|
|||||||
return <PermalinkEmbed transcluded={transcluded + 1} api={api} link={permalink} association={assoc} />
|
return <PermalinkEmbed transcluded={transcluded + 1} api={api} link={permalink} association={assoc} />
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box borderRadius="2" p="2" bg="scales.black05">
|
<Box borderRadius="2" p="2" bg="scales.black05">
|
||||||
<Anchor underline={false} target="_blank" color="black" href={link.url}>
|
<Anchor underline={false} target="_blank" color="black" href={link.url}>
|
||||||
@ -74,11 +75,11 @@ function TranscludedComment(props: {
|
|||||||
group={group}
|
group={group}
|
||||||
/>
|
/>
|
||||||
<Box p="2">
|
<Box p="2">
|
||||||
<MentionText
|
<GraphContentWide
|
||||||
api={api}
|
api={api}
|
||||||
transcluded={transcluded}
|
transcluded={transcluded}
|
||||||
content={comment.post.contents}
|
post={comment.post}
|
||||||
group={group}
|
showOurContact={false}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Col>
|
</Col>
|
||||||
@ -200,8 +201,8 @@ export function TranscludedNode(props: {
|
|||||||
<TranscludedPost
|
<TranscludedPost
|
||||||
api={props.api}
|
api={props.api}
|
||||||
post={node.post}
|
post={node.post}
|
||||||
group={group}
|
group={group}
|
||||||
transcluded={transcluded}
|
transcluded={transcluded}
|
||||||
/>)
|
/>)
|
||||||
;
|
;
|
||||||
default:
|
default:
|
||||||
|
@ -18,7 +18,7 @@ import { GroupLink } from "~/views/components/GroupLink";
|
|||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from "~/logic/api/global";
|
||||||
import { getModuleIcon } from "~/logic/lib/util";
|
import { getModuleIcon } from "~/logic/lib/util";
|
||||||
import useMetadataState from "~/logic/state/metadata";
|
import useMetadataState from "~/logic/state/metadata";
|
||||||
import { Association, resourceFromPath } from "@urbit/api";
|
import { Association, resourceFromPath, GraphNode } from "@urbit/api";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import useGraphState from "~/logic/state/graph";
|
import useGraphState from "~/logic/state/graph";
|
||||||
import { GraphNodeContent } from "../notifications/graph";
|
import { GraphNodeContent } from "../notifications/graph";
|
||||||
@ -51,7 +51,7 @@ function GraphPermalink(
|
|||||||
const { full = false, showOurContact, pending, link, graph, group, index, api, transcluded } = props;
|
const { full = false, showOurContact, pending, link, graph, group, index, api, transcluded } = props;
|
||||||
const { ship, name } = resourceFromPath(graph);
|
const { ship, name } = resourceFromPath(graph);
|
||||||
const node = useGraphState(
|
const node = useGraphState(
|
||||||
useCallback((s) => s.looseNodes?.[`${ship.slice(1)}/${name}`]?.[index], [
|
useCallback((s) => s.looseNodes?.[`${ship.slice(1)}/${name}`]?.[index] as GraphNode, [
|
||||||
graph,
|
graph,
|
||||||
index,
|
index,
|
||||||
])
|
])
|
||||||
@ -63,7 +63,7 @@ function GraphPermalink(
|
|||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
useVirtualResizeProp(node)
|
useVirtualResizeProp(!!node)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (pending || !index) {
|
if (pending || !index) {
|
||||||
|
@ -11,7 +11,7 @@ type PublishResourceProps = StoreState & {
|
|||||||
association: Association;
|
association: Association;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
} & RouteComponentProps;
|
};
|
||||||
|
|
||||||
export function PublishResource(props: PublishResourceProps) {
|
export function PublishResource(props: PublishResourceProps) {
|
||||||
const { association, api, baseUrl, notebooks } = props;
|
const { association, api, baseUrl, notebooks } = props;
|
||||||
|
@ -73,14 +73,14 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
|||||||
if (window.ship === note?.post?.author) {
|
if (window.ship === note?.post?.author) {
|
||||||
adminLinks.push(
|
adminLinks.push(
|
||||||
<Link to={`${baseUrl}/edit`}>
|
<Link to={`${baseUrl}/edit`}>
|
||||||
<Action>Update</Action>
|
<Action backgroundColor="white">Update</Action>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (window.ship === note?.post?.author || ourRole === "admin") {
|
if (window.ship === note?.post?.author || ourRole === "admin") {
|
||||||
adminLinks.push(
|
adminLinks.push(
|
||||||
<Action destructive onClick={deletePost}>
|
<Action backgroundColor="white" destructive onClick={deletePost}>
|
||||||
Delete
|
Delete
|
||||||
</Action>
|
</Action>
|
||||||
)
|
)
|
||||||
@ -115,11 +115,12 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
|||||||
<Row alignItems="center">
|
<Row alignItems="center">
|
||||||
<Author
|
<Author
|
||||||
showImage
|
showImage
|
||||||
|
isRelativeTime
|
||||||
ship={post?.author}
|
ship={post?.author}
|
||||||
date={post?.['time-sent']}
|
date={post?.['time-sent']}
|
||||||
group={group}
|
group={group}
|
||||||
>
|
>
|
||||||
<Row px="2" gapX="2" alignItems="flex-end">
|
<Row px="2" gapX="2" alignItems="flex-end" height="14px">
|
||||||
<Action bg="white" onClick={doCopy}>{copyDisplay}</Action>
|
<Action bg="white" onClick={doCopy}>{copyDisplay}</Action>
|
||||||
{adminLinks}
|
{adminLinks}
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -44,7 +44,7 @@ export function PostForm(props: PostFormProps) {
|
|||||||
validateOnBlur
|
validateOnBlur
|
||||||
>
|
>
|
||||||
<Form style={{ display: 'contents' }}>
|
<Form style={{ display: 'contents' }}>
|
||||||
<Row flexShrink='0' flexDirection={['column-reverse', 'row']} mb={4} gapX={4} justifyContent='space-between'>
|
<Row flexShrink={0} flexDirection={['column-reverse', 'row']} mb={4} gapX={4} justifyContent='space-between'>
|
||||||
<Input maxWidth='40rem' width='100%' flexShrink={[0, 1]} placeholder="Post Title" id="title" />
|
<Input maxWidth='40rem' width='100%' flexShrink={[0, 1]} placeholder="Post Title" id="title" />
|
||||||
<Row flexDirection={['column', 'row']} mb={[4,0]}>
|
<Row flexDirection={['column', 'row']} mb={[4,0]}>
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
|
@ -107,7 +107,7 @@ export default function SettingsScreen(props: any) {
|
|||||||
</Text>
|
</Text>
|
||||||
<Col>
|
<Col>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon='Inbox'
|
icon='Notifications'
|
||||||
text='Notifications'
|
text='Notifications'
|
||||||
hash='notifications'
|
hash='notifications'
|
||||||
/>
|
/>
|
||||||
|
@ -24,6 +24,7 @@ interface AuthorProps {
|
|||||||
unread?: boolean;
|
unread?: boolean;
|
||||||
api?: GlobalApi;
|
api?: GlobalApi;
|
||||||
size?: number;
|
size?: number;
|
||||||
|
lineHeight?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line max-lines-per-function
|
// eslint-disable-next-line max-lines-per-function
|
||||||
@ -38,10 +39,11 @@ export default function Author(props: AuthorProps & PropFunc<typeof Box>): React
|
|||||||
group,
|
group,
|
||||||
isRelativeTime,
|
isRelativeTime,
|
||||||
dontShowTime,
|
dontShowTime,
|
||||||
|
lineHeight = 'tall',
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const time = props.time || false;
|
const time = props.time || props.date || false;
|
||||||
const size = props.size || 16;
|
const size = props.size || 16;
|
||||||
const sigilPadding = props.sigilPadding || 2;
|
const sigilPadding = props.sigilPadding || 2;
|
||||||
|
|
||||||
@ -89,7 +91,7 @@ export default function Author(props: AuthorProps & PropFunc<typeof Box>): React
|
|||||||
) : sigil;
|
) : sigil;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row height="20px" {...rest} alignItems='center' width='auto'>
|
<Row {...rest} alignItems='center' width='auto'>
|
||||||
<Box
|
<Box
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -110,7 +112,7 @@ export default function Author(props: AuthorProps & PropFunc<typeof Box>): React
|
|||||||
color='black'
|
color='black'
|
||||||
fontSize='1'
|
fontSize='1'
|
||||||
cursor='pointer'
|
cursor='pointer'
|
||||||
lineHeight='tall'
|
lineHeight={lineHeight}
|
||||||
fontFamily={showNickname ? 'sans' : 'mono'}
|
fontFamily={showNickname ? 'sans' : 'mono'}
|
||||||
fontWeight={showNickname ? '500' : '400'}
|
fontWeight={showNickname ? '500' : '400'}
|
||||||
mr={showNickname ? 0 : "2px"}
|
mr={showNickname ? 0 : "2px"}
|
||||||
@ -121,6 +123,7 @@ export default function Author(props: AuthorProps & PropFunc<typeof Box>): React
|
|||||||
</Box>
|
</Box>
|
||||||
{ !dontShowTime && time && (
|
{ !dontShowTime && time && (
|
||||||
<Timestamp
|
<Timestamp
|
||||||
|
height="fit-content"
|
||||||
relative={isRelativeTime}
|
relative={isRelativeTime}
|
||||||
stamp={stamp}
|
stamp={stamp}
|
||||||
fontSize={1}
|
fontSize={1}
|
||||||
|
@ -115,8 +115,8 @@ export function ChipInput(props: ChipInputProps): ReactElement {
|
|||||||
<Input
|
<Input
|
||||||
width="auto"
|
width="auto"
|
||||||
height="24px"
|
height="24px"
|
||||||
flexShrink="1"
|
flexShrink={1}
|
||||||
flexGrow="1"
|
flexGrow={1}
|
||||||
pl="0"
|
pl="0"
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
@ -73,7 +73,7 @@ export function ColorInput(props: ColorInputProps) {
|
|||||||
height='100%'
|
height='100%'
|
||||||
alignSelf='stretch'
|
alignSelf='stretch'
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
value={`#${padded}`}
|
value={padded}
|
||||||
disabled={disabled || false}
|
disabled={disabled || false}
|
||||||
type='color'
|
type='color'
|
||||||
opacity={0}
|
opacity={0}
|
||||||
|
@ -35,6 +35,7 @@ interface CommentItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CommentItem(props: CommentItemProps): ReactElement {
|
export function CommentItem(props: CommentItemProps): ReactElement {
|
||||||
|
let { highlighted } = props;
|
||||||
const { ship, name, api, comment, group } = props;
|
const { ship, name, api, comment, group } = props;
|
||||||
const association = useMetadataState(
|
const association = useMetadataState(
|
||||||
useCallback(s => s.associations.graph[`/ship/${ship}/${name}`], [ship,name])
|
useCallback(s => s.associations.graph[`/ship/${ship}/${name}`], [ship,name])
|
||||||
@ -47,6 +48,16 @@ export function CommentItem(props: CommentItemProps): ReactElement {
|
|||||||
await api.graph.removeNodes(ship, name, [comment.post?.index]);
|
await api.graph.removeNodes(ship, name, [comment.post?.index]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ourMention = post?.contents?.some((e) => {
|
||||||
|
return e?.mention && e?.mention === window.ship;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!highlighted) {
|
||||||
|
if (ourMention) {
|
||||||
|
highlighted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const commentIndexArray = (comment.post?.index || '/').split('/');
|
const commentIndexArray = (comment.post?.index || '/').split('/');
|
||||||
const commentIndex = commentIndexArray[commentIndexArray.length - 1];
|
const commentIndex = commentIndexArray[commentIndexArray.length - 1];
|
||||||
|
|
||||||
@ -95,6 +106,7 @@ export function CommentItem(props: CommentItemProps): ReactElement {
|
|||||||
date={post?.['time-sent']}
|
date={post?.['time-sent']}
|
||||||
unread={props.unread}
|
unread={props.unread}
|
||||||
group={group}
|
group={group}
|
||||||
|
isRelativeTime
|
||||||
>
|
>
|
||||||
<Row px="2" gapX="2" height="18px">
|
<Row px="2" gapX="2" height="18px">
|
||||||
<Action bg="white" onClick={doCopy}>{copyDisplay}</Action>
|
<Action bg="white" onClick={doCopy}>{copyDisplay}</Action>
|
||||||
@ -106,7 +118,7 @@ export function CommentItem(props: CommentItemProps): ReactElement {
|
|||||||
borderRadius="1"
|
borderRadius="1"
|
||||||
p="1"
|
p="1"
|
||||||
mb="1"
|
mb="1"
|
||||||
backgroundColor={props.highlighted ? 'washedBlue' : 'white'}
|
backgroundColor={highlighted ? 'washedBlue' : 'white'}
|
||||||
transcluded={0}
|
transcluded={0}
|
||||||
api={api}
|
api={api}
|
||||||
post={post}
|
post={post}
|
||||||
|
@ -130,7 +130,7 @@ export function Comments(props: CommentsProps & PropFunc<typeof Col>) {
|
|||||||
const canComment = isWriter(group, association.resource) || association.metadata.vip === 'reader-comments';
|
const canComment = isWriter(group, association.resource) || association.metadata.vip === 'reader-comments';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col {...rest}>
|
<Col {...rest} minWidth='0'>
|
||||||
{( !editCommentId && canComment ? <CommentInput onSubmit={onSubmit} /> : null )}
|
{( !editCommentId && canComment ? <CommentInput onSubmit={onSubmit} /> : null )}
|
||||||
{( editCommentId ? (
|
{( editCommentId ? (
|
||||||
<CommentInput
|
<CommentInput
|
||||||
|
@ -25,6 +25,7 @@ interface DropdownProps {
|
|||||||
offsetY?: number;
|
offsetY?: number;
|
||||||
width?: string;
|
width?: string;
|
||||||
dropWidth?: string;
|
dropWidth?: string;
|
||||||
|
flexShrink?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ClickBox = styled(Box)`
|
const ClickBox = styled(Box)`
|
||||||
@ -39,7 +40,7 @@ const DropdownOptions = styled(Box)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export function Dropdown(props: DropdownProps): ReactElement {
|
export function Dropdown(props: DropdownProps): ReactElement {
|
||||||
const { children, options, offsetX = 0, offsetY = 0 } = props;
|
const { children, options, offsetX = 0, offsetY = 0, flexShrink = 1 } = props;
|
||||||
const dropdownRef = useRef<HTMLElement>(null);
|
const dropdownRef = useRef<HTMLElement>(null);
|
||||||
const anchorRef = useRef<HTMLElement>(null);
|
const anchorRef = useRef<HTMLElement>(null);
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
@ -47,6 +48,9 @@ export function Dropdown(props: DropdownProps): ReactElement {
|
|||||||
const [coords, setCoords] = useState({});
|
const [coords, setCoords] = useState({});
|
||||||
|
|
||||||
const updatePos = useCallback(() => {
|
const updatePos = useCallback(() => {
|
||||||
|
if(!anchorRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const newCoords = getRelativePosition(anchorRef.current, props.alignX, props.alignY, offsetX, offsetY);
|
const newCoords = getRelativePosition(anchorRef.current, props.alignX, props.alignY, offsetX, offsetY);
|
||||||
if(newCoords) {
|
if(newCoords) {
|
||||||
setCoords(newCoords);
|
setCoords(newCoords);
|
||||||
@ -86,7 +90,7 @@ export function Dropdown(props: DropdownProps): ReactElement {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexShrink={props?.flexShrink ? props.flexShrink : 1} position={open ? 'relative' : 'static'} minWidth='0' width={props?.width ? props.width : 'auto'}>
|
<Box flexShrink={flexShrink} position={open ? 'relative' : 'static'} minWidth='0' width={props?.width ? props.width : 'auto'}>
|
||||||
<ClickBox width='100%' ref={anchorRef} onClick={onOpen}>
|
<ClickBox width='100%' ref={anchorRef} onClick={onOpen}>
|
||||||
{children}
|
{children}
|
||||||
</ClickBox>
|
</ClickBox>
|
||||||
|
@ -23,6 +23,7 @@ import RichText from './RichText';
|
|||||||
import { ProfileStatus } from './ProfileStatus';
|
import { ProfileStatus } from './ProfileStatus';
|
||||||
import useSettingsState from '~/logic/state/settings';
|
import useSettingsState from '~/logic/state/settings';
|
||||||
import {useOutsideClick} from '~/logic/lib/useOutsideClick';
|
import {useOutsideClick} from '~/logic/lib/useOutsideClick';
|
||||||
|
import {useCopy} from '~/logic/lib/useCopy';
|
||||||
import {useContact} from '~/logic/state/contact';
|
import {useContact} from '~/logic/state/contact';
|
||||||
import {useHistory} from 'react-router-dom';
|
import {useHistory} from 'react-router-dom';
|
||||||
import {Portal} from './Portal';
|
import {Portal} from './Portal';
|
||||||
@ -59,6 +60,7 @@ const ProfileOverlay = (props: ProfileOverlayProps) => {
|
|||||||
const hideAvatars = useSettingsState(state => state.calm.hideAvatars);
|
const hideAvatars = useSettingsState(state => state.calm.hideAvatars);
|
||||||
const hideNicknames = useSettingsState(state => state.calm.hideNicknames);
|
const hideNicknames = useSettingsState(state => state.calm.hideNicknames);
|
||||||
const isOwn = useMemo(() => window.ship === ship, [ship]);
|
const isOwn = useMemo(() => window.ship === ship, [ship]);
|
||||||
|
const { copyDisplay, doCopy, didCopy } = useCopy(`~${ship}`);
|
||||||
|
|
||||||
const contact = useContact(`~${ship}`)
|
const contact = useContact(`~${ship}`)
|
||||||
const color = `#${uxToHex(contact?.color ?? '0x0')}`;
|
const color = `#${uxToHex(contact?.color ?? '0x0')}`;
|
||||||
@ -188,9 +190,18 @@ const ProfileOverlay = (props: ProfileOverlayProps) => {
|
|||||||
overflow='hidden'
|
overflow='hidden'
|
||||||
whiteSpace='pre'
|
whiteSpace='pre'
|
||||||
marginBottom='0'
|
marginBottom='0'
|
||||||
|
cursor='pointer'
|
||||||
|
display={didCopy ? 'none' : 'block'}
|
||||||
|
onClick={doCopy}
|
||||||
>
|
>
|
||||||
{showNickname ? contact?.nickname : cite(ship)}
|
{showNickname ? contact?.nickname : cite(ship)}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text
|
||||||
|
fontWeight='600'
|
||||||
|
marginBottom='0'
|
||||||
|
>
|
||||||
|
{copyDisplay}
|
||||||
|
</Text>
|
||||||
</Row>
|
</Row>
|
||||||
{isOwn ? (
|
{isOwn ? (
|
||||||
<ProfileStatus
|
<ProfileStatus
|
||||||
|
@ -48,12 +48,14 @@ class RemoteContent extends Component<RemoteContentProps, RemoteContentState> {
|
|||||||
this.state = {
|
this.state = {
|
||||||
unfold: props.unfold || false,
|
unfold: props.unfold || false,
|
||||||
embed: undefined,
|
embed: undefined,
|
||||||
noCors: false
|
noCors: false,
|
||||||
|
showArrow: false
|
||||||
};
|
};
|
||||||
this.unfoldEmbed = this.unfoldEmbed.bind(this);
|
this.unfoldEmbed = this.unfoldEmbed.bind(this);
|
||||||
this.loadOembed = this.loadOembed.bind(this);
|
this.loadOembed = this.loadOembed.bind(this);
|
||||||
this.wrapInLink = this.wrapInLink.bind(this);
|
this.wrapInLink = this.wrapInLink.bind(this);
|
||||||
this.onError = this.onError.bind(this);
|
this.onError = this.onError.bind(this);
|
||||||
|
this.toggleArrow = this.toggleArrow.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
save = () => {
|
save = () => {
|
||||||
@ -128,7 +130,7 @@ return;
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapInLink(contents, textOnly = false, unfold = false, unfoldEmbed = null, embedContainer = null) {
|
wrapInLink(contents, textOnly = false, unfold = false, unfoldEmbed = null, embedContainer = null, flushPadding = false, noOp = false) {
|
||||||
const { style } = this.props;
|
const { style } = this.props;
|
||||||
return (
|
return (
|
||||||
<Box borderRadius="1" backgroundColor="washedGray" maxWidth="min(100%, 20rem)">
|
<Box borderRadius="1" backgroundColor="washedGray" maxWidth="min(100%, 20rem)">
|
||||||
@ -145,8 +147,8 @@ return;
|
|||||||
)}
|
)}
|
||||||
<BaseAnchor
|
<BaseAnchor
|
||||||
display="flex"
|
display="flex"
|
||||||
p="2"
|
p={flushPadding ? 0 : 2}
|
||||||
onClick={(e) => { e.stopPropagation(); }}
|
onClick={(e) => { noOp ? e.preventDefault() : e.stopPropagation() }}
|
||||||
href={this.props.url}
|
href={this.props.url}
|
||||||
whiteSpace="nowrap"
|
whiteSpace="nowrap"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
@ -157,7 +159,8 @@ return;
|
|||||||
style={{ color: 'inherit', textDecoration: 'none', ...style }}
|
style={{ color: 'inherit', textDecoration: 'none', ...style }}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
cursor={noOp ? 'default' : 'pointer'}
|
||||||
|
>
|
||||||
{contents}
|
{contents}
|
||||||
</BaseAnchor>
|
</BaseAnchor>
|
||||||
</Row>
|
</Row>
|
||||||
@ -171,11 +174,16 @@ return;
|
|||||||
this.setState({ noCors: true });
|
this.setState({ noCors: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleArrow() {
|
||||||
|
this.setState({showArrow: !this.state.showArrow})
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
remoteContentPolicy,
|
remoteContentPolicy,
|
||||||
url,
|
url,
|
||||||
text,
|
text,
|
||||||
|
transcluded,
|
||||||
renderUrl = true,
|
renderUrl = true,
|
||||||
imageProps = {},
|
imageProps = {},
|
||||||
audioProps = {},
|
audioProps = {},
|
||||||
@ -192,22 +200,60 @@ return;
|
|||||||
const isVideo = VIDEO_REGEX.test(url);
|
const isVideo = VIDEO_REGEX.test(url);
|
||||||
const isOembed = hasProvider(url);
|
const isOembed = hasProvider(url);
|
||||||
|
|
||||||
|
const isTranscluded = () => {
|
||||||
|
return transcluded;
|
||||||
|
}
|
||||||
|
|
||||||
if (isImage && remoteContentPolicy.imageShown) {
|
if (isImage && remoteContentPolicy.imageShown) {
|
||||||
return this.wrapInLink(
|
return this.wrapInLink(
|
||||||
<BaseImage
|
<Box
|
||||||
{...(noCors ? {} : { crossOrigin: "anonymous" })}
|
position='relative'
|
||||||
referrerPolicy="no-referrer"
|
onMouseEnter={this.toggleArrow}
|
||||||
flexShrink={0}
|
onMouseLeave={this.toggleArrow}
|
||||||
src={url}
|
>
|
||||||
style={style}
|
<BaseAnchor
|
||||||
onLoad={onLoad}
|
position='absolute'
|
||||||
onError={this.onError}
|
top={2}
|
||||||
height="100%"
|
right={2}
|
||||||
width="100%"
|
display={this.state.showArrow ? 'block' : 'none'}
|
||||||
objectFit="contain"
|
target='_blank'
|
||||||
{...imageProps}
|
rel='noopener noreferrer'
|
||||||
{...props}
|
onClick={(e) => {
|
||||||
/>
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
href={url}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
backgroundColor='white'
|
||||||
|
padding={2}
|
||||||
|
borderRadius='50%'
|
||||||
|
display='flex'
|
||||||
|
>
|
||||||
|
<Icon icon='ArrowNorthEast' />
|
||||||
|
</Box>
|
||||||
|
</BaseAnchor>
|
||||||
|
<BaseImage
|
||||||
|
{...(noCors ? {} : { crossOrigin: 'anonymous' })}
|
||||||
|
referrerPolicy='no-referrer'
|
||||||
|
flexShrink={0}
|
||||||
|
src={url}
|
||||||
|
style={style}
|
||||||
|
onLoad={onLoad}
|
||||||
|
onError={this.onError}
|
||||||
|
height='100%'
|
||||||
|
width='100%'
|
||||||
|
objectFit='contain'
|
||||||
|
borderRadius={2}
|
||||||
|
{...imageProps}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</Box>,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
isTranscluded()
|
||||||
);
|
);
|
||||||
} else if (isAudio && remoteContentPolicy.audioShown) {
|
} else if (isAudio && remoteContentPolicy.audioShown) {
|
||||||
return (
|
return (
|
||||||
@ -271,7 +317,6 @@ return;
|
|||||||
display={this.state.unfold ? 'block' : 'none'}
|
display={this.state.unfold ? 'block' : 'none'}
|
||||||
className='embed-container'
|
className='embed-container'
|
||||||
style={style}
|
style={style}
|
||||||
flexShrink={0}
|
|
||||||
onLoad={this.onLoad}
|
onLoad={this.onLoad}
|
||||||
{...oembedProps}
|
{...oembedProps}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -83,7 +83,7 @@ const StatusBar = (props) => {
|
|||||||
onClick={() => history.push('/')}
|
onClick={() => history.push('/')}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Icon icon='Spaces' color='black' />
|
<Icon icon='Dashboard' color='black' />
|
||||||
</Button>
|
</Button>
|
||||||
<StatusBarItem float={floatLeap} mr={2} onClick={() => toggleOmnibox()}>
|
<StatusBarItem float={floatLeap} mr={2} onClick={() => toggleOmnibox()}>
|
||||||
{!doNotDisturb && (notificationsCount > 0 || invites.length > 0) && (
|
{!doNotDisturb && (notificationsCount > 0 || invites.length > 0) && (
|
||||||
@ -134,7 +134,7 @@ const StatusBar = (props) => {
|
|||||||
mr={2}
|
mr={2}
|
||||||
onClick={() => props.history.push('/~landscape/messages')}
|
onClick={() => props.history.push('/~landscape/messages')}
|
||||||
>
|
>
|
||||||
<Icon icon='Users' />
|
<Icon icon='Messages' />
|
||||||
</StatusBarItem>
|
</StatusBarItem>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
dropWidth='250px'
|
dropWidth='250px'
|
||||||
|
@ -12,6 +12,7 @@ export type TimestampProps = BoxProps & {
|
|||||||
date?: boolean;
|
date?: boolean;
|
||||||
time?: boolean;
|
time?: boolean;
|
||||||
relative?: boolean;
|
relative?: boolean;
|
||||||
|
height?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Timestamp = (props: TimestampProps): ReactElement | null => {
|
const Timestamp = (props: TimestampProps): ReactElement | null => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { Component, useCallback } from 'react';
|
import React, { Component, useCallback, SyntheticEvent } from 'react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import normalizeWheel from 'normalize-wheel';
|
import normalizeWheel from 'normalize-wheel';
|
||||||
import bigInt, { BigInteger } from 'big-integer';
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
@ -76,7 +76,7 @@ interface VirtualScrollerProps<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface VirtualScrollerState<T> {
|
interface VirtualScrollerState<T> {
|
||||||
visibleItems: BigIntOrderedMap<T>;
|
visibleItems: BigInteger[];
|
||||||
scrollbar: number;
|
scrollbar: number;
|
||||||
loaded: {
|
loaded: {
|
||||||
top: boolean;
|
top: boolean;
|
||||||
@ -91,10 +91,9 @@ const log = (level: LogLevel, message: string) => {
|
|||||||
if(logLevel.includes(level)) {
|
if(logLevel.includes(level)) {
|
||||||
console.log(`[${level}]: ${message}`);
|
console.log(`[${level}]: ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ZONE_SIZE = IS_IOS ? 10 : 40;
|
const ZONE_SIZE = IS_IOS ? 10 : 80;
|
||||||
|
|
||||||
|
|
||||||
// nb: in this file, an index refers to a BigInteger and an offset refers to a
|
// nb: in this file, an index refers to a BigInteger and an offset refers to a
|
||||||
@ -114,7 +113,11 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
/**
|
/**
|
||||||
* A map of child refs, used to calculate scroll position
|
* A map of child refs, used to calculate scroll position
|
||||||
*/
|
*/
|
||||||
private childRefs = new BigIntOrderedMap<HTMLElement>();
|
private childRefs = new Map<string, HTMLElement>();
|
||||||
|
/**
|
||||||
|
* A set of child refs which have been unmounted
|
||||||
|
*/
|
||||||
|
private orphans = new Set<string>();
|
||||||
/**
|
/**
|
||||||
* If saving, the bottommost visible element that we pin our scroll to
|
* If saving, the bottommost visible element that we pin our scroll to
|
||||||
*/
|
*/
|
||||||
@ -140,10 +143,12 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
|
|
||||||
private scrollRef: HTMLElement | null = null;
|
private scrollRef: HTMLElement | null = null;
|
||||||
|
|
||||||
|
private cleanupRefInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
constructor(props: VirtualScrollerProps<T>) {
|
constructor(props: VirtualScrollerProps<T>) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
visibleItems: new BigIntOrderedMap(),
|
visibleItems: [],
|
||||||
scrollbar: 0,
|
scrollbar: 0,
|
||||||
loaded: {
|
loaded: {
|
||||||
top: false,
|
top: false,
|
||||||
@ -154,18 +159,33 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
this.updateVisible = this.updateVisible.bind(this);
|
this.updateVisible = this.updateVisible.bind(this);
|
||||||
|
|
||||||
this.invertedKeyHandler = this.invertedKeyHandler.bind(this);
|
this.invertedKeyHandler = this.invertedKeyHandler.bind(this);
|
||||||
this.onScroll = IS_IOS ? _.debounce(this.onScroll.bind(this), 400) : this.onScroll.bind(this);
|
this.onScroll = IS_IOS ? _.debounce(this.onScroll.bind(this), 200) : this.onScroll.bind(this);
|
||||||
this.scrollKeyMap = this.scrollKeyMap.bind(this);
|
this.scrollKeyMap = this.scrollKeyMap.bind(this);
|
||||||
this.setWindow = this.setWindow.bind(this);
|
this.setWindow = this.setWindow.bind(this);
|
||||||
|
this.restore = this.restore.bind(this);
|
||||||
|
this.startOffset = this.startOffset.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.updateVisible(0);
|
this.updateVisible(0);
|
||||||
this.resetScroll();
|
|
||||||
this.loadTop();
|
this.loadTop();
|
||||||
this.loadBottom();
|
this.loadBottom();
|
||||||
|
this.cleanupRefInterval = setInterval(this.cleanupRefs, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
cleanupRefs = () => {
|
||||||
|
if(this.saveDepth > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
[...this.orphans].forEach(o => {
|
||||||
|
const index = bigInt(o);
|
||||||
|
this.childRefs.delete(index.toString());
|
||||||
|
});
|
||||||
|
this.orphans.clear();
|
||||||
|
};
|
||||||
|
|
||||||
// manipulate scrollbar manually, to dodge change detection
|
// manipulate scrollbar manually, to dodge change detection
|
||||||
updateScroll = IS_IOS ? () => {} : _.throttle(() => {
|
updateScroll = IS_IOS ? () => {} : _.throttle(() => {
|
||||||
if(!this.window || !this.scrollRef) {
|
if(!this.window || !this.scrollRef) {
|
||||||
@ -186,27 +206,32 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
|
|
||||||
componentDidUpdate(prevProps: VirtualScrollerProps<T>, _prevState: VirtualScrollerState<T>) {
|
componentDidUpdate(prevProps: VirtualScrollerProps<T>, _prevState: VirtualScrollerState<T>) {
|
||||||
const { id, size, data, offset, pendingSize } = this.props;
|
const { id, size, data, offset, pendingSize } = this.props;
|
||||||
const { visibleItems } = this.state;
|
|
||||||
|
|
||||||
if(size !== prevProps.size || pendingSize !== prevProps.pendingSize) {
|
if(size !== prevProps.size || pendingSize !== prevProps.pendingSize) {
|
||||||
if(this.scrollLocked) {
|
if(this.scrollLocked) {
|
||||||
this.updateVisible(0);
|
this.updateVisible(0);
|
||||||
this.resetScroll();
|
this.resetScroll();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener('keydown', this.invertedKeyHandler);
|
window.removeEventListener('keydown', this.invertedKeyHandler);
|
||||||
|
if(this.cleanupRefInterval) {
|
||||||
|
clearInterval(this.cleanupRefInterval);
|
||||||
|
}
|
||||||
|
this.cleanupRefs();
|
||||||
|
this.childRefs.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
startOffset() {
|
startOffset() {
|
||||||
const startIndex = this.state?.visibleItems?.peekLargest()?.[0];
|
const { data } = this.props;
|
||||||
|
const startIndex = this.state.visibleItems?.[0];
|
||||||
if(!startIndex) {
|
if(!startIndex) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const offset = [...this.props.data].findIndex(([i]) => i.eq(startIndex))
|
const dataList = Array.from(data);
|
||||||
|
const offset = dataList.findIndex(([i]) => i.eq(startIndex))
|
||||||
if(offset === -1) {
|
if(offset === -1) {
|
||||||
// TODO: revisit when we remove nodes for any other reason than
|
// TODO: revisit when we remove nodes for any other reason than
|
||||||
// pending indices being removed
|
// pending indices being removed
|
||||||
@ -226,22 +251,17 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
log('reflow', `from: ${this.startOffset()} to: ${newOffset}`);
|
log('reflow', `from: ${this.startOffset()} to: ${newOffset}`);
|
||||||
|
|
||||||
const { data, onCalculateVisibleItems } = this.props;
|
const { data, onCalculateVisibleItems } = this.props;
|
||||||
const visibleItems = new BigIntOrderedMap<any>(
|
const visibleItems = data.keys().slice(newOffset, newOffset + this.pageSize);
|
||||||
[...data].slice(newOffset, newOffset + this.pageSize)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.save();
|
this.save();
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
visibleItems,
|
visibleItems,
|
||||||
}, () => {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
this.restore();
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.restore();
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollKeyMap(): Map<string, number> {
|
scrollKeyMap(): Map<string, number> {
|
||||||
@ -273,7 +293,6 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
setWindow(element) {
|
setWindow(element) {
|
||||||
if (!element)
|
if (!element)
|
||||||
return;
|
return;
|
||||||
console.log('resetting window');
|
|
||||||
this.save();
|
this.save();
|
||||||
|
|
||||||
if (this.window) {
|
if (this.window) {
|
||||||
@ -286,8 +305,8 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
const { averageHeight } = this.props;
|
const { averageHeight } = this.props;
|
||||||
|
|
||||||
this.window = element;
|
this.window = element;
|
||||||
this.pageSize = Math.floor(element.offsetHeight / Math.floor(averageHeight / 5.5));
|
this.pageSize = Math.floor(element.offsetHeight / Math.floor(averageHeight / 2));
|
||||||
this.pageDelta = Math.floor(this.pageSize / 3);
|
this.pageDelta = Math.floor(this.pageSize / 4);
|
||||||
if (this.props.origin === 'bottom') {
|
if (this.props.origin === 'bottom') {
|
||||||
element.addEventListener('wheel', (event) => {
|
element.addEventListener('wheel', (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@ -333,7 +352,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onScroll(event: UIEvent) {
|
onScroll(event: SyntheticEvent<HTMLElement, ScrollEvent>) {
|
||||||
this.updateScroll();
|
this.updateScroll();
|
||||||
if(!this.window) {
|
if(!this.window) {
|
||||||
// bail if we're going to adjust scroll anyway
|
// bail if we're going to adjust scroll anyway
|
||||||
@ -348,19 +367,19 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
const { scrollTop, scrollHeight } = this.window;
|
const { scrollTop, scrollHeight } = this.window;
|
||||||
|
|
||||||
const startOffset = this.startOffset();
|
const startOffset = this.startOffset();
|
||||||
|
|
||||||
|
const scrollEnd = scrollTop + windowHeight;
|
||||||
if (scrollTop < ZONE_SIZE) {
|
if (scrollTop < ZONE_SIZE) {
|
||||||
log('scroll', `Entered start zone ${scrollTop}`);
|
log('scroll', `Entered start zone ${scrollTop}`);
|
||||||
if (startOffset === 0 && onStartReached) {
|
if (startOffset === 0) {
|
||||||
onStartReached();
|
onStartReached && onStartReached();
|
||||||
|
this.scrollLocked = true;
|
||||||
}
|
}
|
||||||
const newOffset = Math.max(0, startOffset - this.pageDelta);
|
const newOffset = Math.max(0, startOffset - this.pageDelta);
|
||||||
if(newOffset < 10) {
|
if(newOffset < 10) {
|
||||||
this.loadBottom();
|
this.loadBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(newOffset === 0) {
|
|
||||||
this.scrollLocked = true;
|
|
||||||
}
|
|
||||||
if(newOffset !== startOffset) {
|
if(newOffset !== startOffset) {
|
||||||
this.updateVisible(newOffset);
|
this.updateVisible(newOffset);
|
||||||
}
|
}
|
||||||
@ -394,20 +413,37 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
log('bail', 'Deep restore');
|
log('bail', 'Deep restore');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if(this.scrollLocked) {
|
||||||
|
this.resetScroll();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.savedIndex = null;
|
||||||
|
this.savedDistance = 0;
|
||||||
|
this.saveDepth--;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ref = this.childRefs.get(this.savedIndex.toString())
|
||||||
|
if(!ref) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newScrollTop = this.props.origin === 'top'
|
||||||
|
? this.savedDistance + ref.offsetTop
|
||||||
|
: this.window.scrollHeight - ref.offsetTop - this.savedDistance;
|
||||||
|
|
||||||
const ref = this.childRefs.get(this.savedIndex)!;
|
|
||||||
const newScrollTop = this.window.scrollHeight - ref.offsetTop - this.savedDistance;
|
|
||||||
|
|
||||||
this.window.scrollTo(0, newScrollTop);
|
this.window.scrollTo(0, newScrollTop);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this.savedIndex = null;
|
this.savedIndex = null;
|
||||||
this.savedDistance = 0;
|
this.savedDistance = 0;
|
||||||
this.saveDepth--;
|
this.saveDepth--;
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToIndex = (index: BigInteger) => {
|
scrollToIndex = (index: BigInteger) => {
|
||||||
let ref = this.childRefs.get(index);
|
let ref = this.childRefs.get(index.toString());
|
||||||
if(!ref) {
|
if(!ref) {
|
||||||
const offset = [...this.props.data].findIndex(([idx]) => idx.eq(index));
|
const offset = [...this.props.data].findIndex(([idx]) => idx.eq(index));
|
||||||
if(offset === -1) {
|
if(offset === -1) {
|
||||||
@ -415,7 +451,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
}
|
}
|
||||||
this.updateVisible(Math.max(offset - this.pageDelta, 0));
|
this.updateVisible(Math.max(offset - this.pageDelta, 0));
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
ref = this.childRefs.get(index);
|
ref = this.childRefs.get(index.toString());
|
||||||
this.savedIndex = null;
|
this.savedIndex = null;
|
||||||
this.savedDistance = 0;
|
this.savedDistance = 0;
|
||||||
this.saveDepth = 0;
|
this.saveDepth = 0;
|
||||||
@ -435,17 +471,21 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
if(!this.window || this.savedIndex) {
|
if(!this.window || this.savedIndex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.saveDepth++;
|
if(this.saveDepth !== 0) {
|
||||||
if(this.saveDepth !== 1) {
|
|
||||||
console.log('bail', 'deep save');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let bottomIndex: BigInteger | null = null;
|
log('scroll', 'saving...');
|
||||||
|
|
||||||
|
this.saveDepth++;
|
||||||
|
const { visibleItems } = this.state;
|
||||||
|
|
||||||
|
let bottomIndex = visibleItems[visibleItems.length - 1];
|
||||||
const { scrollTop, scrollHeight } = this.window;
|
const { scrollTop, scrollHeight } = this.window;
|
||||||
const topSpacing = scrollHeight - scrollTop;
|
const topSpacing = this.props.origin === 'top' ? scrollTop : scrollHeight - scrollTop;
|
||||||
[...Array.from(this.state.visibleItems)].reverse().forEach(([index, datum]) => {
|
const items = this.props.origin === 'top' ? visibleItems : [...visibleItems].reverse();
|
||||||
const el = this.childRefs.get(index);
|
items.forEach((index) => {
|
||||||
|
const el = this.childRefs.get(index.toString());
|
||||||
if(!el) {
|
if(!el) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -458,24 +498,30 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
if(!bottomIndex) {
|
if(!bottomIndex) {
|
||||||
// weird, shouldn't really happen
|
// weird, shouldn't really happen
|
||||||
this.saveDepth--;
|
this.saveDepth--;
|
||||||
|
log('bail', 'no index found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.savedIndex = bottomIndex;
|
this.savedIndex = bottomIndex;
|
||||||
const ref = this.childRefs.get(bottomIndex)!;
|
const ref = this.childRefs.get(bottomIndex.toString())!;
|
||||||
|
if(!ref) {
|
||||||
|
this.saveDepth--;
|
||||||
|
log('bail', 'missing ref');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { offsetTop } = ref;
|
const { offsetTop } = ref;
|
||||||
this.savedDistance = topSpacing - offsetTop
|
this.savedDistance = topSpacing - offsetTop
|
||||||
}
|
}
|
||||||
|
|
||||||
shiftLayout = { save: this.save.bind(this), restore: this.restore.bind(this) };
|
// disabled until we work out race conditions with loading new nodes
|
||||||
|
shiftLayout = { save: () => {}, restore: () => {} };
|
||||||
|
|
||||||
setRef = (element: HTMLElement | null, index: BigInteger) => {
|
setRef = (element: HTMLElement | null, index: BigInteger) => {
|
||||||
if(element) {
|
if(element) {
|
||||||
this.childRefs.set(index, element);
|
this.childRefs.set(index.toString(), element);
|
||||||
|
this.orphans.delete(index.toString());
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => {
|
this.orphans.add(index.toString());
|
||||||
this.childRefs.delete(index);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -494,12 +540,11 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
|
|
||||||
const isTop = origin === 'top';
|
const isTop = origin === 'top';
|
||||||
|
|
||||||
const indexesToRender = isTop ? visibleItems.keys() : visibleItems.keys().reverse();
|
|
||||||
|
|
||||||
const transform = isTop ? 'scale3d(1, 1, 1)' : 'scale3d(1, -1, 1)';
|
const transform = isTop ? 'scale3d(1, 1, 1)' : 'scale3d(1, -1, 1)';
|
||||||
|
const children = isTop ? visibleItems : [...visibleItems].reverse();
|
||||||
|
|
||||||
const atStart = (this.props.data.peekLargest()?.[0] ?? bigInt.zero).eq(visibleItems.peekLargest()?.[0] || bigInt.zero);
|
const atStart = (this.props.data.peekLargest()?.[0] ?? bigInt.zero).eq(visibleItems?.[0] || bigInt.zero);
|
||||||
const atEnd = this.state.loaded.top;
|
const atEnd = (this.props.data.peekSmallest()?.[0] ?? bigInt.zero).eq(visibleItems?.[visibleItems.length -1 ] || bigInt.zero);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -511,7 +556,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T
|
|||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
</Center>)}
|
</Center>)}
|
||||||
<VirtualContext.Provider value={this.shiftLayout}>
|
<VirtualContext.Provider value={this.shiftLayout}>
|
||||||
{indexesToRender.map(index => (
|
{children.map(index => (
|
||||||
<VirtualChild
|
<VirtualChild
|
||||||
key={index.toString()}
|
key={index.toString()}
|
||||||
setRef={this.setRef}
|
setRef={this.setRef}
|
||||||
@ -544,8 +589,10 @@ function VirtualChild(props: VirtualChildProps) {
|
|||||||
|
|
||||||
const ref = useCallback((el: HTMLElement | null) => {
|
const ref = useCallback((el: HTMLElement | null) => {
|
||||||
setRef(el, props.index);
|
setRef(el, props.index);
|
||||||
}, [setRef, props.index])
|
// VirtualChild should always be keyed on the index, so the index should be
|
||||||
|
// valid for the entire lifecycle of the component, hence no dependencies
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (<Renderer ref={ref} {...rest} />);
|
return <Renderer ref={ref} {...rest} />
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ export class OmniboxResult extends Component {
|
|||||||
<Icon
|
<Icon
|
||||||
display='inline-block'
|
display='inline-block'
|
||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
icon='Inbox'
|
icon='Notifications'
|
||||||
mr='2'
|
mr='2'
|
||||||
size='18px'
|
size='18px'
|
||||||
color={iconFill}
|
color={iconFill}
|
||||||
@ -85,7 +85,7 @@ export class OmniboxResult extends Component {
|
|||||||
<Icon
|
<Icon
|
||||||
display='inline-block'
|
display='inline-block'
|
||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
icon='SignOut'
|
icon='LogOut'
|
||||||
mr='2'
|
mr='2'
|
||||||
size='18px'
|
size='18px'
|
||||||
color={iconFill}
|
color={iconFill}
|
||||||
@ -119,7 +119,7 @@ export class OmniboxResult extends Component {
|
|||||||
<Icon
|
<Icon
|
||||||
display='inline-block'
|
display='inline-block'
|
||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
icon='Inbox'
|
icon='Notifications'
|
||||||
mr='2'
|
mr='2'
|
||||||
size='18px'
|
size='18px'
|
||||||
color={iconFill}
|
color={iconFill}
|
||||||
@ -130,7 +130,7 @@ export class OmniboxResult extends Component {
|
|||||||
<Icon
|
<Icon
|
||||||
display='inline-block'
|
display='inline-block'
|
||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
icon='Users'
|
icon='Messages'
|
||||||
mr='2'
|
mr='2'
|
||||||
size='18px'
|
size='18px'
|
||||||
color={iconFill}
|
color={iconFill}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import useStorage from '~/logic/lib/useStorage';
|
import useStorage, {IuseStorage} from '~/logic/lib/useStorage';
|
||||||
|
|
||||||
const withStorage = (Component, params = {}) => {
|
const withStorage = <P, C extends React.ComponentType<P>>(Component: C, params = {}) => {
|
||||||
return React.forwardRef((props: any, ref) => {
|
return React.forwardRef<C, Omit<C, keyof IuseStorage>>((props, ref) => {
|
||||||
const storage = useStorage(params);
|
const storage = useStorage(params);
|
||||||
|
|
||||||
return <Component ref={ref} {...storage} {...props} />;
|
return <Component ref={ref} {...storage} {...props} />;
|
||||||
|
@ -27,13 +27,13 @@ export function ChannelPopoverRoutesSidebar(props: {
|
|||||||
Preferences
|
Preferences
|
||||||
</Text>
|
</Text>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="Inbox"
|
icon='Notifications'
|
||||||
text="Notifications"
|
text="Notifications"
|
||||||
to={relativePath('/settings#notifications')}
|
to={relativePath('/settings#notifications')}
|
||||||
/>
|
/>
|
||||||
{!isOwner && (
|
{!isOwner && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="SignOut"
|
icon="LogOut"
|
||||||
text="Unsubscribe"
|
text="Unsubscribe"
|
||||||
color="red"
|
color="red"
|
||||||
to={relativePath('/settings#unsubscribe')}
|
to={relativePath('/settings#unsubscribe')}
|
||||||
@ -45,7 +45,7 @@ export function ChannelPopoverRoutesSidebar(props: {
|
|||||||
Administration
|
Administration
|
||||||
</Text>
|
</Text>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="Boot"
|
icon="BootNode"
|
||||||
text="Channel Details"
|
text="Channel Details"
|
||||||
to={relativePath('/settings#details')}
|
to={relativePath('/settings#details')}
|
||||||
/>
|
/>
|
||||||
@ -56,14 +56,14 @@ export function ChannelPopoverRoutesSidebar(props: {
|
|||||||
/>
|
/>
|
||||||
{ isOwner ? (
|
{ isOwner ? (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="TrashCan"
|
icon="X"
|
||||||
text="Archive Channel"
|
text="Archive Channel"
|
||||||
to={relativePath('/settings#archive')}
|
to={relativePath('/settings#archive')}
|
||||||
color="red"
|
color="red"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="TrashCan"
|
icon="X"
|
||||||
text="Archive Channel"
|
text="Archive Channel"
|
||||||
to={relativePath('/settings#remove')}
|
to={relativePath('/settings#remove')}
|
||||||
color="red"
|
color="red"
|
||||||
|
@ -36,7 +36,7 @@ return;
|
|||||||
? 'Permanently delete this group. (All current members will no longer see this group.)'
|
? 'Permanently delete this group. (All current members will no longer see this group.)'
|
||||||
: 'You can rejoin if it is an open group, or if you are reinvited';
|
: 'You can rejoin if it is an open group, or if you are reinvited';
|
||||||
|
|
||||||
const icon = props.owner ? 'X' : 'SignOut';
|
const icon = props.owner ? 'X' : 'LogOut';
|
||||||
const { modal, showModal } = useModal({ modal:
|
const { modal, showModal } = useModal({ modal:
|
||||||
(dismiss: () => void) => {
|
(dismiss: () => void) => {
|
||||||
const onCancel = (e) => {
|
const onCancel = (e) => {
|
||||||
|
@ -58,7 +58,11 @@ function GraphContentWideInner(
|
|||||||
width="fit-content"
|
width="fit-content"
|
||||||
maxWidth="min(500px, 100%)"
|
maxWidth="min(500px, 100%)"
|
||||||
>
|
>
|
||||||
<RemoteContent key={content.url} url={content.url} />
|
<RemoteContent
|
||||||
|
key={content.url}
|
||||||
|
url={content.url}
|
||||||
|
transcluded={transcluded}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
case "mention":
|
case "mention":
|
||||||
|
@ -29,9 +29,9 @@ export function GroupSummary(props: GroupSummaryProps & PropFunc<typeof Col>): R
|
|||||||
width="40px"
|
width="40px"
|
||||||
height="40px"
|
height="40px"
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
flexShrink="0"
|
flexShrink={0}
|
||||||
/>
|
/>
|
||||||
<Col justifyContent="space-between" flexGrow="1" overflow="hidden">
|
<Col justifyContent="space-between" flexGrow={1} overflow="hidden">
|
||||||
<Text
|
<Text
|
||||||
fontSize="1"
|
fontSize="1"
|
||||||
textOverflow="ellipsis"
|
textOverflow="ellipsis"
|
||||||
|
@ -180,7 +180,7 @@ export function GroupSwitcher(props: {
|
|||||||
>
|
>
|
||||||
<Row flexGrow={1} alignItems="center" width='100%' minWidth='0' flexShrink={0}>
|
<Row flexGrow={1} alignItems="center" width='100%' minWidth='0' flexShrink={0}>
|
||||||
{ metadata && <MetadataIcon flexShrink={0} mr="2" metadata={metadata} height="24px" width="24px" /> }
|
{ metadata && <MetadataIcon flexShrink={0} mr="2" metadata={metadata} height="24px" width="24px" /> }
|
||||||
<Text flexShrink={1} lineHeight="1.1" fontSize='2' fontWeight="600" overflow='hidden' display='inline-block' flexShrink='1' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}>{title}</Text>
|
<Text flexShrink={1} lineHeight="1.1" fontSize='2' fontWeight="600" overflow='hidden' display='inline-block' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}>{title}</Text>
|
||||||
</Row>
|
</Row>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Row pr='3' verticalAlign="middle">
|
<Row pr='3' verticalAlign="middle">
|
||||||
|
@ -69,7 +69,7 @@ export function GroupifyForm(props: GroupifyFormProps) {
|
|||||||
onSubmit={onGroupify}
|
onSubmit={onGroupify}
|
||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<Col flexShrink="0" gapY="4" maxWidth="512px">
|
<Col flexShrink={0} gapY="4" maxWidth="512px">
|
||||||
<Box>
|
<Box>
|
||||||
<Text fontWeight="500">Groupify this channel</Text>
|
<Text fontWeight="500">Groupify this channel</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -124,7 +124,6 @@ export function GroupsPane(props: GroupsPaneProps) {
|
|||||||
>
|
>
|
||||||
<Resource
|
<Resource
|
||||||
{...props}
|
{...props}
|
||||||
{...routeProps}
|
|
||||||
association={association}
|
association={association}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
/>
|
/>
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import bigInt from 'big-integer';
|
import bigInt from 'big-integer';
|
||||||
import VirtualScroller from "~/views/components/VirtualScroller";
|
import VirtualScroller from "~/views/components/VirtualScroller";
|
||||||
import PostItem from './PostItem/PostItem';
|
import PostItem from './PostItem/PostItem';
|
||||||
import { Col } from '@tlon/indigo-react';
|
import { Col, Box } from '@tlon/indigo-react';
|
||||||
import { resourceFromPath } from '~/logic/lib/group';
|
import { resourceFromPath } from '~/logic/lib/group';
|
||||||
|
|
||||||
|
|
||||||
@ -15,102 +15,103 @@ export class PostFeed extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.isFetching = false;
|
this.isFetching = false;
|
||||||
this.renderItem = React.forwardRef(({ index, scrollWindow }, ref) => {
|
|
||||||
const {
|
|
||||||
graph,
|
|
||||||
graphPath,
|
|
||||||
api,
|
|
||||||
history,
|
|
||||||
baseUrl,
|
|
||||||
parentNode,
|
|
||||||
grandparentNode,
|
|
||||||
association,
|
|
||||||
group,
|
|
||||||
vip
|
|
||||||
} = this.props;
|
|
||||||
const graphResource = resourceFromPath(graphPath);
|
|
||||||
const node = graph.get(index);
|
|
||||||
if (!node) { return null; }
|
|
||||||
|
|
||||||
const first = graph.peekLargest()?.[0];
|
|
||||||
const post = node?.post;
|
|
||||||
if (!node || !post) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let nodeIndex = parentNode ? parentNode.post.index.split('/').slice(1).map((ind) => {
|
|
||||||
return bigInt(ind);
|
|
||||||
}) : [];
|
|
||||||
|
|
||||||
if (parentNode && index.eq(first ?? bigInt.zero)) {
|
|
||||||
return (
|
|
||||||
<React.Fragment key={index.toString()}>
|
|
||||||
<Col
|
|
||||||
key={index.toString()}
|
|
||||||
mb="3"
|
|
||||||
width="100%"
|
|
||||||
flexShrink={0}
|
|
||||||
>
|
|
||||||
<PostItem
|
|
||||||
key={parentNode.post.index}
|
|
||||||
ref={ref}
|
|
||||||
parentPost={grandparentNode?.post}
|
|
||||||
node={parentNode}
|
|
||||||
parentNode={grandparentNode}
|
|
||||||
graphPath={graphPath}
|
|
||||||
association={association}
|
|
||||||
api={api}
|
|
||||||
index={nodeIndex}
|
|
||||||
baseUrl={baseUrl}
|
|
||||||
history={history}
|
|
||||||
isParent={true}
|
|
||||||
isRelativeTime={false}
|
|
||||||
vip={vip}
|
|
||||||
group={group}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<PostItem
|
|
||||||
ref={ref}
|
|
||||||
node={node}
|
|
||||||
graphPath={graphPath}
|
|
||||||
association={association}
|
|
||||||
api={api}
|
|
||||||
index={[...nodeIndex, index]}
|
|
||||||
baseUrl={baseUrl}
|
|
||||||
history={history}
|
|
||||||
isReply={true}
|
|
||||||
parentPost={parentNode.post}
|
|
||||||
isRelativeTime={true}
|
|
||||||
vip={vip}
|
|
||||||
group={group}
|
|
||||||
/>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PostItem
|
|
||||||
key={index.toString()}
|
|
||||||
ref={ref}
|
|
||||||
node={node}
|
|
||||||
graphPath={graphPath}
|
|
||||||
association={association}
|
|
||||||
api={api}
|
|
||||||
index={[...nodeIndex, index]}
|
|
||||||
baseUrl={baseUrl}
|
|
||||||
history={history}
|
|
||||||
parentPost={parentNode?.post}
|
|
||||||
isReply={!!parentNode}
|
|
||||||
isRelativeTime={true}
|
|
||||||
vip={vip}
|
|
||||||
group={group}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.fetchPosts = this.fetchPosts.bind(this);
|
this.fetchPosts = this.fetchPosts.bind(this);
|
||||||
this.doNotFetch = this.doNotFetch.bind(this);
|
this.doNotFetch = this.doNotFetch.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderItem = React.forwardRef(({ index, scrollWindow }, ref) => {
|
||||||
|
const {
|
||||||
|
graph,
|
||||||
|
graphPath,
|
||||||
|
api,
|
||||||
|
history,
|
||||||
|
baseUrl,
|
||||||
|
parentNode,
|
||||||
|
grandparentNode,
|
||||||
|
association,
|
||||||
|
group,
|
||||||
|
vip
|
||||||
|
} = this.props;
|
||||||
|
const graphResource = resourceFromPath(graphPath);
|
||||||
|
const node = graph.get(index);
|
||||||
|
if (!node) { return null; }
|
||||||
|
|
||||||
|
const first = graph.peekLargest()?.[0];
|
||||||
|
const post = node?.post;
|
||||||
|
if (!node || !post) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let nodeIndex = parentNode ? parentNode.post.index.split('/').slice(1).map((ind) => {
|
||||||
|
return bigInt(ind);
|
||||||
|
}) : [];
|
||||||
|
|
||||||
|
if (parentNode && index.eq(first ?? bigInt.zero)) {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index.toString()}>
|
||||||
|
<Col
|
||||||
|
key={index.toString()}
|
||||||
|
ref={ref}
|
||||||
|
mb="3"
|
||||||
|
width="100%"
|
||||||
|
flexShrink={0}
|
||||||
|
>
|
||||||
|
<PostItem
|
||||||
|
key={parentNode.post.index}
|
||||||
|
parentPost={grandparentNode?.post}
|
||||||
|
node={parentNode}
|
||||||
|
parentNode={grandparentNode}
|
||||||
|
graphPath={graphPath}
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
index={nodeIndex}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
history={history}
|
||||||
|
isParent={true}
|
||||||
|
isRelativeTime={false}
|
||||||
|
vip={vip}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<PostItem
|
||||||
|
node={node}
|
||||||
|
graphPath={graphPath}
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
index={[...nodeIndex, index]}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
history={history}
|
||||||
|
isReply={true}
|
||||||
|
parentPost={parentNode.post}
|
||||||
|
isRelativeTime={true}
|
||||||
|
vip={vip}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box key={index.toString()} ref={ref}>
|
||||||
|
<PostItem
|
||||||
|
node={node}
|
||||||
|
graphPath={graphPath}
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
index={[...nodeIndex, index]}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
history={history}
|
||||||
|
parentPost={parentNode?.post}
|
||||||
|
isReply={!!parentNode}
|
||||||
|
isRelativeTime={true}
|
||||||
|
vip={vip}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
async fetchPosts(newer) {
|
async fetchPosts(newer) {
|
||||||
const { graph, graphPath, api } = this.props;
|
const { graph, graphPath, api } = this.props;
|
||||||
const graphResource = resourceFromPath(graphPath);
|
const graphResource = resourceFromPath(graphPath);
|
||||||
|
@ -24,7 +24,7 @@ function canWrite(props) {
|
|||||||
if(vip === 'host-feed') {
|
if(vip === 'host-feed') {
|
||||||
return isHost(association.group);
|
return isHost(association.group);
|
||||||
}
|
}
|
||||||
|
|
||||||
return isWriter(group, association.resource);
|
return isWriter(group, association.resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ export function PostInput(props) {
|
|||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
) : (
|
) : (
|
||||||
<Icon
|
<Icon
|
||||||
icon='Links'
|
icon='Attachment'
|
||||||
width='16'
|
width='16'
|
||||||
height='16'
|
height='16'
|
||||||
onClick={uploadImage}
|
onClick={uploadImage}
|
||||||
|
@ -46,6 +46,7 @@ export function PostHeader(props) {
|
|||||||
isRelativeTime={true}
|
isRelativeTime={true}
|
||||||
showTime={false}
|
showTime={false}
|
||||||
time={true}
|
time={true}
|
||||||
|
lineHeight='1'
|
||||||
/>
|
/>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
dropWidth="200px"
|
dropWidth="200px"
|
||||||
|
@ -177,7 +177,7 @@ export function JoinGroup(props: JoinGroupProps): ReactElement {
|
|||||||
<Text gray fontSize="1">
|
<Text gray fontSize="1">
|
||||||
Channels
|
Channels
|
||||||
</Text>
|
</Text>
|
||||||
<Box width="100%" flexShrink="0">
|
<Box width="100%" flexShrink={0}>
|
||||||
{Object.values(preview.channels).map(({ metadata }: any) => (
|
{Object.values(preview.channels).map(({ metadata }: any) => (
|
||||||
<Row width="100%">
|
<Row width="100%">
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -11,7 +11,7 @@ import * as Yup from 'yup';
|
|||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { AsyncButton } from '~/views/components/AsyncButton';
|
import { AsyncButton } from '~/views/components/AsyncButton';
|
||||||
import { FormError } from '~/views/components/FormError';
|
import { FormError } from '~/views/components/FormError';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps, useHistory } from 'react-router-dom';
|
||||||
import { stringToSymbol, parentPath, deSig } from '~/logic/lib/util';
|
import { stringToSymbol, parentPath, deSig } from '~/logic/lib/util';
|
||||||
import { resourceFromPath } from '~/logic/lib/group';
|
import { resourceFromPath } from '~/logic/lib/group';
|
||||||
import { Associations } from '@urbit/api/metadata';
|
import { Associations } from '@urbit/api/metadata';
|
||||||
@ -46,12 +46,13 @@ interface NewChannelProps {
|
|||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NewChannel(props: NewChannelProps & RouteComponentProps): ReactElement {
|
export function NewChannel(props: NewChannelProps): ReactElement {
|
||||||
const { history, api, group, workspace } = props;
|
const history = useHistory();
|
||||||
|
const { api, group, workspace } = props;
|
||||||
|
|
||||||
const groups = useGroupState(state => state.groups);
|
const groups = useGroupState(state => state.groups);
|
||||||
const waiter = useWaitForProps({ groups }, 5000);
|
const waiter = useWaitForProps({ groups }, 5000);
|
||||||
|
|
||||||
const onSubmit = async (values: FormSchema, actions) => {
|
const onSubmit = async (values: FormSchema, actions) => {
|
||||||
const name = (values.name) ? values.name : values.moduleType;
|
const name = (values.name) ? values.name : values.moduleType;
|
||||||
const resId: string = stringToSymbol(values.name)
|
const resId: string = stringToSymbol(values.name)
|
||||||
@ -152,7 +153,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps): ReactE
|
|||||||
name="moduleType"
|
name="moduleType"
|
||||||
/>
|
/>
|
||||||
<IconRadio
|
<IconRadio
|
||||||
icon="Publish"
|
icon="Notebook"
|
||||||
label="Notebook"
|
label="Notebook"
|
||||||
id="publish"
|
id="publish"
|
||||||
name="moduleType"
|
name="moduleType"
|
||||||
|
@ -181,9 +181,9 @@ export function Participants(props: {
|
|||||||
mb={2}
|
mb={2}
|
||||||
px={2}
|
px={2}
|
||||||
zIndex={1}
|
zIndex={1}
|
||||||
flexShrink="0"
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
<Row mr="4" flexShrink="0">
|
<Row mr="4" flexShrink={0}>
|
||||||
<Tab
|
<Tab
|
||||||
selected={filter}
|
selected={filter}
|
||||||
setSelected={setFilter}
|
setSelected={setFilter}
|
||||||
@ -206,9 +206,9 @@ export function Participants(props: {
|
|||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
</Row>
|
</Row>
|
||||||
<Col flexShrink="0" width="100%" height="fit-content">
|
<Col flexShrink={0} width="100%" height="fit-content">
|
||||||
<Row alignItems="center" bg="washedGray" borderRadius="1" px="2" my="2">
|
<Row alignItems="center" bg="washedGray" borderRadius="1" px="2" my="2">
|
||||||
<Icon color="gray" icon="MagnifyingGlass" />
|
<Icon color="gray" icon="Search" />
|
||||||
<Input
|
<Input
|
||||||
maxWidth="256px"
|
maxWidth="256px"
|
||||||
color="gray"
|
color="gray"
|
||||||
@ -304,7 +304,7 @@ function Participant(props: {
|
|||||||
}, [api, contact, association]);
|
}, [api, contact, association]);
|
||||||
|
|
||||||
const avatar =
|
const avatar =
|
||||||
contact?.avatar !== null && !hideAvatars ? (
|
contact?.avatar && !hideAvatars ? (
|
||||||
<Image
|
<Image
|
||||||
src={contact.avatar}
|
src={contact.avatar}
|
||||||
height={32}
|
height={32}
|
||||||
|
@ -76,7 +76,7 @@ export function PopoverRoutes(
|
|||||||
<Col gapY="2">
|
<Col gapY="2">
|
||||||
<Text my="1" mx="3" gray>Group</Text>
|
<Text my="1" mx="3" gray>Group</Text>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="Inbox"
|
icon='Notifications'
|
||||||
to={relativeUrl('/settings#notifications')}
|
to={relativeUrl('/settings#notifications')}
|
||||||
text="Notifications"
|
text="Notifications"
|
||||||
/>
|
/>
|
||||||
@ -98,7 +98,7 @@ export function PopoverRoutes(
|
|||||||
text="Group Details"
|
text="Group Details"
|
||||||
/>
|
/>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon="Spaces"
|
icon="Dashboard"
|
||||||
to={relativeUrl('/settings#channels')}
|
to={relativeUrl('/settings#channels')}
|
||||||
text="Channel Management"
|
text="Channel Management"
|
||||||
/>
|
/>
|
||||||
|
@ -15,12 +15,14 @@ import useGroupState from '~/logic/state/group';
|
|||||||
import useContactState from '~/logic/state/contact';
|
import useContactState from '~/logic/state/contact';
|
||||||
import useHarkState from '~/logic/state/hark';
|
import useHarkState from '~/logic/state/hark';
|
||||||
import useMetadataState from '~/logic/state/metadata';
|
import useMetadataState from '~/logic/state/metadata';
|
||||||
|
import {Workspace} from '~/types';
|
||||||
|
|
||||||
type ResourceProps = StoreState & {
|
type ResourceProps = StoreState & {
|
||||||
association: Association;
|
association: Association;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
} & RouteComponentProps;
|
workspace: Workspace;
|
||||||
|
};
|
||||||
|
|
||||||
export function Resource(props: ResourceProps): ReactElement {
|
export function Resource(props: ResourceProps): ReactElement {
|
||||||
const { association, api, notificationsGraphConfig } = props;
|
const { association, api, notificationsGraphConfig } = props;
|
||||||
|
@ -77,7 +77,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
|||||||
fontSize='1'
|
fontSize='1'
|
||||||
mr='12px'
|
mr='12px'
|
||||||
my='1'
|
my='1'
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
display={['block','none']}
|
display={['block','none']}
|
||||||
>
|
>
|
||||||
<Link to={`/~landscape${workspace}`}>
|
<Link to={`/~landscape${workspace}`}>
|
||||||
@ -98,7 +98,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
|||||||
maxWidth={association?.metadata?.description ? ['100%', '50%'] : 'none'}
|
maxWidth={association?.metadata?.description ? ['100%', '50%'] : 'none'}
|
||||||
mr='2'
|
mr='2'
|
||||||
ml='1'
|
ml='1'
|
||||||
flexShrink={['1', '0']}
|
flexShrink={[1, 0]}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
@ -112,7 +112,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
|||||||
mb='0'
|
mb='0'
|
||||||
minWidth='0'
|
minWidth='0'
|
||||||
maxWidth='50%'
|
maxWidth='50%'
|
||||||
flexShrink='1'
|
flexShrink={1}
|
||||||
disableRemoteContent
|
disableRemoteContent
|
||||||
>
|
>
|
||||||
{workspace === '/messages'
|
{workspace === '/messages'
|
||||||
@ -145,7 +145,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
|||||||
return (
|
return (
|
||||||
<Col width='100%' height='100%' overflow='hidden'>
|
<Col width='100%' height='100%' overflow='hidden'>
|
||||||
<Box
|
<Box
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
height='48px'
|
height='48px'
|
||||||
py='2'
|
py='2'
|
||||||
px='2'
|
px='2'
|
||||||
@ -159,7 +159,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
|||||||
display='flex'
|
display='flex'
|
||||||
alignItems='baseline'
|
alignItems='baseline'
|
||||||
width={`calc(100% - ${actionsWidth}px - 16px)`}
|
width={`calc(100% - ${actionsWidth}px - 16px)`}
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
>
|
>
|
||||||
<BackLink />
|
<BackLink />
|
||||||
<Title />
|
<Title />
|
||||||
@ -169,7 +169,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement {
|
|||||||
ml={3}
|
ml={3}
|
||||||
display='flex'
|
display='flex'
|
||||||
alignItems='center'
|
alignItems='center'
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
ref={actionsRef}
|
ref={actionsRef}
|
||||||
>
|
>
|
||||||
{canWrite && <WriterControls />}
|
{canWrite && <WriterControls />}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { ReactElement, useCallback } from 'react';
|
import React, { ReactElement, useCallback } from 'react';
|
||||||
import { FormikHelpers } from 'formik';
|
import { FormikHelpers } from 'formik';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Row,
|
Row,
|
||||||
@ -34,6 +34,7 @@ export function SidebarListHeader(props: {
|
|||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
handleSubmit: (c: SidebarListConfig) => void;
|
handleSubmit: (c: SidebarListConfig) => void;
|
||||||
}): ReactElement {
|
}): ReactElement {
|
||||||
|
const history = useHistory();
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
(values: SidebarListConfig, actions: FormikHelpers<SidebarListConfig>) => {
|
(values: SidebarListConfig, actions: FormikHelpers<SidebarListConfig>) => {
|
||||||
props.handleSubmit(values);
|
props.handleSubmit(values);
|
||||||
@ -65,7 +66,7 @@ export function SidebarListHeader(props: {
|
|||||||
<Box>
|
<Box>
|
||||||
{( !!feedPath) ? (
|
{( !!feedPath) ? (
|
||||||
<Row
|
<Row
|
||||||
flexShrink="0"
|
flexShrink={0}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
py={2}
|
py={2}
|
||||||
@ -74,18 +75,18 @@ export function SidebarListHeader(props: {
|
|||||||
borderBottom={1}
|
borderBottom={1}
|
||||||
borderColor="lightGray"
|
borderColor="lightGray"
|
||||||
backgroundColor={['transparent',
|
backgroundColor={['transparent',
|
||||||
props.history.location.pathname.includes(`/~landscape${groupPath}/feed`)
|
history.location.pathname.includes(`/~landscape${groupPath}/feed`)
|
||||||
? (
|
? (
|
||||||
'washedGray'
|
'washedGray'
|
||||||
) : (
|
) : (
|
||||||
'transparent'
|
'transparent'
|
||||||
)]}
|
)]}
|
||||||
cursor={['pointer', (
|
cursor={(
|
||||||
props.history.location.pathname === `/~landscape${groupPath}/feed`
|
history.location.pathname === `/~landscape${groupPath}/feed`
|
||||||
? 'default' : 'pointer'
|
? 'default' : 'pointer'
|
||||||
)]}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.history.push(`/~landscape${groupPath}/feed`);
|
history.push(`/~landscape${groupPath}/feed`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text>
|
<Text>
|
||||||
@ -98,14 +99,14 @@ export function SidebarListHeader(props: {
|
|||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
<Row
|
<Row
|
||||||
flexShrink="0"
|
flexShrink={0}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
py={2}
|
py={2}
|
||||||
px={3}
|
px={3}
|
||||||
height='48px'
|
height='48px'
|
||||||
>
|
>
|
||||||
<Box flexShrink='0'>
|
<Box flexShrink={0}>
|
||||||
<Text>
|
<Text>
|
||||||
{props.initialValues.hideUnjoined ? `Joined ${noun}` : `All ${noun}`}
|
{props.initialValues.hideUnjoined ? `Joined ${noun}` : `All ${noun}`}
|
||||||
</Text>
|
</Text>
|
||||||
@ -131,7 +132,6 @@ export function SidebarListHeader(props: {
|
|||||||
>
|
>
|
||||||
<NewChannel
|
<NewChannel
|
||||||
api={props.api}
|
api={props.api}
|
||||||
history={props.history}
|
|
||||||
workspace={props.workspace}
|
workspace={props.workspace}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@ -152,7 +152,7 @@ export function SidebarListHeader(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
<Dropdown
|
<Dropdown
|
||||||
flexShrink='0'
|
flexShrink={0}
|
||||||
width="auto"
|
width="auto"
|
||||||
alignY="top"
|
alignY="top"
|
||||||
alignX={['right', 'left']}
|
alignX={['right', 'left']}
|
||||||
|
@ -1,227 +1,79 @@
|
|||||||
import { BigInteger } from "big-integer";
|
import produce, { immerable, castImmutable, castDraft, setAutoFreeze, enablePatches } from 'immer';
|
||||||
import { immerable } from 'immer';
|
import bigInt, { BigInteger } from "big-integer";
|
||||||
|
|
||||||
interface NonemptyNode<V> {
|
setAutoFreeze(false);
|
||||||
n: [BigInteger, V];
|
|
||||||
l: MapNode<V>;
|
enablePatches();
|
||||||
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]> {
|
export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
||||||
private root: MapNode<V> = null;
|
root: Record<string, V> = {}
|
||||||
|
cachedIter: [BigInteger, V][] = [];
|
||||||
[immerable] = true;
|
[immerable] = true;
|
||||||
size: number = 0;
|
|
||||||
|
|
||||||
constructor(initial: [BigInteger, V][] = []) {
|
constructor(items: [BigInteger, V][] = []) {
|
||||||
initial.forEach(([key, val]) => {
|
items.forEach(([key, val]) => {
|
||||||
this.set(key, val);
|
this.set(key, val);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
get size() {
|
||||||
* Retrieve an value for a key
|
return Object.keys(this.root).length;
|
||||||
*/
|
|
||||||
get(key: BigInteger): V | null {
|
|
||||||
const inner = (node: MapNode<V>): V | null => {
|
|
||||||
if (!node) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const [k, v] = node.n;
|
|
||||||
if (key.eq(k)) {
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
|
||||||
return inner(node.l);
|
|
||||||
} else {
|
|
||||||
return inner(node.r);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Put an item by a key
|
|
||||||
*/
|
|
||||||
set(key: BigInteger, value: V): void {
|
|
||||||
|
|
||||||
const inner = (node: MapNode<V>): MapNode<V> => {
|
get(key: BigInteger) {
|
||||||
if (!node) {
|
return this.root[key.toString()] ?? null;
|
||||||
return {
|
}
|
||||||
n: [key, value],
|
|
||||||
l: null,
|
gas(items: [BigInteger, V][]) {
|
||||||
r: null,
|
return produce(this, draft => {
|
||||||
};
|
items.forEach(([key, value]) => {
|
||||||
}
|
draft.root[key.toString()] = castDraft(value);
|
||||||
const [k] = node.n;
|
});
|
||||||
if (key.eq(k)) {
|
draft.generateCachedIter();
|
||||||
this.size--;
|
},
|
||||||
return {
|
(patches) => {
|
||||||
...node,
|
//console.log(`gassed with ${JSON.stringify(patches, null, 2)}`);
|
||||||
n: [k, value],
|
});
|
||||||
};
|
}
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
set(key: BigInteger, value: V) {
|
||||||
const l = inner(node.l);
|
return produce(this, draft => {
|
||||||
if (!l) {
|
draft.root[key.toString()] = castDraft(value);
|
||||||
throw new Error("invariant violation");
|
draft.generateCachedIter();
|
||||||
}
|
});
|
||||||
return {
|
|
||||||
...node,
|
|
||||||
l,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const r = inner(node.r);
|
|
||||||
if (!r) {
|
|
||||||
throw new Error("invariant violation");
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...node, r };
|
|
||||||
};
|
|
||||||
this.size++;
|
|
||||||
this.root = inner(this.root);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all entries
|
|
||||||
*/
|
|
||||||
clear() {
|
clear() {
|
||||||
this.root = null;
|
return produce(this, draft => {
|
||||||
|
draft.cachedIter = [];
|
||||||
|
draft.root = {}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
has(key: BigInteger) {
|
||||||
* Predicate testing if map contains key
|
return key.toString() in this.root;
|
||||||
*/
|
|
||||||
has(key: BigInteger): boolean {
|
|
||||||
const inner = (node: MapNode<V>): boolean => {
|
|
||||||
if (!node) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const [k] = node.n;
|
|
||||||
|
|
||||||
if (k.eq(key)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (key.gt(k)) {
|
|
||||||
return inner(node.l);
|
|
||||||
}
|
|
||||||
return inner(node.r);
|
|
||||||
};
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove value associated with key, returning whether that key
|
|
||||||
* existed in the first place
|
|
||||||
*/
|
|
||||||
delete(key: BigInteger) {
|
delete(key: BigInteger) {
|
||||||
const inner = (node: MapNode<V>): [boolean, MapNode<V>] => {
|
return produce(this, draft => {
|
||||||
if (!node) {
|
delete draft.root[key.toString()];
|
||||||
return [false, null];
|
draft.cachedIter = draft.cachedIter.filter(([x]) => x.eq(key));
|
||||||
}
|
});
|
||||||
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>): MapNode<V> => {
|
|
||||||
if (!node.l) {
|
|
||||||
return node.r;
|
|
||||||
}
|
|
||||||
if (!node.r) {
|
|
||||||
return node.l;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...node.l,
|
|
||||||
r: inner(node.r),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
return inner(nod);
|
|
||||||
}
|
|
||||||
|
|
||||||
peekLargest(): [BigInteger, V] | undefined {
|
|
||||||
const inner = (node: MapNode<V>): [BigInteger, V] | undefined => {
|
|
||||||
if(!node) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if(node.l) {
|
|
||||||
return inner(node.l);
|
|
||||||
}
|
|
||||||
return node.n;
|
|
||||||
}
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
peekSmallest(): [BigInteger, V] | undefined {
|
|
||||||
const inner = (node: MapNode<V>): [BigInteger, V] | undefined => {
|
|
||||||
if(!node) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
if(node.r) {
|
|
||||||
return inner(node.r);
|
|
||||||
}
|
|
||||||
return node.n;
|
|
||||||
}
|
|
||||||
return inner(this.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
keys(): BigInteger[] {
|
|
||||||
const list = Array.from(this);
|
|
||||||
return list.map(([key]) => key);
|
|
||||||
}
|
|
||||||
|
|
||||||
forEach(f: (value: V, key: BigInteger) => void) {
|
|
||||||
const list = Array.from(this);
|
|
||||||
return list.forEach(([k,v]) => f(v,k));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator](): IterableIterator<[BigInteger, V]> {
|
[Symbol.iterator](): IterableIterator<[BigInteger, V]> {
|
||||||
let result: [BigInteger, V][] = [];
|
|
||||||
const inner = (node: MapNode<V>) => {
|
|
||||||
if (!node) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
inner(node.l);
|
|
||||||
result.push(node.n);
|
|
||||||
inner(node.r);
|
|
||||||
};
|
|
||||||
inner(this.root);
|
|
||||||
|
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
|
let result = [...this.cachedIter];
|
||||||
return {
|
return {
|
||||||
[Symbol.iterator]: this[Symbol.iterator],
|
[Symbol.iterator]: this[Symbol.iterator],
|
||||||
next: (): IteratorResult<[BigInteger, V]> => {
|
next: (): IteratorResult<[BigInteger, V]> => {
|
||||||
@ -232,4 +84,27 @@ 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 Array.from(this).map(([k,v]) => k);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateCachedIter() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user