From 1c1377ab7ddbc74c75306dbc8e699bf12ae854b0 Mon Sep 17 00:00:00 2001 From: Matilde Park Date: Thu, 15 Apr 2021 15:19:48 -0400 Subject: [PATCH 01/69] landscape: bump indigo-react to 1.2.20 --- pkg/interface/config/webpack.dev.js | 2 +- pkg/interface/config/webpack.prod.js | 2 +- pkg/interface/package-lock.json | Bin 429608 -> 471844 bytes pkg/interface/package.json | 4 ++-- pkg/interface/src/logic/lib/util.ts | 4 ++-- .../views/apps/chat/components/ChatInput.tsx | 2 +- pkg/interface/src/views/apps/launch/app.js | 2 +- .../src/views/apps/settings/settings.tsx | 2 +- .../src/views/components/StatusBar.js | 2 +- .../views/components/leap/OmniboxResult.js | 6 +++--- .../ChannelPopoverRoutes/Sidebar.tsx | 10 +++++----- .../landscape/components/DeleteGroup.tsx | 2 +- .../components/Home/Post/PostInput.js | 4 ++-- .../views/landscape/components/NewChannel.tsx | 4 ++-- .../landscape/components/Participants.tsx | 2 +- .../landscape/components/PopoverRoutes.tsx | 4 ++-- 16 files changed, 26 insertions(+), 26 deletions(-) diff --git a/pkg/interface/config/webpack.dev.js b/pkg/interface/config/webpack.dev.js index 736002438..27c5e4e63 100644 --- a/pkg/interface/config/webpack.dev.js +++ b/pkg/interface/config/webpack.dev.js @@ -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, diff --git a/pkg/interface/config/webpack.prod.js b/pkg/interface/config/webpack.prod.js index 8d9f52709..e8f080c93 100644 --- a/pkg/interface/config/webpack.prod.js +++ b/pkg/interface/config/webpack.prod.js @@ -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, diff --git a/pkg/interface/package-lock.json b/pkg/interface/package-lock.json index eb9e97a959c34666425ef5393de036f69b60436c..0cffc3e731601323a0b19c972704b46b201af18d 100644 GIT binary patch delta 41152 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 delta 30394 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 Ij_Y^)54mWlhX4Qo diff --git a/pkg/interface/package.json b/pkg/interface/package.json index ae2d1129c..f5f26c25b 100644 --- a/pkg/interface/package.json +++ b/pkg/interface/package.json @@ -9,8 +9,8 @@ "@reach/menu-button": "^0.10.5", "@reach/tabs": "^0.10.5", "@tlon/indigo-dark": "^1.0.6", - "@tlon/indigo-light": "^1.0.6", - "@tlon/indigo-react": "^1.2.19", + "@tlon/indigo-light": "^1.0.7", + "@tlon/indigo-react": "^1.2.20", "@tlon/sigil-js": "^1.4.3", "@urbit/api": "file:../npm/api", "any-ascii": "^0.1.7", diff --git a/pkg/interface/src/logic/lib/util.ts b/pkg/interface/src/logic/lib/util.ts index 6e6ef0740..d3c7107c5 100644 --- a/pkg/interface/src/logic/lib/util.ts +++ b/pkg/interface/src/logic/lib/util.ts @@ -30,7 +30,7 @@ export const getModuleIcon = (mod: string) => { } if (mod === 'post') { - return 'Spaces'; + return 'Dashboard'; } return _.capitalize(mod); @@ -407,7 +407,7 @@ export const useHovering = (): useHoveringInterface => { onMouseLeave, }), [onMouseLeave, onMouseOver]); - + return useMemo(() => ({ hovering, bind }), [hovering, bind]); }; diff --git a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx index 5fc6c4237..1a170ef45 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx @@ -188,7 +188,7 @@ class ChatInput extends Component { ) : ( diff --git a/pkg/interface/src/views/apps/launch/app.js b/pkg/interface/src/views/apps/launch/app.js index 400e2e1b9..ae4373880 100644 --- a/pkg/interface/src/views/apps/launch/app.js +++ b/pkg/interface/src/views/apps/launch/app.js @@ -220,7 +220,7 @@ export default function LaunchApp(props) { diff --git a/pkg/interface/src/views/components/StatusBar.js b/pkg/interface/src/views/components/StatusBar.js index 3dcec9e5a..f2bc362db 100644 --- a/pkg/interface/src/views/components/StatusBar.js +++ b/pkg/interface/src/views/components/StatusBar.js @@ -83,7 +83,7 @@ const StatusBar = (props) => { onClick={() => history.push('/')} {...props} > - + toggleOmnibox()}> {!doNotDisturb && (notificationsCount > 0 || invites.length > 0) && ( diff --git a/pkg/interface/src/views/components/leap/OmniboxResult.js b/pkg/interface/src/views/components/leap/OmniboxResult.js index d49a971e5..87cdc0768 100644 --- a/pkg/interface/src/views/components/leap/OmniboxResult.js +++ b/pkg/interface/src/views/components/leap/OmniboxResult.js @@ -65,7 +65,7 @@ export class OmniboxResult extends Component { {!isOwner && ( @@ -56,14 +56,14 @@ export function ChannelPopoverRoutesSidebar(props: { /> { isOwner ? ( ) : ( void) => { const onCancel = (e) => { diff --git a/pkg/interface/src/views/landscape/components/Home/Post/PostInput.js b/pkg/interface/src/views/landscape/components/Home/Post/PostInput.js index 5ee0e4e9a..2ac349633 100644 --- a/pkg/interface/src/views/landscape/components/Home/Post/PostInput.js +++ b/pkg/interface/src/views/landscape/components/Home/Post/PostInput.js @@ -24,7 +24,7 @@ function canWrite(props) { if(vip === 'host-feed') { return isHost(association.group); } - + return isWriter(group, association.resource); } @@ -147,7 +147,7 @@ export function PostInput(props) { ) : ( state.groups); const waiter = useWaitForProps({ groups }, 5000); - + const onSubmit = async (values: FormSchema, actions) => { const name = (values.name) ? values.name : values.moduleType; const resId: string = stringToSymbol(values.name) @@ -152,7 +152,7 @@ export function NewChannel(props: NewChannelProps & RouteComponentProps): ReactE name="moduleType" /> - + Group @@ -98,7 +98,7 @@ export function PopoverRoutes( text="Group Details" /> From 87feb741848ef960209f5dd69eb130fd7b28715d Mon Sep 17 00:00:00 2001 From: Logan Allen Date: Mon, 19 Apr 2021 12:53:26 -0500 Subject: [PATCH 02/69] interface: group feed no longer shows in omnibox --- pkg/interface/src/logic/lib/omnibox.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/interface/src/logic/lib/omnibox.js b/pkg/interface/src/logic/lib/omnibox.js index 8bc529555..1bb22528d 100644 --- a/pkg/interface/src/logic/lib/omnibox.js +++ b/pkg/interface/src/logic/lib/omnibox.js @@ -99,9 +99,11 @@ export default function index(contacts, associations, apps, currentGroup, groups Object.keys(associations).filter((e) => { // skip apps with no metadata return Object.keys(associations[e]).length > 0; - }).map((e) => { - // iterate through each app's metadata object - Object.keys(associations[e]).map((association) => { + }).map((e) => { + // iterate through each app's metadata object + Object.keys(associations[e]) + .filter((association) => !associations[e][association].metadata.hidden) + .map((association) => { const each = associations[e][association]; let title = each.resource; if (each.metadata.title !== '') { From 1ab925f84cbe72c224f78a5d828dec8a429b5b5a Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Tue, 20 Apr 2021 13:11:39 +1000 Subject: [PATCH 03/69] interface: unify BigIntOrderedMap imports --- .../src/logic/lib/BigIntOrderedMap.ts | 234 ------------------ pkg/interface/src/logic/lib/publish.ts | 2 +- .../src/logic/reducers/graph-update.ts | 2 +- .../src/logic/reducers/hark-update.ts | 2 +- 4 files changed, 3 insertions(+), 237 deletions(-) delete mode 100644 pkg/interface/src/logic/lib/BigIntOrderedMap.ts diff --git a/pkg/interface/src/logic/lib/BigIntOrderedMap.ts b/pkg/interface/src/logic/lib/BigIntOrderedMap.ts deleted file mode 100644 index 1e6a41e40..000000000 --- a/pkg/interface/src/logic/lib/BigIntOrderedMap.ts +++ /dev/null @@ -1,234 +0,0 @@ -import bigInt, { BigInteger } from 'big-integer'; -import { immerable } from 'immer'; - -interface NonemptyNode { - n: [BigInteger, V]; - l: MapNode; - r: MapNode; -} - -type MapNode = NonemptyNode | null; - -/** - * An implementation of ordered maps for JS - * Plagiarised wholesale from sys/zuse - */ -export class BigIntOrderedMap implements Iterable<[BigInteger, V]> { - private root: MapNode = 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) => { - 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) => { - 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) => { - 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): [boolean, MapNode] => { - 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): MapNode { - const inner = (node: NonemptyNode) => { - 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) => { - 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) => { - 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) => { - 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 }; - } - }; - } -} diff --git a/pkg/interface/src/logic/lib/publish.ts b/pkg/interface/src/logic/lib/publish.ts index ec2fc05fd..672cf0d50 100644 --- a/pkg/interface/src/logic/lib/publish.ts +++ b/pkg/interface/src/logic/lib/publish.ts @@ -1,7 +1,7 @@ import { Post, GraphNode, TextContent, Graph, NodeMap } from '@urbit/api'; import { buntPost } from '~/logic/lib/post'; import { unixToDa } from '~/logic/lib/util'; -import { BigIntOrderedMap } from './BigIntOrderedMap'; +import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap"; import bigInt, { BigInteger } from 'big-integer'; export function newPost( diff --git a/pkg/interface/src/logic/reducers/graph-update.ts b/pkg/interface/src/logic/reducers/graph-update.ts index 06e14eade..50c310af0 100644 --- a/pkg/interface/src/logic/reducers/graph-update.ts +++ b/pkg/interface/src/logic/reducers/graph-update.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap"; +import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap"; import bigInt, { BigInteger } from "big-integer"; import useGraphState, { GraphState } from '../state/graph'; import { reduceState } from '../state/base'; diff --git a/pkg/interface/src/logic/reducers/hark-update.ts b/pkg/interface/src/logic/reducers/hark-update.ts index fbd704755..8ba97f91a 100644 --- a/pkg/interface/src/logic/reducers/hark-update.ts +++ b/pkg/interface/src/logic/reducers/hark-update.ts @@ -8,7 +8,7 @@ import { } from '@urbit/api'; import { makePatDa } from '~/logic/lib/util'; import _ from 'lodash'; -import { BigIntOrderedMap } from '../lib/BigIntOrderedMap'; +import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap"; import useHarkState, { HarkState } from '../state/hark'; import { compose } from 'lodash/fp'; import { reduceState } from '../state/base'; From fc955ab83e061223e13865558d6403b38c80da0f Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Tue, 20 Apr 2021 13:31:57 +1000 Subject: [PATCH 04/69] interface: new BigIntOrderedMap New ordered map implementation that is faster and more space efficient. Stores the map and keys as a flat POJO, sorting them upon iteration. The sorting cost is only typically paid once per render however as the sorted array is cached until the map is mutated. This implementation appears to play nicer with immer's structural sharing, reducing the incidence of 'props are equal by value, but not by reference'. --- pkg/interface/src/logic/state/base.ts | 4 +- pkg/npm/api/lib/BigIntOrderedMap.ts | 261 ++++++-------------------- 2 files changed, 64 insertions(+), 201 deletions(-) diff --git a/pkg/interface/src/logic/state/base.ts b/pkg/interface/src/logic/state/base.ts index b12b9f203..29fea66ce 100644 --- a/pkg/interface/src/logic/state/base.ts +++ b/pkg/interface/src/logic/state/base.ts @@ -1,8 +1,10 @@ -import produce from "immer"; +import produce, { setAutoFreeze } from "immer"; import { compose } from "lodash/fp"; import create, { State, UseStore } from "zustand"; import { persist, devtools } from "zustand/middleware"; +setAutoFreeze(false); + export const stateSetter = ( fn: (state: StateType) => void, diff --git a/pkg/npm/api/lib/BigIntOrderedMap.ts b/pkg/npm/api/lib/BigIntOrderedMap.ts index d7cd00bdf..ce6e0a253 100644 --- a/pkg/npm/api/lib/BigIntOrderedMap.ts +++ b/pkg/npm/api/lib/BigIntOrderedMap.ts @@ -1,227 +1,61 @@ -import { BigInteger } from "big-integer"; -import { immerable } from 'immer'; +import _ from 'lodash'; +import bigInt, { BigInteger } from "big-integer"; -interface NonemptyNode { - n: [BigInteger, V]; - l: MapNode; - r: MapNode; +function sortBigInt(a: BigInteger, b: BigInteger) { + if (a.lt(b)) { + return 1; + } else if (a.eq(b)) { + return 0; + } else { + return -1; + } } - -type MapNode = NonemptyNode | null; - -/** - * An implementation of ordered maps for JS - * Plagiarised wholesale from sys/zuse - */ export default class BigIntOrderedMap implements Iterable<[BigInteger, V]> { - private root: MapNode = null; - [immerable] = true; - size: number = 0; + private root: Record = {} + private cachedIter: [BigInteger, V][] | null = null; - constructor(initial: [BigInteger, V][] = []) { - initial.forEach(([key, val]) => { + constructor(items: [BigInteger, V][] = []) { + _.forEach(items, ([key, val]) => { this.set(key, val); }); + this.generateCachedIter(); } - /** - * Retrieve an value for a key - */ - get(key: BigInteger): V | null { - const inner = (node: MapNode): V | null => { - if (!node) { - return null; - } - const [k, v] = node.n; - if (key.eq(k)) { - return v; - } - if (key.gt(k)) { - return inner(node.l); - } else { - return inner(node.r); - } - }; - - return inner(this.root); + get size() { + return this.cachedIter?.length ?? Object.keys(this.root).length; } - /** - * Put an item by a key - */ - set(key: BigInteger, value: V): void { - const inner = (node: MapNode): MapNode => { - if (!node) { - return { - n: [key, value], - l: null, - r: null, - }; - } - const [k] = node.n; - if (key.eq(k)) { - this.size--; - return { - ...node, - n: [k, value], - }; - } - if (key.gt(k)) { - const l = inner(node.l); - if (!l) { - throw new Error("invariant violation"); - } - return { - ...node, - l, - }; - } - const r = inner(node.r); - if (!r) { - throw new Error("invariant violation"); - } - - return { ...node, r }; - }; - this.size++; - this.root = inner(this.root); + get(key: BigInteger) { + return this.root[key.toString()] ?? null; + } + + set(key: BigInteger, value: V) { + this.root[key.toString()] = value; + this.cachedIter = null; } - /** - * Remove all entries - */ clear() { - this.root = null; + this.cachedIter = null; + this.root = {} } - /** - * Predicate testing if map contains key - */ - has(key: BigInteger): boolean { - const inner = (node: MapNode): boolean => { - if (!node) { - return false; - } - const [k] = node.n; - - if (k.eq(key)) { - return true; - } - if (key.gt(k)) { - return inner(node.l); - } - return inner(node.r); - }; - return inner(this.root); + has(key: BigInteger) { + return key.toString() in this.root; } - /** - * Remove value associated with key, returning whether that key - * existed in the first place - */ delete(key: BigInteger) { - const inner = (node: MapNode): [boolean, MapNode] => { - if (!node) { - return [false, null]; - } - const [k] = node.n; - if (k.eq(key)) { - return [true, this.nip(node)]; - } - if (key.gt(k)) { - const [bool, l] = inner(node.l); - return [ - bool, - { - ...node, - l, - }, - ]; - } - - const [bool, r] = inner(node.r); - return [ - bool, - { - ...node, - r, - }, - ]; - }; - const [ret, newRoot] = inner(this.root); - if(ret) { - this.size--; + const had = this.has(key); + if(had) { + delete this.root[key.toString()]; + this.cachedIter = null; } - this.root = newRoot; - return ret; - } - - private nip(nod: NonemptyNode): MapNode { - const inner = (node: NonemptyNode): MapNode => { - 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): [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): [BigInteger, V] | undefined => { - if(!node) { - return undefined; - } - if(node.r) { - return inner(node.r); - } - return node.n; - } - return inner(this.root); - } - - keys(): BigInteger[] { - const list = Array.from(this); - return list.map(([key]) => key); - } - - forEach(f: (value: V, key: BigInteger) => void) { - const list = Array.from(this); - return list.forEach(([k,v]) => f(v,k)); + return had; } [Symbol.iterator](): IterableIterator<[BigInteger, V]> { - let result: [BigInteger, V][] = []; - const inner = (node: MapNode) => { - if (!node) { - return; - } - inner(node.l); - result.push(node.n); - inner(node.r); - }; - inner(this.root); - let idx = 0; + const result = this.generateCachedIter(); return { [Symbol.iterator]: this[Symbol.iterator], next: (): IteratorResult<[BigInteger, V]> => { @@ -232,4 +66,31 @@ export default class BigIntOrderedMap implements Iterable<[BigInteger, V]> { }, }; } + + peekLargest() { + const sorted = Array.from(this); + return sorted[0] as [BigInteger, V] | null; + } + + peekSmallest() { + const sorted = Array.from(this); + return sorted[sorted.length - 1] as [BigInteger, V] | null; + } + + keys() { + return Object.keys(this.root).map(k => bigInt(k)).sort(sortBigInt) + } + + private generateCachedIter() { + if(this.cachedIter) { + return this.cachedIter; + } + const result = Object.keys(this.root).map(key => { + const num = bigInt(key); + return [num, this.root[key]] as [BigInteger, V]; + }).sort(([a], [b]) => sortBigInt(a,b)); + this.cachedIter = result; + return result; + } } + From ec7c456b196e88e4ce86f4859976c77772ca7999 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Tue, 20 Apr 2021 18:39:56 +1000 Subject: [PATCH 05/69] interface: mark bigintorderedmap as immerable --- pkg/npm/api/lib/BigIntOrderedMap.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/npm/api/lib/BigIntOrderedMap.ts b/pkg/npm/api/lib/BigIntOrderedMap.ts index ce6e0a253..605793118 100644 --- a/pkg/npm/api/lib/BigIntOrderedMap.ts +++ b/pkg/npm/api/lib/BigIntOrderedMap.ts @@ -1,4 +1,5 @@ import _ from 'lodash'; +import { immerable } from 'immer'; import bigInt, { BigInteger } from "big-integer"; function sortBigInt(a: BigInteger, b: BigInteger) { @@ -13,6 +14,7 @@ function sortBigInt(a: BigInteger, b: BigInteger) { export default class BigIntOrderedMap implements Iterable<[BigInteger, V]> { private root: Record = {} private cachedIter: [BigInteger, V][] | null = null; + [immerable] = true; constructor(items: [BigInteger, V][] = []) { _.forEach(items, ([key, val]) => { From eb8d9b3f6044de9adffdfe589fd0f3eda17ad1f9 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Wed, 21 Apr 2021 16:39:35 +1000 Subject: [PATCH 06/69] BigIntOrderedMap: remove lodash dependency --- pkg/npm/api/lib/BigIntOrderedMap.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/npm/api/lib/BigIntOrderedMap.ts b/pkg/npm/api/lib/BigIntOrderedMap.ts index 605793118..630445c2f 100644 --- a/pkg/npm/api/lib/BigIntOrderedMap.ts +++ b/pkg/npm/api/lib/BigIntOrderedMap.ts @@ -1,4 +1,3 @@ -import _ from 'lodash'; import { immerable } from 'immer'; import bigInt, { BigInteger } from "big-integer"; @@ -17,7 +16,7 @@ export default class BigIntOrderedMap implements Iterable<[BigInteger, V]> { [immerable] = true; constructor(items: [BigInteger, V][] = []) { - _.forEach(items, ([key, val]) => { + items.forEach(([key, val]) => { this.set(key, val); }); this.generateCachedIter(); From ab6239bf59210d21045dbeee94f50c5aff3e8fc4 Mon Sep 17 00:00:00 2001 From: Matilde Park Date: Wed, 21 Apr 2021 11:20:14 -0400 Subject: [PATCH 07/69] landscape: pass background colors to actions Fixes urbit/landscape#802 --- pkg/interface/src/views/apps/notifications/notifications.tsx | 3 ++- pkg/interface/src/views/apps/publish/components/Note.tsx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/interface/src/views/apps/notifications/notifications.tsx b/pkg/interface/src/views/apps/notifications/notifications.tsx index cb916dd68..babe13e60 100644 --- a/pkg/interface/src/views/apps/notifications/notifications.tsx +++ b/pkg/interface/src/views/apps/notifications/notifications.tsx @@ -92,6 +92,7 @@ export default function NotificationsScreen(props: any): ReactElement { Mark All Read @@ -106,7 +107,7 @@ export default function NotificationsScreen(props: any): ReactElement { {!view && } diff --git a/pkg/interface/src/views/apps/publish/components/Note.tsx b/pkg/interface/src/views/apps/publish/components/Note.tsx index 3383b60da..17150dae2 100644 --- a/pkg/interface/src/views/apps/publish/components/Note.tsx +++ b/pkg/interface/src/views/apps/publish/components/Note.tsx @@ -73,14 +73,14 @@ export function Note(props: NoteProps & RouteComponentProps) { if (window.ship === note?.post?.author) { adminLinks.push( - Update + Update ) }; if (window.ship === note?.post?.author || ourRole === "admin") { adminLinks.push( - + Delete ) From 156d0d380d7bd18f8eeee9c418b1287eb296b1ed Mon Sep 17 00:00:00 2001 From: Matilde Park Date: Wed, 21 Apr 2021 11:44:00 -0400 Subject: [PATCH 08/69] ColorInput: remove "#" from input value Fixes urbit/landscape#803 --- pkg/interface/src/views/components/ColorInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/interface/src/views/components/ColorInput.tsx b/pkg/interface/src/views/components/ColorInput.tsx index 0a4036e7c..adbe0532f 100644 --- a/pkg/interface/src/views/components/ColorInput.tsx +++ b/pkg/interface/src/views/components/ColorInput.tsx @@ -73,7 +73,7 @@ export function ColorInput(props: ColorInputProps) { height='100%' alignSelf='stretch' onChange={onChange} - value={`#${padded}`} + value={padded} disabled={disabled || false} type='color' opacity={0} From 8124f63215f8167a92c50ce76449e08229a777a2 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Wed, 21 Apr 2021 14:58:16 -0400 Subject: [PATCH 09/69] chat: flush borders + round corners for images --- pkg/interface/src/views/components/RemoteContent.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/interface/src/views/components/RemoteContent.tsx b/pkg/interface/src/views/components/RemoteContent.tsx index 648ab72b1..7825b3598 100644 --- a/pkg/interface/src/views/components/RemoteContent.tsx +++ b/pkg/interface/src/views/components/RemoteContent.tsx @@ -145,7 +145,7 @@ return; )} { e.stopPropagation(); }} href={this.props.url} whiteSpace="nowrap" @@ -205,6 +205,7 @@ return; height="100%" width="100%" objectFit="contain" + borderRadius={2} {...imageProps} {...props} /> From 6d9508abfce86eed4186816269447f0c5d62b2bc Mon Sep 17 00:00:00 2001 From: James Acklin Date: Wed, 21 Apr 2021 14:59:16 -0400 Subject: [PATCH 10/69] chat: flush borders + round corners for images --- pkg/interface/src/views/components/RemoteContent.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/interface/src/views/components/RemoteContent.tsx b/pkg/interface/src/views/components/RemoteContent.tsx index 7825b3598..8e0ee5bf6 100644 --- a/pkg/interface/src/views/components/RemoteContent.tsx +++ b/pkg/interface/src/views/components/RemoteContent.tsx @@ -128,7 +128,7 @@ return; }); } - wrapInLink(contents, textOnly = false, unfold = false, unfoldEmbed = null, embedContainer = null) { + wrapInLink(contents, textOnly = false, unfold = false, unfoldEmbed = null, embedContainer = null, flushPadding = false) { const { style } = this.props; return ( @@ -145,7 +145,7 @@ return; )} { e.stopPropagation(); }} href={this.props.url} whiteSpace="nowrap" @@ -208,7 +208,7 @@ return; borderRadius={2} {...imageProps} {...props} - /> + />, false, false, null, null, true ); } else if (isAudio && remoteContentPolicy.audioShown) { return ( @@ -272,7 +272,6 @@ return; display={this.state.unfold ? 'block' : 'none'} className='embed-container' style={style} - flexShrink={0} onLoad={this.onLoad} {...oembedProps} {...props} From 3d40d37e801884d1a7950a630421dcf0a98db5b7 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Wed, 21 Apr 2021 15:00:14 -0400 Subject: [PATCH 11/69] interface: new messages icon partially fixes urbit/landscape#560 --- pkg/interface/src/views/components/StatusBar.js | 2 +- pkg/interface/src/views/components/leap/OmniboxResult.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/interface/src/views/components/StatusBar.js b/pkg/interface/src/views/components/StatusBar.js index f2bc362db..66e12843f 100644 --- a/pkg/interface/src/views/components/StatusBar.js +++ b/pkg/interface/src/views/components/StatusBar.js @@ -134,7 +134,7 @@ const StatusBar = (props) => { mr={2} onClick={() => props.history.push('/~landscape/messages')} > - + Date: Wed, 21 Apr 2021 15:02:57 -0400 Subject: [PATCH 12/69] chat: pointer cursor for attachment + dojo in input --- pkg/interface/src/views/apps/chat/components/ChatInput.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx index 1a170ef45..dab2c1169 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx @@ -189,6 +189,7 @@ class ChatInput extends Component { ) : ( @@ -201,6 +202,7 @@ class ChatInput extends Component { From d585c58b8f46db34899daffba94d57702ed42ab0 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Wed, 21 Apr 2021 15:41:14 -0400 Subject: [PATCH 13/69] chat: click name to copy ~patp in ProfileOverlay --- pkg/interface/src/views/components/ProfileOverlay.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pkg/interface/src/views/components/ProfileOverlay.tsx b/pkg/interface/src/views/components/ProfileOverlay.tsx index 2e9432748..0e9218604 100644 --- a/pkg/interface/src/views/components/ProfileOverlay.tsx +++ b/pkg/interface/src/views/components/ProfileOverlay.tsx @@ -23,6 +23,7 @@ import RichText from './RichText'; import { ProfileStatus } from './ProfileStatus'; import useSettingsState from '~/logic/state/settings'; import {useOutsideClick} from '~/logic/lib/useOutsideClick'; +import {useCopy} from '~/logic/lib/useCopy'; import {useContact} from '~/logic/state/contact'; import {useHistory} from 'react-router-dom'; import {Portal} from './Portal'; @@ -59,6 +60,7 @@ const ProfileOverlay = (props: ProfileOverlayProps) => { const hideAvatars = useSettingsState(state => state.calm.hideAvatars); const hideNicknames = useSettingsState(state => state.calm.hideNicknames); const isOwn = useMemo(() => window.ship === ship, [ship]); + const { copyDisplay, doCopy, didCopy } = useCopy(`~${ship}`); const contact = useContact(`~${ship}`) const color = `#${uxToHex(contact?.color ?? '0x0')}`; @@ -188,9 +190,18 @@ const ProfileOverlay = (props: ProfileOverlayProps) => { overflow='hidden' whiteSpace='pre' marginBottom='0' + cursor='pointer' + display={didCopy ? 'none' : 'block'} + onClick={doCopy} > {showNickname ? contact?.nickname : cite(ship)} + + {copyDisplay} + {isOwn ? ( Date: Wed, 21 Apr 2021 15:46:29 -0400 Subject: [PATCH 14/69] chat: adjust ChatMessage color --- .../apps/chat/components/ChatMessage.tsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index 306d221ef..82f96e9bc 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx @@ -569,7 +569,7 @@ export const MessagePlaceholder = ({ > From b3f31516d290d00bed2e2c7927be51115cdd7f97 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Wed, 21 Apr 2021 16:21:33 -0400 Subject: [PATCH 15/69] chat: show clickable arrow action on remote image hover --- .../src/views/components/RemoteContent.tsx | 70 ++++++++++++++----- 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/pkg/interface/src/views/components/RemoteContent.tsx b/pkg/interface/src/views/components/RemoteContent.tsx index 8e0ee5bf6..557d0c5af 100644 --- a/pkg/interface/src/views/components/RemoteContent.tsx +++ b/pkg/interface/src/views/components/RemoteContent.tsx @@ -48,12 +48,14 @@ class RemoteContent extends Component { this.state = { unfold: props.unfold || false, embed: undefined, - noCors: false + noCors: false, + showArrow: false }; this.unfoldEmbed = this.unfoldEmbed.bind(this); this.loadOembed = this.loadOembed.bind(this); this.wrapInLink = this.wrapInLink.bind(this); this.onError = this.onError.bind(this); + this.toggleArrow = this.toggleArrow.bind(this); } save = () => { @@ -171,6 +173,10 @@ return; this.setState({ noCors: true }); } + toggleArrow() { + this.setState({showArrow: !this.state.showArrow}) + } + render() { const { remoteContentPolicy, @@ -194,21 +200,53 @@ return; if (isImage && remoteContentPolicy.imageShown) { return this.wrapInLink( - , false, false, null, null, true + + { + e.stopPropagation(); + }} + href={url} + > + + + + + + , + false, + false, + null, + null, + true ); } else if (isAudio && remoteContentPolicy.audioShown) { return ( From 9587ccf5cade9ea65edfdcd0415d3cd83ce07d63 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Wed, 21 Apr 2021 16:43:48 -0400 Subject: [PATCH 16/69] links: prevent click in transcluded images, use hovered external arrow --- .../src/views/components/RemoteContent.tsx | 15 +++++++++++---- .../components/Graph/GraphContentWide.tsx | 6 +++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pkg/interface/src/views/components/RemoteContent.tsx b/pkg/interface/src/views/components/RemoteContent.tsx index 557d0c5af..b60db40bd 100644 --- a/pkg/interface/src/views/components/RemoteContent.tsx +++ b/pkg/interface/src/views/components/RemoteContent.tsx @@ -130,7 +130,7 @@ return; }); } - wrapInLink(contents, textOnly = false, unfold = false, unfoldEmbed = null, embedContainer = null, flushPadding = false) { + wrapInLink(contents, textOnly = false, unfold = false, unfoldEmbed = null, embedContainer = null, flushPadding = false, noOp = false) { const { style } = this.props; return ( @@ -148,7 +148,7 @@ return; { e.stopPropagation(); }} + onClick={(e) => { noOp ? e.preventDefault() : e.stopPropagation() }} href={this.props.url} whiteSpace="nowrap" overflow="hidden" @@ -159,7 +159,8 @@ return; style={{ color: 'inherit', textDecoration: 'none', ...style }} target="_blank" rel="noopener noreferrer" - > + cursor={noOp ? 'default' : 'pointer'} + > {contents} @@ -182,6 +183,7 @@ return; remoteContentPolicy, url, text, + transcluded, renderUrl = true, imageProps = {}, audioProps = {}, @@ -198,6 +200,10 @@ return; const isVideo = VIDEO_REGEX.test(url); const isOembed = hasProvider(url); + const isTranscluded = () => { + return transcluded; + } + if (isImage && remoteContentPolicy.imageShown) { return this.wrapInLink( - + ); case "mention": From 75f06a7c9e8583f787183d1662465c3f1d58bdb9 Mon Sep 17 00:00:00 2001 From: Matilde Park Date: Wed, 21 Apr 2021 17:22:08 -0400 Subject: [PATCH 17/69] landscape/logic: type safety adjustments --- pkg/interface/package-lock.json | Bin 429608 -> 429552 bytes pkg/interface/package.json | 2 +- pkg/interface/src/logic/lib/publish.ts | 25 +++++++++++------- pkg/interface/src/logic/lib/tutorialModal.ts | 5 ++-- .../src/logic/lib/useLocalStorageState.ts | 3 ++- pkg/interface/src/logic/lib/useModal.tsx | 6 +---- .../src/logic/lib/usePreviousValue.ts | 2 +- pkg/interface/src/logic/lib/useRunIO.ts | 3 +-- .../logic/lib/useStatelessAsyncClickable.ts | 2 +- pkg/interface/src/logic/lib/useStorage.ts | 2 +- pkg/interface/src/logic/lib/util.ts | 7 +++-- pkg/interface/src/logic/lib/workspace.ts | 3 ++- .../src/logic/reducers/connection.ts | 3 +-- .../src/logic/reducers/group-update.ts | 14 +++++----- .../src/logic/reducers/hark-update.ts | 20 ++++++-------- .../src/logic/reducers/launch-update.ts | 2 +- .../src/logic/reducers/settings-update.ts | 21 ++++++++------- pkg/interface/src/logic/state/launch.ts | 2 +- pkg/interface/src/logic/store/store.ts | 4 --- 19 files changed, 62 insertions(+), 64 deletions(-) diff --git a/pkg/interface/package-lock.json b/pkg/interface/package-lock.json index eb9e97a959c34666425ef5393de036f69b60436c..df58334137985b74a5f67df1f0c4d1ce3c84b931 100644 GIT binary patch delta 165 zcmV;W09ya3oEq?(8i0fWgaU*Egaot&#mJYbJ_9zF`^W_t12ir&lfhynRaPryc5GQl zT5eHmVR|@JQbKijR6#j1d0IkHZ*+1sNGn%pQ8!{lGiX{>Ze(~hbVpBNF=ll`XJu1r zctLtiF;ZzxHA#AQVPr{IR90nXb#-@ILsFAbl^2&_%moj(waEo?L6?w81|7Fx3kKl` TmvHL?IG6BI1{Jre6b62Ix)(g< delta 218 zcmew`TWZA|sfHHD7N!>F7M3lnM^7*t>sd}$(key: string, initial: T): T { interface SetStateFunc { (t: T): T; } -type SetState = T | SetStateFunc; +// See microsoft/typescript#37663 for filed bug +type SetState = T extends any ? SetStateFunc : never; export function useLocalStorageState(key: string, initial: T) { const [state, _setState] = useState(() => retrieve(key, initial)); diff --git a/pkg/interface/src/logic/lib/useModal.tsx b/pkg/interface/src/logic/lib/useModal.tsx index 3d19343cd..61c13d4d0 100644 --- a/pkg/interface/src/logic/lib/useModal.tsx +++ b/pkg/interface/src/logic/lib/useModal.tsx @@ -2,18 +2,14 @@ import React, { useState, ReactNode, useCallback, - SyntheticEvent, useMemo, - useEffect, useRef } from 'react'; import { Box } from '@tlon/indigo-react'; -import { useOutsideClick } from './useOutsideClick'; import { ModalOverlay } from '~/views/components/ModalOverlay'; import { Portal } from '~/views/components/Portal'; -import { ModalPortal } from '~/views/components/ModalPortal'; -import { PropFunc } from '@urbit/api'; +import { PropFunc } from '~/types'; type ModalFunc = (dismiss: () => void) => JSX.Element; interface UseModalProps { diff --git a/pkg/interface/src/logic/lib/usePreviousValue.ts b/pkg/interface/src/logic/lib/usePreviousValue.ts index 109f9ffa8..e4ee5a8f5 100644 --- a/pkg/interface/src/logic/lib/usePreviousValue.ts +++ b/pkg/interface/src/logic/lib/usePreviousValue.ts @@ -1,5 +1,5 @@ import { useRef } from 'react'; -import { Primitive } from '@urbit/api'; +import { Primitive } from '~/types'; export default function usePreviousValue(value: T): T { const prev = useRef(null); diff --git a/pkg/interface/src/logic/lib/useRunIO.ts b/pkg/interface/src/logic/lib/useRunIO.ts index bfaee2c54..a08ac8268 100644 --- a/pkg/interface/src/logic/lib/useRunIO.ts +++ b/pkg/interface/src/logic/lib/useRunIO.ts @@ -1,5 +1,4 @@ import { useState, useEffect } from "react"; -import { useWaitForProps } from "./useWaitForProps"; import {unstable_batchedUpdates} from "react-dom"; export type IOInstance = ( @@ -10,7 +9,7 @@ export function useRunIO( io: (i: I) => Promise, after: (o: O) => void, key: string -): () => Promise { +): (i: I) => Promise { const [resolve, setResolve] = useState<() => void>(() => () => {}); const [reject, setReject] = useState<(e: any) => void>(() => () => {}); const [output, setOutput] = useState(null); diff --git a/pkg/interface/src/logic/lib/useStatelessAsyncClickable.ts b/pkg/interface/src/logic/lib/useStatelessAsyncClickable.ts index 154f903ba..8bf79bdfc 100644 --- a/pkg/interface/src/logic/lib/useStatelessAsyncClickable.ts +++ b/pkg/interface/src/logic/lib/useStatelessAsyncClickable.ts @@ -5,7 +5,7 @@ export function useStatelessAsyncClickable( onClick: (e: MouseEvent) => Promise, name: string ) { - const [state, setState] = useState('waiting'); + const [state, setState] = useState('waiting'); const handleClick = useCallback( async (e: MouseEvent) => { try { diff --git a/pkg/interface/src/logic/lib/useStorage.ts b/pkg/interface/src/logic/lib/useStorage.ts index 64f9bb450..6ad463009 100644 --- a/pkg/interface/src/logic/lib/useStorage.ts +++ b/pkg/interface/src/logic/lib/useStorage.ts @@ -16,7 +16,7 @@ export interface IuseStorage { upload: (file: File, bucket: string) => Promise; uploadDefault: (file: File) => Promise; uploading: boolean; - promptUpload: () => Promise; + promptUpload: () => Promise; } const useStorage = ({ accept = '*' } = { accept: '*' }): IuseStorage => { diff --git a/pkg/interface/src/logic/lib/util.ts b/pkg/interface/src/logic/lib/util.ts index f34e8f76b..fe2ca5259 100644 --- a/pkg/interface/src/logic/lib/util.ts +++ b/pkg/interface/src/logic/lib/util.ts @@ -192,7 +192,10 @@ export function uxToHex(ux: string) { export const hexToUx = (hex) => { const ux = f.flow( f.chunk(4), - f.map(x => _.dropWhile(x, y => y === 0).join('')), + // eslint-disable-next-line prefer-arrow-callback + f.map(x => _.dropWhile(x, function(y: unknown) { + return y === 0; + }).join('')), f.join('.') )(hex.split('')); return `0x${ux}`; @@ -417,7 +420,7 @@ export const useHovering = (): useHoveringInterface => { onMouseLeave, }), [onMouseLeave, onMouseOver]); - + return useMemo(() => ({ hovering, bind }), [hovering, bind]); }; diff --git a/pkg/interface/src/logic/lib/workspace.ts b/pkg/interface/src/logic/lib/workspace.ts index f13df6195..ae64610cb 100644 --- a/pkg/interface/src/logic/lib/workspace.ts +++ b/pkg/interface/src/logic/lib/workspace.ts @@ -1,4 +1,5 @@ -import { Associations, Workspace } from '@urbit/api'; +import { Associations } from '@urbit/api'; +import { Workspace } from '~/types'; export function getTitleFromWorkspace( associations: Associations, diff --git a/pkg/interface/src/logic/reducers/connection.ts b/pkg/interface/src/logic/reducers/connection.ts index 958465433..aca093701 100644 --- a/pkg/interface/src/logic/reducers/connection.ts +++ b/pkg/interface/src/logic/reducers/connection.ts @@ -1,5 +1,4 @@ -import _ from 'lodash'; -import { StoreState } from '../../store/type'; +import { StoreState } from '../store/type'; import { Cage } from '~/types/cage'; type LocalState = Pick; diff --git a/pkg/interface/src/logic/reducers/group-update.ts b/pkg/interface/src/logic/reducers/group-update.ts index 6b8c25c92..404b848e8 100644 --- a/pkg/interface/src/logic/reducers/group-update.ts +++ b/pkg/interface/src/logic/reducers/group-update.ts @@ -5,16 +5,14 @@ import { Group, Tags, GroupPolicy, - GroupPolicyDiff, OpenPolicyDiff, OpenPolicy, InvitePolicyDiff, InvitePolicy } from '@urbit/api/groups'; -import { Enc, PatpNoSig } from '@urbit/api'; +import { Enc } from '@urbit/api'; import { resourceAsPath } from '../lib/util'; import useGroupState, { GroupState } from '../state/group'; -import { compose } from 'lodash/fp'; import { reduceState } from '../state/base'; function decodeGroup(group: Enc): Group { @@ -45,11 +43,11 @@ function decodeTags(tags: Enc): Tags { tags, (acc, ships, key): Tags => { if (key.search(/\\/) === -1) { - acc.role[key] = new Set(ships); + acc.role[key] = new Set([ships]); return acc; } else { const [app, tag, resource] = key.split('\\'); - _.set(acc, [app, resource, tag], new Set(ships)); + _.set(acc, [app, resource, tag], new Set([ships])); return acc; } }, @@ -125,9 +123,9 @@ const addMembers = (json: GroupUpdate, state: GroupState): GroupState => { state.groups[resourcePath].members.add(member); if ( 'invite' in state.groups[resourcePath].policy && - state.groups[resourcePath].policy.invite.pending.has(member) + state.groups[resourcePath].policy['invite'].pending.has(member) ) { - state.groups[resourcePath].policy.invite.pending.delete(member) + state.groups[resourcePath].policy['invite'].pending.delete(member); } } } @@ -159,7 +157,7 @@ const addTag = (json: GroupUpdate, state: GroupState): GroupState => { _.set(tags, tagAccessors, tagged); } return state; -} +}; const removeTag = (json: GroupUpdate, state: GroupState): GroupState => { if ('removeTag' in json) { diff --git a/pkg/interface/src/logic/reducers/hark-update.ts b/pkg/interface/src/logic/reducers/hark-update.ts index 8ba97f91a..66392fb8d 100644 --- a/pkg/interface/src/logic/reducers/hark-update.ts +++ b/pkg/interface/src/logic/reducers/hark-update.ts @@ -1,18 +1,14 @@ import { - Notifications, NotifIndex, - NotificationGraphConfig, - GroupNotificationsConfig, - UnreadStats, Timebox } from '@urbit/api'; import { makePatDa } from '~/logic/lib/util'; import _ from 'lodash'; -import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap"; +import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap'; import useHarkState, { HarkState } from '../state/hark'; import { compose } from 'lodash/fp'; import { reduceState } from '../state/base'; -import bigInt, {BigInteger} from 'big-integer'; +import {BigInteger} from 'big-integer'; export const HarkReducer = (json: any) => { const data = _.get(json, 'harkUpdate', false); @@ -151,7 +147,7 @@ function graphWatchSelf(json: any, state: HarkState): HarkState { function readAll(json: any, state: HarkState): HarkState { const data = _.get(json, 'read-all'); - if(data) { + if(data) { clearState(state); } return state; @@ -264,7 +260,7 @@ function updateUnreads(state: HarkState, index: NotifIndex, f: (us: Set) if(!('graph' in index)) { return state; } - let unreads = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set()); + let unreads: any = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set()); f(unreads); _.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], unreads); @@ -278,7 +274,7 @@ function addNotificationToUnread(state: HarkState, index: NotifIndex, time: BigI _.set(state.unreads.graph, path, [ ...curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))), - { time, index} + { time, index } ] ); } else if ('group' in index) { @@ -287,7 +283,7 @@ function addNotificationToUnread(state: HarkState, index: NotifIndex, time: BigI _.set(state.unreads.group, path, [ ...curr.filter(c => !(c.time.eq(time) && notifIdxEqual(c.index, index))), - { time, index} + { time, index } ] ); } @@ -312,10 +308,10 @@ function removeNotificationFromUnread(state: HarkState, index: NotifIndex, time: function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'unreads' | 'last', f: (x: number) => number) { if('graph' in index) { - const curr = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, statField], 0); + const curr: any = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, statField], 0); _.set(state.unreads.graph, [index.graph.graph, index.graph.index, statField], f(curr)); } else if('group' in index) { - const curr = _.get(state.unreads.group, [index.group.group, statField], 0); + const curr: any = _.get(state.unreads.group, [index.group.group, statField], 0); _.set(state.unreads.group, [index.group.group, statField], f(curr)); } } diff --git a/pkg/interface/src/logic/reducers/launch-update.ts b/pkg/interface/src/logic/reducers/launch-update.ts index 598731bf8..7dfe948e3 100644 --- a/pkg/interface/src/logic/reducers/launch-update.ts +++ b/pkg/interface/src/logic/reducers/launch-update.ts @@ -18,7 +18,7 @@ export default class LaunchReducer { ]); } - const weatherData: WeatherState = _.get(json, 'weather', false); + const weatherData: WeatherState | boolean | Record = _.get(json, 'weather', false); if (weatherData) { useLaunchState.getState().set(state => { state.weather = weatherData; diff --git a/pkg/interface/src/logic/reducers/settings-update.ts b/pkg/interface/src/logic/reducers/settings-update.ts index 1d43f517b..025afb29b 100644 --- a/pkg/interface/src/logic/reducers/settings-update.ts +++ b/pkg/interface/src/logic/reducers/settings-update.ts @@ -1,7 +1,8 @@ import _ from 'lodash'; -import useSettingsState, { SettingsState } from "~/logic/state/settings"; -import { SettingsUpdate } from '@urbit/api/dist/settings'; +import useSettingsState, { SettingsState } from '~/logic/state/settings'; +import { SettingsUpdate } from '@urbit/api/settings'; import { reduceState } from '../state/base'; +import { string } from 'prop-types'; export default class SettingsReducer { reduce(json: any) { @@ -40,21 +41,21 @@ export default class SettingsReducer { return state; } - putEntry(json: SettingsUpdate, state: SettingsState): SettingsState { - const data = _.get(json, 'put-entry', false); + putEntry(json: SettingsUpdate, state: any): SettingsState { + const data: Record = _.get(json, 'put-entry', false); if (data) { - if (!state[data["bucket-key"]]) { - state[data["bucket-key"]] = {}; + if (!state[data['bucket-key']]) { + state[data['bucket-key']] = {}; } - state[data["bucket-key"]][data["entry-key"]] = data.value; + state[data['bucket-key']][data['entry-key']] = data.value; } return state; } - delEntry(json: SettingsUpdate, state: SettingsState): SettingsState { + delEntry(json: SettingsUpdate, state: any): SettingsState { const data = _.get(json, 'del-entry', false); if (data) { - delete state[data["bucket-key"]][data["entry-key"]]; + delete state[data['bucket-key']][data['entry-key']]; } return state; } @@ -76,7 +77,7 @@ export default class SettingsReducer { return state; } - getEntry(json: any, state: SettingsState) { + getEntry(json: any, state: any) { const bucketKey = _.get(json, 'bucket-key', false); const entryKey = _.get(json, 'entry-key', false); const entry = _.get(json, 'entry', false); diff --git a/pkg/interface/src/logic/state/launch.ts b/pkg/interface/src/logic/state/launch.ts index 14f2113e5..225c00294 100644 --- a/pkg/interface/src/logic/state/launch.ts +++ b/pkg/interface/src/logic/state/launch.ts @@ -9,7 +9,7 @@ export interface LaunchState extends BaseState { tiles: { [app: string]: Tile; }, - weather: WeatherState | null, + weather: WeatherState | null | Record | boolean, userLocation: string | null; baseHash: string | null; }; diff --git a/pkg/interface/src/logic/store/store.ts b/pkg/interface/src/logic/store/store.ts index 3f77beb9c..bfc26accc 100644 --- a/pkg/interface/src/logic/store/store.ts +++ b/pkg/interface/src/logic/store/store.ts @@ -3,10 +3,8 @@ import _ from 'lodash'; import BaseStore from './base'; import InviteReducer from '../reducers/invite-update'; import MetadataReducer from '../reducers/metadata-update'; -import LocalReducer from '../reducers/local'; import { StoreState } from './type'; -import { Timebox } from '@urbit/api'; import { Cage } from '~/types/cage'; import S3Reducer from '../reducers/s3-update'; import { GraphReducer } from '../reducers/graph-update'; @@ -17,8 +15,6 @@ import LaunchReducer from '../reducers/launch-update'; import ConnectionReducer from '../reducers/connection'; import SettingsReducer from '../reducers/settings-update'; import GcpReducer from '../reducers/gcp-reducer'; -import { OrderedMap } from '../lib/OrderedMap'; -import { BigIntOrderedMap } from '../lib/BigIntOrderedMap'; import { GroupViewReducer } from '../reducers/group-view'; import { unstable_batchedUpdates } from 'react-dom'; From f80ca5a3da32c17b683a91262d5e6a3d4497e785 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Thu, 22 Apr 2021 15:02:21 +1000 Subject: [PATCH 18/69] virtualContext: fix useVirtualResizeProp --- pkg/interface/src/logic/lib/virtualContext.tsx | 5 +++-- pkg/interface/src/views/apps/permalinks/embed.tsx | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/interface/src/logic/lib/virtualContext.tsx b/pkg/interface/src/logic/lib/virtualContext.tsx index 8c4678294..ef57e6788 100644 --- a/pkg/interface/src/logic/lib/virtualContext.tsx +++ b/pkg/interface/src/logic/lib/virtualContext.tsx @@ -7,6 +7,7 @@ import React, { useEffect, } from "react"; import usePreviousValue from "./usePreviousValue"; +import {Primitive} from "~/types"; export interface VirtualContextProps { save: () => void; @@ -49,7 +50,7 @@ export function useVirtualResizeState(s: boolean) { return [state, setState] as const; } -export function useVirtualResizeProp(prop: T) { +export function useVirtualResizeProp(prop: Primitive) { const { save, restore } = useVirtual(); const oldProp = usePreviousValue(prop) @@ -58,7 +59,7 @@ export function useVirtualResizeProp(prop: T) { } useLayoutEffect(() => { - restore(); + requestAnimationFrame(restore); }, [prop]); diff --git a/pkg/interface/src/views/apps/permalinks/embed.tsx b/pkg/interface/src/views/apps/permalinks/embed.tsx index 2f7c8e11c..032cf054b 100644 --- a/pkg/interface/src/views/apps/permalinks/embed.tsx +++ b/pkg/interface/src/views/apps/permalinks/embed.tsx @@ -18,7 +18,7 @@ import { GroupLink } from "~/views/components/GroupLink"; import GlobalApi from "~/logic/api/global"; import { getModuleIcon } from "~/logic/lib/util"; import useMetadataState from "~/logic/state/metadata"; -import { Association, resourceFromPath } from "@urbit/api"; +import { Association, resourceFromPath, GraphNode } from "@urbit/api"; import { Link } from "react-router-dom"; import useGraphState from "~/logic/state/graph"; import { GraphNodeContent } from "../notifications/graph"; @@ -51,7 +51,7 @@ function GraphPermalink( const { full = false, showOurContact, pending, link, graph, group, index, api, transcluded } = props; const { ship, name } = resourceFromPath(graph); const node = useGraphState( - useCallback((s) => s.looseNodes?.[`${ship.slice(1)}/${name}`]?.[index], [ + useCallback((s) => s.looseNodes?.[`${ship.slice(1)}/${name}`]?.[index] as GraphNode, [ graph, index, ]) @@ -63,7 +63,7 @@ function GraphPermalink( ]) ); - useVirtualResizeProp(node) + useVirtualResizeProp(!!node) useEffect(() => { (async () => { if (pending || !index) { From 6f7ed005aee37ef22bbf2ff4bd7825328731fa33 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Thu, 22 Apr 2021 15:04:38 +1000 Subject: [PATCH 19/69] VirtualScroller: fix race condition in ref deletion A callback ref is called after the component is mounted, but before the component is unmounted. However, we might still be adjusting scroll position based on a component that is going to be remounted. Previously, we delayed the deletion until the next tick with setTimeout. With the faster ordered map implementation, the component may be remounted before the next tick, leading to the deletion of a ref that is still mounted. To work around this, we store a set of 'orphans' and clear the map of orphans on an interval, and only clear the map if we are not currently adjusting our scroll position. Also includes fixes for jumpy scroll behaviour on initial mount. --- .../src/views/components/VirtualScroller.tsx | 60 ++++++++++++++++--- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/pkg/interface/src/views/components/VirtualScroller.tsx b/pkg/interface/src/views/components/VirtualScroller.tsx index 70f6dd889..c7a3361ee 100644 --- a/pkg/interface/src/views/components/VirtualScroller.tsx +++ b/pkg/interface/src/views/components/VirtualScroller.tsx @@ -115,6 +115,10 @@ export default class VirtualScroller extends Component(); + /** + * A set of child refs which have been unmounted + */ + private orphans = new Set(); /** * If saving, the bottommost visible element that we pin our scroll to */ @@ -140,6 +144,10 @@ export default class VirtualScroller extends Component) { super(props); this.state = { @@ -157,6 +165,7 @@ export default class VirtualScroller extends Component extends Component { + log('scroll', 'initialised scroll'); + this.restore(); + this.initScroll = null; + }, 100); } + + + cleanupRefs = () => { + if(this.saveDepth > 0) { + return; + } + [...this.orphans].forEach(o => { + const index = bigInt(o); + this.childRefs.delete(index); + }); + this.orphans.clear(); + }; + // manipulate scrollbar manually, to dodge change detection updateScroll = IS_IOS ? () => {} : _.throttle(() => { if(!this.window || !this.scrollRef) { @@ -199,6 +227,12 @@ export default class VirtualScroller extends Component extends Component { requestAnimationFrame(() => { this.restore(); - requestAnimationFrame(() => { - - }); }); }); } @@ -339,6 +370,10 @@ export default class VirtualScroller extends Component 0) { log('bail', 'deep scroll queue'); return; @@ -394,8 +429,15 @@ export default class VirtualScroller extends Component extends Component extends Component { if(element) { this.childRefs.set(index, element); + this.orphans.delete(index.toString()); } else { - setTimeout(() => { - this.childRefs.delete(index); - }); + this.orphans.add(index.toString()); } } From 24259dab876c15c7b71ded1dcba530abc20d8806 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Thu, 22 Apr 2021 15:16:02 +1000 Subject: [PATCH 20/69] Dropdown: check ref exists Fixes urbit/landscape#805 --- pkg/interface/src/views/components/Dropdown.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/interface/src/views/components/Dropdown.tsx b/pkg/interface/src/views/components/Dropdown.tsx index 75c17b32a..cf9d9cfec 100644 --- a/pkg/interface/src/views/components/Dropdown.tsx +++ b/pkg/interface/src/views/components/Dropdown.tsx @@ -47,6 +47,9 @@ export function Dropdown(props: DropdownProps): ReactElement { const [coords, setCoords] = useState({}); const updatePos = useCallback(() => { + if(!anchorRef.current) { + return; + } const newCoords = getRelativePosition(anchorRef.current, props.alignX, props.alignY, offsetX, offsetY); if(newCoords) { setCoords(newCoords); From a7afaf065d81dc30afaced6198980ed6b120a02a Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Thu, 22 Apr 2021 16:30:13 +1000 Subject: [PATCH 21/69] interface: fix flex-shrink, grow typings --- pkg/interface/src/views/components/ChipInput.tsx | 4 ++-- .../src/views/landscape/components/GroupSummary.tsx | 4 ++-- .../src/views/landscape/components/GroupifyForm.tsx | 2 +- pkg/interface/src/views/landscape/components/JoinGroup.tsx | 2 +- .../src/views/landscape/components/Participants.tsx | 6 +++--- .../landscape/components/Sidebar/SidebarListHeader.tsx | 6 +++--- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/interface/src/views/components/ChipInput.tsx b/pkg/interface/src/views/components/ChipInput.tsx index 02471d817..67a773af0 100644 --- a/pkg/interface/src/views/components/ChipInput.tsx +++ b/pkg/interface/src/views/components/ChipInput.tsx @@ -115,8 +115,8 @@ export function ChipInput(props: ChipInputProps): ReactElement { ): R width="40px" height="40px" metadata={metadata} - flexShrink="0" + flexShrink={0} /> - +
- + Groupify this channel diff --git a/pkg/interface/src/views/landscape/components/JoinGroup.tsx b/pkg/interface/src/views/landscape/components/JoinGroup.tsx index df51fe604..1a303354d 100644 --- a/pkg/interface/src/views/landscape/components/JoinGroup.tsx +++ b/pkg/interface/src/views/landscape/components/JoinGroup.tsx @@ -177,7 +177,7 @@ export function JoinGroup(props: JoinGroupProps): ReactElement { Channels - + {Object.values(preview.channels).map(({ metadata }: any) => ( - + - + {( !!feedPath) ? ( - + {props.initialValues.hideUnjoined ? `Joined ${noun}` : `All ${noun}`} From 08452efef8d68df19af4b0cdb2944c1b459f5464 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald Date: Thu, 22 Apr 2021 16:35:44 +1000 Subject: [PATCH 22/69] interface: fix flex-shrink, grow typings pt.2 --- .../views/apps/chat/components/ChatMessage.tsx | 6 +++--- .../views/apps/publish/components/NoteForm.tsx | 2 +- pkg/interface/src/views/components/Dropdown.tsx | 5 +++-- .../views/landscape/components/GroupSwitcher.tsx | 2 +- .../views/landscape/components/NewChannel.tsx | 7 ++++--- .../landscape/components/ResourceSkeleton.tsx | 12 ++++++------ .../components/Sidebar/SidebarListHeader.tsx | 16 ++++++++-------- 7 files changed, 26 insertions(+), 24 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index 4839817c6..776a3c745 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx @@ -67,7 +67,7 @@ export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => ( { width='auto' alignY='top' alignX='right' - flexShrink={'0'} + flexShrink={0} offsetY={8} offsetX={-24} options={ diff --git a/pkg/interface/src/views/apps/publish/components/NoteForm.tsx b/pkg/interface/src/views/apps/publish/components/NoteForm.tsx index 15c3b221b..bb00fc8e0 100644 --- a/pkg/interface/src/views/apps/publish/components/NoteForm.tsx +++ b/pkg/interface/src/views/apps/publish/components/NoteForm.tsx @@ -44,7 +44,7 @@ export function PostForm(props: PostFormProps) { validateOnBlur > - + (null); const anchorRef = useRef(null); const { pathname } = useLocation(); @@ -86,7 +87,7 @@ export function Dropdown(props: DropdownProps): ReactElement { }, []); return ( - + {children} diff --git a/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx b/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx index ea55bac9a..11472d145 100644 --- a/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx +++ b/pkg/interface/src/views/landscape/components/GroupSwitcher.tsx @@ -180,7 +180,7 @@ export function GroupSwitcher(props: { > { metadata && } - {title} + {title} diff --git a/pkg/interface/src/views/landscape/components/NewChannel.tsx b/pkg/interface/src/views/landscape/components/NewChannel.tsx index cad416e2c..fd025e932 100644 --- a/pkg/interface/src/views/landscape/components/NewChannel.tsx +++ b/pkg/interface/src/views/landscape/components/NewChannel.tsx @@ -11,7 +11,7 @@ import * as Yup from 'yup'; import GlobalApi from '~/logic/api/global'; import { AsyncButton } from '~/views/components/AsyncButton'; import { FormError } from '~/views/components/FormError'; -import { RouteComponentProps } from 'react-router-dom'; +import { RouteComponentProps, useHistory } from 'react-router-dom'; import { stringToSymbol, parentPath, deSig } from '~/logic/lib/util'; import { resourceFromPath } from '~/logic/lib/group'; import { Associations } from '@urbit/api/metadata'; @@ -46,8 +46,9 @@ interface NewChannelProps { workspace: Workspace; } -export function NewChannel(props: NewChannelProps & RouteComponentProps): ReactElement { - const { history, api, group, workspace } = props; +export function NewChannel(props: NewChannelProps): ReactElement { + const history = useHistory(); + const { api, group, workspace } = props; const groups = useGroupState(state => state.groups); const waiter = useWaitForProps({ groups }, 5000); diff --git a/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx b/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx index 9c8e88cc4..9d32efff9 100644 --- a/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx +++ b/pkg/interface/src/views/landscape/components/ResourceSkeleton.tsx @@ -77,7 +77,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement { fontSize='1' mr='12px' my='1' - flexShrink='0' + flexShrink={0} display={['block','none']} > @@ -98,7 +98,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement { maxWidth={association?.metadata?.description ? ['100%', '50%'] : 'none'} mr='2' ml='1' - flexShrink={['1', '0']} + flexShrink={[1, 0]} > {title} @@ -112,7 +112,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement { mb='0' minWidth='0' maxWidth='50%' - flexShrink='1' + flexShrink={1} disableRemoteContent > {workspace === '/messages' @@ -145,7 +145,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement { return ( @@ -169,7 +169,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps): ReactElement { ml={3} display='flex' alignItems='center' - flexShrink='0' + flexShrink={0} ref={actionsRef} > {canWrite && <WriterControls />} diff --git a/pkg/interface/src/views/landscape/components/Sidebar/SidebarListHeader.tsx b/pkg/interface/src/views/landscape/components/Sidebar/SidebarListHeader.tsx index c4a820476..f0f60e2df 100644 --- a/pkg/interface/src/views/landscape/components/Sidebar/SidebarListHeader.tsx +++ b/pkg/interface/src/views/landscape/components/Sidebar/SidebarListHeader.tsx @@ -1,6 +1,6 @@ import React, { ReactElement, useCallback } from 'react'; import { FormikHelpers } from 'formik'; -import { Link } from 'react-router-dom'; +import { Link, useHistory } from 'react-router-dom'; import { Row, @@ -34,6 +34,7 @@ export function SidebarListHeader(props: { workspace: Workspace; handleSubmit: (c: SidebarListConfig) => void; }): ReactElement { + const history = useHistory(); const onSubmit = useCallback( (values: SidebarListConfig, actions: FormikHelpers<SidebarListConfig>) => { props.handleSubmit(values); @@ -74,18 +75,18 @@ export function SidebarListHeader(props: { borderBottom={1} borderColor="lightGray" backgroundColor={['transparent', - props.history.location.pathname.includes(`/~landscape${groupPath}/feed`) + history.location.pathname.includes(`/~landscape${groupPath}/feed`) ? ( 'washedGray' ) : ( 'transparent' )]} - cursor={['pointer', ( - props.history.location.pathname === `/~landscape${groupPath}/feed` + cursor={( + history.location.pathname === `/~landscape${groupPath}/feed` ? 'default' : 'pointer' - )]} + )} onClick={() => { - props.history.push(`/~landscape${groupPath}/feed`); + history.push(`/~landscape${groupPath}/feed`); }} > <Text> @@ -131,7 +132,6 @@ export function SidebarListHeader(props: { > <NewChannel api={props.api} - history={props.history} workspace={props.workspace} /> </Col> @@ -152,7 +152,7 @@ export function SidebarListHeader(props: { ) } <Dropdown - flexShrink='0' + flexShrink={0} width="auto" alignY="top" alignX={['right', 'left']} From 8532c0c362445db047c1c9ae244aafccbed20c4a Mon Sep 17 00:00:00 2001 From: Matilde Park <matilde@tlon.io> Date: Thu, 22 Apr 2021 17:48:24 -0400 Subject: [PATCH 23/69] landscape: restore timestamps across interface --- .../src/views/apps/links/components/LinkItem.tsx | 6 ++++-- pkg/interface/src/views/apps/publish/components/Note.tsx | 3 ++- pkg/interface/src/views/components/Author.tsx | 9 ++++++--- pkg/interface/src/views/components/CommentItem.tsx | 1 + pkg/interface/src/views/components/Timestamp.tsx | 1 + .../components/Home/Post/PostItem/PostHeader.js | 1 + 6 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pkg/interface/src/views/apps/links/components/LinkItem.tsx b/pkg/interface/src/views/apps/links/components/LinkItem.tsx index a3e206543..aa1506454 100644 --- a/pkg/interface/src/views/apps/links/components/LinkItem.tsx +++ b/pkg/interface/src/views/apps/links/components/LinkItem.tsx @@ -19,7 +19,7 @@ interface LinkItemProps { node: GraphNode; association: Association; resource: string; api: GlobalApi; group: Group; path: string; } -export const LinkItem = (props: LinkItemProps): ReactElement => { +export const LinkItem = (props: LinkItemProps): ReactElement => { const { association, node, @@ -86,7 +86,7 @@ export const LinkItem = (props: LinkItemProps): ReactElement => { permalink, 'Copy reference' ); - + const deleteLink = () => { if (confirm('Are you sure you want to delete this link?')) { api.graph.removeNodes(`~${ship}`, name, [node.post.index]); @@ -167,9 +167,11 @@ export const LinkItem = (props: LinkItemProps): ReactElement => { <Row minWidth='0' flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white"> <Author showImage + isRelativeTime ship={author} date={node.post['time-sent']} group={group} + lineHeight="1" /> <Box ml="auto"> <Link diff --git a/pkg/interface/src/views/apps/publish/components/Note.tsx b/pkg/interface/src/views/apps/publish/components/Note.tsx index 17150dae2..d51aa0e13 100644 --- a/pkg/interface/src/views/apps/publish/components/Note.tsx +++ b/pkg/interface/src/views/apps/publish/components/Note.tsx @@ -115,11 +115,12 @@ export function Note(props: NoteProps & RouteComponentProps) { <Row alignItems="center"> <Author showImage + isRelativeTime ship={post?.author} date={post?.['time-sent']} 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> {adminLinks} </Row> diff --git a/pkg/interface/src/views/components/Author.tsx b/pkg/interface/src/views/components/Author.tsx index 41d0fb723..11b002149 100644 --- a/pkg/interface/src/views/components/Author.tsx +++ b/pkg/interface/src/views/components/Author.tsx @@ -24,6 +24,7 @@ interface AuthorProps { unread?: boolean; api?: GlobalApi; size?: number; + lineHeight?: string; } // eslint-disable-next-line max-lines-per-function @@ -38,10 +39,11 @@ export default function Author(props: AuthorProps & PropFunc<typeof Box>): React group, isRelativeTime, dontShowTime, + lineHeight = 'tall', ...rest } = props; - const time = props.time || false; + const time = props.time || props.date || false; const size = props.size || 16; const sigilPadding = props.sigilPadding || 2; @@ -89,7 +91,7 @@ export default function Author(props: AuthorProps & PropFunc<typeof Box>): React ) : sigil; return ( - <Row height="20px" {...rest} alignItems='center' width='auto'> + <Row {...rest} alignItems='center' width='auto'> <Box onClick={(e) => { e.stopPropagation(); @@ -110,7 +112,7 @@ export default function Author(props: AuthorProps & PropFunc<typeof Box>): React color='black' fontSize='1' cursor='pointer' - lineHeight='tall' + lineHeight={lineHeight} fontFamily={showNickname ? 'sans' : 'mono'} fontWeight={showNickname ? '500' : '400'} mr={showNickname ? 0 : "2px"} @@ -121,6 +123,7 @@ export default function Author(props: AuthorProps & PropFunc<typeof Box>): React </Box> { !dontShowTime && time && ( <Timestamp + height="fit-content" relative={isRelativeTime} stamp={stamp} fontSize={1} diff --git a/pkg/interface/src/views/components/CommentItem.tsx b/pkg/interface/src/views/components/CommentItem.tsx index f65273189..454ae7e84 100644 --- a/pkg/interface/src/views/components/CommentItem.tsx +++ b/pkg/interface/src/views/components/CommentItem.tsx @@ -95,6 +95,7 @@ export function CommentItem(props: CommentItemProps): ReactElement { date={post?.['time-sent']} unread={props.unread} group={group} + isRelativeTime > <Row px="2" gapX="2" height="18px"> <Action bg="white" onClick={doCopy}>{copyDisplay}</Action> diff --git a/pkg/interface/src/views/components/Timestamp.tsx b/pkg/interface/src/views/components/Timestamp.tsx index 6b5599630..1023031f5 100644 --- a/pkg/interface/src/views/components/Timestamp.tsx +++ b/pkg/interface/src/views/components/Timestamp.tsx @@ -12,6 +12,7 @@ export type TimestampProps = BoxProps & { date?: boolean; time?: boolean; relative?: boolean; + height?: string; }; const Timestamp = (props: TimestampProps): ReactElement | null => { diff --git a/pkg/interface/src/views/landscape/components/Home/Post/PostItem/PostHeader.js b/pkg/interface/src/views/landscape/components/Home/Post/PostItem/PostHeader.js index fdf80d171..3374bee2f 100644 --- a/pkg/interface/src/views/landscape/components/Home/Post/PostItem/PostHeader.js +++ b/pkg/interface/src/views/landscape/components/Home/Post/PostItem/PostHeader.js @@ -46,6 +46,7 @@ export function PostHeader(props) { isRelativeTime={true} showTime={false} time={true} + lineHeight='1' /> <Dropdown dropWidth="200px" From 04f46eefe5fcffe463a0dc31dfb068166f96ab08 Mon Sep 17 00:00:00 2001 From: Matilde Park <matilde@tlon.io> Date: Thu, 22 Apr 2021 19:46:43 -0400 Subject: [PATCH 24/69] landscape: update indigo-react to 1.2.21 --- pkg/interface/package-lock.json | Bin 472164 -> 472979 bytes pkg/interface/package.json | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/interface/package-lock.json b/pkg/interface/package-lock.json index f74b80006dcb790e32ecfb5f78add52f65dfaef7..ef26f6949a9ab6917dc3398da1118f990cb1068f 100644 GIT binary patch delta 344 zcmaFzLT2)NnT9Ql3t6Tgv|}=wzJrO8clrYsMyctAI!wx;E`^~czUj$@l~rY?1<t8P zg~`b|VUCW$c`3#&=BZVA*+!|Bm0_0Q#wA|m$^JzJZp9u!E-C&&7M6(t`K39P;pVxf zDaj?iDMjwy$%W<yE?)VGVUs_;(`ui?$_T_vK+L>-4l7HlWV?tw3lOs|7m;VnnQq|8 zDmC4~nAviBT{j!6!1NEB*|euW$YIu<o}k3YGW|n1Bj5D%PnmV5pWn!)F#VD}v(R*d zm2522^KY<;PycSutU7)54K}CgZVt@K(|!Xr)NKNa*ElfqPIssPa%=n8gr`riWa6Jb zcP`NMy$;Mg(+vuk1*T7^WC5CVk4<Mfzaz5=$T-o-2i#bvM>;a6OkaP4&2jqLDi$S> jev|EXkD2rsr=M+N<DY)JpG|$cf)<+`<94ZeY+lm<<Xv|- delta 258 zcmV+d0sa1y?;Pal9DuX|p#+x!mjN@ExdH(Um*4~e8kaGo0V7p&cvMqMNjPI|Oe<<; zQbR8>XirghRzgB#D|k+BVL~`*Old)BayCIVN^3A=MORmHOF=b9MP_0*RyS-~Y->n$ zXHa)qax_<KHaRp}bv8G8b68YPlkx5<hnxighnxihhnxiix10q8Wf_<A5d<TbgmeKC zm#_!{441&M1O%60>jMgxueb#$mk&V$28U}O1cz%M1-EM-29|4=kTe1`m(Sn>AeRvS z0v5McZUZv|mn=yF3zvSE0|=L3mIM&Ds*VP)442SL0~NPE;{q-LmkyBzHn*OD10Dgl INudTxmd~SD3;+NC diff --git a/pkg/interface/package.json b/pkg/interface/package.json index 61e4cbfb9..b53926964 100644 --- a/pkg/interface/package.json +++ b/pkg/interface/package.json @@ -10,7 +10,7 @@ "@reach/tabs": "^0.10.5", "@tlon/indigo-dark": "^1.0.6", "@tlon/indigo-light": "^1.0.7", - "@tlon/indigo-react": "^1.2.20", + "@tlon/indigo-react": "^1.2.21", "@tlon/sigil-js": "^1.4.3", "@urbit/api": "file:../npm/api", "any-ascii": "^0.1.7", From a13542dfa7774c198137b5fbbf25d3b6267cc021 Mon Sep 17 00:00:00 2001 From: Matilde Park <matilde@tlon.io> Date: Thu, 22 Apr 2021 21:21:44 -0400 Subject: [PATCH 25/69] interface/logic: revert group-update type change It broke tags, needs a redo. --- pkg/interface/src/logic/reducers/group-update.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/interface/src/logic/reducers/group-update.ts b/pkg/interface/src/logic/reducers/group-update.ts index 404b848e8..931754ed1 100644 --- a/pkg/interface/src/logic/reducers/group-update.ts +++ b/pkg/interface/src/logic/reducers/group-update.ts @@ -43,11 +43,11 @@ function decodeTags(tags: Enc<Tags>): Tags { tags, (acc, ships, key): Tags => { if (key.search(/\\/) === -1) { - acc.role[key] = new Set([ships]); + acc.role[key] = new Set(ships); return acc; } else { const [app, tag, resource] = key.split('\\'); - _.set(acc, [app, resource, tag], new Set([ships])); + _.set(acc, [app, resource, tag], new Set(ships)); return acc; } }, From 27e5a6aef434d39b2f0bf0e61474dea9c806d13f Mon Sep 17 00:00:00 2001 From: Logan Allen <loganallc@gmail.com> Date: Fri, 23 Apr 2021 14:34:39 -0500 Subject: [PATCH 26/69] omnibox: add maybe operators --- pkg/interface/src/logic/lib/omnibox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/interface/src/logic/lib/omnibox.js b/pkg/interface/src/logic/lib/omnibox.js index 1bb22528d..601530981 100644 --- a/pkg/interface/src/logic/lib/omnibox.js +++ b/pkg/interface/src/logic/lib/omnibox.js @@ -102,7 +102,7 @@ export default function index(contacts, associations, apps, currentGroup, groups }).map((e) => { // iterate through each app's metadata object Object.keys(associations[e]) - .filter((association) => !associations[e][association].metadata.hidden) + .filter((association) => !associations?.[e]?.[association]?.metadata?.hidden) .map((association) => { const each = associations[e][association]; let title = each.resource; From 7ac71c64a32519146f424e7306665ffe042b7ce4 Mon Sep 17 00:00:00 2001 From: Matilde Park <matilde@tlon.io> Date: Fri, 23 Apr 2021 15:39:21 -0400 Subject: [PATCH 27/69] landscape: move transcluded comments to new render Fixes urbit/landscape#814 Fixes urbit/landscape#813 --- .../src/views/apps/permalinks/TranscludedNode.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/interface/src/views/apps/permalinks/TranscludedNode.tsx b/pkg/interface/src/views/apps/permalinks/TranscludedNode.tsx index c84630ff7..784d1b64d 100644 --- a/pkg/interface/src/views/apps/permalinks/TranscludedNode.tsx +++ b/pkg/interface/src/views/apps/permalinks/TranscludedNode.tsx @@ -4,6 +4,7 @@ import ChatMessage from "../chat/components/ChatMessage"; import { Association, GraphNode, Post, Group } from "@urbit/api"; import { useGroupForAssoc } from "~/logic/state/group"; import { MentionText } from "~/views/components/MentionText"; +import { GraphContentWide } from '~/views/landscape/components/Graph/GraphContentWide'; import Author from "~/views/components/Author"; import { NoteContent } from "../publish/components/Note"; 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 ( <Box borderRadius="2" p="2" bg="scales.black05"> <Anchor underline={false} target="_blank" color="black" href={link.url}> @@ -74,11 +75,11 @@ function TranscludedComment(props: { group={group} /> <Box p="2"> - <MentionText + <GraphContentWide api={api} transcluded={transcluded} - content={comment.post.contents} - group={group} + post={comment.post} + showOurContact={false} /> </Box> </Col> @@ -200,8 +201,8 @@ export function TranscludedNode(props: { <TranscludedPost api={props.api} post={node.post} - group={group} - transcluded={transcluded} + group={group} + transcluded={transcluded} />) ; default: From 794af89ec210f5374cff81686bf7cfd7e10ccc64 Mon Sep 17 00:00:00 2001 From: Matilde Park <matilde@tlon.io> Date: Fri, 23 Apr 2021 15:39:44 -0400 Subject: [PATCH 28/69] landscape: flex comments to container width Prevents horizontal overflow. --- pkg/interface/src/views/components/Comments.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/interface/src/views/components/Comments.tsx b/pkg/interface/src/views/components/Comments.tsx index 16c6bf2db..627a8cfc8 100644 --- a/pkg/interface/src/views/components/Comments.tsx +++ b/pkg/interface/src/views/components/Comments.tsx @@ -130,7 +130,7 @@ export function Comments(props: CommentsProps & PropFunc<typeof Col>) { const canComment = isWriter(group, association.resource) || association.metadata.vip === 'reader-comments'; return ( - <Col {...rest}> + <Col {...rest} minWidth='0'> {( !editCommentId && canComment ? <CommentInput onSubmit={onSubmit} /> : null )} {( editCommentId ? ( <CommentInput From a7ed122c5c276fcfae3a2f5d8ff09fd1b9f1af38 Mon Sep 17 00:00:00 2001 From: Matilde Park <matilde@tlon.io> Date: Fri, 23 Apr 2021 15:43:53 -0400 Subject: [PATCH 29/69] chat: prevent shrinking of share banner --- pkg/interface/src/views/apps/chat/components/ShareProfile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/interface/src/views/apps/chat/components/ShareProfile.js b/pkg/interface/src/views/apps/chat/components/ShareProfile.js index 379301c68..7c0e03dbd 100644 --- a/pkg/interface/src/views/apps/chat/components/ShareProfile.js +++ b/pkg/interface/src/views/apps/chat/components/ShareProfile.js @@ -61,6 +61,7 @@ export const ShareProfile = (props) => { justifyContent="space-between" borderBottom={1} borderColor="lightGray" + flexShrink={0} > <Row pl={3} alignItems="center"> {image} From bc4f26b2b31b9bc2adc6703217286a1ee9e4d204 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Sat, 24 Apr 2021 12:19:33 +1000 Subject: [PATCH 30/69] VirtualScroller: rework scroll initialisation Calling restore if the scroll is locked simply resets the scroll, instead of using the usual adjustment algorithm --- .../src/views/components/VirtualScroller.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/pkg/interface/src/views/components/VirtualScroller.tsx b/pkg/interface/src/views/components/VirtualScroller.tsx index c7a3361ee..caf85d1ae 100644 --- a/pkg/interface/src/views/components/VirtualScroller.tsx +++ b/pkg/interface/src/views/components/VirtualScroller.tsx @@ -146,8 +146,6 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T private cleanupRefInterval: NodeJS.Timeout | null = null; - private initScroll: NodeJS.Timeout | null = null; - constructor(props: VirtualScrollerProps<T>) { super(props); this.state = { @@ -170,15 +168,9 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T componentDidMount() { this.updateVisible(0); - this.resetScroll(); this.loadTop(); this.loadBottom(); this.cleanupRefInterval = setInterval(this.cleanupRefs, 5000); - this.initScroll = setTimeout(() => { - log('scroll', 'initialised scroll'); - this.restore(); - this.initScroll = null; - }, 100); } @@ -429,8 +421,11 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T log('bail', 'Deep restore'); return; } - if(this.initScroll) { - log('bail', 'still initialising scroll'); + if(this.scrollLocked) { + this.resetScroll(); + this.savedIndex = null; + this.savedDistance = 0; + this.saveDepth--; return; } From 6a1f0e2ac9e7f2803f0273eebb8485c6b77b4e69 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Sat, 24 Apr 2021 12:20:42 +1000 Subject: [PATCH 31/69] VirtualScroller: aggressively cleanup refs Prevents a memory leak, as it appears React holds onto the class instance after unmounting --- pkg/interface/src/views/components/VirtualScroller.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/interface/src/views/components/VirtualScroller.tsx b/pkg/interface/src/views/components/VirtualScroller.tsx index caf85d1ae..0778f78a8 100644 --- a/pkg/interface/src/views/components/VirtualScroller.tsx +++ b/pkg/interface/src/views/components/VirtualScroller.tsx @@ -222,9 +222,8 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T if(this.cleanupRefInterval) { clearInterval(this.cleanupRefInterval); } - if(this.initScroll) { - clearTimeout(this.initScroll); - } + this.cleanupRefs(); + this.childRefs.clear(); } startOffset() { From 2e7b1cd41db6e8f7bd773b16626423f2ebfe724d Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Sat, 24 Apr 2021 12:22:08 +1000 Subject: [PATCH 32/69] VirtualScroller: lock scroll less aggressively Prevents incorrect scroll adjustment --- .../src/views/components/VirtualScroller.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pkg/interface/src/views/components/VirtualScroller.tsx b/pkg/interface/src/views/components/VirtualScroller.tsx index 0778f78a8..7a0d57aa4 100644 --- a/pkg/interface/src/views/components/VirtualScroller.tsx +++ b/pkg/interface/src/views/components/VirtualScroller.tsx @@ -361,10 +361,6 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T // bail if we're going to adjust scroll anyway return; } - if(this.initScroll) { - clearTimeout(this.initScroll); - this.initScroll = null; - } if(this.saveDepth > 0) { log('bail', 'deep scroll queue'); return; @@ -376,17 +372,15 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T const startOffset = this.startOffset(); if (scrollTop < ZONE_SIZE) { log('scroll', `Entered start zone ${scrollTop}`); - if (startOffset === 0 && onStartReached) { - onStartReached(); + if (startOffset === 0) { + onStartReached && onStartReached(); + this.scrollLocked = true; } const newOffset = Math.max(0, startOffset - this.pageDelta); if(newOffset < 10) { this.loadBottom(); } - if(newOffset === 0) { - this.scrollLocked = true; - } if(newOffset !== startOffset) { this.updateVisible(newOffset); } From 47c19b60c0de7ff5a9a89b479833b9926edc37c2 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Sat, 24 Apr 2021 12:22:44 +1000 Subject: [PATCH 33/69] VirtualScroller: only rerender on scroll lock if necessary --- pkg/interface/src/views/components/VirtualScroller.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/interface/src/views/components/VirtualScroller.tsx b/pkg/interface/src/views/components/VirtualScroller.tsx index 7a0d57aa4..b63426194 100644 --- a/pkg/interface/src/views/components/VirtualScroller.tsx +++ b/pkg/interface/src/views/components/VirtualScroller.tsx @@ -210,7 +210,9 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T if(size !== prevProps.size || pendingSize !== prevProps.pendingSize) { if(this.scrollLocked) { - this.updateVisible(0); + if(!this.state.visibleItems.peekLargest()![0].eq(this.props.data.peekLargest()![0])) { + this.updateVisible(0); + } this.resetScroll(); } From f79e489d675cc7a78e262718326782c29bb7002b Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Sat, 24 Apr 2021 12:23:45 +1000 Subject: [PATCH 34/69] ChatResource: prevent unnecessary state update --- pkg/interface/src/views/apps/chat/ChatResource.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/interface/src/views/apps/chat/ChatResource.tsx b/pkg/interface/src/views/apps/chat/ChatResource.tsx index 8fad13168..e4f02840c 100644 --- a/pkg/interface/src/views/apps/chat/ChatResource.tsx +++ b/pkg/interface/src/views/apps/chat/ChatResource.tsx @@ -75,7 +75,14 @@ export function ChatResource(props: ChatResourceProps) { ); const clearUnsent = useCallback( - () => setUnsent(s => _.omit(s, station)), + () => { + setUnsent(s => { + if(station in s) { + return _.omit(s, station); + } + return s; + }); + }, [station] ); From 6d37c23ae8bbb67c78d0b4ddfb9ee9a766b7704b Mon Sep 17 00:00:00 2001 From: Matilde Park <matilde@tlon.io> Date: Sat, 24 Apr 2021 14:50:49 -0400 Subject: [PATCH 35/69] VirtualScroller: avoid unsafe null access --- .../src/views/components/VirtualScroller.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pkg/interface/src/views/components/VirtualScroller.tsx b/pkg/interface/src/views/components/VirtualScroller.tsx index b63426194..f1e5aceef 100644 --- a/pkg/interface/src/views/components/VirtualScroller.tsx +++ b/pkg/interface/src/views/components/VirtualScroller.tsx @@ -185,7 +185,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T }); this.orphans.clear(); }; - + // manipulate scrollbar manually, to dodge change detection updateScroll = IS_IOS ? () => {} : _.throttle(() => { if(!this.window || !this.scrollRef) { @@ -209,12 +209,11 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T const { visibleItems } = this.state; if(size !== prevProps.size || pendingSize !== prevProps.pendingSize) { - if(this.scrollLocked) { - if(!this.state.visibleItems.peekLargest()![0].eq(this.props.data.peekLargest()![0])) { + if(this.scrollLocked && visibleItems?.peekLargest() && data?.peekLargest()) { + if(!visibleItems.peekLargest()[0].eq(data.peekLargest()[0])) { this.updateVisible(0); - } + } this.resetScroll(); - } } } @@ -424,7 +423,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T return; } - let ref = this.childRefs.get(this.savedIndex) + let ref = this.childRefs.get(this.savedIndex) if(!ref) { return; } @@ -471,7 +470,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T console.log('bail', 'deep save'); return; } - + this.saveDepth++; let bottomIndex: BigInteger | null = null; From 789c5de6152941c63b19e9522a149f7987bb54ad Mon Sep 17 00:00:00 2001 From: Matilde Park <matilde@tlon.io> Date: Sat, 24 Apr 2021 16:05:12 -0400 Subject: [PATCH 36/69] landscape: fix un-set avatars in participants list --- pkg/interface/src/views/landscape/components/Participants.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/interface/src/views/landscape/components/Participants.tsx b/pkg/interface/src/views/landscape/components/Participants.tsx index aeff922e3..9ce425d89 100644 --- a/pkg/interface/src/views/landscape/components/Participants.tsx +++ b/pkg/interface/src/views/landscape/components/Participants.tsx @@ -304,7 +304,7 @@ function Participant(props: { }, [api, contact, association]); const avatar = - contact?.avatar !== null && !hideAvatars ? ( + contact?.avatar && !hideAvatars ? ( <Image src={contact.avatar} height={32} From 47c8075294e8be08477314994145e4a5174e6e4c Mon Sep 17 00:00:00 2001 From: James Acklin <james.acklin@gmail.com> Date: Sat, 24 Apr 2021 21:42:21 -0400 Subject: [PATCH 37/69] chat: threshold send button & chatinput fixes --- .../views/apps/chat/components/ChatInput.tsx | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx index dab2c1169..2ad4cab26 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx @@ -9,7 +9,7 @@ import GlobalApi from '~/logic/api/global'; import { Envelope } from '~/types/chat-update'; import { StorageState } from '~/types'; import { Contacts, Content } from '@urbit/api'; -import { Row, BaseImage, Box, Icon, LoadingSpinner } from '@tlon/indigo-react'; +import { Row, BaseImage, Box, Icon, LoadingSpinner, Text } from '@tlon/indigo-react'; import withStorage from '~/views/components/withStorage'; import { withLocalState } from '~/logic/state/local'; @@ -182,7 +182,15 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> { onPaste={this.onPaste.bind(this)} 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.uploading ? ( <LoadingSpinner /> @@ -199,13 +207,15 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> { ) ) : null} </Box> - <Box mr={2} flexShrink={0} height='16px' width='16px' flexBasis='16px'> - <Icon - icon='Dojo' - cursor='pointer' - onClick={this.toggleCode} - color={state.inCodeMode ? 'blue' : 'black'} - /> + <Box mx={3} flexShrink={0} height='16px'> + <Text + bold + color="blue" + cursor="pointer" + onClick={() => console.log(this.chatEditor.current.submit())} + > + Send + </Text> </Box> </Row> ); From 1d5cff849d56bf061fef57fdb66b8eec0f97ef8f Mon Sep 17 00:00:00 2001 From: Matilde Park <matilde@tlon.io> Date: Sun, 25 Apr 2021 15:45:51 -0400 Subject: [PATCH 38/69] landscape: highlight content with our mentions --- .../src/views/apps/chat/components/ChatMessage.tsx | 12 +++++++++++- pkg/interface/src/views/components/CommentItem.tsx | 13 ++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index 776a3c745..3bbb8f5d3 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx @@ -264,6 +264,7 @@ class ChatMessage extends Component<ChatMessageProps> { componentDidMount() {} render() { + let { highlighted } = this.props; const { msg, previousMsg, @@ -279,12 +280,21 @@ class ChatMessage extends Component<ChatMessageProps> { unreadMarkerRef, history, api, - highlighted, showOurContact, fontSize, hideHover } = this.props; + const ourMention = msg?.contents?.some((e) => { + return e?.mention && e?.mention === window.ship; + }); + + if (!highlighted) { + if (ourMention) { + highlighted = true; + } + } + let onReply = this.props?.onReply ?? (() => {}); const transcluded = this.props?.transcluded ?? 0; let { renderSigil } = this.props; diff --git a/pkg/interface/src/views/components/CommentItem.tsx b/pkg/interface/src/views/components/CommentItem.tsx index f65273189..c314f987f 100644 --- a/pkg/interface/src/views/components/CommentItem.tsx +++ b/pkg/interface/src/views/components/CommentItem.tsx @@ -35,6 +35,7 @@ interface CommentItemProps { } export function CommentItem(props: CommentItemProps): ReactElement { + let { highlighted } = props; const { ship, name, api, comment, group } = props; const association = useMetadataState( 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]); }; + 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 commentIndex = commentIndexArray[commentIndexArray.length - 1]; @@ -106,7 +117,7 @@ export function CommentItem(props: CommentItemProps): ReactElement { borderRadius="1" p="1" mb="1" - backgroundColor={props.highlighted ? 'washedBlue' : 'white'} + backgroundColor={highlighted ? 'washedBlue' : 'white'} transcluded={0} api={api} post={post} From cb6b4e14e0050fec08fa9074b26acc1f93250da7 Mon Sep 17 00:00:00 2001 From: James Acklin <james.acklin@gmail.com> Date: Sun, 25 Apr 2021 23:00:06 -0400 Subject: [PATCH 39/69] chat: stateful send button in ChatInput --- .../views/apps/chat/components/ChatInput.tsx | 50 ++++++++++++++----- .../views/apps/chat/components/chat-editor.js | 8 +++ 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx index 2ad4cab26..a4d333340 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx @@ -9,9 +9,10 @@ import GlobalApi from '~/logic/api/global'; import { Envelope } from '~/types/chat-update'; import { StorageState } from '~/types'; import { Contacts, Content } from '@urbit/api'; -import { Row, BaseImage, Box, Icon, LoadingSpinner, Text } from '@tlon/indigo-react'; +import { Row, BaseImage, Box, Icon, LoadingSpinner } from '@tlon/indigo-react'; import withStorage from '~/views/components/withStorage'; import { withLocalState } from '~/logic/state/local'; +import { MOBILE_BROWSER_REGEX } from "~/logic/lib/util"; type ChatInputProps = IuseStorage & { api: GlobalApi; @@ -30,6 +31,7 @@ interface ChatInputState { inCodeMode: boolean; submitFocus: boolean; uploadingPaste: boolean; + currentInput: string; } class ChatInput extends Component<ChatInputProps, ChatInputState> { @@ -41,7 +43,8 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> { this.state = { inCodeMode: false, submitFocus: false, - uploadingPaste: false + uploadingPaste: false, + currentInput: props.message, }; this.chatEditor = React.createRef(); @@ -50,6 +53,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> { this.toggleCode = this.toggleCode.bind(this); this.uploadSuccess = this.uploadSuccess.bind(this); this.uploadError = this.uploadError.bind(this); + this.eventHandler = this.eventHandler.bind(this); } toggleCode() { @@ -61,6 +65,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> { submit(text) { const { props, state } = this; const [, , ship, name] = props.station.split('/'); + this.setState({ currentInput: '' }); if (state.inCodeMode) { this.setState( { @@ -119,6 +124,14 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> { .catch(this.uploadError); }); } + + toggleFocus(value) { + this.setState({ submitFocus: value }); + } + + eventHandler(value) { + this.setState({ currentInput: value }); + } render() { const { props, state } = this; @@ -180,6 +193,9 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> { onUnmount={props.onUnmount} message={props.message} onPaste={this.onPaste.bind(this)} + focusEvent={() => this.toggleFocus(true)} + blurEvent={() => this.toggleFocus(false)} + changeEvent={this.eventHandler} placeholder='Message...' /> <Box mx='12px' flexShrink={0} height='16px' width='16px' flexBasis='16px'> @@ -207,16 +223,26 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> { ) ) : null} </Box> - <Box mx={3} flexShrink={0} height='16px'> - <Text - bold - color="blue" - cursor="pointer" - onClick={() => console.log(this.chatEditor.current.submit())} - > - Send - </Text> - </Box> + {(MOBILE_BROWSER_REGEX.test(navigator.userAgent) && + state.submitFocus) || + state.currentInput !== "" ? ( + <Box + ml={2} + mr="12px" + flexShrink={0} + display="flex" + 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> ); } diff --git a/pkg/interface/src/views/apps/chat/components/chat-editor.js b/pkg/interface/src/views/apps/chat/components/chat-editor.js index 02fb7c6e9..fb64ef0b5 100644 --- a/pkg/interface/src/views/apps/chat/components/chat-editor.js +++ b/pkg/interface/src/views/apps/chat/components/chat-editor.js @@ -162,6 +162,7 @@ export default class ChatEditor extends Component { editor.showHint(['test', 'foo']); } if (this.state.message !== '' && value == '') { + this.props.changeEvent(value); this.setState({ message: value }); @@ -169,6 +170,7 @@ export default class ChatEditor extends Component { if (value == this.props.message || value == '' || value == ' ') { return; } + this.props.changeEvent(value); this.setState({ message: value }); @@ -179,6 +181,8 @@ export default class ChatEditor extends Component { inCodeMode, placeholder, message, + focusEvent, + blurEvent, ...props } = this.props; @@ -238,6 +242,8 @@ export default class ChatEditor extends Component { rows="1" style={{ width: '100%', background: 'transparent', color: 'currentColor' }} placeholder={inCodeMode ? "Code..." : "Message..."} + onFocus={focusEvent} + onBlur={blurEvent} onChange={event => { this.messageChange(null, null, event.target.value); }} @@ -265,6 +271,8 @@ export default class ChatEditor extends Component { this.editor = editor; editor.focus(); }} + onFocus={focusEvent} + onBlur={blurEvent} {...props} /> } From c039f1e142b2700133a5b5ff02434ef52bc0c546 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Mon, 26 Apr 2021 16:35:11 +1000 Subject: [PATCH 40/69] interface: remove unnecessary routing props --- pkg/interface/src/views/apps/chat/ChatResource.tsx | 9 +++++++-- pkg/interface/src/views/apps/links/LinkResource.tsx | 2 +- pkg/interface/src/views/apps/publish/PublishResource.tsx | 2 +- .../src/views/landscape/components/GroupsPane.tsx | 1 - .../src/views/landscape/components/Resource.tsx | 4 +++- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/ChatResource.tsx b/pkg/interface/src/views/apps/chat/ChatResource.tsx index e4f02840c..8c4fafc69 100644 --- a/pkg/interface/src/views/apps/chat/ChatResource.tsx +++ b/pkg/interface/src/views/apps/chat/ChatResource.tsx @@ -28,9 +28,9 @@ type ChatResourceProps = StoreState & { association: Association; api: GlobalApi; baseUrl: string; -} & RouteComponentProps; +}; -export function ChatResource(props: ChatResourceProps) { +function ChatResource(props: ChatResourceProps) { const station = props.association.resource; const groupPath = props.association.group; const groups = useGroupState(state => state.groups); @@ -196,3 +196,8 @@ export function ChatResource(props: ChatResourceProps) { </Col> ); } + +ChatResource.whyDidYouRender = true; + +export { ChatResource }; + diff --git a/pkg/interface/src/views/apps/links/LinkResource.tsx b/pkg/interface/src/views/apps/links/LinkResource.tsx index 09a4f388b..605c6652c 100644 --- a/pkg/interface/src/views/apps/links/LinkResource.tsx +++ b/pkg/interface/src/views/apps/links/LinkResource.tsx @@ -23,7 +23,7 @@ type LinkResourceProps = StoreState & { association: Association; api: GlobalApi; baseUrl: string; -} & RouteComponentProps; +}; export function LinkResource(props: LinkResourceProps) { const { diff --git a/pkg/interface/src/views/apps/publish/PublishResource.tsx b/pkg/interface/src/views/apps/publish/PublishResource.tsx index ca34b37fe..2bb1320ea 100644 --- a/pkg/interface/src/views/apps/publish/PublishResource.tsx +++ b/pkg/interface/src/views/apps/publish/PublishResource.tsx @@ -11,7 +11,7 @@ type PublishResourceProps = StoreState & { association: Association; api: GlobalApi; baseUrl: string; -} & RouteComponentProps; +}; export function PublishResource(props: PublishResourceProps) { const { association, api, baseUrl, notebooks } = props; diff --git a/pkg/interface/src/views/landscape/components/GroupsPane.tsx b/pkg/interface/src/views/landscape/components/GroupsPane.tsx index e284865c5..568d8b1d8 100644 --- a/pkg/interface/src/views/landscape/components/GroupsPane.tsx +++ b/pkg/interface/src/views/landscape/components/GroupsPane.tsx @@ -124,7 +124,6 @@ export function GroupsPane(props: GroupsPaneProps) { > <Resource {...props} - {...routeProps} association={association} baseUrl={baseUrl} /> diff --git a/pkg/interface/src/views/landscape/components/Resource.tsx b/pkg/interface/src/views/landscape/components/Resource.tsx index 5ee47e34e..0b29e2f90 100644 --- a/pkg/interface/src/views/landscape/components/Resource.tsx +++ b/pkg/interface/src/views/landscape/components/Resource.tsx @@ -15,12 +15,14 @@ import useGroupState from '~/logic/state/group'; import useContactState from '~/logic/state/contact'; import useHarkState from '~/logic/state/hark'; import useMetadataState from '~/logic/state/metadata'; +import {Workspace} from '~/types'; type ResourceProps = StoreState & { association: Association; api: GlobalApi; baseUrl: string; -} & RouteComponentProps; + workspace: Workspace; +}; export function Resource(props: ResourceProps): ReactElement { const { association, api, notificationsGraphConfig } = props; From c1f055d46e7395949254dddac7d1a42c01cf336d Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Mon, 26 Apr 2021 16:59:38 +1000 Subject: [PATCH 41/69] interface: make BigIntOrderedMap immutable --- .../src/logic/reducers/graph-update.ts | 59 +++++++++---------- .../src/logic/reducers/hark-update.ts | 8 +-- pkg/npm/api/lib/BigIntOrderedMap.ts | 57 +++++++++++------- 3 files changed, 67 insertions(+), 57 deletions(-) diff --git a/pkg/interface/src/logic/reducers/graph-update.ts b/pkg/interface/src/logic/reducers/graph-update.ts index 50c310af0..a37b918c9 100644 --- a/pkg/interface/src/logic/reducers/graph-update.ts +++ b/pkg/interface/src/logic/reducers/graph-update.ts @@ -1,5 +1,6 @@ import _ from 'lodash'; import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap"; +import produce from 'immer'; import bigInt, { BigInteger } from "big-integer"; import useGraphState, { GraphState } from '../state/graph'; import { reduceState } from '../state/base'; @@ -85,17 +86,9 @@ const addGraph = (json, state: GraphState): GraphState => { state.graphTimesentMap[resource] = {}; - for (let idx in data.graph) { - let item = data.graph[idx]; - let index = bigInt(idx); - - let node = processNode(item); - - state.graphs[resource].set( - index, - node - ); - } + state.graphs[resource] = state.graphs[resource].gas(Object.keys(data.graph).map(idx => { + return [bigInt(idx), processNode(data.graph[idx])]; + })); state.graphKeys.add(resource); } return state; @@ -128,8 +121,7 @@ const addNodes = (json, state) => { const _addNode = (graph, index, node) => { // set child of graph if (index.length === 1) { - graph.set(index[0], node); - return graph; + return graph.set(index[0], node); } // set parent of graph @@ -138,19 +130,20 @@ const addNodes = (json, state) => { console.error('parent node does not exist, cannot add child'); return graph; } - parNode.children = _addNode(parNode.children, index.slice(1), node); - graph.set(index[0], parNode); - return graph; + return graph.set(index[0], produce(parNode, draft => { + draft.children = _addNode(draft.children, index.slice(1), node); + })); }; const _remove = (graph, index) => { if (index.length === 1) { - graph.delete(index[0]); + return graph.delete(index[0]); } else { const child = graph.get(index[0]); if (child) { - child.children = _remove(child.children, index.slice(1)); - graph.set(index[0], child); + return graph.set(index[0], produce(child, draft => { + draft.children = _remove(draft.children, index.slice(1)); + })); } } @@ -166,10 +159,9 @@ const addNodes = (json, state) => { return bigInt(ind); }); - graph = _remove(graph, indexArr); delete state.graphTimesentMap[resource][timestamp]; + return _remove(graph, indexArr); } - return graph; }; @@ -208,11 +200,12 @@ const addNodes = (json, state) => { return aArr.length - bArr.length; }); - let graph = state.graphs[resource]; - indices.forEach((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; } let indexArr = index.split('/').slice(1).map((ind) => { @@ -227,15 +220,17 @@ const addNodes = (json, state) => { node.children = mapifyChildren(node?.children || {}); - graph = _addNode( - graph, + state.graphs[resource] = _addNode( + state.graphs[resource], indexArr, node ); + if(newSize !== old) { + console.log(`${resource}, (${old}, ${newSize}, ${state.graphs[resource].size})`); + } }); - state.graphs[resource] = graph; } return state; }; @@ -243,13 +238,15 @@ const addNodes = (json, state) => { const removeNodes = (json, state: GraphState): GraphState => { const _remove = (graph, index) => { if (index.length === 1) { - graph.delete(index[0]); + return graph.delete(index[0]); } else { const child = graph.get(index[0]); if (child) { - _remove(child.children, index.slice(1)); - graph.set(index[0], child); + return graph.set(index[0], produce(draft => { + draft.children = _remove(draft.children, index.slice(1)) + })); } + return graph; } }; @@ -264,7 +261,7 @@ const removeNodes = (json, state: GraphState): GraphState => { let indexArr = index.split('/').slice(1).map((ind) => { return bigInt(ind); }); - _remove(state.graphs[res], indexArr); + state.graphs[res] = _remove(state.graphs[res], indexArr); }); } return state; diff --git a/pkg/interface/src/logic/reducers/hark-update.ts b/pkg/interface/src/logic/reducers/hark-update.ts index 66392fb8d..c83746b12 100644 --- a/pkg/interface/src/logic/reducers/hark-update.ts +++ b/pkg/interface/src/logic/reducers/hark-update.ts @@ -329,9 +329,9 @@ function added(json: any, state: HarkState): HarkState { ); if (arrIdx !== -1) { timebox[arrIdx] = { index, notification }; - state.notifications.set(time, timebox); + state.notifications = state.notifications.set(time, timebox); } else { - state.notifications.set(time, [...timebox, { index, notification }]); + state.notifications = state.notifications.set(time, [...timebox, { index, notification }]); } } return state; @@ -350,7 +350,7 @@ const timebox = (json: any, state: HarkState): HarkState => { if (data) { const time = makePatDa(data.time); if (!data.archive) { - state.notifications.set(time, data.notifications); + state.notifications = state.notifications.set(time, data.notifications); } } return state; @@ -403,7 +403,7 @@ function setRead( return state; } timebox[arrIdx].notification.read = read; - state.notifications.set(patDa, timebox); + state.notifications = state.notifications.set(patDa, timebox); return state; } diff --git a/pkg/npm/api/lib/BigIntOrderedMap.ts b/pkg/npm/api/lib/BigIntOrderedMap.ts index 630445c2f..c5e26da2c 100644 --- a/pkg/npm/api/lib/BigIntOrderedMap.ts +++ b/pkg/npm/api/lib/BigIntOrderedMap.ts @@ -1,6 +1,10 @@ -import { immerable } from 'immer'; +import produce, { immerable, castImmutable, castDraft, setAutoFreeze, enablePatches } from 'immer'; import bigInt, { BigInteger } from "big-integer"; +setAutoFreeze(false); + +enablePatches(); + function sortBigInt(a: BigInteger, b: BigInteger) { if (a.lt(b)) { return 1; @@ -11,19 +15,18 @@ function sortBigInt(a: BigInteger, b: BigInteger) { } } export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> { - private root: Record<string, V> = {} - private cachedIter: [BigInteger, V][] | null = null; + root: Record<string, V> = {} + cachedIter: [BigInteger, V][] = []; [immerable] = true; constructor(items: [BigInteger, V][] = []) { items.forEach(([key, val]) => { this.set(key, val); }); - this.generateCachedIter(); } get size() { - return this.cachedIter?.length ?? Object.keys(this.root).length; + return Object.keys(this.root).length; } @@ -31,14 +34,30 @@ export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> { return this.root[key.toString()] ?? null; } + gas(items: [BigInteger, V][]) { + return produce(this, draft => { + items.forEach(([key, value]) => { + draft.root[key.toString()] = castDraft(value); + }); + draft.generateCachedIter(); + }, + (patches) => { + //console.log(`gassed with ${JSON.stringify(patches, null, 2)}`); + }); + } + set(key: BigInteger, value: V) { - this.root[key.toString()] = value; - this.cachedIter = null; + return produce(this, draft => { + draft.root[key.toString()] = castDraft(value); + draft.generateCachedIter(); + }); } clear() { - this.cachedIter = null; - this.root = {} + return produce(this, draft => { + draft.cachedIter = []; + draft.root = {} + }); } has(key: BigInteger) { @@ -46,17 +65,15 @@ export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> { } delete(key: BigInteger) { - const had = this.has(key); - if(had) { - delete this.root[key.toString()]; - this.cachedIter = null; - } - return had; + return produce(this, draft => { + delete draft.root[key.toString()]; + draft.cachedIter = draft.cachedIter.filter(([x]) => x.eq(key)); + }); } [Symbol.iterator](): IterableIterator<[BigInteger, V]> { let idx = 0; - const result = this.generateCachedIter(); + let result = [...this.cachedIter]; return { [Symbol.iterator]: this[Symbol.iterator], next: (): IteratorResult<[BigInteger, V]> => { @@ -79,19 +96,15 @@ export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> { } keys() { - return Object.keys(this.root).map(k => bigInt(k)).sort(sortBigInt) + return Array.from(this).map(([k,v]) => k); } - private generateCachedIter() { - if(this.cachedIter) { - return this.cachedIter; - } + 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; - return result; } } From aaea592cfc0b75e7931a0c9b4b5047b11a4e7e94 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Mon, 26 Apr 2021 17:04:14 +1000 Subject: [PATCH 42/69] VirtualScroller: rework for less memory use, faster speeds --- .../src/views/components/VirtualScroller.tsx | 79 ++++++++++--------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/pkg/interface/src/views/components/VirtualScroller.tsx b/pkg/interface/src/views/components/VirtualScroller.tsx index f1e5aceef..9799c571c 100644 --- a/pkg/interface/src/views/components/VirtualScroller.tsx +++ b/pkg/interface/src/views/components/VirtualScroller.tsx @@ -1,4 +1,4 @@ -import React, { Component, useCallback } from 'react'; +import React, { Component, useCallback, SyntheticEvent } from 'react'; import _ from 'lodash'; import normalizeWheel from 'normalize-wheel'; import bigInt, { BigInteger } from 'big-integer'; @@ -76,7 +76,7 @@ interface VirtualScrollerProps<T> { } interface VirtualScrollerState<T> { - visibleItems: BigIntOrderedMap<T>; + visibleItems: BigInteger[]; scrollbar: number; loaded: { top: boolean; @@ -91,10 +91,9 @@ const log = (level: LogLevel, message: string) => { if(logLevel.includes(level)) { 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 @@ -114,7 +113,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T /** * 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 */ @@ -149,7 +148,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T constructor(props: VirtualScrollerProps<T>) { super(props); this.state = { - visibleItems: new BigIntOrderedMap(), + visibleItems: [], scrollbar: 0, loaded: { top: false, @@ -164,6 +163,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T this.scrollKeyMap = this.scrollKeyMap.bind(this); this.setWindow = this.setWindow.bind(this); this.restore = this.restore.bind(this); + this.startOffset = this.startOffset.bind(this); } componentDidMount() { @@ -181,7 +181,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T } [...this.orphans].forEach(o => { const index = bigInt(o); - this.childRefs.delete(index); + this.childRefs.delete(index.toString()); }); this.orphans.clear(); }; @@ -206,13 +206,10 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T componentDidUpdate(prevProps: VirtualScrollerProps<T>, _prevState: VirtualScrollerState<T>) { const { id, size, data, offset, pendingSize } = this.props; - const { visibleItems } = this.state; if(size !== prevProps.size || pendingSize !== prevProps.pendingSize) { - if(this.scrollLocked && visibleItems?.peekLargest() && data?.peekLargest()) { - if(!visibleItems.peekLargest()[0].eq(data.peekLargest()[0])) { - this.updateVisible(0); - } + if(this.scrollLocked) { + this.updateVisible(0); this.resetScroll(); } } @@ -228,11 +225,13 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T } startOffset() { - const startIndex = this.state?.visibleItems?.peekLargest()?.[0]; + const { data } = this.props; + const startIndex = this.state.visibleItems?.[0]; if(!startIndex) { 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) { // TODO: revisit when we remove nodes for any other reason than // pending indices being removed @@ -252,19 +251,17 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T log('reflow', `from: ${this.startOffset()} to: ${newOffset}`); const { data, onCalculateVisibleItems } = this.props; - const visibleItems = new BigIntOrderedMap<any>( - [...data].slice(newOffset, newOffset + this.pageSize) - ); + const visibleItems = data.keys().slice(newOffset, newOffset + this.pageSize); this.save(); this.setState({ visibleItems, - }, () => { - requestAnimationFrame(() => { - this.restore(); - }); }); + requestAnimationFrame(() => { + this.restore(); + }); + } scrollKeyMap(): Map<string, number> { @@ -296,7 +293,6 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T setWindow(element) { if (!element) return; - console.log('resetting window'); this.save(); if (this.window) { @@ -309,7 +305,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T const { averageHeight } = this.props; this.window = element; - this.pageSize = Math.floor(element.offsetHeight / Math.floor(averageHeight / 5.5)); + this.pageSize = Math.floor(element.offsetHeight / Math.floor(averageHeight / 3)); this.pageDelta = Math.floor(this.pageSize / 3); if (this.props.origin === 'bottom') { element.addEventListener('wheel', (event) => { @@ -356,7 +352,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T } }; - onScroll(event: UIEvent) { + onScroll(event: SyntheticEvent<HTMLElement, ScrollEvent>) { this.updateScroll(); if(!this.window) { // bail if we're going to adjust scroll anyway @@ -368,9 +364,11 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T } const { onStartReached, onEndReached } = this.props; const windowHeight = this.window.offsetHeight; - const { scrollTop, scrollHeight } = this.window; + const { scrollTop, scrollHeight } = event.target as HTMLElement; const startOffset = this.startOffset(); + + const scrollEnd = scrollTop + windowHeight; if (scrollTop < ZONE_SIZE) { log('scroll', `Entered start zone ${scrollTop}`); if (startOffset === 0) { @@ -423,7 +421,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T return; } - let ref = this.childRefs.get(this.savedIndex) + let ref = this.childRefs.get(this.savedIndex.toString()) if(!ref) { return; } @@ -438,7 +436,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T } scrollToIndex = (index: BigInteger) => { - let ref = this.childRefs.get(index); + let ref = this.childRefs.get(index.toString()); if(!ref) { const offset = [...this.props.data].findIndex(([idx]) => idx.eq(index)); if(offset === -1) { @@ -446,7 +444,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T } this.updateVisible(Math.max(offset - this.pageDelta, 0)); requestAnimationFrame(() => { - ref = this.childRefs.get(index); + ref = this.childRefs.get(index.toString()); this.savedIndex = null; this.savedDistance = 0; this.saveDepth = 0; @@ -467,17 +465,18 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T return; } if(this.saveDepth !== 0) { - console.log('bail', 'deep save'); return; } + log('scroll', 'saving...'); + this.saveDepth++; let bottomIndex: BigInteger | null = null; const { scrollTop, scrollHeight } = this.window; const topSpacing = scrollHeight - scrollTop; - [...Array.from(this.state.visibleItems)].reverse().forEach(([index, datum]) => { - const el = this.childRefs.get(index); + ([...this.state.visibleItems]).reverse().forEach((index) => { + const el = this.childRefs.get(index.toString()); if(!el) { return; } @@ -490,11 +489,12 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T if(!bottomIndex) { // weird, shouldn't really happen this.saveDepth--; + log('bail', 'no index found'); return; } this.savedIndex = bottomIndex; - const ref = this.childRefs.get(bottomIndex)!; + const ref = this.childRefs.get(bottomIndex.toString())!; const { offsetTop } = ref; this.savedDistance = topSpacing - offsetTop } @@ -503,7 +503,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T setRef = (element: HTMLElement | null, index: BigInteger) => { if(element) { - this.childRefs.set(index, element); + this.childRefs.set(index.toString(), element); this.orphans.delete(index.toString()); } else { this.orphans.add(index.toString()); @@ -525,11 +525,10 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T const isTop = origin === 'top'; - const indexesToRender = isTop ? visibleItems.keys() : visibleItems.keys().reverse(); - 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; return ( @@ -542,7 +541,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T <LoadingSpinner /> </Center>)} <VirtualContext.Provider value={this.shiftLayout}> - {indexesToRender.map(index => ( + {children.map(index => ( <VirtualChild key={index.toString()} setRef={this.setRef} @@ -575,8 +574,10 @@ function VirtualChild(props: VirtualChildProps) { const ref = useCallback((el: HTMLElement | null) => { 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} /> }; From 7a6b2eb0159c5678ada0bcc208a06a42878cb388 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Mon, 26 Apr 2021 17:04:36 +1000 Subject: [PATCH 43/69] virtualContext: drop useLayoutEffect --- pkg/interface/src/logic/lib/virtualContext.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/interface/src/logic/lib/virtualContext.tsx b/pkg/interface/src/logic/lib/virtualContext.tsx index ef57e6788..570c2726f 100644 --- a/pkg/interface/src/logic/lib/virtualContext.tsx +++ b/pkg/interface/src/logic/lib/virtualContext.tsx @@ -43,8 +43,8 @@ export function useVirtualResizeState(s: boolean) { [_setState, save] ); - useLayoutEffect(() => { - restore(); + useEffect(() => { + requestAnimationFrame(restore); }, [state]); return [state, setState] as const; @@ -58,7 +58,7 @@ export function useVirtualResizeProp(prop: Primitive) { save(); } - useLayoutEffect(() => { + useEffect(() => { requestAnimationFrame(restore); }, [prop]); From 6308579588d1b3ef7edaaeb40731f9a92b7f694f Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Mon, 26 Apr 2021 17:05:00 +1000 Subject: [PATCH 44/69] interface: remove dead pendings --- pkg/interface/src/logic/state/graph.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/interface/src/logic/state/graph.ts b/pkg/interface/src/logic/state/graph.ts index 8f8a337ca..a17d81007 100644 --- a/pkg/interface/src/logic/state/graph.ts +++ b/pkg/interface/src/logic/state/graph.ts @@ -128,6 +128,8 @@ const useGraphState = createState<GraphState>('Graph', { // }); // graphReducer(node); // }, -}, ['graphs', 'graphKeys', 'looseNodes']); +}, ['graphs', 'graphKeys', 'looseNodes', 'graphTimesentMap']); + +window.useGraphState = useGraphState; export default useGraphState; From ae840659da53121cdce0c18bf72d446558cece7b Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Mon, 26 Apr 2021 17:05:57 +1000 Subject: [PATCH 45/69] ChatWindow: remove unnecessary props --- .../views/apps/chat/components/ChatWindow.tsx | 90 +++++++++---------- 1 file changed, 42 insertions(+), 48 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx index bc8cc388f..d03d85484 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx @@ -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 _ from 'lodash'; import bigInt, { BigInteger } from 'big-integer'; @@ -30,10 +30,13 @@ const DEFAULT_BACKLOG_SIZE = 100; const IDLE_THRESHOLD = 64; const MAX_BACKLOG_SIZE = 1000; -type ChatWindowProps = RouteComponentProps<{ - ship: Patp; - station: string; -}> & { +const getCurrGraphSize = (ship: string, name: string) => { + const { graphs } = useGraphState.getState(); + const graph = graphs[`${ship}/${name}`]; + return graph.size; +}; + +type ChatWindowProps = { unreadCount: number; graph: Graph; graphSize: number; @@ -44,6 +47,8 @@ type ChatWindowProps = RouteComponentProps<{ api: GlobalApi; scrollTo?: BigInteger; onReply: (msg: Post) => void; + pendingSize?: number; + showOurContact: boolean; }; interface ChatWindowState { @@ -55,6 +60,7 @@ interface ChatWindowState { const virtScrollerStyle = { height: '100%' }; + class ChatWindow extends Component< ChatWindowProps, ChatWindowState @@ -81,6 +87,7 @@ class ChatWindow extends Component< this.handleWindowBlur = this.handleWindowBlur.bind(this); this.handleWindowFocus = this.handleWindowFocus.bind(this); this.stayLockedIfActive = this.stayLockedIfActive.bind(this); + this.fetchMessages = this.fetchMessages.bind(this); this.virtualList = null; this.unreadMarkerRef = React.createRef(); @@ -109,9 +116,11 @@ class ChatWindow extends Component< } const unreadIndex = graph.keys()[unreadCount]; if (!unreadIndex || unreadCount === 0) { - this.setState({ - unreadIndex: bigInt.zero - }); + if(state.unreadIndex.neq(bigInt.zero)) { + this.setState({ + unreadIndex: bigInt.zero + }); + } return; } this.setState({ @@ -138,7 +147,7 @@ class ChatWindow extends Component< } 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) { this.unreadSet = true; } @@ -195,31 +204,35 @@ class ChatWindow extends Component< this.props.api.hark.markCountAsRead(association, '/', 'message'); } - setActive = () => { - if(this.state.idle) { - this.setState({ idle: false }); - } - } - fetchMessages = async (newer: boolean): Promise<boolean> => { + async fetchMessages(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()!; + const index = graph.peekLargest()?.[0]; + if(!index) { + console.log(`no index for: ${graph}`); + return true; + } await api.graph.getYoungerSiblings( ship, name, pageSize, `/${index.toString()}` ); - return expectedSize !== graph.size; + return expectedSize !== getCurrGraphSize(ship.slice(1), name); } else { - const [index] = graph.peekSmallest()!; + console.log('x'); + const index = graph.peekSmallest()?.[0]; + if(!index) { + console.log(`no index for: ${graph}`); + return true; + } await api.graph.getOlderSiblings(ship, name, pageSize, `/${index.toString()}`); - const done = expectedSize !== graph.size; + const done = expectedSize !== getCurrGraphSize(ship.slice(1), name); if(done) { this.calculateUnreadIndex(); } @@ -238,12 +251,9 @@ class ChatWindow extends Component< const { api, association, - group, showOurContact, graph, - history, - groups, - associations, + group, onReply } = this.props; const { unreadMarkerRef } = this; @@ -252,10 +262,8 @@ class ChatWindow extends Component< group, showOurContact, unreadMarkerRef, - history, api, - groups, - associations, + group, onReply }; @@ -275,10 +283,10 @@ class ChatWindow extends Component< graph.peekLargest()?.[0] ?? 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 prevIdx = keys[graphIdx + 1]; - const nextIdx = keys[graphIdx - 1]; + const prevIdx = keys[graphIdx - 1]; + const nextIdx = keys[graphIdx + 1]; const isLastRead: boolean = this.state.unreadIndex.eq(index); const props = { highlighted, @@ -308,12 +316,8 @@ class ChatWindow extends Component< association, group, graph, - history, - groups, - associations, showOurContact, - pendingSize, - onReply, + pendingSize = 0, } = this.props; const unreadMarkerRef = this.unreadMarkerRef; @@ -321,16 +325,10 @@ class ChatWindow extends Component< association, group, unreadMarkerRef, - history, api, - associations }; const unreadMsg = graph.get(this.state.unreadIndex); - - // hack to force a re-render when we toggle showing contact - const contactsModified = - showOurContact ? 0 : 100; - + return ( <Col height='100%' overflow='hidden' position='relative'> { this.dismissedInitialUnread() && @@ -353,12 +351,11 @@ class ChatWindow extends Component< offset={unreadCount} origin='bottom' style={virtScrollerStyle} - onStartReached={this.setActive} onBottomLoaded={this.onBottomLoaded} onScroll={this.onScroll} data={graph} size={graph.size} - pendingSize={pendingSize + contactsModified} + pendingSize={pendingSize} id={association.resource} averageHeight={22} renderer={this.renderer} @@ -369,8 +366,5 @@ class ChatWindow extends Component< } } -export default withState(ChatWindow, [ - [useGroupState, ['groups']], - [useMetadataState, ['associations']], - [useGraphState, ['pendingSize']] -]); + +export default ChatWindow From 88a9d9ad1cfe37e3117068fbb9d07528070e1b50 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Mon, 26 Apr 2021 17:06:14 +1000 Subject: [PATCH 46/69] ChatResource: remove unnecessary props --- pkg/interface/src/views/apps/chat/ChatResource.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/ChatResource.tsx b/pkg/interface/src/views/apps/chat/ChatResource.tsx index 8c4fafc69..d5e006336 100644 --- a/pkg/interface/src/views/apps/chat/ChatResource.tsx +++ b/pkg/interface/src/views/apps/chat/ChatResource.tsx @@ -165,10 +165,9 @@ function ChatResource(props: ChatResourceProps) { {dragging && <SubmitDragger />} <ChatWindow key={station} - history={props.history} graph={graph} graphSize={graph.size} - unreadCount={unreadCount} + unreadCount={unreadCount as number} showOurContact={ !showBanner && hasLoadedAllowed } association={props.association} pendingSize={Object.keys(graphTimesentMap[graphPath] || {}).length} From 26d822c3b266e1b8f28596a22748aecc3c8f11c9 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Mon, 26 Apr 2021 17:06:33 +1000 Subject: [PATCH 47/69] ChatMessage: remove dead props --- .../src/views/apps/chat/components/ChatMessage.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index 776a3c745..2ff5ab889 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx @@ -141,7 +141,7 @@ const MessageActionItem = (props) => { ); }; -const MessageActions = ({ api, onReply, association, history, msg, group }) => { +const MessageActions = ({ api, onReply, association, msg, group }) => { const isAdmin = () => group.tags.role.admin.has(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'); @@ -244,7 +244,6 @@ interface ChatMessageProps { scrollWindow: HTMLDivElement; isLastMessage?: boolean; unreadMarkerRef: React.RefObject<HTMLDivElement>; - history: unknown; api: GlobalApi; highlighted?: boolean; renderSigil?: boolean; @@ -277,7 +276,6 @@ class ChatMessage extends Component<ChatMessageProps> { scrollWindow, isLastMessage, unreadMarkerRef, - history, api, highlighted, showOurContact, @@ -322,7 +320,6 @@ class ChatMessage extends Component<ChatMessageProps> { containerClass, isPending, showOurContact, - history, api, scrollWindow, highlighted, @@ -374,7 +371,7 @@ class ChatMessage extends Component<ChatMessageProps> { } } -export default React.forwardRef((props, ref) => ( +export default React.forwardRef((props: Omit<ChatMessageProps, 'innerRef'>, ref: any) => ( <ChatMessage {...props} innerRef={ref} /> )); @@ -383,7 +380,6 @@ export const MessageAuthor = ({ msg, group, api, - history, scrollWindow, showOurContact, ...rest From b8c495c563af4dbf99ba932e8a7bbcd9af3890f1 Mon Sep 17 00:00:00 2001 From: James Acklin <james.acklin@gmail.com> Date: Mon, 26 Apr 2021 14:33:12 -0400 Subject: [PATCH 48/69] chat: no shrinking avatar in ChatInput --- pkg/interface/src/views/apps/chat/components/ChatInput.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx index a4d333340..353ce44ab 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx @@ -143,6 +143,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> { const avatar = props.ourContact && props.ourContact?.avatar && !props.hideAvatars ? ( <BaseImage + flexShrink={0} src={props.ourContact.avatar} height={24} width={24} @@ -183,7 +184,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> { className='cf' zIndex={0} > - <Row p='12px 4px 12px 12px' alignItems='center'> + <Row p='12px 4px 12px 12px' flexShrink={0} alignItems='center'> {avatar} </Row> <ChatEditor From ff3d1822c25b228afcd27400f3fb38e84621ab2d Mon Sep 17 00:00:00 2001 From: James Acklin <james.acklin@gmail.com> Date: Mon, 26 Apr 2021 15:49:02 -0400 Subject: [PATCH 49/69] chat: persist submit button --- .../views/apps/chat/components/ChatInput.tsx | 18 +++++------------- .../views/apps/chat/components/chat-editor.js | 6 ------ 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx index 353ce44ab..403ef6427 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx @@ -44,7 +44,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> { inCodeMode: false, submitFocus: false, uploadingPaste: false, - currentInput: props.message, + currentInput: props.message }; this.chatEditor = React.createRef(); @@ -124,10 +124,6 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> { .catch(this.uploadError); }); } - - toggleFocus(value) { - this.setState({ submitFocus: value }); - } eventHandler(value) { this.setState({ currentInput: value }); @@ -194,8 +190,6 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> { onUnmount={props.onUnmount} message={props.message} onPaste={this.onPaste.bind(this)} - focusEvent={() => this.toggleFocus(true)} - blurEvent={() => this.toggleFocus(false)} changeEvent={this.eventHandler} placeholder='Message...' /> @@ -224,9 +218,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> { ) ) : null} </Box> - {(MOBILE_BROWSER_REGEX.test(navigator.userAgent) && - state.submitFocus) || - state.currentInput !== "" ? ( + {MOBILE_BROWSER_REGEX.test(navigator.userAgent) ? <Box ml={2} mr="12px" @@ -237,13 +229,13 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> { width="24px" height="24px" borderRadius="50%" - backgroundColor={state.currentInput !== "" ? "blue" : "gray"} - cursor={state.currentInput !== "" ? "pointer" : "default"} + backgroundColor={state.currentInput !== '' ? 'blue' : 'gray'} + cursor={state.currentInput !== '' ? 'pointer' : 'default'} onClick={() => this.chatEditor.current.submit()} > <Icon icon="ArrowEast" color="white" /> </Box> - ) : null} + : null} </Row> ); } diff --git a/pkg/interface/src/views/apps/chat/components/chat-editor.js b/pkg/interface/src/views/apps/chat/components/chat-editor.js index fb64ef0b5..3fbf94226 100644 --- a/pkg/interface/src/views/apps/chat/components/chat-editor.js +++ b/pkg/interface/src/views/apps/chat/components/chat-editor.js @@ -181,8 +181,6 @@ export default class ChatEditor extends Component { inCodeMode, placeholder, message, - focusEvent, - blurEvent, ...props } = this.props; @@ -242,8 +240,6 @@ export default class ChatEditor extends Component { rows="1" style={{ width: '100%', background: 'transparent', color: 'currentColor' }} placeholder={inCodeMode ? "Code..." : "Message..."} - onFocus={focusEvent} - onBlur={blurEvent} onChange={event => { this.messageChange(null, null, event.target.value); }} @@ -271,8 +267,6 @@ export default class ChatEditor extends Component { this.editor = editor; editor.focus(); }} - onFocus={focusEvent} - onBlur={blurEvent} {...props} /> } From 9304409e8bfcd7c0da766446bb5d959266efffc0 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Tue, 27 Apr 2021 11:33:45 +1000 Subject: [PATCH 50/69] ChatResource: cap initial backlog --- pkg/interface/src/views/apps/chat/ChatResource.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/ChatResource.tsx b/pkg/interface/src/views/apps/chat/ChatResource.tsx index d5e006336..17b74ad6a 100644 --- a/pkg/interface/src/views/apps/chat/ChatResource.tsx +++ b/pkg/interface/src/views/apps/chat/ChatResource.tsx @@ -40,7 +40,7 @@ function ChatResource(props: ChatResourceProps) { const graphPath = station.slice(7); const graph = graphs[graphPath]; const unreads = useHarkState(state => state.unreads); - const unreadCount = unreads.graph?.[station]?.['/']?.unreads || 0; + const unreadCount = unreads.graph?.[station]?.['/']?.unreads as number || 0; const graphTimesentMap = useGraphState(state => state.graphTimesentMap); const [,, owner, name] = station.split('/'); const ourContact = contacts?.[`~${window.ship}`]; @@ -48,7 +48,7 @@ function ChatResource(props: ChatResourceProps) { const canWrite = isWriter(group, station); useEffect(() => { - const count = 100 + unreadCount; + const count = Math.min(400, 100 + unreadCount); props.api.graph.getNewest(owner, name, count); }, [station]); From 19f9dd6009e2d1438fd1173ac709b15ae93a2489 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Tue, 27 Apr 2021 11:34:05 +1000 Subject: [PATCH 51/69] graph-update: fix add-graph --- .../src/logic/reducers/graph-update.ts | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/pkg/interface/src/logic/reducers/graph-update.ts b/pkg/interface/src/logic/reducers/graph-update.ts index a37b918c9..52fe36f4c 100644 --- a/pkg/interface/src/logic/reducers/graph-update.ts +++ b/pkg/interface/src/logic/reducers/graph-update.ts @@ -52,23 +52,18 @@ const keys = (json, state: GraphState): GraphState => { const processNode = (node) => { // is empty if (!node.children) { - node.children = new BigIntOrderedMap(); - return node; + return produce(node, draft => { + draft.children = new BigIntOrderedMap(); + }); } // is graph - let converted = new BigIntOrderedMap(); - for (let idx in node.children) { - let item = node.children[idx]; - let index = bigInt(idx); - - converted.set( - index, - processNode(item) - ); - } - node.children = converted; - return node; + return produce(node, draft => { + draft.children = new BigIntOrderedMap() + .gas(_.map(draft.children, (item, idx) => + [bigInt(idx), processNode(item)] as [BigInteger, any] + )); + }); }; @@ -89,6 +84,7 @@ const addGraph = (json, state: GraphState): GraphState => { state.graphs[resource] = state.graphs[resource].gas(Object.keys(data.graph).map(idx => { return [bigInt(idx), processNode(data.graph[idx])]; })); + state.graphKeys.add(resource); } return state; From ac5bc51da68d2a2338fd40a0478866236052a7b5 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Tue, 27 Apr 2021 15:10:27 +1000 Subject: [PATCH 52/69] ChatMessage: aggressively memoize, remove more dead props --- .../apps/chat/components/ChatMessage.tsx | 270 +++++++++--------- 1 file changed, 134 insertions(+), 136 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index 2ff5ab889..5892e6da7 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx @@ -3,6 +3,7 @@ import bigInt from 'big-integer'; import React, { useState, useEffect, + useMemo, useRef, Component, PureComponent, @@ -40,11 +41,12 @@ import styled from 'styled-components'; import useLocalState from '~/logic/state/local'; import useSettingsState, { selectCalmState } from '~/logic/state/settings'; 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 ProfileOverlay from '~/views/components/ProfileOverlay'; import {useCopy} from '~/logic/lib/useCopy'; import {GraphContentWide} from '~/views/landscape/components/Graph/GraphContentWide'; +import {Contact} from '@urbit/api'; export const DATESTAMP_FORMAT = '[~]YYYY.M.D'; @@ -80,7 +82,7 @@ export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => ( ); export const UnreadMarker = React.forwardRef( - ({ dayBreak, when, api, association }, ref) => { + ({ dayBreak, when, api, association }: any, ref) => { const [visible, setVisible] = useState(false); const idling = useIdlingState(); const dismiss = useCallback(() => { @@ -241,7 +243,6 @@ interface ChatMessageProps { className?: string; isPending: boolean; style?: unknown; - scrollWindow: HTMLDivElement; isLastMessage?: boolean; unreadMarkerRef: React.RefObject<HTMLDivElement>; api: GlobalApi; @@ -250,125 +251,124 @@ interface ChatMessageProps { hideHover?: boolean; innerRef: (el: HTMLDivElement | null) => void; onReply?: (msg: Post) => void; + showOurContact: boolean; } -class ChatMessage extends Component<ChatMessageProps> { - private divRef: React.RefObject<HTMLDivElement>; +function ChatMessage(props: ChatMessageProps) { + const { + msg, + previousMsg, + nextMsg, + isLastRead, + group, + association, + className = '', + isPending, + style, + isLastMessage, + unreadMarkerRef, + api, + highlighted, + showOurContact, + fontSize, + hideHover + } = props; - constructor(props) { - super(props); - this.divRef = React.createRef(); - } - - componentDidMount() {} - - render() { - const { - msg, - previousMsg, - nextMsg, - isLastRead, - group, - association, - className = '', - isPending, - style, - scrollWindow, - isLastMessage, - unreadMarkerRef, - 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 nextDate = nextMsg ? ( - daToUnix(bigInt(nextMsg.index.split('/')[1])) - ) : null; - - const dayBreak = - nextMsg && - new Date(date).getDate() !== - new Date(nextDate).getDate(); - - const containerClass = `${isPending ? 'o-40' : ''} ${className}`; - - const timestamp = moment - .unix(date / 1000) - .format(renderSigil ? 'h:mm A' : 'h:mm'); - - const messageProps = { - msg, - timestamp, - association, - group, - style, - containerClass, - isPending, - showOurContact, - api, - scrollWindow, - highlighted, - fontSize, - hideHover, - transcluded, - onReply - }; - - const unreadContainerStyle = { - height: isLastRead ? '2rem' : '0' - }; - - return ( - <Box - ref={this.props.innerRef} - pt={renderSigil ? 2 : 0} - width="100%" - pb={isLastMessage ? '20px' : 0} - className={containerClass} - style={style} - > - {dayBreak && !isLastRead ? ( - <DayBreak when={date} shimTop={renderSigil} /> - ) : 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> + let onReply = props?.onReply ?? (() => {}); + const transcluded = props?.transcluded ?? 0; + const renderSigil = props.renderSigil ?? (Boolean(nextMsg && msg.author !== nextMsg.author) || + !nextMsg || + msg.number === 1 ); - } + + + + const date = useMemo(() => daToUnix(bigInt(msg.index.split('/')[1])), [msg.index]); + const nextDate = useMemo(() => nextMsg ? ( + daToUnix(bigInt(nextMsg.index.split('/')[1])) + ) : null, + [nextMsg] + ); + + const dayBreak = useMemo(() => + nextDate && + new Date(date).getDate() !== + new Date(nextDate).getDate() + , [nextDate, date]) + + const containerClass = `${isPending ? 'o-40' : ''} ${className}`; + + const timestamp = useMemo(() => moment + .unix(date / 1000) + .format(renderSigil ? 'h:mm A' : 'h:mm'), + [date, renderSigil] + ); + + const messageProps = { + msg, + timestamp, + association, + group, + isPending, + showOurContact, + api, + highlighted, + fontSize, + hideHover, + transcluded, + onReply + }; + + const message = useMemo(() => ( + <Message + msg={msg} + timestamp={timestamp} + timestampHover={!renderSigil} + api={api} + transcluded={transcluded} + showOurContact={showOurContact} + /> + ), [renderSigil, msg, timestamp, api, transcluded, showOurContact]); + + const unreadContainerStyle = { + height: isLastRead ? '2rem' : '0' + }; + + return ( + <Box + ref={props.innerRef} + pt={renderSigil ? 2 : 0} + width="100%" + pb={isLastMessage ? '20px' : 0} + className={containerClass} + style={style} + > + {dayBreak && !isLastRead ? ( + <DayBreak when={date} shimTop={renderSigil} /> + ) : null} + {renderSigil ? ( + <MessageWrapper {...messageProps}> + <MessageAuthor pb={1} {...messageProps} /> + {message} + </MessageWrapper> + ) : ( + <MessageWrapper {...messageProps}> + {message} + </MessageWrapper> + )} + <Box style={unreadContainerStyle}> + {isLastRead ? ( + <UnreadMarker + association={association} + api={api} + dayBreak={dayBreak} + when={date} + ref={unreadMarkerRef} + /> + ) : null} + </Box> + </Box> + ); } export default React.forwardRef((props: Omit<ChatMessageProps, 'innerRef'>, ref: any) => ( @@ -378,29 +378,25 @@ export default React.forwardRef((props: Omit<ChatMessageProps, 'innerRef'>, ref: export const MessageAuthor = ({ timestamp, msg, - group, api, - scrollWindow, showOurContact, - ...rest }) => { const osDark = useLocalState((state) => state.dark); const theme = useSettingsState((s) => s.display.theme); 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 datestamp = moment .unix(date / 1000) .format(DATESTAMP_FORMAT); - const contact = + contact = ((msg.author === window.ship && showOurContact) || - msg.author !== window.ship) && - `~${msg.author}` in contacts - ? contacts[`~${msg.author}`] - : undefined; + msg.author !== window.ship) + ? contact + : null; const showNickname = useShowNickname(contact); const { hideAvatars } = useSettingsState(selectCalmState); @@ -453,7 +449,7 @@ export const MessageAuthor = ({ </Box> ); return ( - <Box display='flex' alignItems='flex-start' {...rest}> + <Box display='flex' alignItems='flex-start'> <Box height={24} pr={2} @@ -505,20 +501,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, msg, - group, api, - scrollWindow, timestampHover, transcluded, - showOurContact, - ...rest -}) => { + showOurContact +}: MessageProps) => { const { hovering, bind } = useHovering(); return ( - <Box width="100%" position='relative' {...rest}> + <Box pl="44px" width="100%" position='relative'> {timestampHover ? ( <Text display={hovering ? 'block' : 'none'} @@ -545,7 +541,9 @@ export const Message = ({ /> </Box> ); -}; +}); + +Message.displayName = 'Message'; export const MessagePlaceholder = ({ height, From 5281d4120557c6ee51e02c0987201ff339abd30b Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Tue, 27 Apr 2021 15:12:10 +1000 Subject: [PATCH 53/69] VirtualScroller: smaller pages, disable children shifting layout --- .../src/views/components/VirtualScroller.tsx | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/pkg/interface/src/views/components/VirtualScroller.tsx b/pkg/interface/src/views/components/VirtualScroller.tsx index 9799c571c..1d9bbbad4 100644 --- a/pkg/interface/src/views/components/VirtualScroller.tsx +++ b/pkg/interface/src/views/components/VirtualScroller.tsx @@ -159,7 +159,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T this.updateVisible = this.updateVisible.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.setWindow = this.setWindow.bind(this); this.restore = this.restore.bind(this); @@ -305,8 +305,8 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T const { averageHeight } = this.props; this.window = element; - this.pageSize = Math.floor(element.offsetHeight / Math.floor(averageHeight / 3)); - this.pageDelta = Math.floor(this.pageSize / 3); + this.pageSize = Math.floor(element.offsetHeight / Math.floor(averageHeight / 2)); + this.pageDelta = Math.floor(this.pageSize / 4); if (this.props.origin === 'bottom') { element.addEventListener('wheel', (event) => { event.preventDefault(); @@ -364,7 +364,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T } const { onStartReached, onEndReached } = this.props; const windowHeight = this.window.offsetHeight; - const { scrollTop, scrollHeight } = event.target as HTMLElement; + const { scrollTop, scrollHeight } = this.window; const startOffset = this.startOffset(); @@ -413,11 +413,13 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T log('bail', 'Deep restore'); return; } - if(this.scrollLocked) { - this.resetScroll(); - this.savedIndex = null; - this.savedDistance = 0; - this.saveDepth--; + if(this.scrollLocked) { + this.resetScroll(); + requestAnimationFrame(() => { + this.savedIndex = null; + this.savedDistance = 0; + this.saveDepth--; + }); return; } @@ -425,14 +427,16 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T if(!ref) { return; } + const newScrollTop = this.window.scrollHeight - ref.offsetTop - this.savedDistance; this.window.scrollTo(0, newScrollTop); requestAnimationFrame(() => { - this.savedIndex = null; - this.savedDistance = 0; - this.saveDepth--; - }); + this.savedIndex = null; + this.savedDistance = 0; + this.saveDepth--; + }); + } scrollToIndex = (index: BigInteger) => { @@ -471,11 +475,12 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T log('scroll', 'saving...'); this.saveDepth++; + const { visibleItems } = this.state; - let bottomIndex: BigInteger | null = null; + let bottomIndex = visibleItems[visibleItems.length - 1]; const { scrollTop, scrollHeight } = this.window; const topSpacing = scrollHeight - scrollTop; - ([...this.state.visibleItems]).reverse().forEach((index) => { + ([...visibleItems]).reverse().forEach((index) => { const el = this.childRefs.get(index.toString()); if(!el) { return; @@ -499,7 +504,8 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T 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) => { if(element) { @@ -529,7 +535,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T const children = isTop ? visibleItems : [...visibleItems].reverse(); 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 ( <> From 215b301be5d0d89894a8389952550331a7467720 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Tue, 27 Apr 2021 15:17:55 +1000 Subject: [PATCH 54/69] glob: update to 0v3.hls3k.gsbae.rm6pr.p6qve.46dh8 --- pkg/arvo/app/glob.hoon | 2 +- pkg/arvo/app/landscape/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/arvo/app/glob.hoon b/pkg/arvo/app/glob.hoon index 0fb9ffa4a..bf7f12d09 100644 --- a/pkg/arvo/app/glob.hoon +++ b/pkg/arvo/app/glob.hoon @@ -5,7 +5,7 @@ /- glob /+ 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))] +$ all-states $% state-0 diff --git a/pkg/arvo/app/landscape/index.html b/pkg/arvo/app/landscape/index.html index 8a76a571f..4f6c71f59 100644 --- a/pkg/arvo/app/landscape/index.html +++ b/pkg/arvo/app/landscape/index.html @@ -24,6 +24,6 @@ <div id="portal-root"></div> <script src="/~landscape/js/channel.js"></script> <script src="/~landscape/js/session.js"></script> - <script src="/~landscape/js/bundle/index.59e682153138f604d358.js"></script> + <script src="/~landscape/js/bundle/index.b253f1f3f824fdeb29d3.js"></script> </body> </html> From 7524dd268fa223cb6458b86a49f2869c205f8082 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Tue, 27 Apr 2021 15:33:38 +1000 Subject: [PATCH 55/69] UnreadNotice: show even if are missing unread message --- .../views/apps/chat/components/ChatWindow.tsx | 4 +-- .../apps/chat/components/unread-notice.js | 28 ++++++++----------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx index d03d85484..cf34168df 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx @@ -131,8 +131,8 @@ class ChatWindow extends Component< dismissedInitialUnread() { const { unreadCount, graph } = this.props; - return this.state.unreadIndex.neq(bigInt.zero) && - this.state.unreadIndex.neq(graph.keys()?.[unreadCount]?.[0] ?? bigInt.zero); + return this.state.unreadIndex.eq(bigInt.zero) ? unreadCount > graph.size : + this.state.unreadIndex.neq(graph.keys()?.[unreadCount]?.[0] ?? bigInt.zero); } handleWindowBlur() { diff --git a/pkg/interface/src/views/apps/chat/components/unread-notice.js b/pkg/interface/src/views/apps/chat/components/unread-notice.js index f3f6e73f1..82ba3e35c 100644 --- a/pkg/interface/src/views/apps/chat/components/unread-notice.js +++ b/pkg/interface/src/views/apps/chat/components/unread-notice.js @@ -8,22 +8,11 @@ import Timestamp from '~/views/components/Timestamp'; export const UnreadNotice = (props) => { const { unreadCount, unreadMsg, dismissUnread, onClick } = props; - if (!unreadMsg || unreadCount === 0) { + if (unreadCount === 0) { return null; } - const stamp = 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; - } + const stamp = unreadMsg && moment.unix(unreadMsg.post['time-sent'] / 1000); return ( <Box @@ -52,15 +41,20 @@ export const UnreadNotice = (props) => { whiteSpace='pre' overflow='hidden' display='flex' - cursor='pointer' + cursor={unreadMsg ? 'pointer' : null} onClick={onClick} > - {unreadCount} new message{unreadCount > 1 ? 's' : ''} since{' '} - <Timestamp stamp={stamp} color='black' date={true} fontSize={1} /> + {unreadCount} new message{unreadCount > 1 ? 's' : ''} + {unreadMsg && ( + <> + since{' '} + <Timestamp stamp={stamp} color='black' date={true} fontSize={1} /> + </> + )} </Text> <Icon icon='X' - ml='4' + ml={unreadMsg ? 4 : 1} color='black' cursor='pointer' textAlign='right' From 27f2f93cc8c21e4e26191e7b80ab8ccdcde869ba Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Tue, 27 Apr 2021 16:25:50 +1000 Subject: [PATCH 56/69] VirtualScroller: fix for origin=top --- .../src/views/apps/links/LinkWindow.tsx | 14 +- .../views/apps/links/components/LinkItem.tsx | 7 +- .../src/views/components/VirtualScroller.tsx | 14 +- .../components/Home/Post/PostFeed.js | 185 +++++++++--------- 4 files changed, 115 insertions(+), 105 deletions(-) diff --git a/pkg/interface/src/views/apps/links/LinkWindow.tsx b/pkg/interface/src/views/apps/links/LinkWindow.tsx index 7b44aa08a..2f085ca00 100644 --- a/pkg/interface/src/views/apps/links/LinkWindow.tsx +++ b/pkg/interface/src/views/apps/links/LinkWindow.tsx @@ -6,7 +6,7 @@ import React, { Component, } from "react"; -import { Col, Text } from "@tlon/indigo-react"; +import { Box, Col, Text } from "@tlon/indigo-react"; import bigInt from "big-integer"; import { Association, Graph, Unreads, Group, Rolodex } from "@urbit/api"; @@ -48,7 +48,7 @@ class LinkWindow extends Component<LinkWindowProps, {}> { return isWriter(group, association.resource); } - renderItem = ({ index, scrollWindow }) => { + renderItem = React.forwardRef(({ index, scrollWindow }, ref) => { const { props } = this; const { association, graph, api } = props; const [, , ship, name] = association.resource.split("/"); @@ -80,12 +80,14 @@ class LinkWindow extends Component<LinkWindowProps, {}> { api={api} /> </Col> - <LinkItem {...linkProps} /> + <LinkItem ref={ref} {...linkProps} /> </React.Fragment> ); } - return <LinkItem key={index.toString()} {...linkProps} />; - }; + return <Box ref={ref}> + <LinkItem ref={ref} key={index.toString()} {...linkProps} />; + </Box> + }); render() { const { graph, api, association } = this.props; @@ -136,4 +138,4 @@ class LinkWindow extends Component<LinkWindowProps, {}> { } } -export default LinkWindow; \ No newline at end of file +export default LinkWindow; diff --git a/pkg/interface/src/views/apps/links/components/LinkItem.tsx b/pkg/interface/src/views/apps/links/components/LinkItem.tsx index a3e206543..8937c24d3 100644 --- a/pkg/interface/src/views/apps/links/components/LinkItem.tsx +++ b/pkg/interface/src/views/apps/links/components/LinkItem.tsx @@ -19,7 +19,7 @@ interface LinkItemProps { node: GraphNode; association: Association; resource: string; api: GlobalApi; group: Group; path: string; } -export const LinkItem = (props: LinkItemProps): ReactElement => { +export const LinkItem = React.forwardRef((props: LinkItemProps, ref): ReactElement => { const { association, node, @@ -30,7 +30,6 @@ export const LinkItem = (props: LinkItemProps): ReactElement => { ...rest } = props; - const ref = useRef<HTMLDivElement | null>(null); const remoteRef = useRef<typeof RemoteContent | null>(null); const index = node.post.index.split('/')[1]; @@ -97,7 +96,7 @@ export const LinkItem = (props: LinkItemProps): ReactElement => { const unreads = useHarkState(state => state.unreads); const commColor = (unreads.graph?.[appPath]?.[`/${index}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray'; const isUnread = unreads.graph?.[appPath]?.['/']?.unreads?.has(node.post.index); - + return ( <Box mx="auto" @@ -208,5 +207,5 @@ export const LinkItem = (props: LinkItemProps): ReactElement => { </Row> </Box>); -}; +}); diff --git a/pkg/interface/src/views/components/VirtualScroller.tsx b/pkg/interface/src/views/components/VirtualScroller.tsx index 1d9bbbad4..b2a37329e 100644 --- a/pkg/interface/src/views/components/VirtualScroller.tsx +++ b/pkg/interface/src/views/components/VirtualScroller.tsx @@ -428,7 +428,11 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T return; } - const newScrollTop = this.window.scrollHeight - ref.offsetTop - this.savedDistance; + const newScrollTop = this.props.origin === 'top' + ? this.savedDistance + ref.offsetTop + : this.window.scrollHeight - ref.offsetTop - this.savedDistance; + + console.log(ref.offsetTop); this.window.scrollTo(0, newScrollTop); requestAnimationFrame(() => { @@ -479,8 +483,10 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T let bottomIndex = visibleItems[visibleItems.length - 1]; const { scrollTop, scrollHeight } = this.window; - const topSpacing = scrollHeight - scrollTop; - ([...visibleItems]).reverse().forEach((index) => { + const topSpacing = this.props.origin === 'top' ? scrollTop : scrollHeight - scrollTop; + console.log(scrollTop); + const items = this.props.origin === 'top' ? visibleItems : [...visibleItems].reverse(); + items.forEach((index) => { const el = this.childRefs.get(index.toString()); if(!el) { return; @@ -499,7 +505,9 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T } this.savedIndex = bottomIndex; + console.log(this.childRefs.size); const ref = this.childRefs.get(bottomIndex.toString())!; + console.log(ref); const { offsetTop } = ref; this.savedDistance = topSpacing - offsetTop } diff --git a/pkg/interface/src/views/landscape/components/Home/Post/PostFeed.js b/pkg/interface/src/views/landscape/components/Home/Post/PostFeed.js index bfd29b9ef..8f805db38 100644 --- a/pkg/interface/src/views/landscape/components/Home/Post/PostFeed.js +++ b/pkg/interface/src/views/landscape/components/Home/Post/PostFeed.js @@ -2,7 +2,7 @@ import React from 'react'; import bigInt from 'big-integer'; import VirtualScroller from "~/views/components/VirtualScroller"; import PostItem from './PostItem/PostItem'; -import { Col } from '@tlon/indigo-react'; +import { Col, Box } from '@tlon/indigo-react'; import { resourceFromPath } from '~/logic/lib/group'; @@ -15,102 +15,103 @@ export class PostFeed extends React.Component { super(props); 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.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) { const { graph, graphPath, api } = this.props; const graphResource = resourceFromPath(graphPath); From 4f6003fd241b4c6f553ec12f55e056bfc70c4f21 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Tue, 27 Apr 2021 16:32:05 +1000 Subject: [PATCH 57/69] graph-update: fix recursive add-nodes case --- pkg/interface/src/logic/reducers/graph-update.ts | 8 +++++--- pkg/interface/src/views/components/VirtualScroller.tsx | 4 ---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pkg/interface/src/logic/reducers/graph-update.ts b/pkg/interface/src/logic/reducers/graph-update.ts index 52fe36f4c..02d43d6da 100644 --- a/pkg/interface/src/logic/reducers/graph-update.ts +++ b/pkg/interface/src/logic/reducers/graph-update.ts @@ -105,7 +105,7 @@ const removeGraph = (json, state: GraphState): GraphState => { }; const mapifyChildren = (children) => { - return new BigIntOrderedMap( + return new BigIntOrderedMap().gas( _.map(children, (node, idx) => { idx = idx && idx.startsWith('/') ? idx.slice(1) : idx; const nd = {...node, children: mapifyChildren(node.children || {}) }; @@ -214,12 +214,14 @@ const addNodes = (json, state) => { state.graphTimesentMap[resource][node.post['time-sent']] = index; } - node.children = mapifyChildren(node?.children || {}); + state.graphs[resource] = _addNode( state.graphs[resource], indexArr, - node + produce(node, draft => { + draft.children = mapifyChildren(draft?.children || {}); + }) ); if(newSize !== old) { console.log(`${resource}, (${old}, ${newSize}, ${state.graphs[resource].size})`); diff --git a/pkg/interface/src/views/components/VirtualScroller.tsx b/pkg/interface/src/views/components/VirtualScroller.tsx index b2a37329e..2986873f2 100644 --- a/pkg/interface/src/views/components/VirtualScroller.tsx +++ b/pkg/interface/src/views/components/VirtualScroller.tsx @@ -432,7 +432,6 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T ? this.savedDistance + ref.offsetTop : this.window.scrollHeight - ref.offsetTop - this.savedDistance; - console.log(ref.offsetTop); this.window.scrollTo(0, newScrollTop); requestAnimationFrame(() => { @@ -484,7 +483,6 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T let bottomIndex = visibleItems[visibleItems.length - 1]; const { scrollTop, scrollHeight } = this.window; const topSpacing = this.props.origin === 'top' ? scrollTop : scrollHeight - scrollTop; - console.log(scrollTop); const items = this.props.origin === 'top' ? visibleItems : [...visibleItems].reverse(); items.forEach((index) => { const el = this.childRefs.get(index.toString()); @@ -505,9 +503,7 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T } this.savedIndex = bottomIndex; - console.log(this.childRefs.size); const ref = this.childRefs.get(bottomIndex.toString())!; - console.log(ref); const { offsetTop } = ref; this.savedDistance = topSpacing - offsetTop } From 2d22823a5bac78a1b8e1661b9617a1c732ef1a34 Mon Sep 17 00:00:00 2001 From: Matilde Park <matilde@tlon.io> Date: Tue, 27 Apr 2021 15:51:02 -0400 Subject: [PATCH 58/69] UnreadNotice: add missing space --- pkg/interface/src/views/apps/chat/components/unread-notice.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/components/unread-notice.js b/pkg/interface/src/views/apps/chat/components/unread-notice.js index 82ba3e35c..53ad6f00b 100644 --- a/pkg/interface/src/views/apps/chat/components/unread-notice.js +++ b/pkg/interface/src/views/apps/chat/components/unread-notice.js @@ -44,10 +44,10 @@ export const UnreadNotice = (props) => { cursor={unreadMsg ? 'pointer' : null} onClick={onClick} > - {unreadCount} new message{unreadCount > 1 ? 's' : ''} + {unreadCount} new message{unreadCount > 1 ? 's' : ''} {unreadMsg && ( <> - since{' '} + {' '}since{' '} <Timestamp stamp={stamp} color='black' date={true} fontSize={1} /> </> )} From 9675f0131e04f5a802d45b5deb74e235219590fe Mon Sep 17 00:00:00 2001 From: Matilde Park <matilde.park@gmail.com> Date: Tue, 27 Apr 2021 16:59:32 -0400 Subject: [PATCH 59/69] ChatMessage: fix props destructure --- pkg/interface/src/views/apps/chat/components/ChatMessage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index 8103f2c80..0384b117d 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx @@ -255,7 +255,7 @@ interface ChatMessageProps { } function ChatMessage(props: ChatMessageProps) { - let { highlighted } = this.props; + let { highlighted } = props; const { msg, previousMsg, From 22645b98149d23ed3e8885b04a61f828e913da7b Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Wed, 28 Apr 2021 13:37:20 +1000 Subject: [PATCH 60/69] interface: add state accessors --- pkg/interface/src/logic/state/contact.ts | 4 ++++ pkg/interface/src/logic/state/graph.ts | 15 ++++++++++++++- pkg/interface/src/logic/state/local.tsx | 4 ++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/pkg/interface/src/logic/state/contact.ts b/pkg/interface/src/logic/state/contact.ts index a57a183ff..d4413e41b 100644 --- a/pkg/interface/src/logic/state/contact.ts +++ b/pkg/interface/src/logic/state/contact.ts @@ -35,4 +35,8 @@ export function useContact(ship: string) { ); } +export function useOurContact() { + return useContact(`~${window.ship}`) +} + export default useContactState; diff --git a/pkg/interface/src/logic/state/graph.ts b/pkg/interface/src/logic/state/graph.ts index a17d81007..34185e015 100644 --- a/pkg/interface/src/logic/state/graph.ts +++ b/pkg/interface/src/logic/state/graph.ts @@ -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"; @@ -130,6 +131,18 @@ const useGraphState = createState<GraphState>('Graph', { // }, }, ['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; diff --git a/pkg/interface/src/logic/state/local.tsx b/pkg/interface/src/logic/state/local.tsx index c5a187d11..3047eee89 100644 --- a/pkg/interface/src/logic/state/local.tsx +++ b/pkg/interface/src/logic/state/local.tsx @@ -90,8 +90,8 @@ const useLocalState = create<LocalStateZus>(persist((set, get) => ({ name: 'localReducer' })); -function withLocalState<P, S extends keyof LocalState>(Component: any, stateMemberKeys?: S[]) { - return React.forwardRef((props: Omit<P, S>, ref) => { +function withLocalState<P, S extends keyof LocalState, C extends React.ComponentType<P>>(Component: C, stateMemberKeys?: S[]) { + return React.forwardRef<C, Omit<P, S>>((props, ref) => { const localState = stateMemberKeys ? useLocalState( state => stateMemberKeys.reduce( (object, key) => ({ ...object, [key]: state[key] }), {} From 163d94e5a8480abac382ac441220d6bc330c34e1 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Wed, 28 Apr 2021 13:50:19 +1000 Subject: [PATCH 61/69] withStorage: fix typings --- pkg/interface/src/views/components/withStorage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/interface/src/views/components/withStorage.tsx b/pkg/interface/src/views/components/withStorage.tsx index 257da7967..0b4c9c621 100644 --- a/pkg/interface/src/views/components/withStorage.tsx +++ b/pkg/interface/src/views/components/withStorage.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import useStorage from '~/logic/lib/useStorage'; +import useStorage, {IuseStorage} from '~/logic/lib/useStorage'; -const withStorage = (Component, params = {}) => { - return React.forwardRef((props: any, ref) => { +const withStorage = <P, C extends React.ComponentType<P>>(Component: C, params = {}) => { + return React.forwardRef<C, Omit<C, keyof IuseStorage>>((props, ref) => { const storage = useStorage(params); return <Component ref={ref} {...storage} {...props} />; From d2b08fbd8eab43efdb658c807ee3cebff0c6f005 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Wed, 28 Apr 2021 13:54:24 +1000 Subject: [PATCH 62/69] ChatMessage: refactor to remove association prop --- .../apps/chat/components/ChatMessage.tsx | 48 +++++++------------ 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index 0384b117d..633f69908 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx @@ -47,6 +47,7 @@ import ProfileOverlay from '~/views/components/ProfileOverlay'; import {useCopy} from '~/logic/lib/useCopy'; 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'; @@ -82,16 +83,13 @@ export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => ( ); export const UnreadMarker = React.forwardRef( - ({ dayBreak, when, api, association }: any, ref) => { + ({ dismissUnread }: any, ref) => { const [visible, setVisible] = useState(false); const idling = useIdlingState(); - const dismiss = useCallback(() => { - api.hark.markCountAsRead(association, '/', 'message'); - }, [api, association]); useEffect(() => { if (visible && !idling) { - dismiss(); + dismissUnread(); } }, [visible, idling]); @@ -143,10 +141,9 @@ const MessageActionItem = (props) => { ); }; -const MessageActions = ({ api, onReply, association, msg, group }) => { - const isAdmin = () => group.tags.role.admin.has(window.ship); +const MessageActions = ({ api, onReply, association, msg, isAdmin, permalink }) => { 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 ( <Box @@ -237,14 +234,13 @@ interface ChatMessageProps { previousMsg?: Post; nextMsg?: Post; isLastRead: boolean; - group: Group; - association: Association; + permalink: string; transcluded?: number; className?: string; isPending: boolean; style?: unknown; isLastMessage?: boolean; - unreadMarkerRef: React.RefObject<HTMLDivElement>; + dismissUnread: () => void; api: GlobalApi; highlighted?: boolean; renderSigil?: boolean; @@ -267,11 +263,12 @@ function ChatMessage(props: ChatMessageProps) { isPending, style, isLastMessage, - unreadMarkerRef, api, showOurContact, fontSize, - hideHover + hideHover, + dismissUnread, + permalink } = props; let onReply = props?.onReply ?? (() => {}); @@ -316,7 +313,6 @@ function ChatMessage(props: ChatMessageProps) { msg, timestamp, association, - group, isPending, showOurContact, api, @@ -354,25 +350,13 @@ function ChatMessage(props: ChatMessageProps) { {dayBreak && !isLastRead ? ( <DayBreak when={date} shimTop={renderSigil} /> ) : null} - {renderSigil ? ( - <MessageWrapper {...messageProps}> - <MessageAuthor pb={1} {...messageProps} /> - {message} - </MessageWrapper> - ) : ( - <MessageWrapper {...messageProps}> - {message} - </MessageWrapper> - )} + <MessageWrapper permalink={permalink} {...messageProps}> + { renderSigil && <MessageAuthor {...messageProps} />} + {message} + </MessageWrapper> <Box style={unreadContainerStyle}> {isLastRead ? ( - <UnreadMarker - association={association} - api={api} - dayBreak={dayBreak} - when={date} - ref={unreadMarkerRef} - /> + <UnreadMarker dismissUnread={dismissUnread} /> ) : null} </Box> </Box> @@ -457,7 +441,7 @@ export const MessageAuthor = ({ </Box> ); return ( - <Box display='flex' alignItems='flex-start'> + <Box pb="1" display='flex' alignItems='flex-start'> <Box height={24} pr={2} From f221cfe1350323dd1070b7cdd5178d91e5db5c3a Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Wed, 28 Apr 2021 13:54:49 +1000 Subject: [PATCH 63/69] ChatInput: lift callbacks, refactor --- .../views/apps/chat/components/ChatInput.tsx | 56 ++++++++----------- 1 file changed, 22 insertions(+), 34 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx index 403ef6427..fe83a0aef 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx @@ -1,4 +1,4 @@ -import React, { Component } from 'react'; +import React, { Component, ReactNode } from 'react'; import ChatEditor from './chat-editor'; import { IuseStorage } from '~/logic/lib/useStorage'; import { uxToHex } from '~/logic/lib/util'; @@ -8,7 +8,7 @@ import tokenizeMessage, { isUrl } from '~/logic/lib/tokenizeMessage'; import GlobalApi from '~/logic/api/global'; import { Envelope } from '~/types/chat-update'; 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 withStorage from '~/views/components/withStorage'; import { withLocalState } from '~/logic/state/local'; @@ -16,15 +16,14 @@ import { MOBILE_BROWSER_REGEX } from "~/logic/lib/util"; type ChatInputProps = IuseStorage & { api: GlobalApi; - numMsgs: number; - station: unknown; - ourContact: unknown; - envelopes: Envelope[]; + ourContact?: Contact; onUnmount(msg: string): void; placeholder: string; message: string; deleteMessage(): void; hideAvatars: boolean; + onSubmit: (contents: Content[]) => void; + children?: ReactNode; }; interface ChatInputState { @@ -62,40 +61,28 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> { }); } - submit(text) { + async submit(text) { const { props, state } = this; - const [, , ship, name] = props.station.split('/'); - this.setState({ currentInput: '' }); - if (state.inCodeMode) { - this.setState( - { - 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)); - + const { onSubmit, api } = this.props; + this.setState({ + inCodeMode: false + }); props.deleteMessage(); - - props.api.graph.addPost(ship, name, post); + if(state.inCodeMode) { + 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; if (this.state.uploadingPaste) { this.chatEditor.current.editor.setValue(url); this.setState({ uploadingPaste: false }); } else { - const [, , ship, name] = props.station.split('/'); - props.api.graph.addPost(ship, name, createPost([{ url }])); + props.onSubmit([{ url }]) } } @@ -241,6 +228,7 @@ class ChatInput extends Component<ChatInputProps, ChatInputState> { } } -export default withLocalState(withStorage(ChatInput, { accept: 'image/*' }), [ - 'hideAvatars' -]); +export default withLocalState<Omit<ChatInputProps, keyof IuseStorage>, 'hideAvatars', ChatInput>( + withStorage<ChatInputProps, ChatInput>(ChatInput, { accept: 'image/*' }), + ['hideAvatars'] +) From 053e392c5cb9ea8bdd9b81e0983396826244bc29 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Wed, 28 Apr 2021 13:55:05 +1000 Subject: [PATCH 64/69] ChatWindow: remove association prop --- .../views/apps/chat/components/ChatWindow.tsx | 112 ++++-------------- 1 file changed, 26 insertions(+), 86 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx index cf34168df..c9558e7da 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx @@ -11,7 +11,9 @@ import { Associations, Group, Groups, - Graph + Graph, + Post, + GraphNode } from '@urbit/api'; import GlobalApi from '~/logic/api/global'; @@ -30,25 +32,21 @@ const DEFAULT_BACKLOG_SIZE = 100; const IDLE_THRESHOLD = 64; const MAX_BACKLOG_SIZE = 1000; -const getCurrGraphSize = (ship: string, name: string) => { - const { graphs } = useGraphState.getState(); - const graph = graphs[`${ship}/${name}`]; - return graph.size; -}; type ChatWindowProps = { unreadCount: number; graph: Graph; graphSize: number; - association: Association; - group: Group; - ship: Patp; station: any; + fetchMessages: (newer: boolean) => Promise<boolean>; api: GlobalApi; scrollTo?: BigInteger; onReply: (msg: Post) => void; + dismissUnread: () => void; pendingSize?: number; showOurContact: boolean; + getPermalink: (index: BigInteger) => string; + isAdmin: boolean; }; interface ChatWindowState { @@ -65,8 +63,7 @@ class ChatWindow extends Component< ChatWindowProps, ChatWindowState > { - private virtualList: VirtualScroller | null; - private unreadMarkerRef: React.RefObject<HTMLDivElement>; + private virtualList: VirtualScroller<GraphNode> | null; private prevSize = 0; private unreadSet = false; @@ -82,15 +79,12 @@ class ChatWindow extends Component< unreadIndex: bigInt.zero }; - this.dismissUnread = this.dismissUnread.bind(this); this.scrollToUnread = this.scrollToUnread.bind(this); this.handleWindowBlur = this.handleWindowBlur.bind(this); this.handleWindowFocus = this.handleWindowFocus.bind(this); this.stayLockedIfActive = this.stayLockedIfActive.bind(this); - this.fetchMessages = this.fetchMessages.bind(this); this.virtualList = null; - this.unreadMarkerRef = React.createRef(); this.prevSize = props.graph.size; } @@ -99,10 +93,9 @@ class ChatWindow extends Component< setTimeout(() => { this.setState({ initialized: true }, () => { if(this.props.scrollTo) { - this.virtualList.scrollToIndex(this.props.scrollTo); - + this.virtualList!.scrollLocked = false; + this.virtualList!.scrollToIndex(this.props.scrollTo); } - }); }, this.INITIALIZATION_MAX_TIME); @@ -142,7 +135,7 @@ class ChatWindow extends Component< handleWindowFocus() { this.setState({ idle: false }); if (this.virtualList?.window?.scrollTop === 0) { - this.dismissUnread(); + this.props.dismissUnread(); } } @@ -159,8 +152,8 @@ class ChatWindow extends Component< } if(this.unreadSet && this.dismissedInitialUnread() && - this.virtualList?.startOffset() < 5) { - this.dismissUnread(); + this.virtualList!.startOffset() < 5) { + this.props.dismissUnread(); } } @@ -178,7 +171,7 @@ class ChatWindow extends Component< stayLockedIfActive() { if (this.virtualList && !this.state.idle) { this.virtualList.resetScroll(); - this.dismissUnread(); + this.props.dismissUnread(); } } @@ -197,49 +190,6 @@ class ChatWindow extends Component< 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'); - } - - - async fetchMessages(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()?.[0]; - if(!index) { - console.log(`no index for: ${graph}`); - return true; - } - await api.graph.getYoungerSiblings( - ship, - name, - pageSize, - `/${index.toString()}` - ); - return expectedSize !== getCurrGraphSize(ship.slice(1), name); - } else { - console.log('x'); - const index = graph.peekSmallest()?.[0]; - if(!index) { - console.log(`no index for: ${graph}`); - return true; - } - await api.graph.getOlderSiblings(ship, name, pageSize, `/${index.toString()}`); - const done = expectedSize !== getCurrGraphSize(ship.slice(1), name); - if(done) { - this.calculateUnreadIndex(); - } - return done; - } - } - onScroll = ({ scrollTop, scrollHeight, windowHeight }) => { if (!this.state.idle && scrollTop > IDLE_THRESHOLD) { this.setState({ idle: true }); @@ -250,21 +200,21 @@ class ChatWindow extends Component< renderer = React.forwardRef(({ index, scrollWindow }, ref) => { const { api, - association, showOurContact, graph, - group, - onReply + onReply, + getPermalink, + dismissUnread, + isAdmin, } = this.props; - const { unreadMarkerRef } = this; + const permalink = getPermalink(index); const messageProps = { - association, - group, showOurContact, - unreadMarkerRef, api, - group, - onReply + onReply, + permalink, + dismissUnread, + isAdmin }; const msg = graph.get(index)?.post; @@ -313,20 +263,11 @@ class ChatWindow extends Component< const { unreadCount, api, - association, - group, graph, showOurContact, pendingSize = 0, } = this.props; - const unreadMarkerRef = this.unreadMarkerRef; - const messageProps = { - association, - group, - unreadMarkerRef, - api, - }; const unreadMsg = graph.get(this.state.unreadIndex); return ( @@ -341,10 +282,10 @@ class ChatWindow extends Component< ? false : unreadMsg } - dismissUnread={this.dismissUnread} + dismissUnread={this.props.dismissUnread} onClick={this.scrollToUnread} />)} - <VirtualScroller + <VirtualScroller<GraphNode> ref={(list) => { this.virtualList = list; }} @@ -356,10 +297,9 @@ class ChatWindow extends Component< data={graph} size={graph.size} pendingSize={pendingSize} - id={association.resource} averageHeight={22} renderer={this.renderer} - loadRows={this.fetchMessages} + loadRows={this.props.fetchMessages} /> </Col> ); From a1c433b455a2191806d0a553593d61467a191b45 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Wed, 28 Apr 2021 13:55:40 +1000 Subject: [PATCH 65/69] ShareProfile: refactor to remove dead props --- .../views/apps/chat/components/ShareProfile.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pkg/interface/src/views/apps/chat/components/ShareProfile.js b/pkg/interface/src/views/apps/chat/components/ShareProfile.js index 7c0e03dbd..abd294133 100644 --- a/pkg/interface/src/views/apps/chat/components/ShareProfile.js +++ b/pkg/interface/src/views/apps/chat/components/ShareProfile.js @@ -40,21 +40,20 @@ export const ShareProfile = (props) => { ); const onClick = async () => { - if(group.hidden && recipients.length > 0) { - await api.contacts.allowShips(recipients); - await Promise.all(recipients.map(r => api.contacts.share(r))) - setShowBanner(false); - } else if (!group.hidden) { - const [,,ship,name] = groupPath.split('/'); + if(typeof recipients === 'string') { + const [,,ship,name] = recipients.split('/'); await api.contacts.allowGroup(ship,name); if(ship !== `~${window.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 height="48px" alignItems="center" From d635d596b823e9e91924692ecda7a911c98148c0 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Wed, 28 Apr 2021 13:56:09 +1000 Subject: [PATCH 66/69] ChatPane: add component --- .../views/apps/chat/components/ChatPane.tsx | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 pkg/interface/src/views/apps/chat/components/ChatPane.tsx diff --git a/pkg/interface/src/views/apps/chat/components/ChatPane.tsx b/pkg/interface/src/views/apps/chat/components/ChatPane.tsx new file mode 100644 index 000000000..aa2bfe2a3 --- /dev/null +++ b/pkg/interface/src/views/apps/chat/components/ChatPane.tsx @@ -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> + ); +} From e937911536f6bf5c76ffe53b31486ec6d1ebcc79 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Wed, 28 Apr 2021 13:56:35 +1000 Subject: [PATCH 67/69] ChatResource: refactor to use ChatPane --- pkg/interface/src/logic/api/contacts.ts | 23 ++ .../src/views/apps/chat/ChatResource.tsx | 275 ++++++++---------- 2 files changed, 144 insertions(+), 154 deletions(-) diff --git a/pkg/interface/src/logic/api/contacts.ts b/pkg/interface/src/logic/api/contacts.ts index d4f799f69..bf8fe00fe 100644 --- a/pkg/interface/src/logic/api/contacts.ts +++ b/pkg/interface/src/logic/api/contacts.ts @@ -2,6 +2,7 @@ import BaseApi from './base'; import { StoreState } from '../store/type'; import { Patp } from '@urbit/api'; import { ContactEdit } from '@urbit/api/contacts'; +import _ from 'lodash'; export default class ContactsApi extends BaseApi<StoreState> { 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) { const resource = { ship, name: '' }; return this.action('contact-pull-hook', 'pull-hook-action', { diff --git a/pkg/interface/src/views/apps/chat/ChatResource.tsx b/pkg/interface/src/views/apps/chat/ChatResource.tsx index 17b74ad6a..f335531c5 100644 --- a/pkg/interface/src/views/apps/chat/ChatResource.tsx +++ b/pkg/interface/src/views/apps/chat/ChatResource.tsx @@ -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 { Col } from '@tlon/indigo-react'; import _ from 'lodash'; -import bigInt from 'big-integer'; +import bigInt, { BigInteger } from 'big-integer'; import { Association } from '@urbit/api/metadata'; import { StoreState } from '~/logic/store/type'; @@ -16,13 +22,20 @@ import { useLocalStorageState } from '~/logic/lib/useLocalStorageState'; import { Loading } from '~/views/components/Loading'; import { isWriter, resourceFromPath } from '~/logic/lib/group'; -import './css/custom.css'; import useContactState from '~/logic/state/contact'; -import useGraphState from '~/logic/state/graph'; -import useGroupState from '~/logic/state/group'; +import useGraphState, { useGraphForAssoc } from '~/logic/state/graph'; +import useGroupState, { useGroupForAssoc } from '~/logic/state/group'; import useHarkState from '~/logic/state/hark'; -import {Post} from '@urbit/api'; -import {getPermalinkForGraph} from '~/logic/lib/permalinks'; +import { Content, createPost, Post } from '@urbit/api'; +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 & { association: Association; @@ -31,172 +44,126 @@ type ChatResourceProps = StoreState & { }; function ChatResource(props: ChatResourceProps) { - const station = props.association.resource; - const groupPath = props.association.group; - const groups = useGroupState(state => state.groups); - const group = groups[groupPath]; - const contacts = useContactState(state => state.contacts); - const graphs = useGraphState(state => state.graphs); - const graphPath = station.slice(7); - const graph = graphs[graphPath]; - const unreads = useHarkState(state => state.unreads); - const unreadCount = unreads.graph?.[station]?.['/']?.unreads as number || 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); + const { association, api } = props; + const { resource } = association; + const [toShare, setToShare] = useState<string[] | string | undefined>(); + const group = useGroupForAssoc(association)!; + const graph = useGraphForAssoc(association); + const unreads = useHarkState((state) => state.unreads); + const unreadCount = + (unreads.graph?.[resource]?.['/']?.unreads as number) || 0; + const canWrite = group ? isWriter(group, resource) : false; useEffect(() => { const count = Math.min(400, 100 + unreadCount); - props.api.graph.getNewest(owner, name, count); - }, [station]); - - 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, [station]: u })), - [station] - ); - - const clearUnsent = useCallback( - () => { - setUnsent(s => { - if(station in s) { - return _.omit(s, station); - } - return s; - }); - }, - [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( + const { ship, name } = resourceFromPath(resource); + props.api.graph.getNewest(ship, name, count); + setToShare(undefined); + (async function() { + if(group.hidden) { + const members = await props.api.contacts.disallowedShipsForOurContact( 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) { - setShowBanner(true); - setRecipients(members); - } else { - setShowBanner(false); + setToShare(members); } } else { - const groupShared = await props.api.contacts.fetchIsAllowed( + const { ship: groupHost } = resourceFromPath(association.group); + const shared = await props.api.contacts.fetchIsAllowed( `~${window.ship}`, 'personal', - res.ship, + groupHost, 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 ( - <Col {...bind} height="100%" overflow="hidden" position="relative"> - <ShareProfile - our={ourContact} - api={props.api} - recipient={owner} - recipients={recipients} - showBanner={showBanner} - setShowBanner={setShowBanner} - group={group} - groupPath={groupPath} - /> - {dragging && <SubmitDragger />} - <ChatWindow - key={station} - graph={graph} - graphSize={graph.size} - unreadCount={unreadCount as number} - 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> + <ChatPane + id={resource.slice(7)} + graph={graph} + unreadCount={unreadCount} + api={api} + canWrite={canWrite} + onReply={onReply} + fetchMessages={fetchMessages} + dismissUnread={dismissUnread} + getPermalink={getPermalink} + isAdmin={isAdmin} + onSubmit={onSubmit} + promptShare={toShare} + /> ); } -ChatResource.whyDidYouRender = true; - export { ChatResource }; - From 54ef64ee7cf0d59c9d9f6710a6fae2381233cd9e Mon Sep 17 00:00:00 2001 From: Matilde Park <matilde@tlon.io> Date: Wed, 28 Apr 2021 13:18:37 -0400 Subject: [PATCH 68/69] ChatPane: invert showOurContact conditional --- pkg/interface/src/views/apps/chat/components/ChatPane.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/interface/src/views/apps/chat/components/ChatPane.tsx b/pkg/interface/src/views/apps/chat/components/ChatPane.tsx index aa2bfe2a3..e8f64669a 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatPane.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatPane.tsx @@ -156,7 +156,7 @@ export function ChatPane(props: ChatPaneProps) { graph={graph} graphSize={graph.size} unreadCount={unreadCount} - showOurContact={promptShare.length === 0 && showBanner} + showOurContact={promptShare.length === 0 && !showBanner} pendingSize={Object.keys(graphTimesentMap[id] || {}).length} onReply={onReply} dismissUnread={dismissUnread} From dbf8c2afb983f3204e8f9917de1bac0339d99409 Mon Sep 17 00:00:00 2001 From: Liam Fitzgerald <liam.fitzgerald@tlon.io> Date: Thu, 29 Apr 2021 14:33:26 +1000 Subject: [PATCH 69/69] VirtualScroller: do not crash if ref is unset --- pkg/interface/src/views/components/VirtualScroller.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/interface/src/views/components/VirtualScroller.tsx b/pkg/interface/src/views/components/VirtualScroller.tsx index 2986873f2..e366ef2b8 100644 --- a/pkg/interface/src/views/components/VirtualScroller.tsx +++ b/pkg/interface/src/views/components/VirtualScroller.tsx @@ -504,6 +504,11 @@ export default class VirtualScroller<T> extends Component<VirtualScrollerProps<T this.savedIndex = bottomIndex; const ref = this.childRefs.get(bottomIndex.toString())!; + if(!ref) { + this.saveDepth--; + log('bail', 'missing ref'); + return; + } const { offsetTop } = ref; this.savedDistance = topSpacing - offsetTop }