mirror of
https://github.com/urbit/shrub.git
synced 2024-12-30 15:44:03 +03:00
Merge remote-tracking branch 'origin/release/2021-5-27' into lf/hark-boxing
This commit is contained in:
commit
55ad8e22ae
14
.github/workflows/typescript-check.yml
vendored
Normal file
14
.github/workflows/typescript-check.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
name: typescript-check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'pkg/interface/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
typescript-check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: "Check pkg/interface types"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- run: cd 'pkg/interface' && npm i && npm run tsc
|
@ -5,7 +5,7 @@
|
|||||||
/- glob
|
/- glob
|
||||||
/+ default-agent, verb, dbug
|
/+ default-agent, verb, dbug
|
||||||
|%
|
|%
|
||||||
++ hash 0v4.vrvkt.4gcnm.dgg5o.e73d6.kqnaq
|
++ hash 0v2.rvlfs.f97fq.hjrpe.d3h68.n54sj
|
||||||
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
||||||
+$ all-states
|
+$ all-states
|
||||||
$% state-0
|
$% state-0
|
||||||
|
@ -24,6 +24,6 @@
|
|||||||
<div id="portal-root"></div>
|
<div id="portal-root"></div>
|
||||||
<script src="/~landscape/js/channel.js"></script>
|
<script src="/~landscape/js/channel.js"></script>
|
||||||
<script src="/~landscape/js/session.js"></script>
|
<script src="/~landscape/js/session.js"></script>
|
||||||
<script src="/~landscape/js/bundle/index.f252a9afb6e952de19c9.js"></script>
|
<script src="/~landscape/js/bundle/index.a6842e8d167b4e66a4e0.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
31
pkg/interface/package-lock.json
generated
31
pkg/interface/package-lock.json
generated
@ -1551,6 +1551,15 @@
|
|||||||
"integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==",
|
"integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/mdast": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-SXPBMnFVQg1s00dlMCc/jCdvPqdE4mXaMMCeRlxLDmTAEoegHT53xKtkDnzDTOcmMHUfcjyf36/YYZ6SxRdnsw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/unist": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/minimatch": {
|
"@types/minimatch": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
|
||||||
@ -6643,9 +6652,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"immer": {
|
"immer": {
|
||||||
"version": "8.0.1",
|
"version": "9.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.2.tgz",
|
||||||
"integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA=="
|
"integrity": "sha512-mkcmzLtIfSp40vAqteRr1MbWNSoI7JE+/PB36FNPoSfJ9RQRmNKuTYCjKkyXyuq3Dgn07HuJBrwJd4ZSk2yUbw=="
|
||||||
},
|
},
|
||||||
"import-fresh": {
|
"import-fresh": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
@ -10804,6 +10813,16 @@
|
|||||||
"resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz",
|
||||||
"integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA=="
|
"integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA=="
|
||||||
},
|
},
|
||||||
|
"ts-mdast": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-mdast/-/ts-mdast-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FmT5GbMU629/ty64741v7TdO8jm5xW09okr2VNExkLuRk5ngjKIDdn/woTB8lDtcgCMRS8lUNubImen0MkdF6g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/mdast": "^3.0.3",
|
||||||
|
"@types/unist": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tslib": {
|
"tslib": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz",
|
||||||
@ -12185,9 +12204,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zustand": {
|
"zustand": {
|
||||||
"version": "3.3.1",
|
"version": "3.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-3.5.1.tgz",
|
||||||
"integrity": "sha512-o0rgrBsi29nCkPHdhtkAHisCIlmRUoXOV+1AmDMeCgkGG0i5edFSpGU0KiZYBvFmBYycnck4Z07JsLYDjSET9g=="
|
"integrity": "sha512-7J56Ve814z4zap71iaKFD+t65LFI//jEq/Vf55BTSVqJZCm+w9rov8OMBg+YSwIPQk54bfoIWHTrOWuAbpEDMw=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
"css-loader": "^3.6.0",
|
"css-loader": "^3.6.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"formik": "^2.1.5",
|
"formik": "^2.1.5",
|
||||||
"immer": "^8.0.1",
|
"immer": "^9.0.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
@ -56,7 +56,7 @@
|
|||||||
"workbox-recipes": "^6.0.2",
|
"workbox-recipes": "^6.0.2",
|
||||||
"workbox-routing": "^6.0.2",
|
"workbox-routing": "^6.0.2",
|
||||||
"yup": "^0.29.3",
|
"yup": "^0.29.3",
|
||||||
"zustand": "^3.3.1"
|
"zustand": "^3.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.10",
|
"@babel/core": "^7.12.10",
|
||||||
@ -91,6 +91,7 @@
|
|||||||
"react-hot-loader": "^4.13.0",
|
"react-hot-loader": "^4.13.0",
|
||||||
"sass": "^1.32.5",
|
"sass": "^1.32.5",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
|
"ts-mdast": "^1.0.0",
|
||||||
"typescript": "^4.2.4",
|
"typescript": "^4.2.4",
|
||||||
"webpack": "^4.46.0",
|
"webpack": "^4.46.0",
|
||||||
"webpack-cli": "^3.3.12",
|
"webpack-cli": "^3.3.12",
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { Patp } from '@urbit/api';
|
import { Patp } from '@urbit/api';
|
||||||
import { ContactEditField } from '@urbit/api/contacts';
|
import { ContactEditField } from '@urbit/api/contacts';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import {edit} from '../reducers/contact-update';
|
||||||
|
import {doOptimistically} from '../state/base';
|
||||||
|
import useContactState from '../state/contact';
|
||||||
import { StoreState } from '../store/type';
|
import { StoreState } from '../store/type';
|
||||||
import BaseApi from './base';
|
import BaseApi from './base';
|
||||||
|
|
||||||
@ -26,13 +29,14 @@ export default class ContactsApi extends BaseApi<StoreState> {
|
|||||||
{add-group: {ship, name}}
|
{add-group: {ship, name}}
|
||||||
{remove-group: {ship, name}}
|
{remove-group: {ship, name}}
|
||||||
*/
|
*/
|
||||||
return this.storeAction({
|
const action = {
|
||||||
edit: {
|
edit: {
|
||||||
ship,
|
ship,
|
||||||
'edit-field': editField,
|
'edit-field': editField,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
doOptimistically(useContactState, action, this.storeAction.bind(this), [edit])
|
||||||
}
|
}
|
||||||
|
|
||||||
allowShips(ships: Patp[]) {
|
allowShips(ships: Patp[]) {
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { Association, GraphNotifDescription, IndexedNotification, NotifIndex } from '@urbit/api';
|
import { Association, GraphNotifDescription, IndexedNotification, NotifIndex } from '@urbit/api';
|
||||||
|
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
|
||||||
import { BigInteger } from 'big-integer';
|
import { BigInteger } from 'big-integer';
|
||||||
import { getParentIndex } from '../lib/notification';
|
import { getParentIndex } from '../lib/notification';
|
||||||
import { dateToDa, decToUd } from '../lib/util';
|
import { dateToDa, decToUd } from '../lib/util';
|
||||||
|
import {reduce} from '../reducers/hark-update';
|
||||||
|
import {doOptimistically, optReduceState} from '../state/base';
|
||||||
import useHarkState from '../state/hark';
|
import useHarkState from '../state/hark';
|
||||||
import { StoreState } from '../store/type';
|
import { StoreState } from '../store/type';
|
||||||
import BaseApi from './base';
|
import BaseApi from './base';
|
||||||
@ -51,8 +54,15 @@ export class HarkApi extends BaseApi<StoreState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
archive(time: BigInteger, index: NotifIndex) {
|
async archive(intTime: BigInteger, index: NotifIndex) {
|
||||||
return this.actOnNotification('archive', time, index);
|
const time = decToUd(intTime.toString());
|
||||||
|
const action = {
|
||||||
|
archive: {
|
||||||
|
time,
|
||||||
|
index
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await doOptimistically(useHarkState, action, this.harkAction.bind(this), [reduce])
|
||||||
}
|
}
|
||||||
|
|
||||||
read(time: BigInteger, index: NotifIndex) {
|
read(time: BigInteger, index: NotifIndex) {
|
||||||
@ -81,15 +91,15 @@ export class HarkApi extends BaseApi<StoreState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
markCountAsRead(association: Association, parent: string, description: GraphNotifDescription) {
|
markCountAsRead(association: Association, parent: string, description: GraphNotifDescription) {
|
||||||
return this.harkAction(
|
const action = { 'read-count': {
|
||||||
{ 'read-count': {
|
|
||||||
graph: {
|
graph: {
|
||||||
graph: association.resource,
|
graph: association.resource,
|
||||||
group: association.group,
|
group: association.group,
|
||||||
description,
|
description,
|
||||||
index: parent
|
index: parent
|
||||||
} }
|
} }
|
||||||
});
|
};
|
||||||
|
doOptimistically(useHarkState, action, this.harkAction.bind(this), [reduce]);
|
||||||
}
|
}
|
||||||
|
|
||||||
markEachAsRead(association: Association, parent: string, child: string, description: GraphNotifDescription, mod: string) {
|
markEachAsRead(association: Association, parent: string, child: string, description: GraphNotifDescription, mod: string) {
|
||||||
|
@ -6,13 +6,15 @@ interface IFormGroupContext {
|
|||||||
onDirty: (id: string, touched: boolean) => void;
|
onDirty: (id: string, touched: boolean) => void;
|
||||||
onErrors: (id: string, errors: boolean) => void;
|
onErrors: (id: string, errors: boolean) => void;
|
||||||
submitAll: () => Promise<any>;
|
submitAll: () => Promise<any>;
|
||||||
|
addReset: (id: string, r: any) => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fallback: IFormGroupContext = {
|
const fallback: IFormGroupContext = {
|
||||||
addSubmit: () => {},
|
addSubmit: () => {},
|
||||||
onDirty: () => {},
|
onDirty: () => {},
|
||||||
onErrors: () => {},
|
onErrors: () => {},
|
||||||
submitAll: () => Promise.resolve()
|
submitAll: () => Promise.resolve(),
|
||||||
|
addReset: () => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FormGroupContext = React.createContext(fallback);
|
export const FormGroupContext = React.createContext(fallback);
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { IndexedNotification, NotificationGraphConfig, Unreads } from '@urbit/api';
|
import { GraphNotifIndex, GroupNotifIndex, IndexedNotification, NotificationGraphConfig, Post, Unreads } from '@urbit/api';
|
||||||
import bigInt, { BigInteger } from 'big-integer';
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import f from 'lodash/fp';
|
import f from 'lodash/fp';
|
||||||
|
import { pluralize } from './util';
|
||||||
|
|
||||||
export function getLastSeen(
|
export function getLastSeen(
|
||||||
unreads: Unreads,
|
unreads: Unreads,
|
||||||
@ -58,3 +59,56 @@ export function getNotificationKey(time: BigInteger, notification: IndexedNotifi
|
|||||||
return `${base}-unknown`;
|
return `${base}-unknown`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function notificationReferent(not: IndexedNotification) {
|
||||||
|
if('graph' in not.index) {
|
||||||
|
return not.index.graph.graph;
|
||||||
|
} else {
|
||||||
|
return not.index.group.group;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function describeNotification(notification: IndexedNotification) {
|
||||||
|
function group(idx: GroupNotifIndex) {
|
||||||
|
switch (idx.description) {
|
||||||
|
case 'add-members':
|
||||||
|
return 'joined';
|
||||||
|
case 'remove-members':
|
||||||
|
return 'left';
|
||||||
|
default:
|
||||||
|
return idx.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function graph(idx: GraphNotifIndex, plural: boolean, singleAuthor: boolean) {
|
||||||
|
const isDm = idx.graph.startsWith('dm--');
|
||||||
|
switch (idx.description) {
|
||||||
|
case 'post':
|
||||||
|
return singleAuthor ? 'replied to you' : 'Your post received replies';
|
||||||
|
case 'link':
|
||||||
|
return `New link${plural ? 's' : ''} in`;
|
||||||
|
case 'comment':
|
||||||
|
return `New comment${plural ? 's' : ''} on`;
|
||||||
|
case 'note':
|
||||||
|
return `New Note${plural ? 's' : ''} in`;
|
||||||
|
// @ts-ignore
|
||||||
|
case 'edit-note':
|
||||||
|
return `updated ${pluralize('note', plural)} in`;
|
||||||
|
case 'mention':
|
||||||
|
return singleAuthor ? 'mentioned you in' : 'You were mentioned in';
|
||||||
|
case 'message':
|
||||||
|
if (isDm) {
|
||||||
|
return 'messaged you';
|
||||||
|
}
|
||||||
|
return `New message${plural ? 's' : ''} in`;
|
||||||
|
default:
|
||||||
|
return idx.description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if('group' in notification.index) {
|
||||||
|
return group(notification.index.group);
|
||||||
|
} else if('graph' in notification.index) {
|
||||||
|
// @ts-ignore needs better type guard
|
||||||
|
const contents = notification.notification?.contents?.graph ?? [] as Post[];
|
||||||
|
return graph(notification.index.graph, contents.length > 1, _.uniq(_.map(contents, 'author')).length === 1)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -76,7 +76,7 @@ export function editPost(rev: number, noteId: BigInteger, title: string, body: s
|
|||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLatestRevision(node: GraphNode): [number, string, string, Post] {
|
export function getLatestRevision(node: GraphNode): [number, string, any, Post] {
|
||||||
const empty = [1, '', '', buntPost()] as [number, string, string, Post];
|
const empty = [1, '', '', buntPost()] as [number, string, string, Post];
|
||||||
const revs = node.children?.get(bigInt(1));
|
const revs = node.children?.get(bigInt(1));
|
||||||
if(!revs) {
|
if(!revs) {
|
||||||
|
70
pkg/interface/src/logic/lib/shortcutContext.tsx
Normal file
70
pkg/interface/src/logic/lib/shortcutContext.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { getChord } from '~/logic/lib/util';
|
||||||
|
|
||||||
|
type Handler = (e: KeyboardEvent) => void;
|
||||||
|
const fallback: ShortcutContextProps = {
|
||||||
|
add: () => {},
|
||||||
|
remove: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const ShortcutContext = createContext(fallback);
|
||||||
|
export interface ShortcutContextProps {
|
||||||
|
add: (cb: (e: KeyboardEvent) => void, key: string) => void;
|
||||||
|
remove: (cb: (e: KeyboardEvent) => void, key: string) => void;
|
||||||
|
}
|
||||||
|
export function ShortcutContextProvider({ children }) {
|
||||||
|
const [shortcuts, setShortcuts] = useState({} as Record<string, Handler>);
|
||||||
|
const handlerRef = useRef<Handler>(() => {});
|
||||||
|
|
||||||
|
const add = useCallback((cb: Handler, key: string) => {
|
||||||
|
setShortcuts((s) => ({ ...s, [key]: cb }));
|
||||||
|
}, []);
|
||||||
|
const remove = useCallback((cb: Handler, key: string) => {
|
||||||
|
setShortcuts((s) => (key in s ? _.omit(s, key) : s));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onKeypress(e: KeyboardEvent) {
|
||||||
|
handlerRef.current(e);
|
||||||
|
}
|
||||||
|
document.addEventListener('keypress', onKeypress);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keypress', onKeypress);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handlerRef.current = function (e: KeyboardEvent) {
|
||||||
|
const chord = getChord(e);
|
||||||
|
shortcuts?.[chord]?.(e);
|
||||||
|
};
|
||||||
|
}, [shortcuts]);
|
||||||
|
|
||||||
|
const value = useMemo(() => ({ add, remove }), [add, remove])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ShortcutContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</ShortcutContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useShortcut(key: string, cb: Handler) {
|
||||||
|
const { add, remove } = useContext(ShortcutContext);
|
||||||
|
useEffect(() => {
|
||||||
|
add(cb, key);
|
||||||
|
return () => {
|
||||||
|
remove(cb, key);
|
||||||
|
};
|
||||||
|
}, [key, cb]);
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { writeText } from './util';
|
import { writeText } from './util';
|
||||||
|
|
||||||
export function useCopy(copied: string, display?: string) {
|
export function useCopy(copied: string, display?: string, replaceText?: string) {
|
||||||
const [didCopy, setDidCopy] = useState(false);
|
const [didCopy, setDidCopy] = useState(false);
|
||||||
const doCopy = useCallback(() => {
|
const doCopy = useCallback(() => {
|
||||||
writeText(copied);
|
writeText(copied);
|
||||||
@ -11,7 +11,7 @@ export function useCopy(copied: string, display?: string) {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}, [copied]);
|
}, [copied]);
|
||||||
|
|
||||||
const copyDisplay = useMemo(() => (didCopy ? 'Copied' : display), [
|
const copyDisplay = useMemo(() => (didCopy ? (replaceText ?? 'Copied') : display), [
|
||||||
didCopy,
|
didCopy,
|
||||||
display
|
display
|
||||||
]);
|
]);
|
||||||
|
@ -17,7 +17,7 @@ interface SetStateFunc<T> {
|
|||||||
}
|
}
|
||||||
// See microsoft/typescript#37663 for filed bug
|
// See microsoft/typescript#37663 for filed bug
|
||||||
type SetState<T> = T extends any ? SetStateFunc<T> : never;
|
type SetState<T> = T extends any ? SetStateFunc<T> : never;
|
||||||
export function useLocalStorageState<T>(key: string, initial: T) {
|
export function useLocalStorageState<T>(key: string, initial: T): any {
|
||||||
const [state, _setState] = useState(() => retrieve(key, initial));
|
const [state, _setState] = useState(() => retrieve(key, initial));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -2,13 +2,16 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { patp2dec } from 'urbit-ob';
|
import { patp2dec } from 'urbit-ob';
|
||||||
import f, { compose, memoize } from 'lodash/fp';
|
import f, { compose, memoize } from 'lodash/fp';
|
||||||
import bigInt, { BigInteger } from 'big-integer';
|
|
||||||
import { Association, Contact, Patp } from '@urbit/api';
|
import { Association, Contact, Patp } from '@urbit/api';
|
||||||
import produce, { enableMapSet } from 'immer';
|
import produce, { enableMapSet } from 'immer';
|
||||||
import useSettingsState from '../state/settings';
|
import useSettingsState from '../state/settings';
|
||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
import anyAscii from 'any-ascii';
|
import anyAscii from 'any-ascii';
|
||||||
import { IconRef } from '~/types';
|
import { sigil as sigiljs, stringRenderer } from '@tlon/sigil-js';
|
||||||
|
import bigInt, { BigInteger } from 'big-integer';
|
||||||
|
import { foregroundFromBackground } from '~/logic/lib/sigil';
|
||||||
|
import { IconRef, Workspace } from '~/types';
|
||||||
|
import useContactState from '../state/contact';
|
||||||
|
|
||||||
enableMapSet();
|
enableMapSet();
|
||||||
|
|
||||||
@ -47,6 +50,42 @@ export function parentPath(path: string) {
|
|||||||
return _.dropRight(path.split('/'), 1).join('/');
|
return _.dropRight(path.split('/'), 1).join('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* undefined -> initial
|
||||||
|
* null -> disabled feed
|
||||||
|
* string -> enabled feed
|
||||||
|
*/
|
||||||
|
export function getFeedPath(association: Association): string | null | undefined {
|
||||||
|
const { metadata } = association;
|
||||||
|
if(metadata.config && 'group' in metadata?.config && metadata.config?.group) {
|
||||||
|
if ('resource' in metadata.config.group) {
|
||||||
|
return metadata.config.group.resource;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getChord = (e: KeyboardEvent) => {
|
||||||
|
let chord = [e.key];
|
||||||
|
if(e.metaKey) {
|
||||||
|
chord.unshift('meta');
|
||||||
|
}
|
||||||
|
if(e.ctrlKey) {
|
||||||
|
chord.unshift('ctrl');
|
||||||
|
}
|
||||||
|
return chord.join('+');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getResourcePath(workspace: Workspace, path: string, joined: boolean, mod: string) {
|
||||||
|
const base = workspace.type === 'group'
|
||||||
|
? `/~landscape${workspace.group}`
|
||||||
|
: workspace.type === 'home'
|
||||||
|
? `/~landscape/home`
|
||||||
|
: `/~landscape/messages`;
|
||||||
|
return `${base}/${joined ? 'resource' : 'join'}/${mod}${path}`
|
||||||
|
}
|
||||||
|
|
||||||
const DA_UNIX_EPOCH = bigInt('170141184475152167957503069145530368000'); // `@ud` ~1970.1.1
|
const DA_UNIX_EPOCH = bigInt('170141184475152167957503069145530368000'); // `@ud` ~1970.1.1
|
||||||
const DA_SECOND = bigInt('18446744073709551616'); // `@ud` ~s1
|
const DA_SECOND = bigInt('18446744073709551616'); // `@ud` ~s1
|
||||||
export function daToUnix(da: BigInteger) {
|
export function daToUnix(da: BigInteger) {
|
||||||
@ -103,6 +142,13 @@ export function clamp(x: number, min: number, max: number) {
|
|||||||
return Math.max(min, Math.min(max, x));
|
return Math.max(min, Math.min(max, x));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Euclidean modulo
|
||||||
|
*/
|
||||||
|
export function modulo(x: number, mod: number) {
|
||||||
|
return x < 0 ? (x % mod + mod) % mod : x % mod;
|
||||||
|
}
|
||||||
|
|
||||||
// color is a #000000 color
|
// color is a #000000 color
|
||||||
export function adjustHex(color: string, amount: number): string {
|
export function adjustHex(color: string, amount: number): string {
|
||||||
return f.flow(
|
return f.flow(
|
||||||
@ -249,14 +295,20 @@ export function cite(ship: string): string {
|
|||||||
return `~${patp}`;
|
return `~${patp}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function stripNonWord(string: string): string {
|
||||||
|
return string.replace(/[^\p{L}\p{N}\p{Z}]/gu, '');
|
||||||
|
}
|
||||||
|
|
||||||
export function alphabeticalOrder(a: string, b: string) {
|
export function alphabeticalOrder(a: string, b: string) {
|
||||||
return a.toLowerCase().localeCompare(b.toLowerCase());
|
return stripNonWord(a).toLowerCase().trim().localeCompare(stripNonWord(b).toLowerCase().trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function lengthOrder(a: string, b: string) {
|
export function lengthOrder(a: string, b: string) {
|
||||||
return b.length - a.length;
|
return b.length - a.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const keys = <T extends {}>(o: T) => Object.keys(o) as (keyof T)[];
|
||||||
|
|
||||||
// TODO: deprecated
|
// TODO: deprecated
|
||||||
export function alphabetiseAssociations(associations: any) {
|
export function alphabetiseAssociations(associations: any) {
|
||||||
const result = {};
|
const result = {};
|
||||||
@ -431,6 +483,7 @@ export const useHovering = (): useHoveringInterface => {
|
|||||||
export function withHovering<T>(Component: React.ComponentType<T>) {
|
export function withHovering<T>(Component: React.ComponentType<T>) {
|
||||||
return React.forwardRef((props, ref) => {
|
return React.forwardRef((props, ref) => {
|
||||||
const { hovering, bind } = useHovering();
|
const { hovering, bind } = useHovering();
|
||||||
|
// @ts-ignore needs type signature on return?
|
||||||
return <Component ref={ref} hovering={hovering} bind={bind} {...props} />
|
return <Component ref={ref} hovering={hovering} bind={bind} {...props} />
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -447,3 +500,22 @@ export function getItemTitle(association: Association): string {
|
|||||||
return association.metadata.title ?? association.resource ?? '';
|
return association.metadata.title ?? association.resource ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const svgDataURL = (svg) => 'data:image/svg+xml;base64,' + btoa(svg);
|
||||||
|
|
||||||
|
export const svgBlobURL = (svg) => URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml' }));
|
||||||
|
|
||||||
|
export const favicon = () => {
|
||||||
|
let background = '#ffffff';
|
||||||
|
const contacts = useContactState.getState().contacts;
|
||||||
|
if (contacts.hasOwnProperty(`~${window.ship}`)) {
|
||||||
|
background = `#${uxToHex(contacts[`~${window.ship}`].color)}`;
|
||||||
|
}
|
||||||
|
const foreground = foregroundFromBackground(background);
|
||||||
|
const svg = sigiljs({
|
||||||
|
patp: window.ship,
|
||||||
|
renderer: stringRenderer,
|
||||||
|
size: 16,
|
||||||
|
colors: [background, foreground]
|
||||||
|
});
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ContactUpdate } from '@urbit/api';
|
import { ContactUpdate, deSig } from '@urbit/api';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { reduceState } from '../state/base';
|
import { reduceState } from '../state/base';
|
||||||
import useContactState, { ContactState } from '../state/contact';
|
import useContactState, { ContactState } from '../state/contact';
|
||||||
@ -52,9 +52,9 @@ const remove = (json: ContactUpdate, state: ContactState): ContactState => {
|
|||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
const edit = (json: ContactUpdate, state: ContactState): ContactState => {
|
export const edit = (json: ContactUpdate, state: ContactState): ContactState => {
|
||||||
const data = _.get(json, 'edit', false);
|
const data = _.get(json, 'edit', false);
|
||||||
const ship = `~${data.ship}`;
|
const ship = `~${deSig(data.ship)}`;
|
||||||
if (
|
if (
|
||||||
data &&
|
data &&
|
||||||
(ship in state.contacts)
|
(ship in state.contacts)
|
||||||
|
@ -102,6 +102,7 @@ const addGraph = (json, state: GraphState): GraphState => {
|
|||||||
const data = _.get(json, 'add-graph', false);
|
const data = _.get(json, 'add-graph', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
if (!('graphs' in state)) {
|
if (!('graphs' in state)) {
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
state.graphs = {};
|
state.graphs = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,6 +123,7 @@ const removeGraph = (json, state: GraphState): GraphState => {
|
|||||||
const data = _.get(json, 'remove-graph', false);
|
const data = _.get(json, 'remove-graph', false);
|
||||||
if (data) {
|
if (data) {
|
||||||
if (!('graphs' in state)) {
|
if (!('graphs' in state)) {
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
state.graphs = {};
|
state.graphs = {};
|
||||||
}
|
}
|
||||||
const resource = data.ship + '/' + data.name;
|
const resource = data.ship + '/' + data.name;
|
||||||
@ -279,7 +281,7 @@ const removePosts = (json, state: GraphState): GraphState => {
|
|||||||
} else {
|
} else {
|
||||||
const child = graph.get(index[0]);
|
const child = graph.get(index[0]);
|
||||||
if (child) {
|
if (child) {
|
||||||
return graph.set(index[0], produce((draft) => {
|
return graph.set(index[0], produce((draft: any) => {
|
||||||
draft.children = _remove(draft.children, index.slice(1));
|
draft.children = _remove(draft.children, index.slice(1));
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -8,8 +8,10 @@ import { BigInteger } from 'big-integer';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { compose } from 'lodash/fp';
|
import { compose } from 'lodash/fp';
|
||||||
import { makePatDa } from '~/logic/lib/util';
|
import { makePatDa } from '~/logic/lib/util';
|
||||||
|
import { describeNotification } from '../lib/hark';
|
||||||
import { reduceState } from '../state/base';
|
import { reduceState } from '../state/base';
|
||||||
import useHarkState, { HarkState } from '../state/hark';
|
import useHarkState, { HarkState } from '../state/hark';
|
||||||
|
import useMetadataState from '../state/metadata';
|
||||||
|
|
||||||
export const HarkReducer = (json: any) => {
|
export const HarkReducer = (json: any) => {
|
||||||
const data = _.get(json, 'harkUpdate', false);
|
const data = _.get(json, 'harkUpdate', false);
|
||||||
@ -20,24 +22,32 @@ export const HarkReducer = (json: any) => {
|
|||||||
const graphHookData = _.get(json, 'hark-graph-hook-update', false);
|
const graphHookData = _.get(json, 'hark-graph-hook-update', false);
|
||||||
if (graphHookData) {
|
if (graphHookData) {
|
||||||
reduceState<HarkState, any>(useHarkState, graphHookData, [
|
reduceState<HarkState, any>(useHarkState, graphHookData, [
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
graphInitial,
|
graphInitial,
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
graphIgnore,
|
graphIgnore,
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
graphListen,
|
graphListen,
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
graphWatchSelf,
|
graphWatchSelf,
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
graphMentions
|
graphMentions
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
const groupHookData = _.get(json, 'hark-group-hook-update', false);
|
const groupHookData = _.get(json, 'hark-group-hook-update', false);
|
||||||
if (groupHookData) {
|
if (groupHookData) {
|
||||||
reduceState<HarkState, any>(useHarkState, groupHookData, [
|
reduceState<HarkState, any>(useHarkState, groupHookData, [
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
groupInitial,
|
groupInitial,
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
groupListen,
|
groupListen,
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
groupIgnore
|
groupIgnore
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function reduce(data, state) {
|
export function reduce(data, state) {
|
||||||
const reducers = [
|
const reducers = [
|
||||||
calculateCount,
|
calculateCount,
|
||||||
read,
|
read,
|
||||||
@ -196,7 +206,7 @@ function readSince(json: any, state: HarkState): HarkState {
|
|||||||
|
|
||||||
function unreadSince(json: any, state: HarkState): HarkState {
|
function unreadSince(json: any, state: HarkState): HarkState {
|
||||||
const data = _.get(json, 'unread-count');
|
const data = _.get(json, 'unread-count');
|
||||||
if(data) {
|
if (data) {
|
||||||
updateUnreadCount(state, data.index, u => u + 1);
|
updateUnreadCount(state, data.index, u => u + 1);
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
@ -314,7 +324,7 @@ function removeNotificationFromUnread(state: HarkState, index: NotifIndex, time:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'unreads' | 'last', f: (x: number) => number) {
|
function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'unreads' | 'last', f: (x: number) => number, notify = false) {
|
||||||
if('graph' in index) {
|
if('graph' in index) {
|
||||||
const curr: any = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, statField], 0);
|
const curr: any = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, statField], 0);
|
||||||
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, statField], f(curr));
|
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, statField], f(curr));
|
||||||
@ -330,6 +340,20 @@ function added(json: any, state: HarkState): HarkState {
|
|||||||
const { index, notification } = data;
|
const { index, notification } = data;
|
||||||
const [fresh, stale] = _.partition(state.unreadNotes, ({ index: idx }) => !notifIdxEqual(index, idx));
|
const [fresh, stale] = _.partition(state.unreadNotes, ({ index: idx }) => !notifIdxEqual(index, idx));
|
||||||
state.unreadNotes = [...fresh, { index, notification }];
|
state.unreadNotes = [...fresh, { index, notification }];
|
||||||
|
|
||||||
|
if ('Notification' in window && !useHarkState.getState().doNotDisturb) {
|
||||||
|
const description = describeNotification(data);
|
||||||
|
const meta = useMetadataState.getState();
|
||||||
|
const referent = 'graph' in data.index ? meta.associations.graph[data.index.graph.graph]?.metadata?.title ?? data.index.graph : meta.associations.groups[data.index.group.group]?.metadata?.title ?? data.index.group;
|
||||||
|
new Notification(`${description} ${referent}`, {
|
||||||
|
tag: 'landscape',
|
||||||
|
image: '/img/favicon.png',
|
||||||
|
icon: '/img/favicon.png',
|
||||||
|
badge: '/img/favicon.png',
|
||||||
|
renotify: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -368,7 +392,7 @@ function notifIdxEqual(a: NotifIndex, b: NotifIndex) {
|
|||||||
return (
|
return (
|
||||||
a.graph.graph === b.graph.graph &&
|
a.graph.graph === b.graph.graph &&
|
||||||
a.graph.group === b.graph.group &&
|
a.graph.group === b.graph.group &&
|
||||||
a.graph.module === b.graph.module &&
|
a.graph.mark === b.graph.mark &&
|
||||||
a.graph.description === b.graph.description
|
a.graph.description === b.graph.description
|
||||||
);
|
);
|
||||||
} else if ('group' in a && 'group' in b) {
|
} else if ('group' in a && 'group' in b) {
|
||||||
|
@ -20,6 +20,7 @@ export default class LaunchReducer {
|
|||||||
const weatherData: WeatherState | boolean | Record<string, never> = _.get(json, 'weather', false);
|
const weatherData: WeatherState | boolean | Record<string, never> = _.get(json, 'weather', false);
|
||||||
if (weatherData) {
|
if (weatherData) {
|
||||||
useLaunchState.getState().set((state) => {
|
useLaunchState.getState().set((state) => {
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
state.weather = weatherData;
|
state.weather = weatherData;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -27,6 +28,7 @@ export default class LaunchReducer {
|
|||||||
const locationData = _.get(json, 'location', false);
|
const locationData = _.get(json, 'location', false);
|
||||||
if (locationData) {
|
if (locationData) {
|
||||||
useLaunchState.getState().set((state) => {
|
useLaunchState.getState().set((state) => {
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
state.userLocation = locationData;
|
state.userLocation = locationData;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -34,6 +36,7 @@ export default class LaunchReducer {
|
|||||||
const baseHash = _.get(json, 'baseHash', false);
|
const baseHash = _.get(json, 'baseHash', false);
|
||||||
if (baseHash) {
|
if (baseHash) {
|
||||||
useLaunchState.getState().set((state) => {
|
useLaunchState.getState().set((state) => {
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
state.baseHash = baseHash;
|
state.baseHash = baseHash;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -41,6 +44,7 @@ export default class LaunchReducer {
|
|||||||
const runtimeLag = _.get(json, 'runtimeLag', null);
|
const runtimeLag = _.get(json, 'runtimeLag', null);
|
||||||
if (runtimeLag !== null) {
|
if (runtimeLag !== null) {
|
||||||
useLaunchState.getState().set(state => {
|
useLaunchState.getState().set(state => {
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
state.runtimeLag = runtimeLag;
|
state.runtimeLag = runtimeLag;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,39 @@
|
|||||||
import produce, { setAutoFreeze } from 'immer';
|
import produce, { applyPatches, Patch, produceWithPatches, setAutoFreeze, enablePatches } from 'immer';
|
||||||
import { compose } from 'lodash/fp';
|
import { compose } from 'lodash/fp';
|
||||||
import create, { State, UseStore } from 'zustand';
|
import _ from 'lodash';
|
||||||
|
import create, { UseStore } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
setAutoFreeze(false);
|
setAutoFreeze(false);
|
||||||
|
enablePatches();
|
||||||
|
|
||||||
export const stateSetter = <StateType>(
|
export const stateSetter = <T extends {}>(
|
||||||
fn: (state: StateType) => void,
|
fn: (state: Readonly<T & BaseState<T>>) => void,
|
||||||
set
|
set: (newState: T & BaseState<T>) => void
|
||||||
): void => {
|
): void => {
|
||||||
set(produce(fn));
|
set(produce(fn) as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const optStateSetter = <T extends {}>(
|
||||||
|
fn: (state: T & BaseState<T>) => void,
|
||||||
|
set: (newState: T & BaseState<T>) => void,
|
||||||
|
get: () => T & BaseState<T>
|
||||||
|
): string => {
|
||||||
|
const old = get();
|
||||||
|
const id = _.uniqueId()
|
||||||
|
const [state, ,patches] = produceWithPatches(old, fn) as readonly [(T & BaseState<T>), any, Patch[]];
|
||||||
|
set({ ...state, patches: { ...state.patches, [id]: patches }});
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export const reduceState = <
|
export const reduceState = <
|
||||||
StateType extends BaseState<StateType>,
|
S extends {},
|
||||||
UpdateType
|
U
|
||||||
>(
|
>(
|
||||||
state: UseStore<StateType>,
|
state: UseStore<S & BaseState<S>>,
|
||||||
data: UpdateType,
|
data: U,
|
||||||
reducers: ((data: UpdateType, state: StateType) => StateType)[]
|
reducers: ((data: U, state: S & BaseState<S>) => S & BaseState<S>)[]
|
||||||
): void => {
|
): void => {
|
||||||
const reducer = compose(reducers.map(r => sta => r(data, sta)));
|
const reducer = compose(reducers.map(r => sta => r(data, sta)));
|
||||||
state.getState().set((state) => {
|
state.getState().set((state) => {
|
||||||
@ -26,6 +41,18 @@ export const reduceState = <
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const optReduceState = <S, U>(
|
||||||
|
state: UseStore<S & BaseState<S>>,
|
||||||
|
data: U,
|
||||||
|
reducers: ((data: U, state: S & BaseState<S>) => BaseState<S> & S)[]
|
||||||
|
): string => {
|
||||||
|
const reducer = compose(reducers.map(r => sta => r(data, sta)));
|
||||||
|
return state.getState().optSet((state) => {
|
||||||
|
reducer(state);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export let stateStorageKeys: string[] = [];
|
export let stateStorageKeys: string[] = [];
|
||||||
|
|
||||||
export const stateStorageKey = (stateName: string) => {
|
export const stateStorageKey = (stateName: string) => {
|
||||||
@ -40,19 +67,59 @@ export const stateStorageKey = (stateName: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface BaseState<StateType> extends State {
|
export interface BaseState<StateType extends {}> {
|
||||||
set: (fn: (state: StateType) => void) => void;
|
rollback: (id: string) => void;
|
||||||
|
patches: {
|
||||||
|
[id: string]: Patch[];
|
||||||
|
};
|
||||||
|
set: (fn: (state: BaseState<StateType>) => void) => void;
|
||||||
|
addPatch: (id: string, ...patch: Patch[]) => void;
|
||||||
|
removePatch: (id: string) => void;
|
||||||
|
optSet: (fn: (state: BaseState<StateType>) => void) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createState = <T extends BaseState<T>>(
|
export const createState = <T extends {}>(
|
||||||
name: string,
|
name: string,
|
||||||
properties: { [K in keyof Omit<T, 'set'>]: T[K] },
|
properties: T,
|
||||||
blacklist: string[] = []
|
blacklist: (keyof BaseState<T> | keyof T)[] = []
|
||||||
): UseStore<T> => create(persist((set, get) => ({
|
): UseStore<T & BaseState<T>> => create<T & BaseState<T>>(persist<T & BaseState<T>>((set, get) => ({
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
set: fn => stateSetter(fn, set),
|
set: fn => stateSetter(fn, set),
|
||||||
...properties as any
|
optSet: fn => {
|
||||||
|
return optStateSetter(fn, set, get);
|
||||||
|
},
|
||||||
|
patches: {},
|
||||||
|
addPatch: (id: string, ...patch: Patch[]) => {
|
||||||
|
// @ts-ignore investigate immer types
|
||||||
|
set(({ patches }) => ({ patches: {...patches, [id]: patch }}));
|
||||||
|
},
|
||||||
|
removePatch: (id: string) => {
|
||||||
|
// @ts-ignore investigate immer types
|
||||||
|
set(({ patches }) => ({ patches: _.omit(patches, id)}));
|
||||||
|
},
|
||||||
|
rollback: (id: string) => {
|
||||||
|
set(state => {
|
||||||
|
const applying = state.patches[id]
|
||||||
|
return {...applyPatches(state, applying), patches: _.omit(state.patches, id) }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
...properties
|
||||||
}), {
|
}), {
|
||||||
blacklist,
|
blacklist,
|
||||||
name: stateStorageKey(name),
|
name: stateStorageKey(name),
|
||||||
version: process.env.LANDSCAPE_SHORTHASH as any
|
version: process.env.LANDSCAPE_SHORTHASH as any
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export async function doOptimistically<A, S extends {}>(state: UseStore<S & BaseState<S>>, action: A, call: (a: A) => Promise<any>, reduce: ((a: A, fn: S & BaseState<S>) => S & BaseState<S>)[]) {
|
||||||
|
let num: string | undefined = undefined;
|
||||||
|
try {
|
||||||
|
num = optReduceState(state, action, reduce);
|
||||||
|
await call(action);
|
||||||
|
state.getState().removePatch(num)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if(num) {
|
||||||
|
state.getState().rollback(num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@ export interface ContactState extends BaseState<ContactState> {
|
|||||||
// fetchIsAllowed: (entity, name, ship, personal) => Promise<boolean>;
|
// fetchIsAllowed: (entity, name, ship, personal) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
const useContactState = createState<ContactState>('Contact', {
|
const useContactState = createState<ContactState>('Contact', {
|
||||||
contacts: {},
|
contacts: {},
|
||||||
nackedContacts: new Set(),
|
nackedContacts: new Set(),
|
||||||
|
@ -27,7 +27,7 @@ export interface GraphState extends BaseState<GraphState> {
|
|||||||
// getGraphSubset: (ship: string, resource: string, start: string, end: string) => Promise<void>;
|
// getGraphSubset: (ship: string, resource: string, start: string, end: string) => Promise<void>;
|
||||||
// getNode: (ship: string, resource: string, index: string) => Promise<void>;
|
// getNode: (ship: string, resource: string, index: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
const useGraphState = createState<GraphState>('Graph', {
|
const useGraphState = createState<GraphState>('Graph', {
|
||||||
graphs: {},
|
graphs: {},
|
||||||
graphKeys: new Set(),
|
graphKeys: new Set(),
|
||||||
|
@ -9,6 +9,7 @@ export interface GroupState extends BaseState<GroupState> {
|
|||||||
pendingJoin: JoinRequests;
|
pendingJoin: JoinRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
const useGroupState = createState<GroupState>('Group', {
|
const useGroupState = createState<GroupState>('Group', {
|
||||||
groups: {},
|
groups: {},
|
||||||
pendingJoin: {}
|
pendingJoin: {}
|
||||||
|
@ -4,11 +4,11 @@ import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap";
|
|||||||
import {useCallback} from "react";
|
import {useCallback} from "react";
|
||||||
|
|
||||||
// import { harkGraphHookReducer, harkGroupHookReducer, harkReducer } from "~/logic/subscription/hark";
|
// import { harkGraphHookReducer, harkGroupHookReducer, harkReducer } from "~/logic/subscription/hark";
|
||||||
import { BaseState, createState } from './base';
|
import { createState } from './base';
|
||||||
|
|
||||||
export const HARK_FETCH_MORE_COUNT = 3;
|
export const HARK_FETCH_MORE_COUNT = 3;
|
||||||
|
|
||||||
export interface HarkState extends BaseState<HarkState> {
|
export interface HarkState {
|
||||||
archivedNotifications: BigIntOrderedMap<Timebox>;
|
archivedNotifications: BigIntOrderedMap<Timebox>;
|
||||||
doNotDisturb: boolean;
|
doNotDisturb: boolean;
|
||||||
// getMore: () => Promise<boolean>;
|
// getMore: () => Promise<boolean>;
|
||||||
|
@ -5,6 +5,7 @@ export interface InviteState extends BaseState<InviteState> {
|
|||||||
invites: Invites;
|
invites: Invites;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
const useInviteState = createState<InviteState>('Invite', {
|
const useInviteState = createState<InviteState>('Invite', {
|
||||||
invites: {}
|
invites: {}
|
||||||
});
|
});
|
||||||
|
@ -13,6 +13,7 @@ export interface LaunchState extends BaseState<LaunchState> {
|
|||||||
runtimeLag: boolean;
|
runtimeLag: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
const useLaunchState = createState<LaunchState>('Launch', {
|
const useLaunchState = createState<LaunchState>('Launch', {
|
||||||
firstTime: true,
|
firstTime: true,
|
||||||
tileOrdering: [],
|
tileOrdering: [],
|
||||||
|
@ -82,6 +82,7 @@ const useLocalState = create<LocalStateZus>(persist((set, get) => ({
|
|||||||
state.suspendedFocus.blur();
|
state.suspendedFocus.blur();
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
set: fn => set(produce(fn))
|
set: fn => set(produce(fn))
|
||||||
}), {
|
}), {
|
||||||
blacklist: [
|
blacklist: [
|
||||||
@ -98,6 +99,7 @@ function withLocalState<P, S extends keyof LocalState, C extends React.Component
|
|||||||
(object, key) => ({ ...object, [key]: state[key] }), {}
|
(object, key) => ({ ...object, [key]: state[key] }), {}
|
||||||
)
|
)
|
||||||
): useLocalState();
|
): useLocalState();
|
||||||
|
// @ts-ignore call signature forwarding unclear
|
||||||
return <Component ref={ref} {...localState} {...props} />;
|
return <Component ref={ref} {...localState} {...props} />;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ export function useGraphsForGroup(group: string) {
|
|||||||
const graphs = useMetadataState(s => s.associations.graph);
|
const graphs = useMetadataState(s => s.associations.graph);
|
||||||
return _.pickBy(graphs, (a: Association) => a.group === group);
|
return _.pickBy(graphs, (a: Association) => a.group === group);
|
||||||
}
|
}
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
const useMetadataState = createState<MetadataState>('Metadata', {
|
const useMetadataState = createState<MetadataState>('Metadata', {
|
||||||
associations: { groups: {}, graph: {}, contacts: {}, chat: {}, link: {}, publish: {} }
|
associations: { groups: {}, graph: {}, contacts: {}, chat: {}, link: {}, publish: {} }
|
||||||
// preview: async (group): Promise<MetadataUpdatePreview> => {
|
// preview: async (group): Promise<MetadataUpdatePreview> => {
|
||||||
|
@ -1,6 +1,17 @@
|
|||||||
import f from 'lodash/fp';
|
import f from 'lodash/fp';
|
||||||
|
import { RemoteContentPolicy, LeapCategories, leapCategories } from "~/types/local-update";
|
||||||
|
import { useShortcut as usePlainShortcut } from '~/logic/lib/shortcutContext';
|
||||||
import { BaseState, createState } from '~/logic/state/base';
|
import { BaseState, createState } from '~/logic/state/base';
|
||||||
import { LeapCategories, leapCategories, RemoteContentPolicy } from '~/types/local-update';
|
import {useCallback} from 'react';
|
||||||
|
|
||||||
|
export interface ShortcutMapping {
|
||||||
|
cycleForward: string;
|
||||||
|
cycleBack: string;
|
||||||
|
navForward: string;
|
||||||
|
navBack: string;
|
||||||
|
hideSidebar: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface SettingsState extends BaseState<SettingsState> {
|
export interface SettingsState extends BaseState<SettingsState> {
|
||||||
display: {
|
display: {
|
||||||
@ -16,6 +27,7 @@ export interface SettingsState extends BaseState<SettingsState> {
|
|||||||
hideGroups: boolean;
|
hideGroups: boolean;
|
||||||
hideUtilities: boolean;
|
hideUtilities: boolean;
|
||||||
};
|
};
|
||||||
|
keyboard: ShortcutMapping;
|
||||||
remoteContentPolicy: RemoteContentPolicy;
|
remoteContentPolicy: RemoteContentPolicy;
|
||||||
leap: {
|
leap: {
|
||||||
categories: LeapCategories[];
|
categories: LeapCategories[];
|
||||||
@ -33,6 +45,7 @@ export const selectCalmState = (s: SettingsState) => s.calm;
|
|||||||
|
|
||||||
export const selectDisplayState = (s: SettingsState) => s.display;
|
export const selectDisplayState = (s: SettingsState) => s.display;
|
||||||
|
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
const useSettingsState = createState<SettingsState>('Settings', {
|
const useSettingsState = createState<SettingsState>('Settings', {
|
||||||
display: {
|
display: {
|
||||||
backgroundType: 'none',
|
backgroundType: 'none',
|
||||||
@ -59,7 +72,19 @@ const useSettingsState = createState<SettingsState>('Settings', {
|
|||||||
tutorial: {
|
tutorial: {
|
||||||
seen: true,
|
seen: true,
|
||||||
joined: undefined
|
joined: undefined
|
||||||
|
},
|
||||||
|
keyboard: {
|
||||||
|
cycleForward: 'ctrl+\'',
|
||||||
|
cycleBack: 'ctrl+;',
|
||||||
|
navForward: 'ctrl+[',
|
||||||
|
navBack: 'ctrl+[',
|
||||||
|
hideSidebar: 'ctrl+\\'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function useShortcut<T extends keyof ShortcutMapping>(name: T, cb: (e: KeyboardEvent) => void) {
|
||||||
|
const key = useSettingsState(useCallback(s => s.keyboard[name], [name]));
|
||||||
|
return usePlainShortcut(key, cb);
|
||||||
|
}
|
||||||
|
|
||||||
export default useSettingsState;
|
export default useSettingsState;
|
||||||
|
@ -19,6 +19,7 @@ export interface StorageState extends BaseState<StorageState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
const useStorageState = createState<StorageState>('Storage', {
|
const useStorageState = createState<StorageState>('Storage', {
|
||||||
gcp: {},
|
gcp: {},
|
||||||
s3: {
|
s3: {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import dark from '@tlon/indigo-dark';
|
import dark from '@tlon/indigo-dark';
|
||||||
import light from '@tlon/indigo-light';
|
import light from '@tlon/indigo-light';
|
||||||
import { sigil as sigiljs, stringRenderer } from '@tlon/sigil-js';
|
|
||||||
import Mousetrap from 'mousetrap';
|
import Mousetrap from 'mousetrap';
|
||||||
import 'mousetrap-global-bind';
|
import 'mousetrap-global-bind';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
@ -11,13 +10,14 @@ import { BrowserRouter as Router, withRouter } from 'react-router-dom';
|
|||||||
import styled, { ThemeProvider } from 'styled-components';
|
import styled, { ThemeProvider } from 'styled-components';
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import gcpManager from '~/logic/lib/gcpManager';
|
import gcpManager from '~/logic/lib/gcpManager';
|
||||||
import { foregroundFromBackground } from '~/logic/lib/sigil';
|
import { favicon, svgDataURL } from '~/logic/lib/util';
|
||||||
import { uxToHex } from '~/logic/lib/util';
|
|
||||||
import withState from '~/logic/lib/withState';
|
import withState from '~/logic/lib/withState';
|
||||||
import useContactState from '~/logic/state/contact';
|
import useContactState from '~/logic/state/contact';
|
||||||
import useGroupState from '~/logic/state/group';
|
import useGroupState from '~/logic/state/group';
|
||||||
import useLocalState from '~/logic/state/local';
|
import useLocalState from '~/logic/state/local';
|
||||||
import useSettingsState from '~/logic/state/settings';
|
import useSettingsState from '~/logic/state/settings';
|
||||||
|
import { ShortcutContextProvider } from '~/logic/lib/shortcutContext';
|
||||||
|
|
||||||
import GlobalStore from '~/logic/store/store';
|
import GlobalStore from '~/logic/store/store';
|
||||||
import GlobalSubscription from '~/logic/subscription/global';
|
import GlobalSubscription from '~/logic/subscription/global';
|
||||||
import ErrorBoundary from '~/views/components/ErrorBoundary';
|
import ErrorBoundary from '~/views/components/ErrorBoundary';
|
||||||
@ -86,7 +86,6 @@ class App extends React.Component {
|
|||||||
|
|
||||||
this.updateTheme = this.updateTheme.bind(this);
|
this.updateTheme = this.updateTheme.bind(this);
|
||||||
this.updateMobile = this.updateMobile.bind(this);
|
this.updateMobile = this.updateMobile.bind(this);
|
||||||
this.faviconString = this.faviconString.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -131,22 +130,6 @@ class App extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
faviconString() {
|
|
||||||
let background = '#ffffff';
|
|
||||||
if (this.props.contacts.hasOwnProperty(`~${window.ship}`)) {
|
|
||||||
background = `#${uxToHex(this.props.contacts[`~${window.ship}`].color)}`;
|
|
||||||
}
|
|
||||||
const foreground = foregroundFromBackground(background);
|
|
||||||
const svg = sigiljs({
|
|
||||||
patp: window.ship,
|
|
||||||
renderer: stringRenderer,
|
|
||||||
size: 16,
|
|
||||||
colors: [background, foreground]
|
|
||||||
});
|
|
||||||
const dataurl = 'data:image/svg+xml;base64,' + btoa(svg);
|
|
||||||
return dataurl;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTheme() {
|
getTheme() {
|
||||||
const { props } = this;
|
const { props } = this;
|
||||||
return ((props.dark && props?.display?.theme == 'auto') ||
|
return ((props.dark && props?.display?.theme == 'auto') ||
|
||||||
@ -161,9 +144,10 @@ class App extends React.Component {
|
|||||||
const ourContact = this.props.contacts[`~${this.ship}`] || null;
|
const ourContact = this.props.contacts[`~${this.ship}`] || null;
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
|
<ShortcutContextProvider>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
{window.ship.length < 14
|
{window.ship.length < 14
|
||||||
? <link rel="icon" type="image/svg+xml" href={this.faviconString()} />
|
? <link rel="icon" type="image/svg+xml" href={svgDataURL(favicon())} />
|
||||||
: null}
|
: null}
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Root>
|
<Root>
|
||||||
@ -198,6 +182,7 @@ class App extends React.Component {
|
|||||||
</Router>
|
</Router>
|
||||||
</Root>
|
</Root>
|
||||||
<div id="portal-root" />
|
<div id="portal-root" />
|
||||||
|
</ShortcutContextProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -128,10 +128,27 @@ export default class ChatEditor extends Component<ChatEditorProps, ChatEditorSta
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.editor = null;
|
this.editor = null;
|
||||||
|
this.onKeyPress = this.onKeyPress.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.addEventListener('keydown', this.onKeyPress);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
componentWillUnmount(): void {
|
||||||
this.props.onUnmount(this.state.message);
|
this.props.onUnmount(this.state.message);
|
||||||
|
document.removeEventListener('keydown', this.onKeyPress);
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyPress(e) {
|
||||||
|
const focusedTag = document.activeElement?.nodeName?.toLowerCase();
|
||||||
|
const shouldCapture = !(focusedTag === 'textarea' || focusedTag === 'input' || e.metaKey || e.ctrlKey);
|
||||||
|
if(/^[a-z]|[A-Z]$/.test(e.key) && shouldCapture) {
|
||||||
|
this.editor.focus();
|
||||||
|
}
|
||||||
|
if(e.key === 'Escape') {
|
||||||
|
this.editor.getInputField().blur();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: ChatEditorProps): void {
|
componentDidUpdate(prevProps: ChatEditorProps): void {
|
||||||
@ -140,9 +157,9 @@ export default class ChatEditor extends Component<ChatEditorProps, ChatEditorSta
|
|||||||
if (prevProps.message !== props.message && this.editor) {
|
if (prevProps.message !== props.message && this.editor) {
|
||||||
this.editor.setValue(props.message);
|
this.editor.setValue(props.message);
|
||||||
this.editor.setOption('mode', MARKDOWN_CONFIG);
|
this.editor.setOption('mode', MARKDOWN_CONFIG);
|
||||||
this.editor?.focus();
|
//this.editor?.focus();
|
||||||
this.editor.execCommand('goDocEnd');
|
//this.editor.execCommand('goDocEnd');
|
||||||
this.editor?.focus();
|
//this.editor?.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,7 +298,6 @@ return;
|
|||||||
onChange={(e, d, v) => this.messageChange(e, d, v)}
|
onChange={(e, d, v) => this.messageChange(e, d, v)}
|
||||||
editorDidMount={(editor) => {
|
editorDidMount={(editor) => {
|
||||||
this.editor = editor;
|
this.editor = editor;
|
||||||
editor.focus();
|
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
@ -226,7 +226,9 @@ export class ChatInput extends Component<ChatInputProps, ChatInputState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore withLocalState prop passing weirdness
|
||||||
export default withLocalState<Omit<ChatInputProps, keyof IuseStorage>, 'hideAvatars', ChatInput>(
|
export default withLocalState<Omit<ChatInputProps, keyof IuseStorage>, 'hideAvatars', ChatInput>(
|
||||||
|
// @ts-ignore withLocalState prop passing weirdness
|
||||||
withStorage<ChatInputProps, ChatInput>(ChatInput, { accept: 'image/*' }),
|
withStorage<ChatInputProps, ChatInput>(ChatInput, { accept: 'image/*' }),
|
||||||
['hideAvatars']
|
['hideAvatars']
|
||||||
);
|
);
|
||||||
|
@ -251,8 +251,7 @@ function ChatMessage(props: ChatMessageProps) {
|
|||||||
let onDelete = props?.onDelete ?? (() => {});
|
let onDelete = props?.onDelete ?? (() => {});
|
||||||
const transcluded = props?.transcluded ?? 0;
|
const transcluded = props?.transcluded ?? 0;
|
||||||
const renderSigil = props.renderSigil ?? (Boolean(nextMsg && msg.author !== nextMsg.author) ||
|
const renderSigil = props.renderSigil ?? (Boolean(nextMsg && msg.author !== nextMsg.author) ||
|
||||||
!nextMsg ||
|
!nextMsg
|
||||||
msg.number === 1
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const ourMention = msg?.contents?.some((e: MentionContent) => {
|
const ourMention = msg?.contents?.some((e: MentionContent) => {
|
||||||
|
@ -34,7 +34,7 @@ interface ChatPaneProps {
|
|||||||
* Get contents of reply message
|
* Get contents of reply message
|
||||||
*/
|
*/
|
||||||
onReply: (msg: Post) => string;
|
onReply: (msg: Post) => string;
|
||||||
onDelete: (msg: Post) => void;
|
onDelete?: (msg: Post) => void;
|
||||||
/**
|
/**
|
||||||
* Fetch more messages
|
* Fetch more messages
|
||||||
*
|
*
|
||||||
@ -136,6 +136,7 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// @ts-ignore
|
||||||
<Col {...bind} height="100%" overflow="hidden" position="relative">
|
<Col {...bind} height="100%" overflow="hidden" position="relative">
|
||||||
<ShareProfile
|
<ShareProfile
|
||||||
our={ourContact}
|
our={ourContact}
|
||||||
|
@ -239,6 +239,7 @@ class ChatWindow extends Component<
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// @ts-ignore
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key={index.toString()}
|
key={index.toString()}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -281,6 +282,7 @@ class ChatWindow extends Component<
|
|||||||
origin='bottom'
|
origin='bottom'
|
||||||
style={virtScrollerStyle}
|
style={virtScrollerStyle}
|
||||||
onBottomLoaded={this.onBottomLoaded}
|
onBottomLoaded={this.onBottomLoaded}
|
||||||
|
// @ts-ignore paging @liam-fitzgerald on virtualscroller props
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
data={graph}
|
data={graph}
|
||||||
size={graph.size}
|
size={graph.size}
|
||||||
|
@ -56,15 +56,14 @@ export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
|
|||||||
const path = group?.group;
|
const path = group?.group;
|
||||||
const unreadCount = graphUnreads(path);
|
const unreadCount = graphUnreads(path);
|
||||||
const notCount = graphNotifications(path);
|
const notCount = graphNotifications(path);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group
|
<Group
|
||||||
|
key={group.metadata.title}
|
||||||
updates={notCount}
|
updates={notCount}
|
||||||
first={index === 0}
|
first={index === 0}
|
||||||
unreads={unreadCount}
|
unreads={unreadCount}
|
||||||
path={group?.group}
|
path={group?.group}
|
||||||
title={group.metadata.title}
|
title={group.metadata.title}
|
||||||
picture={group.metadata.picture}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -96,7 +95,7 @@ function Group(props: GroupProps) {
|
|||||||
.diff(moment()))
|
.diff(moment()))
|
||||||
.as('days'))) || 0;
|
.as('days'))) || 0;
|
||||||
return (
|
return (
|
||||||
<Tile ref={anchorRef} position="relative" bg={isTutorialGroup ? 'lightBlue' : undefined} to={`/~landscape${path}`} gridColumnStart={first ? '1' : null}>
|
<Tile ref={anchorRef} position="relative" bg={isTutorialGroup ? 'lightBlue' : undefined} to={`/~landscape${path}`} gridColumnStart={first ? 1 : null}>
|
||||||
<Col height="100%" justifyContent="space-between">
|
<Col height="100%" justifyContent="space-between">
|
||||||
<Text>{title}</Text>
|
<Text>{title}</Text>
|
||||||
{!hideUnreads && (<Col>
|
{!hideUnreads && (<Col>
|
||||||
|
@ -35,6 +35,7 @@ const Tiles = (props: TileProps): ReactElement => {
|
|||||||
return (
|
return (
|
||||||
<WeatherTile
|
<WeatherTile
|
||||||
key={key}
|
key={key}
|
||||||
|
// @ts-ignore withState not passing props
|
||||||
api={props.api}
|
api={props.api}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -135,7 +135,7 @@ class WeatherTile extends React.Component<WeatherTileProps, WeatherTileState> {
|
|||||||
{locationName ? ` Current location is near ${locationName}.` : ''}
|
{locationName ? ` Current location is near ${locationName}.` : ''}
|
||||||
</Text>
|
</Text>
|
||||||
{error}
|
{error}
|
||||||
<Box mt='auto' display='flex' marginBlockEnd={0}>
|
<Box mt='auto' display='flex' style={{ marginBlockEnd: '0' }}>
|
||||||
<BaseInput
|
<BaseInput
|
||||||
id="location"
|
id="location"
|
||||||
size={10}
|
size={10}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Box, Center, Col, LoadingSpinner, Text } from '@tlon/indigo-react';
|
import { Box, Center, Col, LoadingSpinner, Text } from '@tlon/indigo-react';
|
||||||
|
import { Group } from '@urbit/api';
|
||||||
import { Association } from '@urbit/api/metadata';
|
import { Association } from '@urbit/api/metadata';
|
||||||
import bigInt from 'big-integer';
|
import bigInt from 'big-integer';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
@ -35,7 +36,7 @@ export function LinkResource(props: LinkResourceProps) {
|
|||||||
|
|
||||||
const [, , ship, name] = rid.split('/');
|
const [, , ship, name] = rid.split('/');
|
||||||
const resourcePath = `${ship.slice(1)}/${name}`;
|
const resourcePath = `${ship.slice(1)}/${name}`;
|
||||||
const resource = associations.graph[rid]
|
const resource: any = associations.graph[rid]
|
||||||
? associations.graph[rid]
|
? associations.graph[rid]
|
||||||
: { metadata: {} };
|
: { metadata: {} };
|
||||||
const groups = useGroupState(state => state.groups);
|
const groups = useGroupState(state => state.groups);
|
||||||
@ -62,13 +63,14 @@ export function LinkResource(props: LinkResourceProps) {
|
|||||||
path={relativePath('')}
|
path={relativePath('')}
|
||||||
render={(props) => {
|
render={(props) => {
|
||||||
return (
|
return (
|
||||||
|
// @ts-ignore
|
||||||
<LinkWindow
|
<LinkWindow
|
||||||
key={rid}
|
key={rid}
|
||||||
association={resource}
|
association={resource}
|
||||||
resource={resourcePath}
|
resource={resourcePath}
|
||||||
graph={graph}
|
graph={graph}
|
||||||
baseUrl={resourceUrl}
|
baseUrl={resourceUrl}
|
||||||
group={group}
|
group={group as Group}
|
||||||
path={resource.group}
|
path={resource.group}
|
||||||
pendingSize={Object.keys(graphTimesentMap[resourcePath] || {}).length}
|
pendingSize={Object.keys(graphTimesentMap[resourcePath] || {}).length}
|
||||||
api={api}
|
api={api}
|
||||||
@ -110,7 +112,7 @@ export function LinkResource(props: LinkResourceProps) {
|
|||||||
node={node}
|
node={node}
|
||||||
baseUrl={resourceUrl}
|
baseUrl={resourceUrl}
|
||||||
association={association}
|
association={association}
|
||||||
group={group}
|
group={group as Group}
|
||||||
path={resource?.group}
|
path={resource?.group}
|
||||||
api={api}
|
api={api}
|
||||||
mt={3}
|
mt={3}
|
||||||
@ -125,8 +127,8 @@ export function LinkResource(props: LinkResourceProps) {
|
|||||||
api={api}
|
api={api}
|
||||||
editCommentId={editCommentId}
|
editCommentId={editCommentId}
|
||||||
history={props.history}
|
history={props.history}
|
||||||
baseUrl={`${resourceUrl}/${props.match.params.index}`}
|
baseUrl={`${resourceUrl}/index/${props.match.params.index}`}
|
||||||
group={group}
|
group={group as Group}
|
||||||
px={3}
|
px={3}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
@ -21,6 +21,7 @@ interface LinkWindowProps {
|
|||||||
path: string;
|
path: string;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
pendingSize: number;
|
pendingSize: number;
|
||||||
|
mb?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
@ -48,6 +49,7 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
|
|||||||
const { props } = this;
|
const { props } = this;
|
||||||
const { association, graph, api } = props;
|
const { association, graph, api } = props;
|
||||||
const [, , ship, name] = association.resource.split('/');
|
const [, , ship, name] = association.resource.split('/');
|
||||||
|
// @ts-ignore Uint8Array vs. BigInt mismatch?
|
||||||
const node = graph.get(index);
|
const node = graph.get(index);
|
||||||
const first = graph.peekLargest()?.[0];
|
const first = graph.peekLargest()?.[0];
|
||||||
const post = node?.post;
|
const post = node?.post;
|
||||||
@ -58,6 +60,7 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
|
|||||||
...props,
|
...props,
|
||||||
node
|
node
|
||||||
};
|
};
|
||||||
|
{/* @ts-ignore calling @liam-fitzgerald on Uint8Array props */}
|
||||||
if (this.canWrite() && index.eq(first ?? bigInt.zero)) {
|
if (this.canWrite() && index.eq(first ?? bigInt.zero)) {
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={index.toString()}>
|
<React.Fragment key={index.toString()}>
|
||||||
@ -125,6 +128,7 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Col width="100%" height="100%" position="relative">
|
<Col width="100%" height="100%" position="relative">
|
||||||
|
{/* @ts-ignore calling @liam-fitzgerald on virtualscroller */}
|
||||||
<VirtualScroller
|
<VirtualScroller
|
||||||
origin="top"
|
origin="top"
|
||||||
offset={0}
|
offset={0}
|
||||||
|
@ -20,6 +20,8 @@ interface LinkItemProps {
|
|||||||
group: Group;
|
group: Group;
|
||||||
path: string;
|
path: string;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
mt?: number;
|
||||||
|
measure?: any;
|
||||||
}
|
}
|
||||||
export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<HTMLDivElement>): ReactElement => {
|
export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<HTMLDivElement>): ReactElement => {
|
||||||
const {
|
const {
|
||||||
@ -49,6 +51,7 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<H
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
console.log(remoteRef.current);
|
console.log(remoteRef.current);
|
||||||
if(document.activeElement instanceof HTMLIFrameElement
|
if(document.activeElement instanceof HTMLIFrameElement
|
||||||
|
// @ts-ignore forwardref prop passing
|
||||||
&& remoteRef?.current?.containerRef?.contains(document.activeElement)) {
|
&& remoteRef?.current?.containerRef?.contains(document.activeElement)) {
|
||||||
markRead();
|
markRead();
|
||||||
}
|
}
|
||||||
@ -100,6 +103,7 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<H
|
|||||||
const appPath = `/ship/~${resource}`;
|
const appPath = `/ship/~${resource}`;
|
||||||
const unreads = useHarkState(state => state.unreads);
|
const unreads = useHarkState(state => state.unreads);
|
||||||
const commColor = (unreads.graph?.[appPath]?.[`/${index}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray';
|
const commColor = (unreads.graph?.[appPath]?.[`/${index}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray';
|
||||||
|
// @ts-ignore hark will have to choose between sets and numbers
|
||||||
const isUnread = unreads.graph?.[appPath]?.['/']?.unreads?.has(node.post.index);
|
const isUnread = unreads.graph?.[appPath]?.['/']?.unreads?.has(node.post.index);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -135,8 +139,10 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<H
|
|||||||
<>
|
<>
|
||||||
<RemoteContent
|
<RemoteContent
|
||||||
ref={(r) => {
|
ref={(r) => {
|
||||||
|
// @ts-ignore RemoteContent weirdness
|
||||||
remoteRef.current = r;
|
remoteRef.current = r;
|
||||||
}}
|
}}
|
||||||
|
// @ts-ignore RemoteContent weirdness
|
||||||
renderUrl={false}
|
renderUrl={false}
|
||||||
url={href}
|
url={href}
|
||||||
text={contents[0].text}
|
text={contents[0].text}
|
||||||
|
@ -12,6 +12,7 @@ interface LinkSubmitProps {
|
|||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
name: string;
|
name: string;
|
||||||
ship: string;
|
ship: string;
|
||||||
|
parentIndex?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LinkSubmit = (props: LinkSubmitProps) => {
|
const LinkSubmit = (props: LinkSubmitProps) => {
|
||||||
@ -157,6 +158,7 @@ const LinkSubmit = (props: LinkSubmitProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* @ts-ignore archaic event type mismatch */}
|
||||||
<Box
|
<Box
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
position='relative'
|
position='relative'
|
||||||
@ -194,6 +196,7 @@ const LinkSubmit = (props: LinkSubmitProps) => {
|
|||||||
onBlur={() => [setUrlFocused(false), setSubmitFocused(false)]}
|
onBlur={() => [setUrlFocused(false), setSubmitFocused(false)]}
|
||||||
onFocus={() => [setUrlFocused(true), setSubmitFocused(true)]}
|
onFocus={() => [setUrlFocused(true), setSubmitFocused(true)]}
|
||||||
spellCheck="false"
|
spellCheck="false"
|
||||||
|
// @ts-ignore archaic event type mismatch error
|
||||||
onPaste={onPaste}
|
onPaste={onPaste}
|
||||||
onKeyPress={onKeyPress}
|
onKeyPress={onKeyPress}
|
||||||
value={linkValue}
|
value={linkValue}
|
||||||
|
@ -292,7 +292,7 @@ export function GraphNotification(props: {
|
|||||||
const first = contents[0];
|
const first = contents[0];
|
||||||
history.push(
|
history.push(
|
||||||
getNodeUrl(
|
getNodeUrl(
|
||||||
index.module,
|
index.mark,
|
||||||
groups[association?.group]?.hidden,
|
groups[association?.group]?.hidden,
|
||||||
association?.group,
|
association?.group,
|
||||||
association?.resource,
|
association?.resource,
|
||||||
@ -328,7 +328,6 @@ export function GraphNotification(props: {
|
|||||||
hideAuthors={hideAuthors}
|
hideAuthors={hideAuthors}
|
||||||
posts={contents.slice(0, 4)}
|
posts={contents.slice(0, 4)}
|
||||||
mod={index.mark}
|
mod={index.mark}
|
||||||
description={index.description}
|
|
||||||
index={contents?.[0].index}
|
index={contents?.[0].index}
|
||||||
association={association}
|
association={association}
|
||||||
hidden={groups[association?.group]?.hidden}
|
hidden={groups[association?.group]?.hidden}
|
||||||
|
@ -49,7 +49,7 @@ function TranscludedLinkNode(props: {
|
|||||||
<Author
|
<Author
|
||||||
pt='12px'
|
pt='12px'
|
||||||
pl='12px'
|
pl='12px'
|
||||||
size='24'
|
size={24}
|
||||||
sigilPadding='6'
|
sigilPadding='6'
|
||||||
showImage
|
showImage
|
||||||
ship={node.post.author}
|
ship={node.post.author}
|
||||||
@ -121,7 +121,7 @@ function TranscludedComment(props: {
|
|||||||
<Author
|
<Author
|
||||||
pt='12px'
|
pt='12px'
|
||||||
pl='12px'
|
pl='12px'
|
||||||
size='24'
|
size={24}
|
||||||
sigilPadding='6'
|
sigilPadding='6'
|
||||||
showImage
|
showImage
|
||||||
ship={comment.post.author}
|
ship={comment.post.author}
|
||||||
@ -175,7 +175,7 @@ function TranscludedPublishNode(props: {
|
|||||||
<Author
|
<Author
|
||||||
pl='12px'
|
pl='12px'
|
||||||
pt='12px'
|
pt='12px'
|
||||||
size='24'
|
size={24}
|
||||||
sigilPadding='6'
|
sigilPadding='6'
|
||||||
showImage
|
showImage
|
||||||
ship={post.post.author}
|
ship={post.post.author}
|
||||||
@ -235,7 +235,7 @@ export function TranscludedPost(props: {
|
|||||||
<Author
|
<Author
|
||||||
pt='12px'
|
pt='12px'
|
||||||
pl='12px'
|
pl='12px'
|
||||||
size='24'
|
size={24}
|
||||||
sigilPadding='6'
|
sigilPadding='6'
|
||||||
showImage
|
showImage
|
||||||
ship={post.author}
|
ship={post.author}
|
||||||
@ -273,7 +273,7 @@ export function TranscludedNode(props: {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
typeof node?.post === "string" &&
|
typeof node?.post === "string" &&
|
||||||
assoc.metadata.config.graph === "chat"
|
(assoc.metadata.config as GraphConfig).graph === "chat"
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -296,6 +296,7 @@ export function TranscludedNode(props: {
|
|||||||
renderSigil
|
renderSigil
|
||||||
transcluded={transcluded + 1}
|
transcluded={transcluded + 1}
|
||||||
className="items-top cf hide-child"
|
className="items-top cf hide-child"
|
||||||
|
// @ts-ignore isn't forwarding props to memo
|
||||||
association={assoc}
|
association={assoc}
|
||||||
msg={node.post}
|
msg={node.post}
|
||||||
fontSize={0}
|
fontSize={0}
|
||||||
|
@ -1,20 +1,12 @@
|
|||||||
import {
|
import { BaseAnchor, Box, Center, Col, Icon, Row, Text } from "@tlon/indigo-react";
|
||||||
BaseAnchor, Box,
|
import { Association, GraphNode, resourceFromPath, GraphConfig } from '@urbit/api';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Center, Col, Icon, Row, Text
|
|
||||||
} from "@tlon/indigo-react";
|
|
||||||
import { Association, GraphNode, resourceFromPath } from '@urbit/api';
|
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { useHistory, useLocation } from 'react-router-dom';
|
import { useHistory, useLocation } from 'react-router-dom';
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import {
|
import {
|
||||||
getPermalinkForGraph, GraphPermalink as IGraphPermalink, parsePermalink
|
getPermalinkForGraph, GraphPermalink as IGraphPermalink, parsePermalink
|
||||||
} from '~/logic/lib/permalinks';
|
} from '~/logic/lib/permalinks';
|
||||||
import { getModuleIcon } from "~/logic/lib/util";
|
import { getModuleIcon, GraphModule } from "~/logic/lib/util";
|
||||||
import { useVirtualResizeProp } from "~/logic/lib/virtualContext";
|
import { useVirtualResizeProp } from "~/logic/lib/virtualContext";
|
||||||
import useGraphState from "~/logic/state/graph";
|
import useGraphState from "~/logic/state/graph";
|
||||||
import useMetadataState from "~/logic/state/metadata";
|
import useMetadataState from "~/logic/state/metadata";
|
||||||
@ -129,7 +121,7 @@ function GraphPermalink(
|
|||||||
<PermalinkDetails
|
<PermalinkDetails
|
||||||
known
|
known
|
||||||
showTransclusion={showTransclusion}
|
showTransclusion={showTransclusion}
|
||||||
icon={getModuleIcon(association.metadata.config.graph)}
|
icon={getModuleIcon((association.metadata.config as GraphConfig).graph as GraphModule)}
|
||||||
title={association.metadata.title}
|
title={association.metadata.title}
|
||||||
permalink={permalink}
|
permalink={permalink}
|
||||||
/>
|
/>
|
||||||
@ -197,6 +189,7 @@ export function PermalinkEmbed(props: {
|
|||||||
transcluded: number;
|
transcluded: number;
|
||||||
showOurContact?: boolean;
|
showOurContact?: boolean;
|
||||||
full?: boolean;
|
full?: boolean;
|
||||||
|
pending?: any;
|
||||||
}) {
|
}) {
|
||||||
const permalink = parsePermalink(props.link);
|
const permalink = parsePermalink(props.link);
|
||||||
|
|
||||||
|
@ -90,11 +90,12 @@ export function EditProfile(props: any): ReactElement {
|
|||||||
|
|
||||||
const onSubmit = async (values: any, actions: any) => {
|
const onSubmit = async (values: any, actions: any) => {
|
||||||
try {
|
try {
|
||||||
await Object.keys(values).reduce((acc, key) => {
|
Object.keys(values).forEach((key) => {
|
||||||
const newValue = key !== 'color' ? values[key] : uxToHex(values[key]);
|
const newValue = key !== 'color' ? values[key] : uxToHex(values[key]);
|
||||||
if (newValue !== contact[key]) {
|
if (newValue !== contact[key]) {
|
||||||
if (key === 'isPublic') {
|
if (key === 'isPublic') {
|
||||||
return acc.then(() => api.contacts.setPublic(newValue));
|
api.contacts.setPublic(newValue)
|
||||||
|
return;
|
||||||
} else if (key === 'groups') {
|
} else if (key === 'groups') {
|
||||||
const toRemove: string[] = _.difference(
|
const toRemove: string[] = _.difference(
|
||||||
contact?.groups || [],
|
contact?.groups || [],
|
||||||
@ -104,24 +105,18 @@ export function EditProfile(props: any): ReactElement {
|
|||||||
newValue,
|
newValue,
|
||||||
contact?.groups || []
|
contact?.groups || []
|
||||||
);
|
);
|
||||||
const promises: Promise<any>[] = [];
|
toRemove.forEach(e =>
|
||||||
promises.concat(
|
|
||||||
toRemove.map(e =>
|
|
||||||
api.contacts.edit(ship, { 'remove-group': resourceFromPath(e) })
|
api.contacts.edit(ship, { 'remove-group': resourceFromPath(e) })
|
||||||
)
|
)
|
||||||
);
|
toAdd.forEach(e =>
|
||||||
promises.concat(
|
|
||||||
toAdd.map(e =>
|
|
||||||
api.contacts.edit(ship, { 'add-group': resourceFromPath(e) })
|
api.contacts.edit(ship, { 'add-group': resourceFromPath(e) })
|
||||||
)
|
)
|
||||||
);
|
|
||||||
return acc.then(() => Promise.all(promises));
|
|
||||||
} else if (key !== 'last-updated' && key !== 'isPublic') {
|
} else if (key !== 'last-updated' && key !== 'isPublic') {
|
||||||
return acc.then(() => api.contacts.edit(ship, { [key]: newValue }));
|
api.contacts.edit(ship, { [key]: newValue });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return acc;
|
});
|
||||||
}, Promise.resolve());
|
|
||||||
// actions.setStatus({ success: null });
|
// actions.setStatus({ success: null });
|
||||||
history.push(`/~profile/${ship}`);
|
history.push(`/~profile/${ship}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -39,7 +39,7 @@ export function SetStatus(props: any) {
|
|||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
onChange={onStatusChange}
|
onChange={onStatusChange}
|
||||||
value={_status}
|
value={_status}
|
||||||
autocomplete='off'
|
autoComplete='off'
|
||||||
width='75%'
|
width='75%'
|
||||||
mr={2}
|
mr={2}
|
||||||
onKeyPress={(evt) => {
|
onKeyPress={(evt) => {
|
||||||
|
@ -10,10 +10,13 @@ type PublishResourceProps = StoreState & {
|
|||||||
association: Association;
|
association: Association;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
history?: any;
|
||||||
|
match?: any;
|
||||||
|
location?: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PublishResource(props: PublishResourceProps) {
|
export function PublishResource(props: PublishResourceProps) {
|
||||||
const { association, api, baseUrl, notebooks } = props;
|
const { association, api, baseUrl } = props;
|
||||||
const rid = association.resource;
|
const rid = association.resource;
|
||||||
const [, , ship, book] = rid.split('/');
|
const [, , ship, book] = rid.split('/');
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
@ -69,6 +69,7 @@ export function NotePreview(props: NotePreviewProps) {
|
|||||||
const [rev, title, body, content] = getLatestRevision(node);
|
const [rev, title, body, content] = getLatestRevision(node);
|
||||||
const appPath = `/ship/${props.host}/${props.book}`;
|
const appPath = `/ship/${props.host}/${props.book}`;
|
||||||
const unreads = useHarkState(state => state.unreads);
|
const unreads = useHarkState(state => state.unreads);
|
||||||
|
// @ts-ignore hark will have to choose between sets and numbers
|
||||||
const isUnread = unreads.graph?.[appPath]?.['/']?.unreads?.has(`/${noteId}/1/1`);
|
const isUnread = unreads.graph?.[appPath]?.['/']?.unreads?.has(`/${noteId}/1/1`);
|
||||||
|
|
||||||
const snippet = getSnippet(body);
|
const snippet = getSnippet(body);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Box, Col, Row, Text } from '@tlon/indigo-react';
|
import { Box, Col, Row, Text } from '@tlon/indigo-react';
|
||||||
import { Association, Graph, Unreads } from '@urbit/api';
|
import { Association, Graph } from '@urbit/api';
|
||||||
import React, { ReactElement } from 'react';
|
import React, { ReactElement } from 'react';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import { useShowNickname } from '~/logic/lib/util';
|
import { useShowNickname } from '~/logic/lib/util';
|
||||||
@ -14,7 +14,6 @@ interface NotebookProps {
|
|||||||
association: Association;
|
association: Association;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
rootUrl: string;
|
rootUrl: string;
|
||||||
unreads: Unreads;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Notebook(props: NotebookProps & RouteComponentProps): ReactElement | null {
|
export function Notebook(props: NotebookProps & RouteComponentProps): ReactElement | null {
|
||||||
|
@ -15,7 +15,6 @@ interface NotebookPostsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function NotebookPosts(props: NotebookPostsProps) {
|
export function NotebookPosts(props: NotebookPostsProps) {
|
||||||
const contacts = useContactState(state => state.contacts);
|
|
||||||
return (
|
return (
|
||||||
<Col>
|
<Col>
|
||||||
{Array.from(props.graph || []).map(
|
{Array.from(props.graph || []).map(
|
||||||
@ -25,7 +24,6 @@ export function NotebookPosts(props: NotebookPostsProps) {
|
|||||||
key={date.toString()}
|
key={date.toString()}
|
||||||
host={props.host}
|
host={props.host}
|
||||||
book={props.book}
|
book={props.book}
|
||||||
contact={contacts[`~${node.post.author}`]}
|
|
||||||
node={node}
|
node={node}
|
||||||
baseUrl={props.baseUrl}
|
baseUrl={props.baseUrl}
|
||||||
group={props.group}
|
group={props.group}
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
|
|
||||||
ManagedRadioButtonField as Radio, Row, Text
|
ManagedRadioButtonField as Radio, Row, Text
|
||||||
} from '@tlon/indigo-react';
|
} from '@tlon/indigo-react';
|
||||||
|
import {useField} from 'formik';
|
||||||
import React, { ReactElement } from 'react';
|
import React, { ReactElement } from 'react';
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { ColorInput } from '~/views/components/ColorInput';
|
import { ColorInput } from '~/views/components/ColorInput';
|
||||||
@ -10,11 +11,7 @@ import { ImageInput } from '~/views/components/ImageInput';
|
|||||||
|
|
||||||
export type BgType = 'none' | 'url' | 'color';
|
export type BgType = 'none' | 'url' | 'color';
|
||||||
|
|
||||||
export function BackgroundPicker({
|
export function BackgroundPicker({ api }: {
|
||||||
bgType,
|
|
||||||
bgUrl,
|
|
||||||
api
|
|
||||||
}: {
|
|
||||||
bgType: BgType;
|
bgType: BgType;
|
||||||
bgUrl?: string;
|
bgUrl?: string;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
@ -40,7 +37,6 @@ export function BackgroundPicker({
|
|||||||
id="bgUrl"
|
id="bgUrl"
|
||||||
placeholder="Drop or upload a file, or paste a link here"
|
placeholder="Drop or upload a file, or paste a link here"
|
||||||
name="bgUrl"
|
name="bgUrl"
|
||||||
url={bgUrl || ''}
|
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -6,9 +6,11 @@ import {
|
|||||||
import { Form, Formik, FormikHelpers } from 'formik';
|
import { Form, Formik, FormikHelpers } from 'formik';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import useSettingsState, { selectSettingsState } from '~/logic/state/settings';
|
import useSettingsState, { selectSettingsState, SettingsState } from '~/logic/state/settings';
|
||||||
import { AsyncButton } from '~/views/components/AsyncButton';
|
import { AsyncButton } from '~/views/components/AsyncButton';
|
||||||
import { BackButton } from './BackButton';
|
import { BackButton } from './BackButton';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import {FormikOnBlur} from '~/views/components/FormikOnBlur';
|
||||||
|
|
||||||
interface FormSchema {
|
interface FormSchema {
|
||||||
hideAvatars: boolean;
|
hideAvatars: boolean;
|
||||||
@ -22,57 +24,39 @@ interface FormSchema {
|
|||||||
videoShown: boolean;
|
videoShown: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsSel = selectSettingsState(['calm', 'remoteContentPolicy']);
|
const settingsSel = (s: SettingsState): FormSchema => ({
|
||||||
|
hideAvatars: s.calm.hideAvatars,
|
||||||
|
hideNicknames: s.calm.hideAvatars,
|
||||||
|
hideUnreads: s.calm.hideUnreads,
|
||||||
|
hideGroups: s.calm.hideGroups,
|
||||||
|
hideUtilities: s.calm.hideUtilities,
|
||||||
|
imageShown: !s.remoteContentPolicy.imageShown,
|
||||||
|
videoShown: !s.remoteContentPolicy.videoShown,
|
||||||
|
oembedShown: !s.remoteContentPolicy.oembedShown,
|
||||||
|
audioShown: !s.remoteContentPolicy.audioShown
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
export function CalmPrefs(props: {
|
export function CalmPrefs(props: {
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
}) {
|
}) {
|
||||||
const { api } = props;
|
const { api } = props;
|
||||||
const {
|
const initialValues = useSettingsState(settingsSel);
|
||||||
calm: {
|
|
||||||
hideAvatars,
|
|
||||||
hideNicknames,
|
|
||||||
hideUnreads,
|
|
||||||
hideGroups,
|
|
||||||
hideUtilities
|
|
||||||
},
|
|
||||||
remoteContentPolicy: {
|
|
||||||
imageShown,
|
|
||||||
videoShown,
|
|
||||||
oembedShown,
|
|
||||||
audioShown
|
|
||||||
}
|
|
||||||
} = useSettingsState(settingsSel);
|
|
||||||
|
|
||||||
const initialValues: FormSchema = {
|
|
||||||
hideAvatars,
|
|
||||||
hideNicknames,
|
|
||||||
hideUnreads,
|
|
||||||
hideGroups,
|
|
||||||
hideUtilities,
|
|
||||||
imageShown: !imageShown,
|
|
||||||
videoShown: !videoShown,
|
|
||||||
oembedShown: !oembedShown,
|
|
||||||
audioShown: !audioShown
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = useCallback(async (v: FormSchema, actions: FormikHelpers<FormSchema>) => {
|
const onSubmit = useCallback(async (v: FormSchema, actions: FormikHelpers<FormSchema>) => {
|
||||||
await Promise.all([
|
let promises: Promise<any>[] = [];
|
||||||
api.settings.putEntry('calm', 'hideAvatars', v.hideAvatars),
|
_.forEach(v, (bool, key) => {
|
||||||
api.settings.putEntry('calm', 'hideNicknames', v.hideNicknames),
|
const bucket = ['imageShown', 'videoShown', 'audioShown', 'oembedShown'].includes(key) ? 'remoteContentPolicy' : 'calm';
|
||||||
api.settings.putEntry('calm', 'hideUnreads', v.hideUnreads),
|
if(initialValues[key] !== bool) {
|
||||||
api.settings.putEntry('calm', 'hideGroups', v.hideGroups),
|
promises.push(api.settings.putEntry(bucket, key, bool));
|
||||||
api.settings.putEntry('calm', 'hideUtilities', v.hideUtilities),
|
}
|
||||||
api.settings.putEntry('remoteContentPolicy', 'imageShown', !v.imageShown),
|
})
|
||||||
api.settings.putEntry('remoteContentPolicy', 'videoShown', !v.videoShown),
|
await Promise.all(promises);
|
||||||
api.settings.putEntry('remoteContentPolicy', 'audioShown', !v.audioShown),
|
|
||||||
api.settings.putEntry('remoteContentPolicy', 'oembedShown', !v.oembedShown)
|
|
||||||
]);
|
|
||||||
actions.setStatus({ success: null });
|
actions.setStatus({ success: null });
|
||||||
}, [api]);
|
}, [api]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik initialValues={initialValues} onSubmit={onSubmit}>
|
<FormikOnBlur initialValues={initialValues} onSubmit={onSubmit}>
|
||||||
<Form>
|
<Form>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
<Col borderBottom={1} borderBottomColor="washedGray" p={5} pt={4} gapY={5}>
|
<Col borderBottom={1} borderBottomColor="washedGray" p={5} pt={4} gapY={5}>
|
||||||
@ -132,12 +116,8 @@ export function CalmPrefs(props: {
|
|||||||
id="oembedShown"
|
id="oembedShown"
|
||||||
caption="Embedded content may contain scripts that can track you"
|
caption="Embedded content may contain scripts that can track you"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AsyncButton primary width="fit-content" type="submit">
|
|
||||||
Save
|
|
||||||
</AsyncButton>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</FormikOnBlur>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ import { BackButton } from './BackButton';
|
|||||||
|
|
||||||
interface StoreDebuggerProps {
|
interface StoreDebuggerProps {
|
||||||
name: string;
|
name: string;
|
||||||
useStore: UseStore<BaseState<any>>;
|
useStore: UseStore<BaseState<any> & any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const objectToString = (obj: any): string => JSON.stringify(obj, null, ' ');
|
const objectToString = (obj: any): string => JSON.stringify(obj, null, ' ');
|
||||||
@ -57,7 +57,9 @@ const StoreDebugger = (props: StoreDebuggerProps) => {
|
|||||||
placeholder="Drill Down"
|
placeholder="Drill Down"
|
||||||
width="100%"
|
width="100%"
|
||||||
onKeyUp={(event) => {
|
onKeyUp={(event) => {
|
||||||
|
// @ts-ignore clearly value is in eventtarget
|
||||||
if (event.target.value) {
|
if (event.target.value) {
|
||||||
|
// @ts-ignore clearly value is in eventtarget
|
||||||
tryFilter(event.target.value);
|
tryFilter(event.target.value);
|
||||||
} else {
|
} else {
|
||||||
setFilter('');
|
setFilter('');
|
||||||
|
@ -11,6 +11,7 @@ import GlobalApi from '~/logic/api/global';
|
|||||||
import { uxToHex } from '~/logic/lib/util';
|
import { uxToHex } from '~/logic/lib/util';
|
||||||
import useSettingsState, { selectSettingsState } from '~/logic/state/settings';
|
import useSettingsState, { selectSettingsState } from '~/logic/state/settings';
|
||||||
import { AsyncButton } from '~/views/components/AsyncButton';
|
import { AsyncButton } from '~/views/components/AsyncButton';
|
||||||
|
import {FormikOnBlur} from '~/views/components/FormikOnBlur';
|
||||||
import { BackButton } from './BackButton';
|
import { BackButton } from './BackButton';
|
||||||
import { BackgroundPicker, BgType } from './BackgroundPicker';
|
import { BackgroundPicker, BgType } from './BackgroundPicker';
|
||||||
|
|
||||||
@ -58,7 +59,7 @@ export default function DisplayForm(props: DisplayFormProps) {
|
|||||||
const bgType = backgroundType || 'none';
|
const bgType = backgroundType || 'none';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik
|
<FormikOnBlur
|
||||||
validationSchema={formSchema}
|
validationSchema={formSchema}
|
||||||
initialValues={
|
initialValues={
|
||||||
{
|
{
|
||||||
@ -86,7 +87,6 @@ export default function DisplayForm(props: DisplayFormProps) {
|
|||||||
actions.setStatus({ success: null });
|
actions.setStatus({ success: null });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props => (
|
|
||||||
<Form>
|
<Form>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
<Col p={5} pt={4} gapY={5}>
|
<Col p={5} pt={4} gapY={5}>
|
||||||
@ -99,9 +99,8 @@ export default function DisplayForm(props: DisplayFormProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
</Col>
|
</Col>
|
||||||
<BackgroundPicker
|
<BackgroundPicker
|
||||||
bgType={props.values.bgType}
|
|
||||||
bgUrl={props.values.bgUrl}
|
|
||||||
api={api}
|
api={api}
|
||||||
|
bgType={bgType}
|
||||||
/>
|
/>
|
||||||
<Label>Theme</Label>
|
<Label>Theme</Label>
|
||||||
<Radio name="theme" id="light" label="Light" />
|
<Radio name="theme" id="light" label="Light" />
|
||||||
@ -112,7 +111,6 @@ export default function DisplayForm(props: DisplayFormProps) {
|
|||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</Col>
|
</Col>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
</FormikOnBlur>
|
||||||
</Formik>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,8 @@ import {
|
|||||||
|
|
||||||
Center, Col, Icon,
|
Center, Col, Icon,
|
||||||
|
|
||||||
StatelessToggleSwitchField, Text
|
ToggleSwitch, Text,
|
||||||
|
StatelessToggleSwitchField
|
||||||
} from '@tlon/indigo-react';
|
} from '@tlon/indigo-react';
|
||||||
import { Association, GraphConfig, resourceFromPath } from '@urbit/api';
|
import { Association, GraphConfig, resourceFromPath } from '@urbit/api';
|
||||||
import { useField } from 'formik';
|
import { useField } from 'formik';
|
||||||
@ -100,7 +101,7 @@ function Channel(props: { association: Association }) {
|
|||||||
return isWatching(config, association.resource);
|
return isWatching(config, association.resource);
|
||||||
});
|
});
|
||||||
|
|
||||||
const [{ value }, meta, { setValue }] = useField(
|
const [{ value }, meta, { setValue, setTouched }] = useField(
|
||||||
`graph["${association.resource}"]`
|
`graph["${association.resource}"]`
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -108,9 +109,11 @@ function Channel(props: { association: Association }) {
|
|||||||
setValue(watching);
|
setValue(watching);
|
||||||
}, [watching]);
|
}, [watching]);
|
||||||
|
|
||||||
const onChange = () => {
|
const onClick = () => {
|
||||||
setValue(!value);
|
setValue(!value);
|
||||||
};
|
setTouched(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const icon = getModuleIcon((metadata.config as GraphConfig)?.graph as GraphModule);
|
const icon = getModuleIcon((metadata.config as GraphConfig)?.graph as GraphModule);
|
||||||
|
|
||||||
@ -123,7 +126,7 @@ function Channel(props: { association: Association }) {
|
|||||||
<Text> {metadata.title}</Text>
|
<Text> {metadata.title}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box gridColumn={4}>
|
<Box gridColumn={4}>
|
||||||
<StatelessToggleSwitchField selected={value} onChange={onChange} />
|
<StatelessToggleSwitchField selected={value} onClick={onClick} />
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -50,7 +50,6 @@ export function LeapSettings(props: { api: GlobalApi; }) {
|
|||||||
const { leap, set: setSettingsState } = useSettingsState(settingsSel);
|
const { leap, set: setSettingsState } = useSettingsState(settingsSel);
|
||||||
const categories = leap.categories as LeapCategories[];
|
const categories = leap.categories as LeapCategories[];
|
||||||
const missing = _.difference(leapCategories, categories);
|
const missing = _.difference(leapCategories, categories);
|
||||||
console.log(categories);
|
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
categories: [
|
categories: [
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
Col,
|
Col,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ManagedToggleSwitchField as Toggle, Text
|
ManagedToggleSwitchField as Toggle, Text
|
||||||
} from '@tlon/indigo-react';
|
} from '@tlon/indigo-react';
|
||||||
import { Form, Formik, FormikHelpers } from 'formik';
|
import { Form, FormikHelpers } from 'formik';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { isWatching } from '~/logic/lib/hark';
|
import { isWatching } from '~/logic/lib/hark';
|
||||||
import useHarkState from '~/logic/state/hark';
|
import useHarkState from '~/logic/state/hark';
|
||||||
import { AsyncButton } from '~/views/components/AsyncButton';
|
import { FormikOnBlur } from '~/views/components/FormikOnBlur';
|
||||||
import { BackButton } from './BackButton';
|
import { BackButton } from './BackButton';
|
||||||
import { GroupChannelPicker } from './GroupChannelPicker';
|
import { GroupChannelPicker } from './GroupChannelPicker';
|
||||||
|
|
||||||
@ -69,6 +73,8 @@ export function NotificationPreferences(props: {
|
|||||||
}
|
}
|
||||||
}, [api, graphConfig, dnd]);
|
}, [api, graphConfig, dnd]);
|
||||||
|
|
||||||
|
const [notificationsAllowed, setNotificationsAllowed] = useState('Notification' in window && Notification.permission !== 'default');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BackButton />
|
<BackButton />
|
||||||
@ -82,9 +88,17 @@ export function NotificationPreferences(props: {
|
|||||||
messaging
|
messaging
|
||||||
</Text>
|
</Text>
|
||||||
</Col>
|
</Col>
|
||||||
<Formik initialValues={initialValues} onSubmit={onSubmit}>
|
<FormikOnBlur initialValues={initialValues} onSubmit={onSubmit}>
|
||||||
<Form>
|
<Form>
|
||||||
<Col gapY={4}>
|
<Col gapY="4">
|
||||||
|
{notificationsAllowed || !('Notification' in window)
|
||||||
|
? null
|
||||||
|
: <Button alignSelf='flex-start' onClick={() => {
|
||||||
|
Notification.requestPermission().then(() => {
|
||||||
|
setNotificationsAllowed(Notification.permission !== 'default');
|
||||||
|
});
|
||||||
|
}}>Allow Browser Notifications</Button>
|
||||||
|
}
|
||||||
<Toggle
|
<Toggle
|
||||||
label="Do not disturb"
|
label="Do not disturb"
|
||||||
id="dnd"
|
id="dnd"
|
||||||
@ -109,12 +123,9 @@ export function NotificationPreferences(props: {
|
|||||||
</Text>
|
</Text>
|
||||||
<GroupChannelPicker />
|
<GroupChannelPicker />
|
||||||
</Col>
|
</Col>
|
||||||
<AsyncButton primary width="fit-content">
|
|
||||||
Save
|
|
||||||
</AsyncButton>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</FormikOnBlur>
|
||||||
</Col>
|
</Col>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,117 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { Box, Col, Text } from '@tlon/indigo-react';
|
||||||
|
import { Formik, Form, useField } from 'formik';
|
||||||
|
|
||||||
|
import GlobalApi from '~/logic/api/global';
|
||||||
|
import { getChord } from '~/logic/lib/util';
|
||||||
|
import useSettingsState, {
|
||||||
|
selectSettingsState,
|
||||||
|
ShortcutMapping,
|
||||||
|
} from '~/logic/state/settings';
|
||||||
|
import { AsyncButton } from '~/views/components/AsyncButton';
|
||||||
|
import { BackButton } from './BackButton';
|
||||||
|
|
||||||
|
interface ShortcutSettingsProps {
|
||||||
|
api: GlobalApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsSel = selectSettingsState(['keyboard']);
|
||||||
|
|
||||||
|
export function ChordInput(props: { id: string; label: string }) {
|
||||||
|
const { id, label } = props;
|
||||||
|
const [capturing, setCapturing] = useState(false);
|
||||||
|
const [{ value }, , { setValue }] = useField(id);
|
||||||
|
const onCapture = useCallback(() => {
|
||||||
|
setCapturing(true);
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!capturing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
function onKeydown(e: KeyboardEvent) {
|
||||||
|
if (['Control', 'Shift', 'Meta'].includes(e.key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const chord = getChord(e);
|
||||||
|
setValue(chord);
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
setCapturing(false);
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', onKeydown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', onKeydown);
|
||||||
|
};
|
||||||
|
}, [capturing]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box p="1">
|
||||||
|
<Text>{label}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
border="1"
|
||||||
|
borderColor="lightGray"
|
||||||
|
borderRadius="2"
|
||||||
|
onClick={onCapture}
|
||||||
|
p="1"
|
||||||
|
>
|
||||||
|
<Text>{capturing ? 'Press' : value}</Text>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ShortcutSettings(props: ShortcutSettingsProps) {
|
||||||
|
const { api } = props;
|
||||||
|
|
||||||
|
const { keyboard } = useSettingsState(settingsSel);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={keyboard}
|
||||||
|
onSubmit={async (values: ShortcutMapping, actions) => {
|
||||||
|
const promises = _.map(values, (value, key) => {
|
||||||
|
return keyboard[key] !== value
|
||||||
|
? api.settings.putEntry('keyboard', key, value)
|
||||||
|
: Promise.resolve();
|
||||||
|
});
|
||||||
|
await Promise.all(promises);
|
||||||
|
actions.setStatus({ success: null });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<BackButton />
|
||||||
|
<Col p="5" pt="4" gapY="5">
|
||||||
|
<Col gapY="1" mt="0">
|
||||||
|
<Text color="black" fontSize={2} fontWeight="medium">
|
||||||
|
Shortcuts
|
||||||
|
</Text>
|
||||||
|
<Text gray>Customize keyboard shortcuts for landscape</Text>
|
||||||
|
</Col>
|
||||||
|
<Box
|
||||||
|
display="grid"
|
||||||
|
gridTemplateColumns="1fr 100px"
|
||||||
|
gridGap={3}
|
||||||
|
maxWidth="500px"
|
||||||
|
>
|
||||||
|
<ChordInput id="navForward" label="Go forward in history" />
|
||||||
|
<ChordInput id="navBack" label="Go backward in history" />
|
||||||
|
<ChordInput
|
||||||
|
id="cycleForward"
|
||||||
|
label="Cycle forward through channel list"
|
||||||
|
/>
|
||||||
|
<ChordInput
|
||||||
|
id="cycleBack"
|
||||||
|
label="Cycle backward through channel list"
|
||||||
|
/>
|
||||||
|
<ChordInput id="hideSidebar" label="Show/hide group sidebar" />
|
||||||
|
</Box>
|
||||||
|
<AsyncButton primary width="fit-content">Save Changes</AsyncButton>
|
||||||
|
</Col>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
@ -13,6 +13,7 @@ import { NotificationPreferences } from './components/lib/NotificationPref';
|
|||||||
import S3Form from './components/lib/S3Form';
|
import S3Form from './components/lib/S3Form';
|
||||||
import SecuritySettings from './components/lib/Security';
|
import SecuritySettings from './components/lib/Security';
|
||||||
import {DmSettings} from './components/lib/DmSettings';
|
import {DmSettings} from './components/lib/DmSettings';
|
||||||
|
import ShortcutSettings from './components/lib/ShortcutSettings';
|
||||||
|
|
||||||
export const Skeleton = (props: { children: ReactNode }) => (
|
export const Skeleton = (props: { children: ReactNode }) => (
|
||||||
<Box height='100%' width='100%' px={[0, 3]} pb={[0, 3]} borderRadius={1}>
|
<Box height='100%' width='100%' px={[0, 3]} pb={[0, 3]} borderRadius={1}>
|
||||||
@ -115,6 +116,7 @@ return;
|
|||||||
<SidebarItem icon='LeapArrow' text='Leap' hash='leap' />
|
<SidebarItem icon='LeapArrow' text='Leap' hash='leap' />
|
||||||
<SidebarItem icon='Messages' text='Direct Messages' hash='dm' />
|
<SidebarItem icon='Messages' text='Direct Messages' hash='dm' />
|
||||||
<SidebarItem icon='Node' text='CalmEngine' hash='calm' />
|
<SidebarItem icon='Node' text='CalmEngine' hash='calm' />
|
||||||
|
<SidebarItem icon='Keyboard' text='Shortcuts' hash='shortcuts' />
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
icon='Locked'
|
icon='Locked'
|
||||||
text='Devices + Security'
|
text='Devices + Security'
|
||||||
@ -132,6 +134,7 @@ return;
|
|||||||
)}
|
)}
|
||||||
{hash === 'display' && <DisplayForm api={props.api} />}
|
{hash === 'display' && <DisplayForm api={props.api} />}
|
||||||
{hash === 'dm' && <DmSettings api={props.api} />}
|
{hash === 'dm' && <DmSettings api={props.api} />}
|
||||||
|
{hash === 'shortcuts' && <ShortcutSettings api={props.api} />}
|
||||||
{hash === 's3' && <S3Form api={props.api} />}
|
{hash === 's3' && <S3Form api={props.api} />}
|
||||||
{hash === 'leap' && <LeapSettings api={props.api} />}
|
{hash === 'leap' && <LeapSettings api={props.api} />}
|
||||||
{hash === 'calm' && <CalmPrefs api={props.api} />}
|
{hash === 'calm' && <CalmPrefs api={props.api} />}
|
||||||
|
@ -78,6 +78,7 @@ class TermApp extends Component {
|
|||||||
border={['0','1']}
|
border={['0','1']}
|
||||||
cursor='text'
|
cursor='text'
|
||||||
>
|
>
|
||||||
|
{/* @ts-ignore declare props in later pass */}
|
||||||
<History log={this.state.lines.slice(0, -1)} />
|
<History log={this.state.lines.slice(0, -1)} />
|
||||||
<Input
|
<Input
|
||||||
ship={this.props.ship}
|
ship={this.props.ship}
|
||||||
|
@ -21,7 +21,9 @@ export class History extends Component {
|
|||||||
<Box
|
<Box
|
||||||
mt='auto'
|
mt='auto'
|
||||||
>
|
>
|
||||||
|
{/* @ts-ignore declare props in later pass */}
|
||||||
{this.props.log.map((line, i) => {
|
{this.props.log.map((line, i) => {
|
||||||
|
// @ts-ignore react memo not passing props
|
||||||
return <Line key={i} line={line} />;
|
return <Line key={i} line={line} />;
|
||||||
})}
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -15,10 +15,11 @@ export class Input extends Component {
|
|||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
if (
|
if (
|
||||||
!document.activeElement == document.body
|
document.activeElement == this.inputRef.current
|
||||||
|| document.activeElement == this.inputRef.current
|
|
||||||
) {
|
) {
|
||||||
|
// @ts-ignore ref type issues
|
||||||
this.inputRef.current.focus();
|
this.inputRef.current.focus();
|
||||||
|
// @ts-ignore ref type issues
|
||||||
this.inputRef.current.setSelectionRange(this.props.cursor, this.props.cursor);
|
this.inputRef.current.setSelectionRange(this.props.cursor, this.props.cursor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -26,7 +27,7 @@ export class Input extends Component {
|
|||||||
keyPress(e) {
|
keyPress(e) {
|
||||||
const key = e.key;
|
const key = e.key;
|
||||||
// let paste and leap events pass
|
// let paste and leap events pass
|
||||||
if ((e.getModifierState('Control') || event.getModifierState('Meta'))
|
if ((e.getModifierState('Control') || e.getModifierState('Meta'))
|
||||||
&& (e.key === 'v' || e.key === '/')) {
|
&& (e.key === 'v' || e.key === '/')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -115,6 +116,7 @@ belt = { met: 'bac' };
|
|||||||
onKeyDown={this.keyPress}
|
onKeyDown={this.keyPress}
|
||||||
onClick={this.click}
|
onClick={this.click}
|
||||||
onPaste={this.paste}
|
onPaste={this.paste}
|
||||||
|
// @ts-ignore indigo-react doesn't let us pass refs
|
||||||
ref={this.inputRef}
|
ref={this.inputRef}
|
||||||
defaultValue="connecting..."
|
defaultValue="connecting..."
|
||||||
value={prompt}
|
value={prompt}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Text } from '@tlon/indigo-react';
|
import { Text } from '@tlon/indigo-react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
// @ts-ignore line isn't in props?
|
||||||
export default React.memo(({ line }) => {
|
export default React.memo(({ line }) => {
|
||||||
// line body to jsx
|
// line body to jsx
|
||||||
// NOTE lines are lists of characters that might span multiple codepoints
|
// NOTE lines are lists of characters that might span multiple codepoints
|
||||||
|
@ -46,7 +46,7 @@ export function CommentItem(props: CommentItemProps) {
|
|||||||
const children = Array.from(revs.children);
|
const children = Array.from(revs.children);
|
||||||
const indices = [];
|
const indices = [];
|
||||||
for (const child in children) {
|
for (const child in children) {
|
||||||
const node = children[child];
|
const node = children[child] as any;
|
||||||
if (!node?.post || typeof node.post !== 'string') {
|
if (!node?.post || typeof node.post !== 'string') {
|
||||||
indices.push(node.post?.index);
|
indices.push(node.post?.index);
|
||||||
}
|
}
|
||||||
|
@ -129,6 +129,7 @@ export function DropdownSearch<C>(props: DropdownSearchProps<C>): ReactElement {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box {...rest} position="relative" zIndex={9}>
|
<Box {...rest} position="relative" zIndex={9}>
|
||||||
|
{ /* @ts-ignore investigate onblur on styled-system component later */}
|
||||||
<Input
|
<Input
|
||||||
ref={textarea}
|
ref={textarea}
|
||||||
onChange={changeCallback}
|
onChange={changeCallback}
|
||||||
|
@ -1,28 +1,32 @@
|
|||||||
import { FormikConfig, FormikProvider, FormikValues, useFormik } from 'formik';
|
import { FormikConfig, FormikProvider, FormikValues, useFormik } from 'formik';
|
||||||
import React, { useEffect, useImperativeHandle } from 'react';
|
import React, { useEffect, useImperativeHandle, useState } from 'react';
|
||||||
|
|
||||||
export function FormikOnBlur<
|
export function FormikOnBlur<
|
||||||
Values extends FormikValues = FormikValues,
|
Values extends FormikValues = FormikValues,
|
||||||
ExtraProps = {}
|
ExtraProps = {}
|
||||||
>(props: FormikConfig<Values> & ExtraProps) {
|
>(props: FormikConfig<Values> & ExtraProps) {
|
||||||
const formikBag = useFormik<Values>({ ...props, validateOnBlur: true });
|
const formikBag = useFormik<Values>({ ...props, validateOnBlur: true });
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
Object.keys(formikBag.errors || {}).length === 0 &&
|
Object.keys(formikBag.errors || {}).length === 0 &&
|
||||||
Object.keys(formikBag.touched || {}).length !== 0 &&
|
formikBag.dirty &&
|
||||||
!formikBag.isSubmitting
|
!formikBag.isSubmitting &&
|
||||||
|
!submitting
|
||||||
) {
|
) {
|
||||||
|
setSubmitting(true);
|
||||||
const { values } = formikBag;
|
const { values } = formikBag;
|
||||||
formikBag.submitForm().then(() => {
|
formikBag.submitForm().then(() => {
|
||||||
formikBag.resetForm({ values, touched: {} });
|
formikBag.resetForm({ values })
|
||||||
|
setSubmitting(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
formikBag.errors,
|
formikBag.errors,
|
||||||
formikBag.touched,
|
formikBag.dirty,
|
||||||
formikBag.submitForm,
|
submitting,
|
||||||
formikBag.values
|
formikBag.isSubmitting
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const { children, innerRef } = props;
|
const { children, innerRef } = props;
|
||||||
|
@ -77,16 +77,6 @@ export function GroupLink(
|
|||||||
<Box pt='1' ml='2' display='flex' alignItems='center'>
|
<Box pt='1' ml='2' display='flex' alignItems='center'>
|
||||||
{preview ?
|
{preview ?
|
||||||
<>
|
<>
|
||||||
<Box pr='2' display='flex' alignItems='center'>
|
|
||||||
<Icon
|
|
||||||
icon={preview.metadata.hidden ? 'Locked' : 'Public'}
|
|
||||||
color='gray'
|
|
||||||
mr='1'
|
|
||||||
/>
|
|
||||||
<Text fontSize='0' color='gray'>
|
|
||||||
{preview.metadata.hidden ? 'Private' : 'Public'}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<Box display='flex' alignItems='center'>
|
<Box display='flex' alignItems='center'>
|
||||||
<Icon icon='Users' color='gray' mr='1' />
|
<Icon icon='Users' color='gray' mr='1' />
|
||||||
<Text fontSize='0'color='gray' >
|
<Text fontSize='0'color='gray' >
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
ErrorLabel, Icon, Label,
|
ErrorLabel, Icon, Label,
|
||||||
Row, Text
|
Row, Text
|
||||||
} from '@tlon/indigo-react';
|
} from '@tlon/indigo-react';
|
||||||
|
import { OpenPolicy } from '@urbit/api';
|
||||||
import { Association } from '@urbit/api/metadata';
|
import { Association } from '@urbit/api/metadata';
|
||||||
import { FieldArray, useFormikContext } from 'formik';
|
import { FieldArray, useFormikContext } from 'formik';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@ -100,7 +101,7 @@ export function GroupSearch<I extends string, V extends FormValues<I>>(props: Gr
|
|||||||
return Object.values(
|
return Object.values(
|
||||||
Object.keys(associations.groups)
|
Object.keys(associations.groups)
|
||||||
.filter(
|
.filter(
|
||||||
e => groupState?.[e]?.policy?.open
|
e => (groupState?.[e]?.policy as OpenPolicy)?.open
|
||||||
)
|
)
|
||||||
.reduce((obj, key) => {
|
.reduce((obj, key) => {
|
||||||
obj[key] = associations.groups[key];
|
obj[key] = associations.groups[key];
|
||||||
|
@ -13,7 +13,7 @@ import useStorage from '~/logic/lib/useStorage';
|
|||||||
|
|
||||||
type ImageInputProps = Parameters<typeof Box>[0] & {
|
type ImageInputProps = Parameters<typeof Box>[0] & {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
Metadata, MetadataUpdatePreview,
|
Metadata, MetadataUpdatePreview,
|
||||||
resourceFromPath
|
resourceFromPath
|
||||||
} from '@urbit/api';
|
} from '@urbit/api';
|
||||||
import { GraphConfig } from '@urbit/api/dist';
|
import { GraphConfig } from '@urbit/api';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import React, { ReactElement, ReactNode, useCallback } from 'react';
|
import React, { ReactElement, ReactNode, useCallback } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
@ -217,6 +217,7 @@ function InviteActions(props: {
|
|||||||
const hideJoin = useCallback(async (e) => {
|
const hideJoin = useCallback(async (e) => {
|
||||||
if(status?.progress === 'done') {
|
if(status?.progress === 'done') {
|
||||||
set(s => {
|
set(s => {
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
delete s.pendingJoin[resource]
|
delete s.pendingJoin[resource]
|
||||||
});
|
});
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -245,14 +246,14 @@ function InviteActions(props: {
|
|||||||
color="blue"
|
color="blue"
|
||||||
height={4}
|
height={4}
|
||||||
backgroundColor="white"
|
backgroundColor="white"
|
||||||
onClick={inviteAccept}
|
onClick={inviteAccept as any}
|
||||||
>
|
>
|
||||||
Accept
|
Accept
|
||||||
</StatelessAsyncButton>
|
</StatelessAsyncButton>
|
||||||
<StatelessAsyncButton
|
<StatelessAsyncButton
|
||||||
height={4}
|
height={4}
|
||||||
backgroundColor="white"
|
backgroundColor="white"
|
||||||
onClick={inviteDecline}
|
onClick={inviteDecline as any}
|
||||||
>
|
>
|
||||||
Decline
|
Decline
|
||||||
</StatelessAsyncButton>
|
</StatelessAsyncButton>
|
||||||
|
@ -218,7 +218,7 @@ const ProfileOverlay = (props: ProfileOverlayProps) => {
|
|||||||
textOverflow='ellipsis'
|
textOverflow='ellipsis'
|
||||||
overflow='hidden'
|
overflow='hidden'
|
||||||
whiteSpace='pre'
|
whiteSpace='pre'
|
||||||
marginBottom={0}
|
mb={0}
|
||||||
disableRemoteContent
|
disableRemoteContent
|
||||||
gray
|
gray
|
||||||
title={contact?.status ? contact.status : ''}
|
title={contact?.status ? contact.status : ''}
|
||||||
|
@ -41,7 +41,7 @@ export const ProfileStatus = (props) => {
|
|||||||
<Input
|
<Input
|
||||||
onChange={onStatusChange}
|
onChange={onStatusChange}
|
||||||
value={_status}
|
value={_status}
|
||||||
autocomplete='off'
|
autoComplete='off'
|
||||||
width='100%'
|
width='100%'
|
||||||
placeholder='Set Status'
|
placeholder='Set Status'
|
||||||
onKeyPress={(evt) => {
|
onKeyPress={(evt) => {
|
||||||
|
@ -8,15 +8,15 @@ const ReconnectButton = ({ connection, subscription }) => {
|
|||||||
if (connectedStatus === 'disconnected') {
|
if (connectedStatus === 'disconnected') {
|
||||||
return (
|
return (
|
||||||
<Button onClick={reconnect} borderColor='red' px={2}>
|
<Button onClick={reconnect} borderColor='red' px={2}>
|
||||||
<Text display={['none', 'inline']} textAlign='middle' color='red'>Reconnect</Text>
|
<Text display={['none', 'inline']} textAlign='center' color='red'>Reconnect</Text>
|
||||||
<Text color='red'> ↻</Text>
|
<Text color='red'> ↻</Text>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
} else if (connectedStatus === 'reconnecting') {
|
} else if (connectedStatus === 'reconnecting') {
|
||||||
return (
|
return (
|
||||||
<Button borderColor='yellow' px={2} onClick={() => {}} cursor='default'>
|
<Button borderColor='yellow' px={2} onClick={() => {}} cursor='default'>
|
||||||
<LoadingSpinner pr={['0','2']} foreground='scales.yellow60' background='scales.yellow30' />
|
<LoadingSpinner foreground='scales.yellow60' background='scales.yellow30' />
|
||||||
<Text display={['none', 'inline']} textAlign='middle' color='yellow'>Reconnecting</Text>
|
<Text display={['none', 'inline']} pl={['0','2']} textAlign='center' color='yellow'>Reconnecting</Text>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -25,7 +25,7 @@ const DISABLED_BLOCK_TOKENS = [
|
|||||||
const DISABLED_INLINE_TOKENS = [];
|
const DISABLED_INLINE_TOKENS = [];
|
||||||
|
|
||||||
type RichTextProps = ReactMarkdownProps & {
|
type RichTextProps = ReactMarkdownProps & {
|
||||||
api: GlobalApi;
|
api?: GlobalApi;
|
||||||
disableRemoteContent?: boolean;
|
disableRemoteContent?: boolean;
|
||||||
contact?: Contact;
|
contact?: Contact;
|
||||||
group?: Group;
|
group?: Group;
|
||||||
@ -35,7 +35,21 @@ type RichTextProps = ReactMarkdownProps & {
|
|||||||
color?: string;
|
color?: string;
|
||||||
children?: any;
|
children?: any;
|
||||||
width?: string;
|
width?: string;
|
||||||
} & PropFunc<typeof Box>;
|
display?: string[] | string;
|
||||||
|
mono?: boolean;
|
||||||
|
mb?: number;
|
||||||
|
minWidth?: number | string;
|
||||||
|
maxWidth?: number | string;
|
||||||
|
flexShrink?: number;
|
||||||
|
textOverflow?: string;
|
||||||
|
overflow?: string;
|
||||||
|
whiteSpace?: string;
|
||||||
|
gray?: boolean;
|
||||||
|
title?: string;
|
||||||
|
py?: number;
|
||||||
|
overflowX?: any;
|
||||||
|
verticalAlign?: any;
|
||||||
|
};
|
||||||
|
|
||||||
const RichText = React.memo(({ disableRemoteContent = false, api, ...props }: RichTextProps) => (
|
const RichText = React.memo(({ disableRemoteContent = false, api, ...props }: RichTextProps) => (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
@ -49,6 +63,7 @@ const RichText = React.memo(({ disableRemoteContent = false, api, ...props }: Ri
|
|||||||
oembedShown: false
|
oembedShown: false
|
||||||
} : null;
|
} : null;
|
||||||
if (!disableRemoteContent) {
|
if (!disableRemoteContent) {
|
||||||
|
// @ts-ignore RemoteContent weirdness
|
||||||
return <RemoteContent className="mw-100" url={linkProps.href} />;
|
return <RemoteContent className="mw-100" url={linkProps.href} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,15 +76,15 @@ const RichText = React.memo(({ disableRemoteContent = false, api, ...props }: Ri
|
|||||||
remoteContentPolicy={remoteContentPolicy}
|
remoteContentPolicy={remoteContentPolicy}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
{...linkProps}
|
{...linkProps}
|
||||||
>{linkProps.children}</Anchor>
|
>{linkProps.children}</Anchor>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
linkReference: (linkProps) => {
|
linkReference: (linkProps): any => {
|
||||||
const linkText = String(linkProps.children[0].props.children);
|
const linkText = String(linkProps.children[0].props.children);
|
||||||
if (isValidPatp(linkText)) {
|
if (isValidPatp(linkText)) {
|
||||||
return <Mention contact={props.contact || {}} group={props.group} ship={deSig(linkText)} api={api} />;
|
return <Mention ship={deSig(linkText)} api={api} />;
|
||||||
} else if(linkText.startsWith('web+urbitgraph://')) {
|
} else if(linkText.startsWith('web+urbitgraph://')) {
|
||||||
return (
|
return (
|
||||||
<PermalinkEmbed
|
<PermalinkEmbed
|
||||||
|
@ -10,7 +10,6 @@ const Spinner = ({
|
|||||||
<LoadingSpinner
|
<LoadingSpinner
|
||||||
foreground='black'
|
foreground='black'
|
||||||
background='gray'
|
background='gray'
|
||||||
style={{ flexShrink: 0 }}
|
|
||||||
/>
|
/>
|
||||||
<Text display='inline-block' ml={2} verticalAlign='middle' flexShrink={0}>{text}</Text>
|
<Text display='inline-block' ml={2} verticalAlign='middle' flexShrink={0}>{text}</Text>
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -23,7 +23,7 @@ export function StatelessAsyncAction({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Action
|
<Action
|
||||||
height="18px"
|
height="16px"
|
||||||
hideDisabled={!disabled}
|
hideDisabled={!disabled}
|
||||||
disabled={disabled || state === 'loading'}
|
disabled={disabled || state === 'loading'}
|
||||||
onClick={handleClick} {...rest}
|
onClick={handleClick} {...rest}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
BaseImage, Box,
|
BaseImage,
|
||||||
|
Box,
|
||||||
Button, Col,
|
Button,
|
||||||
|
Col,
|
||||||
Icon, Row,
|
Icon,
|
||||||
|
Row,
|
||||||
Text
|
Text,
|
||||||
} from '@tlon/indigo-react';
|
} from "@tlon/indigo-react";
|
||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { Sigil } from '~/logic/lib/sigil';
|
import { Sigil } from '~/logic/lib/sigil';
|
||||||
@ -73,7 +73,7 @@ const StatusBar = (props) => {
|
|||||||
px={3}
|
px={3}
|
||||||
pb={3}
|
pb={3}
|
||||||
>
|
>
|
||||||
<Row collapse>
|
<Row>
|
||||||
<Button
|
<Button
|
||||||
width='32px'
|
width='32px'
|
||||||
borderColor='lightGray'
|
borderColor='lightGray'
|
||||||
@ -108,14 +108,15 @@ const StatusBar = (props) => {
|
|||||||
subscription={props.subscription}
|
subscription={props.subscription}
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row justifyContent='flex-end' collapse>
|
<Row justifyContent='flex-end'>
|
||||||
<StatusBarItem
|
<StatusBarItem
|
||||||
|
width='32px'
|
||||||
mr={2}
|
mr={2}
|
||||||
backgroundColor='yellow'
|
backgroundColor='yellow'
|
||||||
display={
|
display={
|
||||||
process.env.LANDSCAPE_STREAM === 'development' ? 'flex' : 'none'
|
process.env.LANDSCAPE_STREAM === 'development' ? 'flex' : 'none'
|
||||||
}
|
}
|
||||||
justifyContent='flex-end'
|
justifyContent='center'
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
window.open(
|
window.open(
|
||||||
@ -190,6 +191,7 @@ const StatusBar = (props) => {
|
|||||||
px={xPadding}
|
px={xPadding}
|
||||||
width='32px'
|
width='32px'
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
|
border={0}
|
||||||
backgroundColor={bgColor}
|
backgroundColor={bgColor}
|
||||||
>
|
>
|
||||||
{profileImage}
|
{profileImage}
|
||||||
|
@ -100,7 +100,9 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
|||||||
}
|
}
|
||||||
Mousetrap.bind('escape', props.toggle);
|
Mousetrap.bind('escape', props.toggle);
|
||||||
const touchstart = new Event('touchstart');
|
const touchstart = new Event('touchstart');
|
||||||
|
// @ts-ignore
|
||||||
inputRef?.current?.input?.dispatchEvent(touchstart);
|
inputRef?.current?.input?.dispatchEvent(touchstart);
|
||||||
|
// @ts-ignore
|
||||||
inputRef?.current?.input?.focus();
|
inputRef?.current?.input?.focus();
|
||||||
return () => {
|
return () => {
|
||||||
Mousetrap.unbind('escape');
|
Mousetrap.unbind('escape');
|
||||||
@ -173,6 +175,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
|||||||
const totalLength = flattenedResults.length;
|
const totalLength = flattenedResults.length;
|
||||||
if (selected.length) {
|
if (selected.length) {
|
||||||
const currentIndex = flattenedResults.indexOf(
|
const currentIndex = flattenedResults.indexOf(
|
||||||
|
// @ts-ignore unclear how to give this spread a return signature
|
||||||
...flattenedResults.filter((e) => {
|
...flattenedResults.filter((e) => {
|
||||||
return e.link === selected[1];
|
return e.link === selected[1];
|
||||||
})
|
})
|
||||||
@ -194,6 +197,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
|||||||
const flattenedResults = Array.from(results.values()).flat();
|
const flattenedResults = Array.from(results.values()).flat();
|
||||||
if (selected.length) {
|
if (selected.length) {
|
||||||
const currentIndex = flattenedResults.indexOf(
|
const currentIndex = flattenedResults.indexOf(
|
||||||
|
// @ts-ignore unclear how to give this spread a return signature
|
||||||
...flattenedResults.filter((e) => {
|
...flattenedResults.filter((e) => {
|
||||||
return e.link === selected[1];
|
return e.link === selected[1];
|
||||||
})
|
})
|
||||||
@ -325,6 +329,7 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
|||||||
{categoryResults.sort(sortResults).map((result, i2) => (
|
{categoryResults.sort(sortResults).map((result, i2) => (
|
||||||
<OmniboxResult
|
<OmniboxResult
|
||||||
key={i2}
|
key={i2}
|
||||||
|
// @ts-ignore withHovering doesn't pass props
|
||||||
icon={result.app}
|
icon={result.app}
|
||||||
text={result.title}
|
text={result.title}
|
||||||
subtext={result.host}
|
subtext={result.host}
|
||||||
@ -365,8 +370,10 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
|||||||
omniboxRef.current = el;
|
omniboxRef.current = el;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{ /* @ts-ignore investigate zustand types */ }
|
||||||
<OmniboxInput
|
<OmniboxInput
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
|
// @ts-ignore investigate refs
|
||||||
inputRef.current = el;
|
inputRef.current = el;
|
||||||
}}
|
}}
|
||||||
control={e => control(e)}
|
control={e => control(e)}
|
||||||
@ -380,5 +387,5 @@ export function Omnibox(props: OmniboxProps): ReactElement {
|
|||||||
</Portal>
|
</Portal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// @ts-ignore investigate zustand types
|
||||||
export default withLocalState(Omnibox, ['toggleOmnibox', 'omniboxShown']);
|
export default withLocalState(Omnibox, ['toggleOmnibox', 'omniboxShown']);
|
||||||
|
@ -4,6 +4,7 @@ import React, { Component, ReactElement } from 'react';
|
|||||||
import defaultApps from '~/logic/lib/default-apps';
|
import defaultApps from '~/logic/lib/default-apps';
|
||||||
import Sigil from '~/logic/lib/sigil';
|
import Sigil from '~/logic/lib/sigil';
|
||||||
import { cite, uxToHex } from '~/logic/lib/util';
|
import { cite, uxToHex } from '~/logic/lib/util';
|
||||||
|
import { IconRef } from '~/types/util';
|
||||||
import withState from '~/logic/lib/withState';
|
import withState from '~/logic/lib/withState';
|
||||||
import useContactState from '~/logic/state/contact';
|
import useContactState from '~/logic/state/contact';
|
||||||
import useHarkState from '~/logic/state/hark';
|
import useHarkState from '~/logic/state/hark';
|
||||||
@ -18,6 +19,7 @@ interface OmniboxResultProps {
|
|||||||
link: string;
|
link: string;
|
||||||
navigate: () => void;
|
navigate: () => void;
|
||||||
notificationsCount: number;
|
notificationsCount: number;
|
||||||
|
runtimeLag: any;
|
||||||
selected: string;
|
selected: string;
|
||||||
setSelection: () => void;
|
setSelection: () => void;
|
||||||
subtext: string;
|
subtext: string;
|
||||||
@ -50,6 +52,7 @@ export class OmniboxResult extends Component<OmniboxResultProps, OmniboxResultSt
|
|||||||
props.selected === props.link
|
props.selected === props.link
|
||||||
&& this.result.current
|
&& this.result.current
|
||||||
) {
|
) {
|
||||||
|
// @ts-ignore ref is forwarded as never, investigate later
|
||||||
this.result.current.scrollIntoView({ block: 'nearest' });
|
this.result.current.scrollIntoView({ block: 'nearest' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,7 +66,7 @@ export class OmniboxResult extends Component<OmniboxResultProps, OmniboxResultSt
|
|||||||
notificationsCount: number,
|
notificationsCount: number,
|
||||||
text: string,
|
text: string,
|
||||||
color: string
|
color: string
|
||||||
): (typeof Icon) {
|
): (any) {
|
||||||
const iconFill =
|
const iconFill =
|
||||||
(this.state.hovered || selected === link) ? 'white' : 'black';
|
(this.state.hovered || selected === link) ? 'white' : 'black';
|
||||||
const bulletFill =
|
const bulletFill =
|
||||||
@ -81,7 +84,7 @@ export class OmniboxResult extends Component<OmniboxResultProps, OmniboxResultSt
|
|||||||
icon.toLowerCase() === 'terminal'
|
icon.toLowerCase() === 'terminal'
|
||||||
) {
|
) {
|
||||||
if (icon === 'Link') {
|
if (icon === 'Link') {
|
||||||
link = 'Collection';
|
icon = 'Collection';
|
||||||
} else if (icon === 'Terminal') {
|
} else if (icon === 'Terminal') {
|
||||||
icon = 'Dojo';
|
icon = 'Dojo';
|
||||||
}
|
}
|
||||||
@ -89,7 +92,7 @@ export class OmniboxResult extends Component<OmniboxResultProps, OmniboxResultSt
|
|||||||
<Icon
|
<Icon
|
||||||
display='inline-block'
|
display='inline-block'
|
||||||
verticalAlign='middle'
|
verticalAlign='middle'
|
||||||
icon={icon}
|
icon={icon as IconRef}
|
||||||
mr={2}
|
mr={2}
|
||||||
size='18px'
|
size='18px'
|
||||||
color={iconFill}
|
color={iconFill}
|
||||||
@ -254,6 +257,7 @@ export class OmniboxResult extends Component<OmniboxResultProps, OmniboxResultSt
|
|||||||
onClick={navigate}
|
onClick={navigate}
|
||||||
width='100%'
|
width='100%'
|
||||||
justifyContent='space-between'
|
justifyContent='space-between'
|
||||||
|
// @ts-ignore indigo-react doesn't allow us to pass refs
|
||||||
ref={this.result}
|
ref={this.result}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
import deep_diff from 'deep-diff';
|
|
||||||
import React, { Component, useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
const withPropsChecker = (WrappedComponent: Component) => {
|
|
||||||
return (props: any) => {
|
|
||||||
const prevProps = useRef(props);
|
|
||||||
useEffect(() => {
|
|
||||||
const diff = deep_diff.diff(prevProps.current, props);
|
|
||||||
if (diff) {
|
|
||||||
console.log(diff);
|
|
||||||
}
|
|
||||||
prevProps.current = props;
|
|
||||||
});
|
|
||||||
return <WrappedComponent {...props} />;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withPropsChecker;
|
|
@ -1,6 +1,6 @@
|
|||||||
import { Box } from '@tlon/indigo-react';
|
import { Box } from '@tlon/indigo-react';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { Route, Switch } from 'react-router-dom';
|
import { Route, Switch, useHistory } from 'react-router-dom';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
|
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
|
||||||
import LaunchApp from '~/views/apps/launch/App';
|
import LaunchApp from '~/views/apps/launch/App';
|
||||||
@ -10,6 +10,8 @@ import Profile from '~/views/apps/profile/profile';
|
|||||||
import Settings from '~/views/apps/settings/settings';
|
import Settings from '~/views/apps/settings/settings';
|
||||||
import TermApp from '~/views/apps/term/app';
|
import TermApp from '~/views/apps/term/app';
|
||||||
import ErrorComponent from '~/views/components/Error';
|
import ErrorComponent from '~/views/components/Error';
|
||||||
|
import { useShortcut } from '~/logic/state/settings';
|
||||||
|
|
||||||
import Landscape from '~/views/landscape/index';
|
import Landscape from '~/views/landscape/index';
|
||||||
import GraphApp from '../../apps/graph/App';
|
import GraphApp from '../../apps/graph/App';
|
||||||
|
|
||||||
@ -21,6 +23,21 @@ export const Container = styled(Box)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const Content = (props) => {
|
export const Content = (props) => {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
useShortcut('navForward', useCallback((e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
history.goForward();
|
||||||
|
}, [history.goForward]));
|
||||||
|
|
||||||
|
useShortcut('navBack', useCallback((e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
history.goBack();
|
||||||
|
}, [history.goBack]));
|
||||||
|
|
||||||
|
|
||||||
const [hasProtocol, setHasProtocol] = useLocalStorageState(
|
const [hasProtocol, setHasProtocol] = useLocalStorageState(
|
||||||
'registeredProtocol', false
|
'registeredProtocol', false
|
||||||
);
|
);
|
||||||
|
@ -9,7 +9,7 @@ import { Content, ReferenceContent } from '@urbit/api';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import {
|
import {
|
||||||
BlockContent, Content as AstContent, Parent, Root
|
BlockContent, Content as AstContent, Parent, Root
|
||||||
} from 'mdast';
|
} from 'ts-mdast';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { referenceToPermalink } from '~/logic/lib/permalinks';
|
import { referenceToPermalink } from '~/logic/lib/permalinks';
|
||||||
@ -350,12 +350,18 @@ const renderers = {
|
|||||||
'graph-mention': ({ ship }) => <Mention api={{} as any} ship={ship} />,
|
'graph-mention': ({ ship }) => <Mention api={{} as any} ship={ship} />,
|
||||||
image: ({ url }) => (
|
image: ({ url }) => (
|
||||||
<Box mt="1" mb="2" flexShrink={0}>
|
<Box mt="1" mb="2" flexShrink={0}>
|
||||||
<RemoteContent key={url} url={url} />
|
<RemoteContent
|
||||||
|
// @ts-ignore RemoteContent weirdness
|
||||||
|
key={url} url={url}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
),
|
),
|
||||||
'graph-url': ({ url }) => (
|
'graph-url': ({ url }) => (
|
||||||
<Box mt={1} mb={2} flexShrink={0}>
|
<Box mt={1} mb={2} flexShrink={0}>
|
||||||
<RemoteContent key={url} url={url} />
|
<RemoteContent
|
||||||
|
// @ts-ignore RemoteContent weirdness
|
||||||
|
key={url} url={url}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
),
|
),
|
||||||
'graph-reference': ({ api, reference, transcluded }) => {
|
'graph-reference': ({ api, reference, transcluded }) => {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { Col, Row, Text } from '@tlon/indigo-react';
|
import { Col, Row, Text, Icon } from '@tlon/indigo-react';
|
||||||
import { Metadata } from '@urbit/api';
|
import { Metadata } from '@urbit/api';
|
||||||
import React, { ReactElement, ReactNode, useRef } from 'react';
|
import React, { ReactElement, ReactNode, useRef } from 'react';
|
||||||
import { TUTORIAL_GROUP, TUTORIAL_HOST } from '~/logic/lib/tutorialModal';
|
import { TUTORIAL_GROUP, TUTORIAL_HOST } from '~/logic/lib/tutorialModal';
|
||||||
import { PropFunc } from '~/types';
|
import { PropFunc, IconRef } from '~/types';
|
||||||
import { useTutorialModal } from '~/views/components/useTutorialModal';
|
import { useTutorialModal } from '~/views/components/useTutorialModal';
|
||||||
import { MetadataIcon } from './MetadataIcon';
|
import { MetadataIcon } from './MetadataIcon';
|
||||||
|
import { useCopy } from '~/logic/lib/useCopy';
|
||||||
interface GroupSummaryProps {
|
interface GroupSummaryProps {
|
||||||
metadata: Metadata;
|
metadata: Metadata;
|
||||||
memberCount: number;
|
memberCount: number;
|
||||||
@ -13,6 +13,8 @@ interface GroupSummaryProps {
|
|||||||
resource?: string;
|
resource?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
gray?: boolean;
|
gray?: boolean;
|
||||||
|
AllowCopy?: boolean;
|
||||||
|
locked?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupSummary(props: GroupSummaryProps & PropFunc<typeof Col>): ReactElement {
|
export function GroupSummary(props: GroupSummaryProps & PropFunc<typeof Col>): ReactElement {
|
||||||
@ -23,6 +25,7 @@ export function GroupSummary(props: GroupSummaryProps & PropFunc<typeof Col>): R
|
|||||||
resource === `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
|
resource === `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`,
|
||||||
anchorRef
|
anchorRef
|
||||||
);
|
);
|
||||||
|
const { doCopy, copyDisplay } = useCopy(`web+urbitgraph://group${resource?.slice(5)}`, "Copy", "Checkmark");
|
||||||
return (
|
return (
|
||||||
<Col {...rest} ref={anchorRef} gapY={4} maxWidth={['100%', '288px']}>
|
<Col {...rest} ref={anchorRef} gapY={4} maxWidth={['100%', '288px']}>
|
||||||
<Row gapX={2} width="100%">
|
<Row gapX={2} width="100%">
|
||||||
@ -33,13 +36,24 @@ export function GroupSummary(props: GroupSummaryProps & PropFunc<typeof Col>): R
|
|||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
/>
|
/>
|
||||||
<Col justifyContent="space-between" flexGrow={1} overflow="hidden">
|
<Col justifyContent="space-between" flexGrow={1} overflow="hidden">
|
||||||
|
<Row justifyContent="space-between">
|
||||||
<Text
|
<Text
|
||||||
fontSize={1}
|
fontSize={1}
|
||||||
textOverflow="ellipsis"
|
textOverflow="ellipsis"
|
||||||
whiteSpace="nowrap"
|
whiteSpace="nowrap"
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
>{metadata.title}</Text>
|
>{metadata.title}
|
||||||
<Row gapX={4} >
|
</Text>
|
||||||
|
{props?.AllowCopy &&
|
||||||
|
<Icon
|
||||||
|
color="gray"
|
||||||
|
icon={props?.locked ? "Locked" : copyDisplay as IconRef}
|
||||||
|
onClick={!props?.locked ? doCopy : null}
|
||||||
|
cursor={props?.locked ? "default" : "pointer"}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</Row>
|
||||||
|
<Row gapX={4} justifyContent="space-between">
|
||||||
<Text fontSize={1} gray>
|
<Text fontSize={1} gray>
|
||||||
{memberCount} participants
|
{memberCount} participants
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -23,9 +23,11 @@ export function EmptyGroupHome(props) {
|
|||||||
{ groupAssociation?.group ? (
|
{ groupAssociation?.group ? (
|
||||||
<GroupSummary
|
<GroupSummary
|
||||||
memberCount={groups[groupAssociation.group].members.size}
|
memberCount={groups[groupAssociation.group].members.size}
|
||||||
|
locked={Boolean('invite' in groups[groupAssociation.group].policy)}
|
||||||
channelCount={channelCount}
|
channelCount={channelCount}
|
||||||
metadata={groupAssociation.metadata}
|
metadata={groupAssociation.metadata}
|
||||||
resource={groupAssociation.group}
|
resource={groupAssociation.group}
|
||||||
|
AllowCopy
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Box p={4}>
|
<Box p={4}>
|
||||||
|
@ -8,6 +8,7 @@ import { AddFeedBanner } from './AddFeedBanner';
|
|||||||
import { EmptyGroupHome } from './EmptyGroupHome';
|
import { EmptyGroupHome } from './EmptyGroupHome';
|
||||||
import { EnableGroupFeed } from './EnableGroupFeed';
|
import { EnableGroupFeed } from './EnableGroupFeed';
|
||||||
import { GroupFeed } from './GroupFeed';
|
import { GroupFeed } from './GroupFeed';
|
||||||
|
import {getFeedPath} from '~/logic/lib/util';
|
||||||
|
|
||||||
function GroupHome(props) {
|
function GroupHome(props) {
|
||||||
const {
|
const {
|
||||||
@ -21,23 +22,15 @@ function GroupHome(props) {
|
|||||||
const associations = useMetadataState(state => state.associations);
|
const associations = useMetadataState(state => state.associations);
|
||||||
const groups = useGroupState(state => state.groups);
|
const groups = useGroupState(state => state.groups);
|
||||||
|
|
||||||
const metadata = associations?.groups[groupPath]?.metadata;
|
const association = associations?.groups[groupPath];
|
||||||
|
|
||||||
const askFeedBanner =
|
const feedPath = getFeedPath(association);
|
||||||
ship === `~${window.ship}` &&
|
|
||||||
metadata &&
|
|
||||||
metadata.config &&
|
|
||||||
'group' in metadata.config &&
|
|
||||||
metadata.config.group === null;
|
|
||||||
|
|
||||||
const isFeedEnabled =
|
const askFeedBanner = feedPath === undefined;
|
||||||
metadata &&
|
|
||||||
metadata.config &&
|
|
||||||
(metadata.config as GroupConfig).group &&
|
|
||||||
'resource' in (metadata.config as GroupConfig).group;
|
|
||||||
|
|
||||||
const graphPath = (metadata.config as GroupConfig)?.group?.resource;
|
const isFeedEnabled = !!feedPath;
|
||||||
const graphMetadata = associations?.graph[graphPath]?.metadata;
|
|
||||||
|
const graphMetadata = associations?.graph[feedPath]?.metadata;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box width="100%" height="100%" overflow="hidden">
|
<Box width="100%" height="100%" overflow="hidden">
|
||||||
@ -62,7 +55,7 @@ function GroupHome(props) {
|
|||||||
) : null }
|
) : null }
|
||||||
<Route path={`${baseUrl}/feed`}>
|
<Route path={`${baseUrl}/feed`}>
|
||||||
<GroupFeed
|
<GroupFeed
|
||||||
graphPath={graphPath}
|
graphPath={feedPath}
|
||||||
groupPath={groupPath}
|
groupPath={groupPath}
|
||||||
vip={graphMetadata?.vip || ''}
|
vip={graphMetadata?.vip || ''}
|
||||||
api={api}
|
api={api}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Box, Col } from '@tlon/indigo-react';
|
import { Box, Col } from '@tlon/indigo-react';
|
||||||
import { Association, Graph, GraphNode, Group } from '@urbit/api';
|
import { Association, Graph, GraphNode, Group } from '@urbit/api';
|
||||||
|
import { History } from 'history';
|
||||||
import bigInt from 'big-integer';
|
import bigInt from 'big-integer';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
@ -26,7 +27,7 @@ interface PostFeedProps {
|
|||||||
pendingSize: number;
|
pendingSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PostFeed extends React.Component<PostFeedProps, PostFeedState> {
|
class PostFeed extends React.Component<PostFeedProps, any> {
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -36,7 +37,7 @@ class PostFeed extends React.Component<PostFeedProps, PostFeedState> {
|
|||||||
this.fetchPosts = this.fetchPosts.bind(this);
|
this.fetchPosts = this.fetchPosts.bind(this);
|
||||||
this.doNotFetch = this.doNotFetch.bind(this);
|
this.doNotFetch = this.doNotFetch.bind(this);
|
||||||
}
|
}
|
||||||
|
// @ts-ignore needs @liam-fitzgerald peek at props for virtualscroller
|
||||||
renderItem = React.forwardRef(({ index, scrollWindow }, ref) => {
|
renderItem = React.forwardRef(({ index, scrollWindow }, ref) => {
|
||||||
const {
|
const {
|
||||||
graph,
|
graph,
|
||||||
@ -69,6 +70,7 @@ class PostFeed extends React.Component<PostFeedProps, PostFeedState> {
|
|||||||
<React.Fragment key={index.toString()}>
|
<React.Fragment key={index.toString()}>
|
||||||
<Col
|
<Col
|
||||||
key={index.toString()}
|
key={index.toString()}
|
||||||
|
// @ts-ignore indigo-react doesn't allow us to pass refs
|
||||||
ref={ref}
|
ref={ref}
|
||||||
mb={3}
|
mb={3}
|
||||||
width="100%"
|
width="100%"
|
||||||
@ -76,6 +78,7 @@ class PostFeed extends React.Component<PostFeedProps, PostFeedState> {
|
|||||||
>
|
>
|
||||||
<PostItem
|
<PostItem
|
||||||
key={parentNode.post.index}
|
key={parentNode.post.index}
|
||||||
|
// @ts-ignore withHovering prop pass is broken?
|
||||||
parentPost={grandparentNode?.post}
|
parentPost={grandparentNode?.post}
|
||||||
node={parentNode}
|
node={parentNode}
|
||||||
parentNode={grandparentNode}
|
parentNode={grandparentNode}
|
||||||
@ -92,6 +95,7 @@ class PostFeed extends React.Component<PostFeedProps, PostFeedState> {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<PostItem
|
<PostItem
|
||||||
|
// @ts-ignore withHovering prop pass is broken?
|
||||||
node={node}
|
node={node}
|
||||||
graphPath={graphPath}
|
graphPath={graphPath}
|
||||||
association={association}
|
association={association}
|
||||||
@ -110,8 +114,10 @@ class PostFeed extends React.Component<PostFeedProps, PostFeedState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
// @ts-ignore indigo-react doesn't allow us to pass refs
|
||||||
<Box key={index.toString()} ref={ref}>
|
<Box key={index.toString()} ref={ref}>
|
||||||
<PostItem
|
<PostItem
|
||||||
|
// @ts-ignore withHovering prop pass is broken?
|
||||||
node={node}
|
node={node}
|
||||||
graphPath={graphPath}
|
graphPath={graphPath}
|
||||||
association={association}
|
association={association}
|
||||||
@ -179,6 +185,7 @@ class PostFeed extends React.Component<PostFeedProps, PostFeedState> {
|
|||||||
data={graph}
|
data={graph}
|
||||||
averageHeight={106}
|
averageHeight={106}
|
||||||
size={graph.size}
|
size={graph.size}
|
||||||
|
totalSize={graph.size}
|
||||||
style={virtualScrollerStyle}
|
style={virtualScrollerStyle}
|
||||||
pendingSize={pendingSize}
|
pendingSize={pendingSize}
|
||||||
renderer={this.renderItem}
|
renderer={this.renderItem}
|
||||||
|
@ -120,7 +120,7 @@ const PostInput = (props: PostInputProps): ReactElement | null => {
|
|||||||
fontSize={1}
|
fontSize={1}
|
||||||
minHeight="62px"
|
minHeight="62px"
|
||||||
fontFamily={code ? 'mono' : 'sans'}
|
fontFamily={code ? 'mono' : 'sans'}
|
||||||
lineNumber={3}
|
rows={3}
|
||||||
style={{
|
style={{
|
||||||
resize: 'vertical'
|
resize: 'vertical'
|
||||||
}}
|
}}
|
||||||
|
@ -15,6 +15,7 @@ interface PostHeaderProps {
|
|||||||
association: Association;
|
association: Association;
|
||||||
isReply: boolean;
|
isReply: boolean;
|
||||||
showTimestamp: boolean;
|
showTimestamp: boolean;
|
||||||
|
graphPath: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PostHeader = (props: PostHeaderProps): ReactElement => {
|
const PostHeader = (props: PostHeaderProps): ReactElement => {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Box, Col, Text } from '@tlon/indigo-react';
|
import { Box, Col, Text } from '@tlon/indigo-react';
|
||||||
|
import { GraphNode } from '@urbit/api';
|
||||||
import bigInt from 'big-integer';
|
import bigInt from 'big-integer';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { resourceFromPath } from '~/logic/lib/group';
|
import { resourceFromPath } from '~/logic/lib/group';
|
||||||
@ -37,7 +38,7 @@ export default function PostReplies(props) {
|
|||||||
return bigInt(ind);
|
return bigInt(ind);
|
||||||
});
|
});
|
||||||
|
|
||||||
let node;
|
let node: GraphNode;
|
||||||
let parentNode;
|
let parentNode;
|
||||||
nodeIndex.forEach((i, idx) => {
|
nodeIndex.forEach((i, idx) => {
|
||||||
if (!graph) {
|
if (!graph) {
|
||||||
@ -69,6 +70,7 @@ export default function PostReplies(props) {
|
|||||||
<Box mt={3} width="100%" alignItems="center">
|
<Box mt={3} width="100%" alignItems="center">
|
||||||
<PostItem
|
<PostItem
|
||||||
key={node.post.index}
|
key={node.post.index}
|
||||||
|
// @ts-ignore withHovering prop pass is broken?
|
||||||
node={node}
|
node={node}
|
||||||
graphPath={graphPath}
|
graphPath={graphPath}
|
||||||
association={association}
|
association={association}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Box, Col, Text } from '@tlon/indigo-react';
|
import { Box, Col, Text } from '@tlon/indigo-react';
|
||||||
import { Association, Graph, Group } from '@urbit/api';
|
import { Association, Graph, Group } from '@urbit/api';
|
||||||
|
import { History } from 'history';
|
||||||
import React, { ReactElement } from 'react';
|
import React, { ReactElement } from 'react';
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { Loading } from '~/views/components/Loading';
|
import { Loading } from '~/views/components/Loading';
|
||||||
@ -15,6 +16,7 @@ interface PostTimelineProps {
|
|||||||
group: Group;
|
group: Group;
|
||||||
pendingSize: number;
|
pendingSize: number;
|
||||||
vip: string;
|
vip: string;
|
||||||
|
history?: History;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PostTimeline = (props: PostTimelineProps): ReactElement => {
|
const PostTimeline = (props: PostTimelineProps): ReactElement => {
|
||||||
|
@ -31,7 +31,7 @@ interface NewGroupProps {
|
|||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NewGroup(props: NewGroupProps & RouteComponentProps): ReactElement {
|
export function NewGroup(props: NewGroupProps): ReactElement {
|
||||||
const { api } = props;
|
const { api } = props;
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const initialValues: FormSchema = {
|
const initialValues: FormSchema = {
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import React, { ReactElement } from 'react';
|
import React, { ReactElement, useCallback } from 'react';
|
||||||
import { AppAssociations, Associations, Graph, UnreadStats } from '@urbit/api';
|
import { AppAssociations, Associations, Graph, UnreadStats } from '@urbit/api';
|
||||||
import { patp, patp2dec } from 'urbit-ob';
|
import { patp, patp2dec } from 'urbit-ob';
|
||||||
|
|
||||||
import { SidebarAssociationItem, SidebarDmItem } from './SidebarItem';
|
import { SidebarAssociationItem, SidebarDmItem } from './SidebarItem';
|
||||||
import useMetadataState from '~/logic/state/metadata';
|
import useGraphState, {useInbox} from '~/logic/state/graph';
|
||||||
import {useInbox} from '~/logic/state/graph';
|
|
||||||
import useHarkState from '~/logic/state/hark';
|
import useHarkState from '~/logic/state/hark';
|
||||||
import { alphabeticalOrder } from '~/logic/lib/util';
|
import { alphabeticalOrder, getResourcePath, modulo } from '~/logic/lib/util';
|
||||||
import { Workspace } from '~/types/workspace';
|
|
||||||
import { SidebarAppConfigs, SidebarListConfig, SidebarSort } from './types';
|
import { SidebarAppConfigs, SidebarListConfig, SidebarSort } from './types';
|
||||||
|
import { Workspace } from '~/types/workspace';
|
||||||
|
import useMetadataState from '~/logic/state/metadata';
|
||||||
|
import {useHistory} from 'react-router';
|
||||||
|
import { useShortcut } from '~/logic/state/settings';
|
||||||
|
|
||||||
function sidebarSort(
|
function sidebarSort(
|
||||||
associations: AppAssociations,
|
associations: AppAssociations,
|
||||||
@ -46,7 +48,6 @@ function sidebarSort(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getItems(associations: Associations, workspace: Workspace, inbox: Graph) {
|
function getItems(associations: Associations, workspace: Workspace, inbox: Graph) {
|
||||||
const filtered = Object.keys(associations.graph).filter((a) => {
|
const filtered = Object.keys(associations.graph).filter((a) => {
|
||||||
const assoc = associations.graph[a];
|
const assoc = associations.graph[a];
|
||||||
@ -95,11 +96,39 @@ export function SidebarList(props: {
|
|||||||
const associations = useMetadataState(state => state.associations);
|
const associations = useMetadataState(state => state.associations);
|
||||||
const inbox = useInbox();
|
const inbox = useInbox();
|
||||||
const unreads = useHarkState(s => s.unreads.graph?.[`/ship/~${window.ship}/dm-inbox`]);
|
const unreads = useHarkState(s => s.unreads.graph?.[`/ship/~${window.ship}/dm-inbox`]);
|
||||||
|
const graphKeys = useGraphState(s => s.graphKeys);
|
||||||
|
|
||||||
|
|
||||||
const ordered = getItems(associations, workspace, inbox)
|
const ordered = getItems(associations, workspace, inbox)
|
||||||
.sort(sidebarSort(associations.graph, props.apps, unreads)[config.sortBy]);
|
.sort(sidebarSort(associations.graph, props.apps, unreads)[config.sortBy]);
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const cycleChannels = useCallback((backward: boolean) => {
|
||||||
|
const idx = ordered.findIndex(s => s === selected);
|
||||||
|
const offset = backward ? -1 : 1
|
||||||
|
|
||||||
|
const newIdx = modulo(idx+offset, ordered.length - 1);
|
||||||
|
const { metadata, resource } = associations[ordered[newIdx]];
|
||||||
|
const joined = graphKeys.has(resource.slice(7));
|
||||||
|
let path = '/~landscape/home';
|
||||||
|
if ('graph' in metadata.config) {
|
||||||
|
path = getResourcePath(workspace, resource, joined, metadata.config.graph);
|
||||||
|
}
|
||||||
|
history.push(path)
|
||||||
|
}, [selected, history.push]);
|
||||||
|
|
||||||
|
useShortcut('cycleForward', useCallback((e: KeyboardEvent) => {
|
||||||
|
cycleChannels(false);
|
||||||
|
e.preventDefault();
|
||||||
|
}, [cycleChannels]));
|
||||||
|
|
||||||
|
useShortcut('cycleBack', useCallback((e: KeyboardEvent) => {
|
||||||
|
cycleChannels(true);
|
||||||
|
e.preventDefault();
|
||||||
|
}, [cycleChannels]))
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ordered.map((pathOrShip) => {
|
{ordered.map((pathOrShip) => {
|
||||||
|
@ -21,6 +21,7 @@ import { Dropdown } from '~/views/components/Dropdown';
|
|||||||
import { FormikOnBlur } from '~/views/components/FormikOnBlur';
|
import { FormikOnBlur } from '~/views/components/FormikOnBlur';
|
||||||
import { NewChannel } from '~/views/landscape/components/NewChannel';
|
import { NewChannel } from '~/views/landscape/components/NewChannel';
|
||||||
import { SidebarListConfig } from './types';
|
import { SidebarListConfig } from './types';
|
||||||
|
import {getFeedPath} from '~/logic/lib/util';
|
||||||
|
|
||||||
export function SidebarListHeader(props: {
|
export function SidebarListHeader(props: {
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
@ -52,10 +53,7 @@ export function SidebarListHeader(props: {
|
|||||||
|
|
||||||
const noun = (props.workspace?.type === 'messages') ? 'Messages' : 'Channels';
|
const noun = (props.workspace?.type === 'messages') ? 'Messages' : 'Channels';
|
||||||
|
|
||||||
let feedPath: string = null;
|
let feedPath = groupPath ? getFeedPath(associations.groups[groupPath]) : undefined;
|
||||||
if (metadata?.config && 'group' in metadata?.config && metadata.config?.group && 'resource' in metadata.config.group) {
|
|
||||||
feedPath = metadata.config.group.resource;
|
|
||||||
}
|
|
||||||
|
|
||||||
const unreadCount = useHarkState(
|
const unreadCount = useHarkState(
|
||||||
s => s.unreads?.graph?.[feedPath ?? '']?.['/']?.unreads as number ?? 0
|
s => s.unreads?.graph?.[feedPath ?? '']?.['/']?.unreads as number ?? 0
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
|
import React, { Children, ReactElement, ReactNode, useCallback, useMemo, useState } from 'react';
|
||||||
|
import { Sidebar } from './Sidebar/Sidebar';
|
||||||
import { AppName } from '@urbit/api';
|
import { AppName } from '@urbit/api';
|
||||||
import React, { ReactElement, ReactNode, useMemo } from 'react';
|
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import useGraphState from '~/logic/state/graph';
|
import useGraphState from '~/logic/state/graph';
|
||||||
import useHarkState from '~/logic/state/hark';
|
import useHarkState from '~/logic/state/hark';
|
||||||
import { Workspace } from '~/types/workspace';
|
import { Workspace } from '~/types/workspace';
|
||||||
import { Body } from '~/views/components/Body';
|
import { Body } from '~/views/components/Body';
|
||||||
import ErrorBoundary from '~/views/components/ErrorBoundary';
|
import ErrorBoundary from '~/views/components/ErrorBoundary';
|
||||||
|
import { useShortcut } from '~/logic/state/settings';
|
||||||
import { useGraphModule } from './Sidebar/Apps';
|
import { useGraphModule } from './Sidebar/Apps';
|
||||||
import { Sidebar } from './Sidebar/Sidebar';
|
|
||||||
|
|
||||||
interface SkeletonProps {
|
interface SkeletonProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -21,6 +22,10 @@ interface SkeletonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Skeleton(props: SkeletonProps): ReactElement {
|
export function Skeleton(props: SkeletonProps): ReactElement {
|
||||||
|
const [sidebar, setSidebar] = useState(true)
|
||||||
|
useShortcut('hideSidebar', useCallback(() => {
|
||||||
|
setSidebar(s => !s);
|
||||||
|
}, []));
|
||||||
const graphs = useGraphState(state => state.graphs);
|
const graphs = useGraphState(state => state.graphs);
|
||||||
const graphKeys = useGraphState(state => state.graphKeys);
|
const graphKeys = useGraphState(state => state.graphKeys);
|
||||||
const unreads = useHarkState(state => state.unreads);
|
const unreads = useHarkState(state => state.unreads);
|
||||||
@ -32,7 +37,7 @@ export function Skeleton(props: SkeletonProps): ReactElement {
|
|||||||
[graphConfig]
|
[graphConfig]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return !sidebar ? (<Body> {props.children} </Body>) : (
|
||||||
<Body
|
<Body
|
||||||
display="grid"
|
display="grid"
|
||||||
gridTemplateColumns={
|
gridTemplateColumns={
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (false && process.env.NODE_ENV === 'development') {
|
||||||
const whyDidYouRender = require('@welldone-software/why-did-you-render');
|
const whyDidYouRender = require('@welldone-software/why-did-you-render');
|
||||||
whyDidYouRender(React, {
|
whyDidYouRender(React, {
|
||||||
trackAllPureComponents: true
|
trackAllPureComponents: true
|
||||||
|
@ -20,7 +20,7 @@ export interface GraphNotifIndex {
|
|||||||
graph: string;
|
graph: string;
|
||||||
group: string;
|
group: string;
|
||||||
description: GraphNotifDescription;
|
description: GraphNotifDescription;
|
||||||
module: string;
|
mark: string;
|
||||||
index: string;
|
index: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ export default class BigIntOrderedMap<V> implements Iterable<[BigInteger, V]> {
|
|||||||
items.forEach(([key, value]) => {
|
items.forEach(([key, value]) => {
|
||||||
draft.root[key.toString()] = castDraft(value);
|
draft.root[key.toString()] = castDraft(value);
|
||||||
});
|
});
|
||||||
draft.generateCachedIter();
|
draft.cachedIter = null;
|
||||||
},
|
},
|
||||||
(patches) => {
|
(patches) => {
|
||||||
//console.log(`gassed with ${JSON.stringify(patches, null, 2)}`);
|
//console.log(`gassed with ${JSON.stringify(patches, null, 2)}`);
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { Resource } from "..";
|
|
||||||
import { AppName, Path, Patp } from "../lib";
|
import { AppName, Path, Patp } from "../lib";
|
||||||
|
|
||||||
export type MetadataUpdate =
|
export type MetadataUpdate =
|
||||||
|
Loading…
Reference in New Issue
Block a user