diff --git a/.github/workflows/typescript-check.yml b/.github/workflows/typescript-check.yml new file mode 100644 index 0000000000..6ad913bba4 --- /dev/null +++ b/.github/workflows/typescript-check.yml @@ -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 diff --git a/pkg/arvo/app/glob.hoon b/pkg/arvo/app/glob.hoon index d0acf23371..7205d65bd1 100644 --- a/pkg/arvo/app/glob.hoon +++ b/pkg/arvo/app/glob.hoon @@ -5,7 +5,7 @@ /- glob /+ 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))] +$ all-states $% state-0 diff --git a/pkg/arvo/app/landscape/index.html b/pkg/arvo/app/landscape/index.html index 2dcc4a987a..42eb68b171 100644 --- a/pkg/arvo/app/landscape/index.html +++ b/pkg/arvo/app/landscape/index.html @@ -24,6 +24,6 @@
- + diff --git a/pkg/interface/package-lock.json b/pkg/interface/package-lock.json index bcf7c4061f..fb2f3977e6 100644 --- a/pkg/interface/package-lock.json +++ b/pkg/interface/package-lock.json @@ -1551,6 +1551,15 @@ "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==", "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": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -6643,9 +6652,9 @@ "dev": true }, "immer": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz", - "integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==" + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.2.tgz", + "integrity": "sha512-mkcmzLtIfSp40vAqteRr1MbWNSoI7JE+/PB36FNPoSfJ9RQRmNKuTYCjKkyXyuq3Dgn07HuJBrwJd4ZSk2yUbw==" }, "import-fresh": { "version": "3.3.0", @@ -10804,6 +10813,16 @@ "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", "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": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.1.0.tgz", @@ -12185,9 +12204,9 @@ } }, "zustand": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.3.1.tgz", - "integrity": "sha512-o0rgrBsi29nCkPHdhtkAHisCIlmRUoXOV+1AmDMeCgkGG0i5edFSpGU0KiZYBvFmBYycnck4Z07JsLYDjSET9g==" + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-3.5.1.tgz", + "integrity": "sha512-7J56Ve814z4zap71iaKFD+t65LFI//jEq/Vf55BTSVqJZCm+w9rov8OMBg+YSwIPQk54bfoIWHTrOWuAbpEDMw==" } } } diff --git a/pkg/interface/package.json b/pkg/interface/package.json index 3faf280f74..5f7978e499 100644 --- a/pkg/interface/package.json +++ b/pkg/interface/package.json @@ -22,7 +22,7 @@ "css-loader": "^3.6.0", "file-saver": "^2.0.5", "formik": "^2.1.5", - "immer": "^8.0.1", + "immer": "^9.0.2", "lodash": "^4.17.21", "moment": "^2.29.1", "mousetrap": "^1.6.5", @@ -56,7 +56,7 @@ "workbox-recipes": "^6.0.2", "workbox-routing": "^6.0.2", "yup": "^0.29.3", - "zustand": "^3.3.1" + "zustand": "^3.5.0" }, "devDependencies": { "@babel/core": "^7.12.10", @@ -91,6 +91,7 @@ "react-hot-loader": "^4.13.0", "sass": "^1.32.5", "sass-loader": "^8.0.2", + "ts-mdast": "^1.0.0", "typescript": "^4.2.4", "webpack": "^4.46.0", "webpack-cli": "^3.3.12", diff --git a/pkg/interface/src/logic/api/contacts.ts b/pkg/interface/src/logic/api/contacts.ts index 4828bf8bea..c0be499023 100644 --- a/pkg/interface/src/logic/api/contacts.ts +++ b/pkg/interface/src/logic/api/contacts.ts @@ -1,6 +1,9 @@ import { Patp } from '@urbit/api'; import { ContactEditField } from '@urbit/api/contacts'; 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 BaseApi from './base'; @@ -26,13 +29,14 @@ export default class ContactsApi extends BaseApi { {add-group: {ship, name}} {remove-group: {ship, name}} */ - return this.storeAction({ + const action = { edit: { ship, 'edit-field': editField, timestamp: Date.now() } - }); + } + doOptimistically(useContactState, action, this.storeAction.bind(this), [edit]) } allowShips(ships: Patp[]) { diff --git a/pkg/interface/src/logic/api/hark.ts b/pkg/interface/src/logic/api/hark.ts index 43543343f4..887848f105 100644 --- a/pkg/interface/src/logic/api/hark.ts +++ b/pkg/interface/src/logic/api/hark.ts @@ -1,7 +1,10 @@ import { Association, GraphNotifDescription, IndexedNotification, NotifIndex } from '@urbit/api'; +import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap'; import { BigInteger } from 'big-integer'; import { getParentIndex } from '../lib/notification'; import { dateToDa, decToUd } from '../lib/util'; +import {reduce} from '../reducers/hark-update'; +import {doOptimistically, optReduceState} from '../state/base'; import useHarkState from '../state/hark'; import { StoreState } from '../store/type'; import BaseApi from './base'; @@ -51,8 +54,15 @@ export class HarkApi extends BaseApi { }); } - archive(time: BigInteger, index: NotifIndex) { - return this.actOnNotification('archive', time, index); + async archive(intTime: BigInteger, index: NotifIndex) { + const time = decToUd(intTime.toString()); + const action = { + archive: { + time, + index + } + }; + await doOptimistically(useHarkState, action, this.harkAction.bind(this), [reduce]) } read(time: BigInteger, index: NotifIndex) { @@ -81,15 +91,15 @@ export class HarkApi extends BaseApi { } markCountAsRead(association: Association, parent: string, description: GraphNotifDescription) { - return this.harkAction( - { 'read-count': { + const action = { 'read-count': { graph: { graph: association.resource, group: association.group, description, index: parent } } - }); + }; + doOptimistically(useHarkState, action, this.harkAction.bind(this), [reduce]); } markEachAsRead(association: Association, parent: string, child: string, description: GraphNotifDescription, mod: string) { diff --git a/pkg/interface/src/logic/lib/formGroup.ts b/pkg/interface/src/logic/lib/formGroup.ts index db679f2d3d..91a18532fe 100644 --- a/pkg/interface/src/logic/lib/formGroup.ts +++ b/pkg/interface/src/logic/lib/formGroup.ts @@ -6,13 +6,15 @@ interface IFormGroupContext { onDirty: (id: string, touched: boolean) => void; onErrors: (id: string, errors: boolean) => void; submitAll: () => Promise; + addReset: (id: string, r: any) => any; } const fallback: IFormGroupContext = { addSubmit: () => {}, onDirty: () => {}, onErrors: () => {}, - submitAll: () => Promise.resolve() + submitAll: () => Promise.resolve(), + addReset: () => {} }; export const FormGroupContext = React.createContext(fallback); diff --git a/pkg/interface/src/logic/lib/hark.ts b/pkg/interface/src/logic/lib/hark.ts index a5fe5883ea..a3f215668e 100644 --- a/pkg/interface/src/logic/lib/hark.ts +++ b/pkg/interface/src/logic/lib/hark.ts @@ -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 _ from 'lodash'; import f from 'lodash/fp'; +import { pluralize } from './util'; export function getLastSeen( unreads: Unreads, @@ -58,3 +59,56 @@ export function getNotificationKey(time: BigInteger, notification: IndexedNotifi 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) + + } +} diff --git a/pkg/interface/src/logic/lib/publish.ts b/pkg/interface/src/logic/lib/publish.ts index 22fd166cca..8fa32d5e09 100644 --- a/pkg/interface/src/logic/lib/publish.ts +++ b/pkg/interface/src/logic/lib/publish.ts @@ -76,7 +76,7 @@ export function editPost(rev: number, noteId: BigInteger, title: string, body: s 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 revs = node.children?.get(bigInt(1)); if(!revs) { diff --git a/pkg/interface/src/logic/lib/shortcutContext.tsx b/pkg/interface/src/logic/lib/shortcutContext.tsx new file mode 100644 index 0000000000..4f14a86e8f --- /dev/null +++ b/pkg/interface/src/logic/lib/shortcutContext.tsx @@ -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); + const handlerRef = useRef(() => {}); + + 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 ( + + {children} + + ); +} + +export function useShortcut(key: string, cb: Handler) { + const { add, remove } = useContext(ShortcutContext); + useEffect(() => { + add(cb, key); + return () => { + remove(cb, key); + }; + }, [key, cb]); +} diff --git a/pkg/interface/src/logic/lib/useCopy.ts b/pkg/interface/src/logic/lib/useCopy.ts index 6bccb0f536..c73d3163ea 100644 --- a/pkg/interface/src/logic/lib/useCopy.ts +++ b/pkg/interface/src/logic/lib/useCopy.ts @@ -1,7 +1,7 @@ import { useCallback, useMemo, useState } from 'react'; 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 doCopy = useCallback(() => { writeText(copied); @@ -11,7 +11,7 @@ export function useCopy(copied: string, display?: string) { }, 2000); }, [copied]); - const copyDisplay = useMemo(() => (didCopy ? 'Copied' : display), [ + const copyDisplay = useMemo(() => (didCopy ? (replaceText ?? 'Copied') : display), [ didCopy, display ]); diff --git a/pkg/interface/src/logic/lib/useLocalStorageState.ts b/pkg/interface/src/logic/lib/useLocalStorageState.ts index 5e35a6a761..d64b9f6d09 100644 --- a/pkg/interface/src/logic/lib/useLocalStorageState.ts +++ b/pkg/interface/src/logic/lib/useLocalStorageState.ts @@ -17,7 +17,7 @@ interface SetStateFunc { } // See microsoft/typescript#37663 for filed bug type SetState = T extends any ? SetStateFunc : never; -export function useLocalStorageState(key: string, initial: T) { +export function useLocalStorageState(key: string, initial: T): any { const [state, _setState] = useState(() => retrieve(key, initial)); useEffect(() => { diff --git a/pkg/interface/src/logic/lib/util.tsx b/pkg/interface/src/logic/lib/util.tsx index 439d3985fb..38ec6c8704 100644 --- a/pkg/interface/src/logic/lib/util.tsx +++ b/pkg/interface/src/logic/lib/util.tsx @@ -2,13 +2,16 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import _ from 'lodash'; import { patp2dec } from 'urbit-ob'; import f, { compose, memoize } from 'lodash/fp'; -import bigInt, { BigInteger } from 'big-integer'; import { Association, Contact, Patp } from '@urbit/api'; import produce, { enableMapSet } from 'immer'; import useSettingsState from '../state/settings'; /* eslint-disable max-lines */ 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(); @@ -47,6 +50,42 @@ export function parentPath(path: string) { 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_SECOND = bigInt('18446744073709551616'); // `@ud` ~s1 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)); } +/** + * Euclidean modulo + */ +export function modulo(x: number, mod: number) { + return x < 0 ? (x % mod + mod) % mod : x % mod; +} + // color is a #000000 color export function adjustHex(color: string, amount: number): string { return f.flow( @@ -249,14 +295,20 @@ export function cite(ship: string): string { 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) { - return a.toLowerCase().localeCompare(b.toLowerCase()); + return stripNonWord(a).toLowerCase().trim().localeCompare(stripNonWord(b).toLowerCase().trim()); } export function lengthOrder(a: string, b: string) { return b.length - a.length; } +export const keys = (o: T) => Object.keys(o) as (keyof T)[]; + // TODO: deprecated export function alphabetiseAssociations(associations: any) { const result = {}; @@ -431,6 +483,7 @@ export const useHovering = (): useHoveringInterface => { export function withHovering(Component: React.ComponentType) { return React.forwardRef((props, ref) => { const { hovering, bind } = useHovering(); + // @ts-ignore needs type signature on return? return }) } @@ -447,3 +500,22 @@ export function getItemTitle(association: Association): string { 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; +} diff --git a/pkg/interface/src/logic/reducers/contact-update.ts b/pkg/interface/src/logic/reducers/contact-update.ts index c998d584f2..87692cf2ef 100644 --- a/pkg/interface/src/logic/reducers/contact-update.ts +++ b/pkg/interface/src/logic/reducers/contact-update.ts @@ -1,4 +1,4 @@ -import { ContactUpdate } from '@urbit/api'; +import { ContactUpdate, deSig } from '@urbit/api'; import _ from 'lodash'; import { reduceState } from '../state/base'; import useContactState, { ContactState } from '../state/contact'; @@ -52,9 +52,9 @@ const remove = (json: ContactUpdate, state: ContactState): ContactState => { return state; }; -const edit = (json: ContactUpdate, state: ContactState): ContactState => { +export const edit = (json: ContactUpdate, state: ContactState): ContactState => { const data = _.get(json, 'edit', false); - const ship = `~${data.ship}`; + const ship = `~${deSig(data.ship)}`; if ( data && (ship in state.contacts) diff --git a/pkg/interface/src/logic/reducers/graph-update.ts b/pkg/interface/src/logic/reducers/graph-update.ts index 6c2e0c498c..639cf77bf6 100644 --- a/pkg/interface/src/logic/reducers/graph-update.ts +++ b/pkg/interface/src/logic/reducers/graph-update.ts @@ -102,6 +102,7 @@ const addGraph = (json, state: GraphState): GraphState => { const data = _.get(json, 'add-graph', false); if (data) { if (!('graphs' in state)) { + // @ts-ignore investigate zustand types state.graphs = {}; } @@ -122,6 +123,7 @@ const removeGraph = (json, state: GraphState): GraphState => { const data = _.get(json, 'remove-graph', false); if (data) { if (!('graphs' in state)) { + // @ts-ignore investigate zustand types state.graphs = {}; } const resource = data.ship + '/' + data.name; @@ -279,7 +281,7 @@ const removePosts = (json, state: GraphState): GraphState => { } else { const child = graph.get(index[0]); 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)); })); } diff --git a/pkg/interface/src/logic/reducers/hark-update.ts b/pkg/interface/src/logic/reducers/hark-update.ts index 6c09febcb9..b50721ba4c 100644 --- a/pkg/interface/src/logic/reducers/hark-update.ts +++ b/pkg/interface/src/logic/reducers/hark-update.ts @@ -8,8 +8,10 @@ import { BigInteger } from 'big-integer'; import _ from 'lodash'; import { compose } from 'lodash/fp'; import { makePatDa } from '~/logic/lib/util'; +import { describeNotification } from '../lib/hark'; import { reduceState } from '../state/base'; import useHarkState, { HarkState } from '../state/hark'; +import useMetadataState from '../state/metadata'; export const HarkReducer = (json: any) => { const data = _.get(json, 'harkUpdate', false); @@ -20,24 +22,32 @@ export const HarkReducer = (json: any) => { const graphHookData = _.get(json, 'hark-graph-hook-update', false); if (graphHookData) { reduceState(useHarkState, graphHookData, [ + // @ts-ignore investigate zustand types graphInitial, + // @ts-ignore investigate zustand types graphIgnore, + // @ts-ignore investigate zustand types graphListen, + // @ts-ignore investigate zustand types graphWatchSelf, + // @ts-ignore investigate zustand types graphMentions ]); } const groupHookData = _.get(json, 'hark-group-hook-update', false); if (groupHookData) { reduceState(useHarkState, groupHookData, [ + // @ts-ignore investigate zustand types groupInitial, + // @ts-ignore investigate zustand types groupListen, + // @ts-ignore investigate zustand types groupIgnore ]); } }; -function reduce(data, state) { +export function reduce(data, state) { const reducers = [ calculateCount, read, @@ -196,7 +206,7 @@ function readSince(json: any, state: HarkState): HarkState { function unreadSince(json: any, state: HarkState): HarkState { const data = _.get(json, 'unread-count'); - if(data) { + if (data) { updateUnreadCount(state, data.index, u => u + 1); } 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) { 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)); @@ -330,6 +340,20 @@ function added(json: any, state: HarkState): HarkState { const { index, notification } = data; const [fresh, stale] = _.partition(state.unreadNotes, ({ index: idx }) => !notifIdxEqual(index, idx)); 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; } @@ -368,7 +392,7 @@ function notifIdxEqual(a: NotifIndex, b: NotifIndex) { return ( a.graph.graph === b.graph.graph && a.graph.group === b.graph.group && - a.graph.module === b.graph.module && + a.graph.mark === b.graph.mark && a.graph.description === b.graph.description ); } else if ('group' in a && 'group' in b) { diff --git a/pkg/interface/src/logic/reducers/launch-update.ts b/pkg/interface/src/logic/reducers/launch-update.ts index 441a3b0a05..2046733434 100644 --- a/pkg/interface/src/logic/reducers/launch-update.ts +++ b/pkg/interface/src/logic/reducers/launch-update.ts @@ -20,6 +20,7 @@ export default class LaunchReducer { const weatherData: WeatherState | boolean | Record = _.get(json, 'weather', false); if (weatherData) { useLaunchState.getState().set((state) => { + // @ts-ignore investigate zustand types state.weather = weatherData; }); } @@ -27,6 +28,7 @@ export default class LaunchReducer { const locationData = _.get(json, 'location', false); if (locationData) { useLaunchState.getState().set((state) => { + // @ts-ignore investigate zustand types state.userLocation = locationData; }); } @@ -34,6 +36,7 @@ export default class LaunchReducer { const baseHash = _.get(json, 'baseHash', false); if (baseHash) { useLaunchState.getState().set((state) => { + // @ts-ignore investigate zustand types state.baseHash = baseHash; }); } @@ -41,6 +44,7 @@ export default class LaunchReducer { const runtimeLag = _.get(json, 'runtimeLag', null); if (runtimeLag !== null) { useLaunchState.getState().set(state => { + // @ts-ignore investigate zustand types state.runtimeLag = runtimeLag; }); } diff --git a/pkg/interface/src/logic/state/base.ts b/pkg/interface/src/logic/state/base.ts index c987df77a7..0a6552980c 100644 --- a/pkg/interface/src/logic/state/base.ts +++ b/pkg/interface/src/logic/state/base.ts @@ -1,24 +1,39 @@ -import produce, { setAutoFreeze } from 'immer'; +import produce, { applyPatches, Patch, produceWithPatches, setAutoFreeze, enablePatches } from 'immer'; import { compose } from 'lodash/fp'; -import create, { State, UseStore } from 'zustand'; +import _ from 'lodash'; +import create, { UseStore } from 'zustand'; import { persist } from 'zustand/middleware'; setAutoFreeze(false); +enablePatches(); -export const stateSetter = ( - fn: (state: StateType) => void, - set +export const stateSetter = ( + fn: (state: Readonly>) => void, + set: (newState: T & BaseState) => void ): void => { - set(produce(fn)); + set(produce(fn) as any); }; +export const optStateSetter = ( + fn: (state: T & BaseState) => void, + set: (newState: T & BaseState) => void, + get: () => T & BaseState +): string => { + const old = get(); + const id = _.uniqueId() + const [state, ,patches] = produceWithPatches(old, fn) as readonly [(T & BaseState), any, Patch[]]; + set({ ...state, patches: { ...state.patches, [id]: patches }}); + return id; +}; + + export const reduceState = < - StateType extends BaseState, - UpdateType + S extends {}, + U >( - state: UseStore, - data: UpdateType, - reducers: ((data: UpdateType, state: StateType) => StateType)[] + state: UseStore>, + data: U, + reducers: ((data: U, state: S & BaseState) => S & BaseState)[] ): void => { const reducer = compose(reducers.map(r => sta => r(data, sta))); state.getState().set((state) => { @@ -26,6 +41,18 @@ export const reduceState = < }); }; +export const optReduceState = ( + state: UseStore>, + data: U, + reducers: ((data: U, state: S & BaseState) => BaseState & S)[] +): string => { + const reducer = compose(reducers.map(r => sta => r(data, sta))); + return state.getState().optSet((state) => { + reducer(state); + }); +}; + + export let stateStorageKeys: string[] = []; export const stateStorageKey = (stateName: string) => { @@ -40,19 +67,59 @@ export const stateStorageKey = (stateName: string) => { }); }; -export interface BaseState extends State { - set: (fn: (state: StateType) => void) => void; +export interface BaseState { + rollback: (id: string) => void; + patches: { + [id: string]: Patch[]; + }; + set: (fn: (state: BaseState) => void) => void; + addPatch: (id: string, ...patch: Patch[]) => void; + removePatch: (id: string) => void; + optSet: (fn: (state: BaseState) => void) => string; } -export const createState = >( +export const createState = ( name: string, - properties: { [K in keyof Omit]: T[K] }, - blacklist: string[] = [] -): UseStore => create(persist((set, get) => ({ + properties: T, + blacklist: (keyof BaseState | keyof T)[] = [] +): UseStore> => create>(persist>((set, get) => ({ + // @ts-ignore investigate zustand types 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, name: stateStorageKey(name), version: process.env.LANDSCAPE_SHORTHASH as any })); + +export async function doOptimistically(state: UseStore>, action: A, call: (a: A) => Promise, reduce: ((a: A, fn: S & BaseState) => S & BaseState)[]) { + 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); + } + } +} diff --git a/pkg/interface/src/logic/state/contact.ts b/pkg/interface/src/logic/state/contact.ts index c28ccb996d..1f48833cfc 100644 --- a/pkg/interface/src/logic/state/contact.ts +++ b/pkg/interface/src/logic/state/contact.ts @@ -9,6 +9,7 @@ export interface ContactState extends BaseState { // fetchIsAllowed: (entity, name, ship, personal) => Promise; } +// @ts-ignore investigate zustand types const useContactState = createState('Contact', { contacts: {}, nackedContacts: new Set(), diff --git a/pkg/interface/src/logic/state/graph.ts b/pkg/interface/src/logic/state/graph.ts index f9553355f6..fb371b8935 100644 --- a/pkg/interface/src/logic/state/graph.ts +++ b/pkg/interface/src/logic/state/graph.ts @@ -27,7 +27,7 @@ export interface GraphState extends BaseState { // getGraphSubset: (ship: string, resource: string, start: string, end: string) => Promise; // getNode: (ship: string, resource: string, index: string) => Promise; } - +// @ts-ignore investigate zustand types const useGraphState = createState('Graph', { graphs: {}, graphKeys: new Set(), diff --git a/pkg/interface/src/logic/state/group.ts b/pkg/interface/src/logic/state/group.ts index bd6c478eb2..5f7324124c 100644 --- a/pkg/interface/src/logic/state/group.ts +++ b/pkg/interface/src/logic/state/group.ts @@ -9,6 +9,7 @@ export interface GroupState extends BaseState { pendingJoin: JoinRequests; } +// @ts-ignore investigate zustand types const useGroupState = createState('Group', { groups: {}, pendingJoin: {} diff --git a/pkg/interface/src/logic/state/hark.ts b/pkg/interface/src/logic/state/hark.ts index 34c68e2332..8649361613 100644 --- a/pkg/interface/src/logic/state/hark.ts +++ b/pkg/interface/src/logic/state/hark.ts @@ -4,11 +4,11 @@ import BigIntOrderedMap from "@urbit/api/lib/BigIntOrderedMap"; import {useCallback} from "react"; // 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 interface HarkState extends BaseState { +export interface HarkState { archivedNotifications: BigIntOrderedMap; doNotDisturb: boolean; // getMore: () => Promise; diff --git a/pkg/interface/src/logic/state/invite.ts b/pkg/interface/src/logic/state/invite.ts index 0aab291b32..54bd3906b2 100644 --- a/pkg/interface/src/logic/state/invite.ts +++ b/pkg/interface/src/logic/state/invite.ts @@ -5,6 +5,7 @@ export interface InviteState extends BaseState { invites: Invites; } +// @ts-ignore investigate zustand types const useInviteState = createState('Invite', { invites: {} }); diff --git a/pkg/interface/src/logic/state/launch.ts b/pkg/interface/src/logic/state/launch.ts index adeb8a721a..4cefc57e2e 100644 --- a/pkg/interface/src/logic/state/launch.ts +++ b/pkg/interface/src/logic/state/launch.ts @@ -13,6 +13,7 @@ export interface LaunchState extends BaseState { runtimeLag: boolean; }; +// @ts-ignore investigate zustand types const useLaunchState = createState('Launch', { firstTime: true, tileOrdering: [], diff --git a/pkg/interface/src/logic/state/local.tsx b/pkg/interface/src/logic/state/local.tsx index 5d6b883719..fe9becd8c7 100644 --- a/pkg/interface/src/logic/state/local.tsx +++ b/pkg/interface/src/logic/state/local.tsx @@ -82,6 +82,7 @@ const useLocalState = create(persist((set, get) => ({ state.suspendedFocus.blur(); } })), + // @ts-ignore investigate zustand types set: fn => set(produce(fn)) }), { blacklist: [ @@ -98,6 +99,7 @@ function withLocalState ({ ...object, [key]: state[key] }), {} ) ): useLocalState(); + // @ts-ignore call signature forwarding unclear return ; }); } diff --git a/pkg/interface/src/logic/state/metadata.ts b/pkg/interface/src/logic/state/metadata.ts index 38a4a627ee..4983d6d580 100644 --- a/pkg/interface/src/logic/state/metadata.ts +++ b/pkg/interface/src/logic/state/metadata.ts @@ -22,7 +22,7 @@ export function useGraphsForGroup(group: string) { const graphs = useMetadataState(s => s.associations.graph); return _.pickBy(graphs, (a: Association) => a.group === group); } - +// @ts-ignore investigate zustand types const useMetadataState = createState('Metadata', { associations: { groups: {}, graph: {}, contacts: {}, chat: {}, link: {}, publish: {} } // preview: async (group): Promise => { diff --git a/pkg/interface/src/logic/state/settings.ts b/pkg/interface/src/logic/state/settings.ts index ae802ed15d..a1026e3919 100644 --- a/pkg/interface/src/logic/state/settings.ts +++ b/pkg/interface/src/logic/state/settings.ts @@ -1,6 +1,17 @@ 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 { 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 { display: { @@ -16,6 +27,7 @@ export interface SettingsState extends BaseState { hideGroups: boolean; hideUtilities: boolean; }; + keyboard: ShortcutMapping; remoteContentPolicy: RemoteContentPolicy; leap: { categories: LeapCategories[]; @@ -33,6 +45,7 @@ export const selectCalmState = (s: SettingsState) => s.calm; export const selectDisplayState = (s: SettingsState) => s.display; +// @ts-ignore investigate zustand types const useSettingsState = createState('Settings', { display: { backgroundType: 'none', @@ -59,7 +72,19 @@ const useSettingsState = createState('Settings', { tutorial: { seen: true, joined: undefined + }, + keyboard: { + cycleForward: 'ctrl+\'', + cycleBack: 'ctrl+;', + navForward: 'ctrl+[', + navBack: 'ctrl+[', + hideSidebar: 'ctrl+\\' } }); +export function useShortcut(name: T, cb: (e: KeyboardEvent) => void) { + const key = useSettingsState(useCallback(s => s.keyboard[name], [name])); + return usePlainShortcut(key, cb); +} + export default useSettingsState; diff --git a/pkg/interface/src/logic/state/storage.ts b/pkg/interface/src/logic/state/storage.ts index d803ade400..7ad79ceeea 100644 --- a/pkg/interface/src/logic/state/storage.ts +++ b/pkg/interface/src/logic/state/storage.ts @@ -19,6 +19,7 @@ export interface StorageState extends BaseState { } } +// @ts-ignore investigate zustand types const useStorageState = createState('Storage', { gcp: {}, s3: { diff --git a/pkg/interface/src/views/App.js b/pkg/interface/src/views/App.js index 2bbdf6def3..7c0ab1cc82 100644 --- a/pkg/interface/src/views/App.js +++ b/pkg/interface/src/views/App.js @@ -1,6 +1,5 @@ import dark from '@tlon/indigo-dark'; import light from '@tlon/indigo-light'; -import { sigil as sigiljs, stringRenderer } from '@tlon/sigil-js'; import Mousetrap from 'mousetrap'; import 'mousetrap-global-bind'; 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 GlobalApi from '~/logic/api/global'; import gcpManager from '~/logic/lib/gcpManager'; -import { foregroundFromBackground } from '~/logic/lib/sigil'; -import { uxToHex } from '~/logic/lib/util'; +import { favicon, svgDataURL } from '~/logic/lib/util'; import withState from '~/logic/lib/withState'; import useContactState from '~/logic/state/contact'; import useGroupState from '~/logic/state/group'; import useLocalState from '~/logic/state/local'; import useSettingsState from '~/logic/state/settings'; +import { ShortcutContextProvider } from '~/logic/lib/shortcutContext'; + import GlobalStore from '~/logic/store/store'; import GlobalSubscription from '~/logic/subscription/global'; import ErrorBoundary from '~/views/components/ErrorBoundary'; @@ -86,7 +86,6 @@ class App extends React.Component { this.updateTheme = this.updateTheme.bind(this); this.updateMobile = this.updateMobile.bind(this); - this.faviconString = this.faviconString.bind(this); } 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() { const { props } = this; return ((props.dark && props?.display?.theme == 'auto') || @@ -161,9 +144,10 @@ class App extends React.Component { const ourContact = this.props.contacts[`~${this.ship}`] || null; return ( + {window.ship.length < 14 - ? + ? : null} @@ -198,6 +182,7 @@ class App extends React.Component {
+ ); } diff --git a/pkg/interface/src/views/apps/chat/components/ChatEditor.tsx b/pkg/interface/src/views/apps/chat/components/ChatEditor.tsx index 2f7a0b99d6..cf9cf0340a 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatEditor.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatEditor.tsx @@ -128,10 +128,27 @@ export default class ChatEditor extends Component this.messageChange(e, d, v)} editorDidMount={(editor) => { this.editor = editor; - editor.focus(); }} {...props} /> diff --git a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx index 5d0524ed43..62422a0167 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx @@ -226,7 +226,9 @@ export class ChatInput extends Component { } } +// @ts-ignore withLocalState prop passing weirdness export default withLocalState, 'hideAvatars', ChatInput>( + // @ts-ignore withLocalState prop passing weirdness withStorage(ChatInput, { accept: 'image/*' }), ['hideAvatars'] ); diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index afebc453f1..7da4e90fd4 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx @@ -251,8 +251,7 @@ function ChatMessage(props: ChatMessageProps) { let onDelete = props?.onDelete ?? (() => {}); const transcluded = props?.transcluded ?? 0; const renderSigil = props.renderSigil ?? (Boolean(nextMsg && msg.author !== nextMsg.author) || - !nextMsg || - msg.number === 1 + !nextMsg ); const ourMention = msg?.contents?.some((e: MentionContent) => { diff --git a/pkg/interface/src/views/apps/chat/components/ChatPane.tsx b/pkg/interface/src/views/apps/chat/components/ChatPane.tsx index dd6ebf46fb..056bc1c408 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatPane.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatPane.tsx @@ -34,7 +34,7 @@ interface ChatPaneProps { * Get contents of reply message */ onReply: (msg: Post) => string; - onDelete: (msg: Post) => void; + onDelete?: (msg: Post) => void; /** * Fetch more messages * @@ -136,6 +136,7 @@ export function ChatPane(props: ChatPaneProps): ReactElement { } return ( + // @ts-ignore [0]) { const path = group?.group; const unreadCount = graphUnreads(path); const notCount = graphNotifications(path); - return ( ); })} @@ -96,7 +95,7 @@ function Group(props: GroupProps) { .diff(moment())) .as('days'))) || 0; return ( - + {title} {!hideUnreads && ( diff --git a/pkg/interface/src/views/apps/launch/components/tiles.tsx b/pkg/interface/src/views/apps/launch/components/tiles.tsx index 377e0d2674..c788b03aab 100644 --- a/pkg/interface/src/views/apps/launch/components/tiles.tsx +++ b/pkg/interface/src/views/apps/launch/components/tiles.tsx @@ -35,6 +35,7 @@ const Tiles = (props: TileProps): ReactElement => { return ( ); diff --git a/pkg/interface/src/views/apps/launch/components/tiles/weather.tsx b/pkg/interface/src/views/apps/launch/components/tiles/weather.tsx index 9fa4f59329..ce049c1502 100644 --- a/pkg/interface/src/views/apps/launch/components/tiles/weather.tsx +++ b/pkg/interface/src/views/apps/launch/components/tiles/weather.tsx @@ -135,7 +135,7 @@ class WeatherTile extends React.Component { {locationName ? ` Current location is near ${locationName}.` : ''} {error} - + state.groups); @@ -62,13 +63,14 @@ export function LinkResource(props: LinkResourceProps) { path={relativePath('')} render={(props) => { return ( + // @ts-ignore diff --git a/pkg/interface/src/views/apps/links/LinkWindow.tsx b/pkg/interface/src/views/apps/links/LinkWindow.tsx index 05a13e5c75..7bd564462e 100644 --- a/pkg/interface/src/views/apps/links/LinkWindow.tsx +++ b/pkg/interface/src/views/apps/links/LinkWindow.tsx @@ -21,6 +21,7 @@ interface LinkWindowProps { path: string; api: GlobalApi; pendingSize: number; + mb?: number; } const style = { @@ -48,6 +49,7 @@ class LinkWindow extends Component { const { props } = this; const { association, graph, api } = props; const [, , ship, name] = association.resource.split('/'); + // @ts-ignore Uint8Array vs. BigInt mismatch? const node = graph.get(index); const first = graph.peekLargest()?.[0]; const post = node?.post; @@ -58,6 +60,7 @@ class LinkWindow extends Component { ...props, node }; + {/* @ts-ignore calling @liam-fitzgerald on Uint8Array props */} if (this.canWrite() && index.eq(first ?? bigInt.zero)) { return ( @@ -125,6 +128,7 @@ class LinkWindow extends Component { return ( + {/* @ts-ignore calling @liam-fitzgerald on virtualscroller */} ): ReactElement => { const { @@ -49,6 +51,7 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject { console.log(remoteRef.current); if(document.activeElement instanceof HTMLIFrameElement + // @ts-ignore forwardref prop passing && remoteRef?.current?.containerRef?.contains(document.activeElement)) { markRead(); } @@ -100,6 +103,7 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject state.unreads); 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); return ( @@ -135,8 +139,10 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject { + // @ts-ignore RemoteContent weirdness remoteRef.current = r; }} + // @ts-ignore RemoteContent weirdness renderUrl={false} url={href} text={contents[0].text} diff --git a/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx b/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx index b5c7b415bc..43e684817f 100644 --- a/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx +++ b/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx @@ -12,6 +12,7 @@ interface LinkSubmitProps { api: GlobalApi; name: string; ship: string; + parentIndex?: any; } const LinkSubmit = (props: LinkSubmitProps) => { @@ -157,6 +158,7 @@ const LinkSubmit = (props: LinkSubmitProps) => { return ( <> + {/* @ts-ignore archaic event type mismatch */} { onBlur={() => [setUrlFocused(false), setSubmitFocused(false)]} onFocus={() => [setUrlFocused(true), setSubmitFocused(true)]} spellCheck="false" + // @ts-ignore archaic event type mismatch error onPaste={onPaste} onKeyPress={onKeyPress} value={linkValue} diff --git a/pkg/interface/src/views/apps/notifications/graph.tsx b/pkg/interface/src/views/apps/notifications/graph.tsx index 2316ceed4c..f0c4075dcc 100644 --- a/pkg/interface/src/views/apps/notifications/graph.tsx +++ b/pkg/interface/src/views/apps/notifications/graph.tsx @@ -292,7 +292,7 @@ export function GraphNotification(props: { const first = contents[0]; history.push( getNodeUrl( - index.module, + index.mark, groups[association?.group]?.hidden, association?.group, association?.resource, @@ -328,7 +328,6 @@ export function GraphNotification(props: { hideAuthors={hideAuthors} posts={contents.slice(0, 4)} mod={index.mark} - description={index.description} index={contents?.[0].index} association={association} hidden={groups[association?.group]?.hidden} diff --git a/pkg/interface/src/views/apps/permalinks/TranscludedNode.tsx b/pkg/interface/src/views/apps/permalinks/TranscludedNode.tsx index 010675ebbc..ddfcaae8f1 100644 --- a/pkg/interface/src/views/apps/permalinks/TranscludedNode.tsx +++ b/pkg/interface/src/views/apps/permalinks/TranscludedNode.tsx @@ -49,7 +49,7 @@ function TranscludedLinkNode(props: { @@ -197,6 +189,7 @@ export function PermalinkEmbed(props: { transcluded: number; showOurContact?: boolean; full?: boolean; + pending?: any; }) { const permalink = parsePermalink(props.link); diff --git a/pkg/interface/src/views/apps/profile/components/EditProfile.tsx b/pkg/interface/src/views/apps/profile/components/EditProfile.tsx index 7705c649c1..4b591a64c6 100644 --- a/pkg/interface/src/views/apps/profile/components/EditProfile.tsx +++ b/pkg/interface/src/views/apps/profile/components/EditProfile.tsx @@ -90,11 +90,12 @@ export function EditProfile(props: any): ReactElement { const onSubmit = async (values: any, actions: any) => { try { - await Object.keys(values).reduce((acc, key) => { + Object.keys(values).forEach((key) => { const newValue = key !== 'color' ? values[key] : uxToHex(values[key]); if (newValue !== contact[key]) { if (key === 'isPublic') { - return acc.then(() => api.contacts.setPublic(newValue)); + api.contacts.setPublic(newValue) + return; } else if (key === 'groups') { const toRemove: string[] = _.difference( contact?.groups || [], @@ -104,24 +105,18 @@ export function EditProfile(props: any): ReactElement { newValue, contact?.groups || [] ); - const promises: Promise[] = []; - promises.concat( - toRemove.map(e => + toRemove.forEach(e => api.contacts.edit(ship, { 'remove-group': resourceFromPath(e) }) - ) - ); - promises.concat( - toAdd.map(e => + ) + toAdd.forEach(e => api.contacts.edit(ship, { 'add-group': resourceFromPath(e) }) - ) - ); - return acc.then(() => Promise.all(promises)); + ) } 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 }); history.push(`/~profile/${ship}`); } catch (e) { diff --git a/pkg/interface/src/views/apps/profile/components/SetStatus.tsx b/pkg/interface/src/views/apps/profile/components/SetStatus.tsx index 11541c077c..af66a6395b 100644 --- a/pkg/interface/src/views/apps/profile/components/SetStatus.tsx +++ b/pkg/interface/src/views/apps/profile/components/SetStatus.tsx @@ -39,7 +39,7 @@ export function SetStatus(props: any) { ref={inputRef} onChange={onStatusChange} value={_status} - autocomplete='off' + autoComplete='off' width='75%' mr={2} onKeyPress={(evt) => { diff --git a/pkg/interface/src/views/apps/publish/PublishResource.tsx b/pkg/interface/src/views/apps/publish/PublishResource.tsx index ea5dc102f4..2b455aa2b8 100644 --- a/pkg/interface/src/views/apps/publish/PublishResource.tsx +++ b/pkg/interface/src/views/apps/publish/PublishResource.tsx @@ -10,10 +10,13 @@ type PublishResourceProps = StoreState & { association: Association; api: GlobalApi; baseUrl: string; + history?: any; + match?: any; + location?: any; }; export function PublishResource(props: PublishResourceProps) { - const { association, api, baseUrl, notebooks } = props; + const { association, api, baseUrl } = props; const rid = association.resource; const [, , ship, book] = rid.split('/'); const location = useLocation(); diff --git a/pkg/interface/src/views/apps/publish/components/NotePreview.tsx b/pkg/interface/src/views/apps/publish/components/NotePreview.tsx index ca8c201681..3f9ecc0954 100644 --- a/pkg/interface/src/views/apps/publish/components/NotePreview.tsx +++ b/pkg/interface/src/views/apps/publish/components/NotePreview.tsx @@ -69,6 +69,7 @@ export function NotePreview(props: NotePreviewProps) { const [rev, title, body, content] = getLatestRevision(node); const appPath = `/ship/${props.host}/${props.book}`; 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 snippet = getSnippet(body); diff --git a/pkg/interface/src/views/apps/publish/components/Notebook.tsx b/pkg/interface/src/views/apps/publish/components/Notebook.tsx index 00b4401b66..ac18958d65 100644 --- a/pkg/interface/src/views/apps/publish/components/Notebook.tsx +++ b/pkg/interface/src/views/apps/publish/components/Notebook.tsx @@ -1,5 +1,5 @@ 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 { RouteComponentProps } from 'react-router-dom'; import { useShowNickname } from '~/logic/lib/util'; @@ -14,7 +14,6 @@ interface NotebookProps { association: Association; baseUrl: string; rootUrl: string; - unreads: Unreads; } export function Notebook(props: NotebookProps & RouteComponentProps): ReactElement | null { diff --git a/pkg/interface/src/views/apps/publish/components/NotebookPosts.tsx b/pkg/interface/src/views/apps/publish/components/NotebookPosts.tsx index 352966c5e6..36d196be0c 100644 --- a/pkg/interface/src/views/apps/publish/components/NotebookPosts.tsx +++ b/pkg/interface/src/views/apps/publish/components/NotebookPosts.tsx @@ -15,7 +15,6 @@ interface NotebookPostsProps { } export function NotebookPosts(props: NotebookPostsProps) { - const contacts = useContactState(state => state.contacts); return ( {Array.from(props.graph || []).map( @@ -25,7 +24,6 @@ export function NotebookPosts(props: NotebookPostsProps) { key={date.toString()} host={props.host} book={props.book} - contact={contacts[`~${node.post.author}`]} node={node} baseUrl={props.baseUrl} group={props.group} diff --git a/pkg/interface/src/views/apps/settings/components/lib/BackgroundPicker.tsx b/pkg/interface/src/views/apps/settings/components/lib/BackgroundPicker.tsx index ebb4163c08..119a112b4f 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/BackgroundPicker.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/BackgroundPicker.tsx @@ -3,6 +3,7 @@ import { ManagedRadioButtonField as Radio, Row, Text } from '@tlon/indigo-react'; +import {useField} from 'formik'; import React, { ReactElement } from 'react'; import GlobalApi from '~/logic/api/global'; import { ColorInput } from '~/views/components/ColorInput'; @@ -10,11 +11,7 @@ import { ImageInput } from '~/views/components/ImageInput'; export type BgType = 'none' | 'url' | 'color'; -export function BackgroundPicker({ - bgType, - bgUrl, - api -}: { +export function BackgroundPicker({ api }: { bgType: BgType; bgUrl?: string; api: GlobalApi; @@ -40,7 +37,6 @@ export function BackgroundPicker({ id="bgUrl" placeholder="Drop or upload a file, or paste a link here" name="bgUrl" - url={bgUrl || ''} /> diff --git a/pkg/interface/src/views/apps/settings/components/lib/CalmPref.tsx b/pkg/interface/src/views/apps/settings/components/lib/CalmPref.tsx index b676499160..d1d8bb9cbc 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/CalmPref.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/CalmPref.tsx @@ -6,9 +6,11 @@ import { import { Form, Formik, FormikHelpers } from 'formik'; import React, { useCallback } from 'react'; 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 { BackButton } from './BackButton'; +import _ from 'lodash'; +import {FormikOnBlur} from '~/views/components/FormikOnBlur'; interface FormSchema { hideAvatars: boolean; @@ -22,57 +24,39 @@ interface FormSchema { 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: { api: GlobalApi; }) { const { api } = props; - const { - 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 initialValues = useSettingsState(settingsSel); const onSubmit = useCallback(async (v: FormSchema, actions: FormikHelpers) => { - await Promise.all([ - api.settings.putEntry('calm', 'hideAvatars', v.hideAvatars), - api.settings.putEntry('calm', 'hideNicknames', v.hideNicknames), - api.settings.putEntry('calm', 'hideUnreads', v.hideUnreads), - api.settings.putEntry('calm', 'hideGroups', v.hideGroups), - api.settings.putEntry('calm', 'hideUtilities', v.hideUtilities), - api.settings.putEntry('remoteContentPolicy', 'imageShown', !v.imageShown), - api.settings.putEntry('remoteContentPolicy', 'videoShown', !v.videoShown), - api.settings.putEntry('remoteContentPolicy', 'audioShown', !v.audioShown), - api.settings.putEntry('remoteContentPolicy', 'oembedShown', !v.oembedShown) - ]); + let promises: Promise[] = []; + _.forEach(v, (bool, key) => { + const bucket = ['imageShown', 'videoShown', 'audioShown', 'oembedShown'].includes(key) ? 'remoteContentPolicy' : 'calm'; + if(initialValues[key] !== bool) { + promises.push(api.settings.putEntry(bucket, key, bool)); + } + }) + await Promise.all(promises); actions.setStatus({ success: null }); }, [api]); return ( - +
@@ -132,12 +116,8 @@ export function CalmPrefs(props: { id="oembedShown" caption="Embedded content may contain scripts that can track you" /> - - - Save - -
+ ); } diff --git a/pkg/interface/src/views/apps/settings/components/lib/Debug.tsx b/pkg/interface/src/views/apps/settings/components/lib/Debug.tsx index e6b6171721..fa4ed43f1f 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/Debug.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/Debug.tsx @@ -16,7 +16,7 @@ import { BackButton } from './BackButton'; interface StoreDebuggerProps { name: string; - useStore: UseStore>; + useStore: UseStore & any>; } const objectToString = (obj: any): string => JSON.stringify(obj, null, ' '); @@ -57,7 +57,9 @@ const StoreDebugger = (props: StoreDebuggerProps) => { placeholder="Drill Down" width="100%" onKeyUp={(event) => { + // @ts-ignore clearly value is in eventtarget if (event.target.value) { + // @ts-ignore clearly value is in eventtarget tryFilter(event.target.value); } else { setFilter(''); diff --git a/pkg/interface/src/views/apps/settings/components/lib/DisplayForm.tsx b/pkg/interface/src/views/apps/settings/components/lib/DisplayForm.tsx index e7b16009b8..a6c3745e5d 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/DisplayForm.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/DisplayForm.tsx @@ -11,6 +11,7 @@ import GlobalApi from '~/logic/api/global'; import { uxToHex } from '~/logic/lib/util'; import useSettingsState, { selectSettingsState } from '~/logic/state/settings'; import { AsyncButton } from '~/views/components/AsyncButton'; +import {FormikOnBlur} from '~/views/components/FormikOnBlur'; import { BackButton } from './BackButton'; import { BackgroundPicker, BgType } from './BackgroundPicker'; @@ -58,7 +59,7 @@ export default function DisplayForm(props: DisplayFormProps) { const bgType = backgroundType || 'none'; return ( - - {props => (
@@ -99,9 +99,8 @@ export default function DisplayForm(props: DisplayFormProps) { @@ -112,7 +111,6 @@ export default function DisplayForm(props: DisplayFormProps) { - )} -
+ ); } diff --git a/pkg/interface/src/views/apps/settings/components/lib/GroupChannelPicker.tsx b/pkg/interface/src/views/apps/settings/components/lib/GroupChannelPicker.tsx index 3ec7a3c96d..f442e9762a 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/GroupChannelPicker.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/GroupChannelPicker.tsx @@ -3,7 +3,8 @@ import { Center, Col, Icon, - StatelessToggleSwitchField, Text + ToggleSwitch, Text, + StatelessToggleSwitchField } from '@tlon/indigo-react'; import { Association, GraphConfig, resourceFromPath } from '@urbit/api'; import { useField } from 'formik'; @@ -100,7 +101,7 @@ function Channel(props: { association: Association }) { return isWatching(config, association.resource); }); - const [{ value }, meta, { setValue }] = useField( + const [{ value }, meta, { setValue, setTouched }] = useField( `graph["${association.resource}"]` ); @@ -108,9 +109,11 @@ function Channel(props: { association: Association }) { setValue(watching); }, [watching]); - const onChange = () => { + const onClick = () => { setValue(!value); - }; + setTouched(true); + } + const icon = getModuleIcon((metadata.config as GraphConfig)?.graph as GraphModule); @@ -123,7 +126,7 @@ function Channel(props: { association: Association }) { {metadata.title}
- + ); diff --git a/pkg/interface/src/views/apps/settings/components/lib/LeapSettings.tsx b/pkg/interface/src/views/apps/settings/components/lib/LeapSettings.tsx index 5f3c616cda..590ae05fed 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/LeapSettings.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/LeapSettings.tsx @@ -50,7 +50,6 @@ export function LeapSettings(props: { api: GlobalApi; }) { const { leap, set: setSettingsState } = useSettingsState(settingsSel); const categories = leap.categories as LeapCategories[]; const missing = _.difference(leapCategories, categories); - console.log(categories); const initialValues = { categories: [ diff --git a/pkg/interface/src/views/apps/settings/components/lib/NotificationPref.tsx b/pkg/interface/src/views/apps/settings/components/lib/NotificationPref.tsx index 2cecbf9da7..5bda6924b8 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/NotificationPref.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/NotificationPref.tsx @@ -1,15 +1,19 @@ import { - Col, + Button, + Col, - ManagedToggleSwitchField as Toggle, Text + + + + ManagedToggleSwitchField as Toggle, Text } from '@tlon/indigo-react'; -import { Form, Formik, FormikHelpers } from 'formik'; +import { Form, FormikHelpers } from 'formik'; import _ from 'lodash'; -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import GlobalApi from '~/logic/api/global'; import { isWatching } from '~/logic/lib/hark'; import useHarkState from '~/logic/state/hark'; -import { AsyncButton } from '~/views/components/AsyncButton'; +import { FormikOnBlur } from '~/views/components/FormikOnBlur'; import { BackButton } from './BackButton'; import { GroupChannelPicker } from './GroupChannelPicker'; @@ -69,6 +73,8 @@ export function NotificationPreferences(props: { } }, [api, graphConfig, dnd]); + const [notificationsAllowed, setNotificationsAllowed] = useState('Notification' in window && Notification.permission !== 'default'); + return ( <> @@ -82,9 +88,17 @@ export function NotificationPreferences(props: { messaging - +
- + + {notificationsAllowed || !('Notification' in window) + ? null + : + } - - Save - -
+ ); diff --git a/pkg/interface/src/views/apps/settings/components/lib/ShortcutSettings.tsx b/pkg/interface/src/views/apps/settings/components/lib/ShortcutSettings.tsx new file mode 100644 index 0000000000..e5dca84799 --- /dev/null +++ b/pkg/interface/src/views/apps/settings/components/lib/ShortcutSettings.tsx @@ -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 ( + <> + + {label} + + + {capturing ? 'Press' : value} + + + ); +} + +export default function ShortcutSettings(props: ShortcutSettingsProps) { + const { api } = props; + + const { keyboard } = useSettingsState(settingsSel); + + return ( + { + 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 }); + }} + > +
+ + + + + Shortcuts + + Customize keyboard shortcuts for landscape + + + + + + + + + Save Changes + + +
+ ); +} diff --git a/pkg/interface/src/views/apps/settings/settings.tsx b/pkg/interface/src/views/apps/settings/settings.tsx index b636425e35..220d605e6f 100644 --- a/pkg/interface/src/views/apps/settings/settings.tsx +++ b/pkg/interface/src/views/apps/settings/settings.tsx @@ -13,6 +13,7 @@ import { NotificationPreferences } from './components/lib/NotificationPref'; import S3Form from './components/lib/S3Form'; import SecuritySettings from './components/lib/Security'; import {DmSettings} from './components/lib/DmSettings'; +import ShortcutSettings from './components/lib/ShortcutSettings'; export const Skeleton = (props: { children: ReactNode }) => ( @@ -115,6 +116,7 @@ return; + } {hash === 'dm' && } + {hash === 'shortcuts' && } {hash === 's3' && } {hash === 'leap' && } {hash === 'calm' && } diff --git a/pkg/interface/src/views/apps/term/app.tsx b/pkg/interface/src/views/apps/term/app.tsx index cd0ba942f1..edd546fdbf 100644 --- a/pkg/interface/src/views/apps/term/app.tsx +++ b/pkg/interface/src/views/apps/term/app.tsx @@ -78,6 +78,7 @@ class TermApp extends Component { border={['0','1']} cursor='text' > + {/* @ts-ignore declare props in later pass */} + {/* @ts-ignore declare props in later pass */} {this.props.log.map((line, i) => { + // @ts-ignore react memo not passing props return ; })} diff --git a/pkg/interface/src/views/apps/term/components/input.tsx b/pkg/interface/src/views/apps/term/components/input.tsx index b2f9dc26ef..8a8ebc1eb7 100644 --- a/pkg/interface/src/views/apps/term/components/input.tsx +++ b/pkg/interface/src/views/apps/term/components/input.tsx @@ -15,10 +15,11 @@ export class Input extends Component { componentDidUpdate() { if ( - !document.activeElement == document.body - || document.activeElement == this.inputRef.current + document.activeElement == this.inputRef.current ) { + // @ts-ignore ref type issues this.inputRef.current.focus(); + // @ts-ignore ref type issues this.inputRef.current.setSelectionRange(this.props.cursor, this.props.cursor); } } @@ -26,7 +27,7 @@ export class Input extends Component { keyPress(e) { const key = e.key; // 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 === '/')) { return; } @@ -115,6 +116,7 @@ belt = { met: 'bac' }; onKeyDown={this.keyPress} onClick={this.click} onPaste={this.paste} + // @ts-ignore indigo-react doesn't let us pass refs ref={this.inputRef} defaultValue="connecting..." value={prompt} diff --git a/pkg/interface/src/views/apps/term/components/line.tsx b/pkg/interface/src/views/apps/term/components/line.tsx index 3852984f8e..5f8f5c1314 100644 --- a/pkg/interface/src/views/apps/term/components/line.tsx +++ b/pkg/interface/src/views/apps/term/components/line.tsx @@ -1,6 +1,6 @@ import { Text } from '@tlon/indigo-react'; import React from 'react'; - +// @ts-ignore line isn't in props? export default React.memo(({ line }) => { // line body to jsx // NOTE lines are lists of characters that might span multiple codepoints diff --git a/pkg/interface/src/views/components/CommentItem.tsx b/pkg/interface/src/views/components/CommentItem.tsx index de8ebd88ab..2b751a81dc 100644 --- a/pkg/interface/src/views/components/CommentItem.tsx +++ b/pkg/interface/src/views/components/CommentItem.tsx @@ -46,7 +46,7 @@ export function CommentItem(props: CommentItemProps) { const children = Array.from(revs.children); const indices = []; for (const child in children) { - const node = children[child]; + const node = children[child] as any; if (!node?.post || typeof node.post !== 'string') { indices.push(node.post?.index); } diff --git a/pkg/interface/src/views/components/DropdownSearch.tsx b/pkg/interface/src/views/components/DropdownSearch.tsx index 9141b41d24..e2ba2dd2e2 100644 --- a/pkg/interface/src/views/components/DropdownSearch.tsx +++ b/pkg/interface/src/views/components/DropdownSearch.tsx @@ -129,6 +129,7 @@ export function DropdownSearch(props: DropdownSearchProps): ReactElement { return ( + { /* @ts-ignore investigate onblur on styled-system component later */} (props: FormikConfig & ExtraProps) { const formikBag = useFormik({ ...props, validateOnBlur: true }); + const [submitting, setSubmitting] = useState(false); useEffect(() => { if ( Object.keys(formikBag.errors || {}).length === 0 && - Object.keys(formikBag.touched || {}).length !== 0 && - !formikBag.isSubmitting + formikBag.dirty && + !formikBag.isSubmitting && + !submitting ) { + setSubmitting(true); const { values } = formikBag; formikBag.submitForm().then(() => { - formikBag.resetForm({ values, touched: {} }); + formikBag.resetForm({ values }) + setSubmitting(false); }); } }, [ formikBag.errors, - formikBag.touched, - formikBag.submitForm, - formikBag.values + formikBag.dirty, + submitting, + formikBag.isSubmitting ]); const { children, innerRef } = props; diff --git a/pkg/interface/src/views/components/GroupLink.tsx b/pkg/interface/src/views/components/GroupLink.tsx index 7e86370b16..290073385a 100644 --- a/pkg/interface/src/views/components/GroupLink.tsx +++ b/pkg/interface/src/views/components/GroupLink.tsx @@ -77,16 +77,6 @@ export function GroupLink( {preview ? <> - - - - {preview.metadata.hidden ? 'Private' : 'Public'} - - diff --git a/pkg/interface/src/views/components/GroupSearch.tsx b/pkg/interface/src/views/components/GroupSearch.tsx index eb4c77f744..563c4e711a 100644 --- a/pkg/interface/src/views/components/GroupSearch.tsx +++ b/pkg/interface/src/views/components/GroupSearch.tsx @@ -6,6 +6,7 @@ import { ErrorLabel, Icon, Label, Row, Text } from '@tlon/indigo-react'; +import { OpenPolicy } from '@urbit/api'; import { Association } from '@urbit/api/metadata'; import { FieldArray, useFormikContext } from 'formik'; import _ from 'lodash'; @@ -100,7 +101,7 @@ export function GroupSearch>(props: Gr return Object.values( Object.keys(associations.groups) .filter( - e => groupState?.[e]?.policy?.open + e => (groupState?.[e]?.policy as OpenPolicy)?.open ) .reduce((obj, key) => { obj[key] = associations.groups[key]; diff --git a/pkg/interface/src/views/components/ImageInput.tsx b/pkg/interface/src/views/components/ImageInput.tsx index 71e27a0def..417258a815 100644 --- a/pkg/interface/src/views/components/ImageInput.tsx +++ b/pkg/interface/src/views/components/ImageInput.tsx @@ -13,7 +13,7 @@ import useStorage from '~/logic/lib/useStorage'; type ImageInputProps = Parameters[0] & { id: string; - label: string; + label?: string; placeholder?: string; }; diff --git a/pkg/interface/src/views/components/Invite/Group.tsx b/pkg/interface/src/views/components/Invite/Group.tsx index e5c6f08f42..642774bf55 100644 --- a/pkg/interface/src/views/components/Invite/Group.tsx +++ b/pkg/interface/src/views/components/Invite/Group.tsx @@ -10,7 +10,7 @@ import { Metadata, MetadataUpdatePreview, resourceFromPath } from '@urbit/api'; -import { GraphConfig } from '@urbit/api/dist'; +import { GraphConfig } from '@urbit/api'; import _ from 'lodash'; import React, { ReactElement, ReactNode, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; @@ -217,6 +217,7 @@ function InviteActions(props: { const hideJoin = useCallback(async (e) => { if(status?.progress === 'done') { set(s => { + // @ts-ignore investigate zustand types delete s.pendingJoin[resource] }); e.stopPropagation(); @@ -245,14 +246,14 @@ function InviteActions(props: { color="blue" height={4} backgroundColor="white" - onClick={inviteAccept} + onClick={inviteAccept as any} > Accept Decline diff --git a/pkg/interface/src/views/components/ProfileOverlay.tsx b/pkg/interface/src/views/components/ProfileOverlay.tsx index 266242b7b2..aad2b92d0f 100644 --- a/pkg/interface/src/views/components/ProfileOverlay.tsx +++ b/pkg/interface/src/views/components/ProfileOverlay.tsx @@ -218,7 +218,7 @@ const ProfileOverlay = (props: ProfileOverlayProps) => { textOverflow='ellipsis' overflow='hidden' whiteSpace='pre' - marginBottom={0} + mb={0} disableRemoteContent gray title={contact?.status ? contact.status : ''} diff --git a/pkg/interface/src/views/components/ProfileStatus.tsx b/pkg/interface/src/views/components/ProfileStatus.tsx index e38e2ddc2e..ef35a3288a 100644 --- a/pkg/interface/src/views/components/ProfileStatus.tsx +++ b/pkg/interface/src/views/components/ProfileStatus.tsx @@ -41,7 +41,7 @@ export const ProfileStatus = (props) => { { diff --git a/pkg/interface/src/views/components/ReconnectButton.tsx b/pkg/interface/src/views/components/ReconnectButton.tsx index 725506ce46..5921780b47 100644 --- a/pkg/interface/src/views/components/ReconnectButton.tsx +++ b/pkg/interface/src/views/components/ReconnectButton.tsx @@ -8,15 +8,15 @@ const ReconnectButton = ({ connection, subscription }) => { if (connectedStatus === 'disconnected') { return ( ); } else if (connectedStatus === 'reconnecting') { return ( ); } else { diff --git a/pkg/interface/src/views/components/RichText.tsx b/pkg/interface/src/views/components/RichText.tsx index 69021c815c..89e1f1e34b 100644 --- a/pkg/interface/src/views/components/RichText.tsx +++ b/pkg/interface/src/views/components/RichText.tsx @@ -25,7 +25,7 @@ const DISABLED_BLOCK_TOKENS = [ const DISABLED_INLINE_TOKENS = []; type RichTextProps = ReactMarkdownProps & { - api: GlobalApi; + api?: GlobalApi; disableRemoteContent?: boolean; contact?: Contact; group?: Group; @@ -35,7 +35,21 @@ type RichTextProps = ReactMarkdownProps & { color?: string; children?: any; width?: string; -} & PropFunc; + 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) => ( ; } @@ -60,16 +75,16 @@ const RichText = React.memo(({ disableRemoteContent = false, api, ...props }: Ri borderBottom='1px solid' remoteContentPolicy={remoteContentPolicy} onClick={(e) => { - e.stopPropagation(); -}} + e.stopPropagation(); + }} {...linkProps} >{linkProps.children} ); }, - linkReference: (linkProps) => { + linkReference: (linkProps): any => { const linkText = String(linkProps.children[0].props.children); if (isValidPatp(linkText)) { - return ; + return ; } else if(linkText.startsWith('web+urbitgraph://')) { return ( {text} diff --git a/pkg/interface/src/views/components/StatelessAsyncAction.tsx b/pkg/interface/src/views/components/StatelessAsyncAction.tsx index 855fafe808..76e8c69559 100644 --- a/pkg/interface/src/views/components/StatelessAsyncAction.tsx +++ b/pkg/interface/src/views/components/StatelessAsyncAction.tsx @@ -23,7 +23,7 @@ export function StatelessAsyncAction({ return ( { px={3} pb={3} > - +