diff --git a/pkg/arvo/app/graph-push-hook.hoon b/pkg/arvo/app/graph-push-hook.hoon index 17a8c862d..e638b8b72 100644 --- a/pkg/arvo/app/graph-push-hook.hoon +++ b/pkg/arvo/app/graph-push-hook.hoon @@ -26,18 +26,15 @@ state-one == :: -+$ cached-transform ++$ post-transform + $- indexed-post:store $-([index:store post:store atom ?] [index:store post:store]) :: -+$ cached-permission ++$ post-to-permission $-(indexed-post:store $-(vip-metadata:metadata permissions:store)) :: -:: TODO: come back to this and potentially use send a %t -:: to be notified of validator changes +$ cache $: graph-to-mark=(map resource:res (unit mark)) - perm-marks=(map [mark @tas] cached-permission) - transform-marks=(map mark cached-transform) == :: +$ inflated-state @@ -47,8 +44,6 @@ :: +$ cache-action $% [%graph-to-mark (pair resource:res (unit mark))] - [%perm-marks (pair (pair mark @tas) cached-permission)] - [%transform-marks (pair mark cached-transform)] == -- :: @@ -90,13 +85,9 @@ =/ a=cache-action !<(cache-action vase) =* c +.state =* graph-to-mark graph-to-mark.c - =* perm-marks perm-marks.c - =* transform-marks transform-marks.c =. c ?- -.a %graph-to-mark c(graph-to-mark (~(put by graph-to-mark) p.a q.a)) - %perm-marks c(perm-marks (~(put by perm-marks) p.a q.a)) - %transform-marks c(transform-marks (~(put by transform-marks) p.a q.a)) == [~ this(+.state c)] :: @@ -142,12 +133,9 @@ |% ++ $ ^- (quip card (unit vase)) - =/ transform=cached-transform - %+ fall - (~(get by transform-marks) u.mark) - =/ =tube:clay - .^(tube:clay (scry:hc %cc %home /[u.mark]/transform-add-nodes)) - !<(cached-transform (tube !>(*indexed-post:store))) + =/ transform + %. *indexed-post:store + .^(post-transform (scry:hc %cf %home /[u.mark]/transform-add-nodes)) =/ [* result=(list [index:store node:store])] %+ roll (flatten-node-map ~(tap by nodes.q.update)) @@ -166,13 +154,6 @@ %+ poke-self:pass:io %graph-cache-hook !> ^- cache-action [%graph-to-mark rid mark] - :: - ?: (~(has by transform-marks) u.mark) - ~ - :_ ~ - %+ poke-self:pass:io %graph-cache-hook - !> ^- cache-action - [%transform-marks u.mark transform] == :: ++ flatten-node-map @@ -322,9 +303,7 @@ [[%no %no %no] ~] =/ key [u.mark (perm-mark-name perm)] =/ convert - %+ fall - (~(get by perm-marks.cache) key) - .^(cached-permission (scry %cf %home /[u.mark]/(perm-mark-name perm))) + .^(post-to-permission (scry %cf %home /[u.mark]/(perm-mark-name perm))) :- ((convert indexed-post) vip) %- zing :~ ?: (~(has by graph-to-mark.cache) resource) @@ -333,12 +312,6 @@ %+ poke-self:pass:io %graph-cache-hook !> ^- cache-action [%graph-to-mark resource mark] - :: - ?: (~(has by perm-marks.cache) key) ~ - :_ ~ - %+ poke-self:pass:io %graph-cache-hook - !> ^- cache-action - [%perm-marks [u.mark (perm-mark-name perm)] convert] == :: ++ perm-mark-name diff --git a/pkg/arvo/app/graph-store.hoon b/pkg/arvo/app/graph-store.hoon index eac6a6daa..321e6302f 100644 --- a/pkg/arvo/app/graph-store.hoon +++ b/pkg/arvo/app/graph-store.hoon @@ -16,20 +16,9 @@ +$ state-5 [%5 network:store] ++ orm orm:store ++ orm-log orm-log:store -:: -+$ cache - $: validators=(map mark $-(indexed-post:store indexed-post:store)) - == -:: -:: TODO: come back to this and potentially use ford runes or otherwise -:: send a %t to be notified of validator changes -+$ inflated-state - $: state-5 - cache - == -- :: -=| inflated-state +=| state-5 =* state - :: %- agent:dbug @@ -41,7 +30,7 @@ def ~(. (default-agent this %|) bowl) :: ++ on-init [~ this] -++ on-save !>(-.state) +++ on-save !>(state) ++ on-load |= =old=vase ^- (quip card _this) @@ -91,7 +80,7 @@ (gas:orm-log ~ [now.bowl logged-update] ~) == :: - %5 [cards this(-.state old, +.state *cache)] + %5 [cards this(state old)] == :: ++ on-watch @@ -593,8 +582,6 @@ ?~ mark [%.y state] =/ validate=$-(indexed-post:store indexed-post:store) - %+ fall - (~(get by validators) u.mark) .^ $-(indexed-post:store indexed-post:store) %cf (scot %p our.bowl) @@ -604,8 +591,6 @@ %graph-indexed-post ~ == - =? validators !(~(has by validators) u.mark) - (~(put by validators) u.mark validate) :_ state |- ^- ? ?~ graph %.y @@ -624,7 +609,7 @@ ++ poke-import |= arc=* ^- (quip card _state) - =^ cards -.state + =^ cards state (import:store arc our.bowl) [cards state] -- diff --git a/pkg/arvo/app/hark-graph-hook.hoon b/pkg/arvo/app/hark-graph-hook.hoon index dcbf6982e..7285f7ce8 100644 --- a/pkg/arvo/app/hark-graph-hook.hoon +++ b/pkg/arvo/app/hark-graph-hook.hoon @@ -74,21 +74,9 @@ == :_ this(state old) =. cards (flop cards) - %+ welp - ?: (~(has by wex.bowl) [/graph our.bowl %graph-store]) - cards - [watch-graph:ha cards] - %+ turn - ^- (list mark) - :~ %graph-validator-chat - %graph-validator-link - %graph-validator-publish - == - |= =mark - ^- card - =/ =wire /validator/[mark] - =/ =rave:clay [%sing %f [%da now.bowl] /[mark]/notification-kind] - [%pass wire %arvo %c %warp our.bowl [%home `rave]] + ?: (~(has by wex.bowl) [/graph our.bowl %graph-store]) + cards + [watch-graph:ha cards] :: ++ on-watch |= =path @@ -281,11 +269,8 @@ ^- (quip card _this) ?+ wire (on-arvo:def wire sign-arvo) :: - [%validator @ ~] - :_ this - =* validator i.t.wire - =/ =rave:clay [%next %f [%da now.bowl] /[validator]/notification-kind] - [%pass wire %arvo %c %warp our.bowl [%home `rave]]~ + :: no longer necessary + [%validator @ ~] [~ this] == ++ on-fail on-fail:def -- diff --git a/pkg/arvo/app/observe-hook.hoon b/pkg/arvo/app/observe-hook.hoon index 07fadb5c2..b3916c510 100644 --- a/pkg/arvo/app/observe-hook.hoon +++ b/pkg/arvo/app/observe-hook.hoon @@ -8,6 +8,12 @@ :: |% +$ card card:agent:gall ++$ state-0 + $: observers=(map serial observer:sur) + warm-cache=_| + static-conversions=(set [term term]) + == +:: +$ versioned-state $% [%0 observers=(map serial observer:sur)] [%1 observers=(map serial observer:sur)] @@ -15,6 +21,7 @@ [%3 observers=(map serial observer:sur)] [%4 observers=(map serial observer:sur)] [%5 observers=(map serial observer:sur) warm-cache=_|] + [%6 state-0] == :: +$ serial @uv @@ -28,7 +35,7 @@ -- :: %- agent:dbug -=| [%5 observers=(map serial observer:sur) warm-cache=_|] +=| [%6 state-0] =* state - :: ^- agent:gall @@ -44,6 +51,33 @@ (act [%watch %group-store /groups %group-on-remove-member]) (act [%watch %metadata-store /updates %md-on-add-group-feed]) (act [%warm-cache-all ~]) + :: + (warm-static %graph-validator-chat %graph-indexed-post) + (warm-static %graph-validator-publish %graph-indexed-post) + (warm-static %graph-validator-link %graph-indexed-post) + (warm-static %graph-validator-post %graph-indexed-post) + (warm-static %graph-validator-dm %graph-indexed-post) + :: + (warm-static %graph-validator-chat %graph-permissions-add) + (warm-static %graph-validator-publish %graph-permissions-add) + (warm-static %graph-validator-link %graph-permissions-add) + (warm-static %graph-validator-post %graph-permissions-add) + :: + (warm-static %graph-validator-chat %graph-permissions-remove) + (warm-static %graph-validator-publish %graph-permissions-remove) + (warm-static %graph-validator-link %graph-permissions-remove) + (warm-static %graph-validator-post %graph-permissions-remove) + :: + (warm-static %graph-validator-chat %notification-kind) + (warm-static %graph-validator-publish %notification-kind) + (warm-static %graph-validator-link %notification-kind) + (warm-static %graph-validator-post %notification-kind) + (warm-static %graph-validator-dm %notification-kind) + :: + (warm-static %graph-validator-chat %transform-add-nodes) + (warm-static %graph-validator-publish %transform-add-nodes) + (warm-static %graph-validator-link %transform-add-nodes) + (warm-static %graph-validator-post %transform-add-nodes) == :: ++ act @@ -57,6 +91,19 @@ %observe-action !>(action) == + :: + ++ warm-static + |= [from=term to=term] + ^- card + :* %pass + /poke + %agent + [our.bowl %observe-hook] + %poke + %observe-action + !> ^- action:sur + [%warm-static-conversion from to] + == -- :: ++ on-save !>(state) @@ -68,8 +115,41 @@ =| cards=(list card) |- ?- -.old-state - %5 + %6 [cards this(state old-state)] + :: + %5 + =. cards + %+ weld cards + :~ (warm-static %graph-validator-chat %graph-indexed-post) + (warm-static %graph-validator-publish %graph-indexed-post) + (warm-static %graph-validator-link %graph-indexed-post) + (warm-static %graph-validator-post %graph-indexed-post) + (warm-static %graph-validator-dm %graph-indexed-post) + :: + (warm-static %graph-validator-chat %graph-permissions-add) + (warm-static %graph-validator-publish %graph-permissions-add) + (warm-static %graph-validator-link %graph-permissions-add) + (warm-static %graph-validator-post %graph-permissions-add) + :: + (warm-static %graph-validator-chat %graph-permissions-remove) + (warm-static %graph-validator-publish %graph-permissions-remove) + (warm-static %graph-validator-link %graph-permissions-remove) + (warm-static %graph-validator-post %graph-permissions-remove) + :: + (warm-static %graph-validator-chat %notification-kind) + (warm-static %graph-validator-publish %notification-kind) + (warm-static %graph-validator-link %notification-kind) + (warm-static %graph-validator-post %notification-kind) + (warm-static %graph-validator-dm %notification-kind) + :: + (warm-static %graph-validator-chat %transform-add-nodes) + (warm-static %graph-validator-publish %transform-add-nodes) + (warm-static %graph-validator-link %transform-add-nodes) + (warm-static %graph-validator-post %transform-add-nodes) + == + $(old-state [%6 observers.old-state %.n ~]) + :: %4 =. cards :_ cards @@ -109,6 +189,19 @@ %observe-action !>(action) == + :: + ++ warm-static + |= [from=term to=term] + ^- card + :* %pass + /poke + %agent + [our.bowl %observe-hook] + %poke + %observe-action + !> ^- action:sur + [%warm-static-conversion from to] + == -- :: ++ on-poke @@ -122,10 +215,12 @@ =* observer observer.action =/ vals (silt ~(val by observers)) ?- -.action - %watch (watch observer vals) - %ignore (ignore observer vals) - %warm-cache-all warm-cache-all - %cool-cache-all cool-cache-all + %watch (watch observer vals) + %ignore (ignore observer vals) + %warm-cache-all warm-cache-all + %cool-cache-all cool-cache-all + %warm-static-conversion (warm-static-conversion from.action to.action) + %cool-static-conversion (cool-static-conversion from.action to.action) == :: ++ watch @@ -170,6 +265,23 @@ ?. warm-cache ~|('cannot cool down cache that is already cool' !!) [~ this(warm-cache %.n)] + :: + ++ warm-static-conversion + |= [from=term to=term] + ^- (quip card _this) + ?: (~(has in static-conversions) [from to]) + ~|('cannot warm up a static conversion that is already warm' !!) + :_ this(static-conversions (~(put in static-conversions) [from to])) + =/ =wire /static-convert/[from]/[to] + =/ =rave:clay [%sing %f [%da now.bowl] /[from]/[to]] + [%pass wire %arvo %c %warp our.bowl %home `rave]~ + :: + ++ cool-static-conversion + |= [from=term to=term] + ^- (quip card _this) + ?. (~(has in static-conversions) [from to]) + ~|('cannot cool a static conversion that is already cool' !!) + [~ this(static-conversions (~(del in static-conversions) [from to]))] -- :: ++ on-agent @@ -326,6 +438,18 @@ ~ =/ =rave:clay [%next %b q.p.u.riot mark] [%pass wire %arvo %c %warp our.bowl %home `rave]~ + :: + [%static-convert @ @ ~] + =* from i.t.wire + =* to i.t.t.wire + ?. (~(has in static-conversions) [from to]) + ~ + ?> ?=([%clay %writ *] sign-arvo) + =* riot p.sign-arvo + ?~ riot + ~ + =/ =rave:clay [%next %f q.p.u.riot /[from]/[to]] + [%pass wire %arvo %c %warp our.bowl %home `rave]~ == :: ++ on-watch on-watch:def diff --git a/pkg/arvo/mar/graph/cache/hook.hoon b/pkg/arvo/mar/graph/cache/hook.hoon index 1b9395a0f..a0d3b6360 100644 --- a/pkg/arvo/mar/graph/cache/hook.hoon +++ b/pkg/arvo/mar/graph/cache/hook.hoon @@ -2,8 +2,6 @@ |% +$ cache-action $% [%graph-to-mark (pair resource:res (unit mark))] - [%perm-marks (pair (pair mark @tas) tube:clay)] - [%transform-marks (pair mark tube:clay)] == -- :: diff --git a/pkg/arvo/mar/transform-add-nodes.hoon b/pkg/arvo/mar/transform-add-nodes.hoon new file mode 100644 index 000000000..ca865eaa5 --- /dev/null +++ b/pkg/arvo/mar/transform-add-nodes.hoon @@ -0,0 +1,12 @@ +/- *post +|_ i=indexed-post +++ grad %noun +++ grow + |% + ++ noun i + -- +++ grab + |% + ++ noun indexed-post + -- +-- diff --git a/pkg/arvo/sur/observe-hook.hoon b/pkg/arvo/sur/observe-hook.hoon index 60030ad5d..5bee6df54 100644 --- a/pkg/arvo/sur/observe-hook.hoon +++ b/pkg/arvo/sur/observe-hook.hoon @@ -10,5 +10,7 @@ :: [%warm-cache-all ~] [%cool-cache-all ~] + [%warm-static-conversion from=term to=term] + [%cool-static-conversion from=term to=term] == -- diff --git a/pkg/interface/.husky/pre-commit b/pkg/interface/.husky/pre-commit index 006147ab7..2d03534b2 100755 --- a/pkg/interface/.husky/pre-commit +++ b/pkg/interface/.husky/pre-commit @@ -1,4 +1,10 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -cd pkg/interface && npx lint-staged \ No newline at end of file +cd pkg/interface + +command -v npx > /dev/null || { + exit 0 +} + +npx lint-staged \ No newline at end of file diff --git a/pkg/interface/package-lock.json b/pkg/interface/package-lock.json index 0f68bc1a7..6aaf2a86d 100644 Binary files a/pkg/interface/package-lock.json and b/pkg/interface/package-lock.json differ diff --git a/pkg/interface/package.json b/pkg/interface/package.json index d5aeb02a6..a636da88c 100644 --- a/pkg/interface/package.json +++ b/pkg/interface/package.json @@ -14,6 +14,7 @@ "@tlon/indigo-react": "^1.2.23", "@tlon/sigil-js": "^1.4.3", "@urbit/api": "file:../npm/api", + "@urbit/http-api": "file:../npm/http-api", "any-ascii": "^0.1.7", "aws-sdk": "^2.830.0", "big-integer": "^1.6.48", diff --git a/pkg/interface/preinstall.sh b/pkg/interface/preinstall.sh index 4f35cfc0f..0fcf27d52 100755 --- a/pkg/interface/preinstall.sh +++ b/pkg/interface/preinstall.sh @@ -9,4 +9,6 @@ for i in $(find . -type d -maxdepth 1) ; do npm ci cd .. fi -done \ No newline at end of file +done +cd http-api +npm run build diff --git a/pkg/interface/src/logic/api/base.ts b/pkg/interface/src/logic/api/base.ts deleted file mode 100644 index a5d021131..000000000 --- a/pkg/interface/src/logic/api/base.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Path, Patp } from '@urbit/api'; -import _ from 'lodash'; -import BaseStore from '../store/base'; - -export default class BaseApi { - bindPaths: Path[] = []; - constructor(public ship: Patp, public channel: any, public store: BaseStore) {} - - unsubscribe(id: number) { - this.channel.unsubscribe(id); - } - - subscribe(path: Path, method, ship = this.ship, app: string, success, fail, quit, queue = false) { - this.bindPaths = _.uniq([...this.bindPaths, path]); - - return this.channel.subscribe( - this.ship, - app, - path, - (err) => { - fail(err); - }, - (event) => { - success({ - data: event, - from: { - ship, - path - } - }); - }, - (qui) => { - quit(qui); - }, - () => {}, - queue - ); - } - - action( - appl: string, - mark: string, - data: any, - ship = (window as any).ship - ): Promise { - return new Promise((resolve, reject) => { - this.channel.poke( - ship, - appl, - mark, - data, - (json) => { - resolve(json); -}, - (err) => { - reject(err); -} - ); - }); - } - - scry(app: string, path: Path): Promise { - return fetch(`/~/scry/${app}${path}.json`).then(r => r.json() as Promise); - } - - async spider(inputMark: string, outputMark: string, threadName: string, body: any): Promise { - const res = await fetch(`/spider/${inputMark}/${threadName}/${outputMark}.json`, { - method: 'POST', - body: JSON.stringify(body) - }); - - return res.json(); - } -} diff --git a/pkg/interface/src/logic/api/bootstrap.ts b/pkg/interface/src/logic/api/bootstrap.ts new file mode 100644 index 000000000..f7de52513 --- /dev/null +++ b/pkg/interface/src/logic/api/bootstrap.ts @@ -0,0 +1,43 @@ +import airlock from '~/logic/api'; +import useHarkState from '~/logic/state/hark'; +import useMetadataState from '~/logic/state/metadata'; +import useContactState from '../state/contact'; +import useGraphState from '../state/graph'; +import useGroupState from '../state/group'; +import useInviteState from '../state/invite'; +import useLaunchState from '../state/launch'; +import useSettingsState from '../state/settings'; +import useLocalState from '../state/local'; + +export const bootstrapApi = async () => { + await airlock.poke({ app: 'hood', mark: 'helm-hi', json: 'opening airlock' }); + + airlock.onError = (e) => { + (async () => { + useLocalState.setState({ subscription: 'disconnected' }); + })(); + }; + + airlock.onRetry = () => { + useLocalState.setState({ subscription: 'reconnecting' }); + }; + + airlock.onOpen = () => { + useLocalState.setState({ subscription: 'connected' }); + }; + + await airlock.eventSource(); + [ + useHarkState, + useMetadataState, + useGroupState, + useContactState, + useSettingsState, + useLaunchState, + useInviteState, + useGraphState + ].forEach((state) => { + state.getState().initialize(airlock); + }); +}; + diff --git a/pkg/interface/src/logic/api/contacts.ts b/pkg/interface/src/logic/api/contacts.ts deleted file mode 100644 index c0be49902..000000000 --- a/pkg/interface/src/logic/api/contacts.ts +++ /dev/null @@ -1,124 +0,0 @@ -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'; - -export default class ContactsApi extends BaseApi { - add(ship: Patp, contact: any) { - contact['last-updated'] = Date.now(); - return this.storeAction({ add: { ship, contact } }); - } - - remove(ship: Patp) { - return this.storeAction({ remove: { ship } }); - } - - edit(ship: Patp, editField: ContactEditField) { - /* editField can be... - {nickname: ''} - {email: ''} - {phone: ''} - {website: ''} - {color: 'fff'} // with no 0x prefix - {avatar: null} - {avatar: ''} - {add-group: {ship, name}} - {remove-group: {ship, name}} - */ - const action = { - edit: { - ship, - 'edit-field': editField, - timestamp: Date.now() - } - } - doOptimistically(useContactState, action, this.storeAction.bind(this), [edit]) - } - - allowShips(ships: Patp[]) { - return this.storeAction({ - allow: { - ships - } - }); - } - - allowGroup(ship: string, name: string) { - const group = { ship, name }; - return this.storeAction({ - allow: { - group - } - }); - } - - setPublic(setPublic: any) { - return this.storeAction({ - 'set-public': setPublic - }); - } - - share(recipient: Patp) { - return this.action( - 'contact-push-hook', - 'contact-share', - { share: recipient } - ); - } - - fetchIsAllowed(entity, name, ship, personal) { - const isPersonal = personal ? 'true' : 'false'; - return this.scry( - 'contact-store', - `/is-allowed/${entity}/${name}/${ship}/${isPersonal}` - ); - } - - async disallowedShipsForOurContact(ships: string[]): Promise { - return _.compact( - await Promise.all( - ships.map( - async (s) => { - const ship = `~${s}`; - if(s === window.ship) { - return null; - } - const allowed = await this.fetchIsAllowed( - `~${window.ship}`, - 'personal', - ship, - true - ); - return allowed ? null : ship; - } - ) - ) - ); - } - - retrieve(ship: string) { - const resource = { ship, name: '' }; - return this.action('contact-pull-hook', 'pull-hook-action', { - add: { - resource, - ship - } - }); - } - - private storeAction(action: any): Promise { - return this.action('contact-store', 'contact-update-0', action); - } - - private viewAction(threadName: string, action: any) { - return this.spider('contact-view-action', 'json', threadName, action); - } - - private hookAction(ship: Patp, action: any): Promise { - return this.action('contact-push-hook', 'contact-update-0', action); - } -} diff --git a/pkg/interface/src/logic/api/gcp.ts b/pkg/interface/src/logic/api/gcp.ts deleted file mode 100644 index f8a72d666..000000000 --- a/pkg/interface/src/logic/api/gcp.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { StoreState } from '../store/type'; -import BaseApi from './base'; - -export default class GcpApi extends BaseApi { - // Does not touch the store; use the value manually. - async isConfigured(): Promise { - return this.spider('noun', 'json', 'gcp-is-configured', {}); - } - - // Does not return the token; read it out of the store. - async getToken(): Promise { - return this.spider('noun', 'gcp-token', 'gcp-get-token', {}) - .then((token) => { - this.store.handleEvent({ - data: token - }); - }); - } -} diff --git a/pkg/interface/src/logic/api/global.ts b/pkg/interface/src/logic/api/global.ts deleted file mode 100644 index 159509488..000000000 --- a/pkg/interface/src/logic/api/global.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Patp } from '@urbit/api'; -import GlobalStore from '../store/store'; -import { StoreState } from '../store/type'; -import BaseApi from './base'; -import ContactsApi from './contacts'; -import GcpApi from './gcp'; -import GraphApi from './graph'; -import GroupsApi from './groups'; -import { HarkApi } from './hark'; -import InviteApi from './invite'; -import LaunchApi from './launch'; -import LocalApi from './local'; -import MetadataApi from './metadata'; -import S3Api from './s3'; -import SettingsApi from './settings'; -import TermApi from './term'; - -export default class GlobalApi extends BaseApi { - local = new LocalApi(this.ship, this.channel, this.store); - invite = new InviteApi(this.ship, this.channel, this.store); - metadata = new MetadataApi(this.ship, this.channel, this.store); - contacts = new ContactsApi(this.ship, this.channel, this.store); - groups = new GroupsApi(this.ship, this.channel, this.store); - launch = new LaunchApi(this.ship, this.channel, this.store); - gcp = new GcpApi(this.ship, this.channel, this.store); - s3 = new S3Api(this.ship, this.channel, this.store); - graph = new GraphApi(this.ship, this.channel, this.store); - hark = new HarkApi(this.ship, this.channel, this.store); - settings = new SettingsApi(this.ship, this.channel, this.store); - term = new TermApi(this.ship, this.channel, this.store); - - constructor( - public ship: Patp, - public channel: any, - public store: GlobalStore - ) { - super(ship, channel, store); - } -} - diff --git a/pkg/interface/src/logic/api/graph.ts b/pkg/interface/src/logic/api/graph.ts deleted file mode 100644 index 13094f0c1..000000000 --- a/pkg/interface/src/logic/api/graph.ts +++ /dev/null @@ -1,456 +0,0 @@ -import { patp2dec } from 'urbit-ob'; -import { Content, Enc, GraphNode, GroupPolicy, Path, Patp, Post, Resource } from '@urbit/api'; -import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap'; -import _ from 'lodash'; -import { decToUd, deSig, resourceAsPath, unixToDa } from '~/logic/lib/util'; -import { makeResource, resourceFromPath } from '../lib/group'; -import { StoreState } from '../store/type'; -import BaseApi from './base'; - -export const createBlankNodeWithChildPost = ( - parentIndex = '', - childIndex = '', - contents: Content[] -): GraphNode => { - const date = unixToDa(Date.now()).toString(); - const nodeIndex = parentIndex + '/' + date; - - const childGraph = {}; - childGraph[childIndex] = { - post: { - author: `~${window.ship}`, - index: nodeIndex + '/' + childIndex, - 'time-sent': Date.now(), - contents, - hash: null, - signatures: [] - }, - children: null - }; - - return { - post: { - author: `~${window.ship}`, - index: nodeIndex, - 'time-sent': Date.now(), - contents: [], - hash: null, - signatures: [] - }, - children: childGraph as BigIntOrderedMap - }; -}; - -function markPending(nodes: any) { - _.forEach(nodes, (node) => { - node.post.author = deSig(node.post.author); - node.post.pending = true; - markPending(node.children || {}); - }); -} - -export const createPost = ( - contents: Content[], - parentIndex = '', - childIndex = 'DATE_PLACEHOLDER' -) => { - if (childIndex === 'DATE_PLACEHOLDER') { - childIndex = unixToDa(Date.now()).toString(); - } - return { - author: `~${window.ship}`, - index: parentIndex + '/' + childIndex, - 'time-sent': Date.now(), - contents, - hash: null, - signatures: [] - }; -}; - -function moduleToMark(mod: string): string | undefined { - if(mod === 'link') { - return 'graph-validator-link'; - } - if(mod === 'publish') { - return 'graph-validator-publish'; - } - if(mod === 'chat') { - return 'graph-validator-chat'; - } - return undefined; -} - -export default class GraphApi extends BaseApi { - joiningGraphs = new Set(); - - private storeAction(action: any): Promise { - return this.action('graph-store', 'graph-update-2', action); - } - - private viewAction(threadName: string, action: any) { - return this.spider('graph-view-action', 'json', threadName, action); - } - - private hookAction(ship: Patp, action: any): Promise { - return this.action('graph-push-hook', 'graph-update-2', action); - } - - createManagedGraph( - name: string, - title: string, - description: string, - group: Path, - mod: string - ) { - const associated = { group: resourceFromPath(group) }; - const resource = makeResource(`~${window.ship}`, name); - - return this.viewAction('graph-create', { - 'create': { - resource, - title, - description, - associated, - 'module': mod, - mark: moduleToMark(mod) - } - }); - } - - createUnmanagedGraph( - name: string, - title: string, - description: string, - policy: Enc, - mod: string - ) { - const resource = makeResource(`~${window.ship}`, name); - - return this.viewAction('graph-create', { - 'create': { - resource, - title, - description, - associated: { policy }, - 'module': mod, - mark: moduleToMark(mod) - } - }); - } - - joinGraph(ship: Patp, name: string) { - const resource = makeResource(ship, name); - const rid = resourceAsPath(resource); - if(this.joiningGraphs.has(rid)) { - return Promise.resolve(); - } - this.joiningGraphs.add(rid); - return this.viewAction('graph-join', { - join: { - resource, - ship - } - }).then((res) => { - this.joiningGraphs.delete(rid); - return res; - }); - } - - deleteGraph(name: string) { - const resource = makeResource(`~${window.ship}`, name); - return this.viewAction('graph-delete', { - 'delete': { - resource - } - }); - } - - leaveGraph(ship: Patp, name: string) { - const resource = makeResource(ship, name); - return this.viewAction('graph-leave', { - 'leave': { - resource - } - }); - } - - groupifyGraph(ship: Patp, name: string, toPath?: string) { - const resource = makeResource(ship, name); - const to = toPath && resourceFromPath(toPath); - - return this.viewAction('graph-groupify', { - groupify: { - resource, - to - } - }); - } - - eval(cord: string): Promise { - return this.spider('graph-view-action', 'tang', 'graph-eval', { - eval: cord - }); - } - - addGraph(ship: Patp, name: string, graph: any, mark: any) { - return this.storeAction({ - 'add-graph': { - resource: { ship, name }, - graph, - mark - } - }); - } - - addDmMessage(ship: Patp, contents: Content[]) { - const post = createPost(contents, `/${patp2dec(ship)}`); - const action = { - 'add-nodes': { - resource: { ship: `~${window.ship}`, name: 'dm-inbox' }, - nodes: { - [post.index]: { - post, - children: null - } - } - } - }; - this.action('dm-hook', 'graph-update-2', action); - markPending(action['add-nodes'].nodes); - action['add-nodes'].resource.ship = - action['add-nodes'].resource.ship.slice(1); - this.store.handleEvent({ data: { - 'graph-update': action - } }); - } - - acceptDm(ship: Patp) { - return this.action('dm-hook', 'dm-hook-action', { 'accept' : ship }); - } - - declineDm(ship: Patp) { - return this.action('dm-hook', 'dm-hook-action', { 'decline' : ship }); - } - - setScreen(screen: boolean) { - return this.action('dm-hook', 'dm-hook-action', { screen }); - } - - addPost(ship: Patp, name: string, post: Post) { - const nodes = {}; - nodes[post.index] = { - post, - children: null - }; - return this.addNodes(ship, name, nodes); - } - - addNode(ship: Patp, name: string, node: GraphNode) { - const nodes = {}; - nodes[node.post.index] = node; - - return this.addNodes(ship, name, nodes); - } - - addNodes(ship: Patp, name: string, nodes: Object) { - const action = { - 'add-nodes': { - resource: { ship, name }, - nodes - } - }; - - const pendingPromise = this.spider( - 'graph-update-2', - 'graph-view-action', - 'graph-add-nodes', - action - ); - - markPending(action['add-nodes'].nodes); - action['add-nodes'].resource.ship = - action['add-nodes'].resource.ship.slice(1); - - this.store.handleEvent({ data: { - 'graph-update': action - } }); - - return pendingPromise; - /* TODO: stop lying to our users about pending states - return pendingPromise.then((pendingHashes) => { - for (let index in action['add-nodes'].nodes) { - action['add-nodes'].nodes[index].post.hash = - pendingHashes['pending-indices'][index] || null; - } - - this.store.handleEvent({ data: { - 'graph-update': { - 'pending-indices': pendingHashes['pending-indices'], - ...action - } - } }); - }); - */ - } - - async enableGroupFeed(group: Resource, vip: any = ''): Promise { - const { resource } = await this.spider( - 'graph-view-action', - 'resource', - 'graph-create-group-feed', - { - 'create-group-feed': { resource: group, vip } - } - ); - return resource; - } - - async disableGroupFeed(group: Resource): Promise { - await this.spider( - 'graph-view-action', - 'json', - 'graph-disable-group-feed', - { - 'disable-group-feed': { resource: group } - } - ); - } - - removePosts(ship: Patp, name: string, indices: string[]) { - return this.hookAction(ship, { - 'remove-posts': { - resource: { ship, name }, - indices - } - }); - } - - getKeys() { - return this.scry('graph-store', '/keys') - .then((keys) => { - this.store.handleEvent({ - data: keys - }); - }); - } - - getTags() { - return this.scry('graph-store', '/tags') - .then((tags) => { - this.store.handleEvent({ - data: tags - }); - }); - } - - getTagQueries() { - return this.scry('graph-store', '/tag-queries') - .then((tagQueries) => { - this.store.handleEvent({ - data: tagQueries - }); - }); - } - - getGraph(ship: string, resource: string) { - return this.scry('graph-store', `/graph/${ship}/${resource}`) - .then((graph) => { - this.store.handleEvent({ - data: graph - }); - }); - } - - async getNewest(ship: string, resource: string, count: number, index = '') { - const data = await this.scry('graph-store', `/newest/${ship}/${resource}/${count}${index}`); - data['graph-update'].fetch = true; - this.store.handleEvent({ data }); - } - - async getOlderSiblings(ship: string, resource: string, count: number, index = '') { - const idx = index.split('/').map(decToUd).join('/'); - const data = await this.scry('graph-store', - `/node-siblings/older/${ship}/${resource}/${count}${idx}` - ); - data['graph-update'].fetch = true; - this.store.handleEvent({ data }); - } - - async getYoungerSiblings(ship: string, resource: string, count: number, index = '') { - const idx = index.split('/').map(decToUd).join('/'); - const data = await this.scry('graph-store', - `/node-siblings/younger/${ship}/${resource}/${count}${idx}` - ); - data['graph-update'].fetch = true; - this.store.handleEvent({ data }); - } - - async getShallowChildren(ship: string, name: string, index = '') { - const idx = index.split('/').map(decToUd).join('/'); - const data = await this.scry('graph-store', - `/shallow-children/${ship}/${name}${idx}` - ); - data['graph-update'].fetch = true; - this.store.handleEvent({ data }); - } - - async getDeepOlderThan(ship: string, resource: string, startTime = null, count: number) { - const start = startTime ? decToUd(startTime) : 'null'; - const data = await this.scry('graph-store', - `/deep-nodes-older-than/${ship}/${resource}/${count}/${start}` - ); - data['graph-update'].fetch = true; - const node = data['graph-update']; - this.store.handleEvent({ - data: { - 'graph-update-flat': node, - 'graph-update': node - } - }); - } - - async getFirstborn(ship: string, resource: string, index = '') { - const idx = index.split('/').map(decToUd).join('/'); - const data = await this.scry('graph-store', - `/firstborn/${ship}/${resource}${idx}` - ); - data['graph-update'].fetch = true; - const node = data['graph-update']; - this.store.handleEvent({ - data: { - 'graph-update-thread': { - index, - ...node - }, - 'graph-update': node - } - }); - } - - getGraphSubset(ship: string, resource: string, start: string, end: string) { - return this.scry( - 'graph-store', - `/graph-subset/${ship}/${resource}/${end}/${start}` - ).then((subset) => { - this.store.handleEvent({ - data: subset - }); - }); - } - - async getNode(ship: string, resource: string, index: string) { - const idx = index.split('/').map(decToUd).join('/'); - const data = await this.scry( - 'graph-store', - `/node/${ship}/${resource}${idx}` - ); - data['graph-update'].fetch = true; - const node = data['graph-update']; - this.store.handleEvent({ - data: { - 'graph-update-loose': node - } - }); - } -} - diff --git a/pkg/interface/src/logic/api/groups.ts b/pkg/interface/src/logic/api/groups.ts deleted file mode 100644 index ee1807c51..000000000 --- a/pkg/interface/src/logic/api/groups.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Enc, Patp } from '@urbit/api'; -import { - GroupAction, - GroupPolicy, - - GroupPolicyDiff, Resource, - Tag -} from '@urbit/api/groups'; -import { makeResource } from '../lib/group'; -import { StoreState } from '../store/type'; -import BaseApi from './base'; - -export default class GroupsApi extends BaseApi { - remove(resource: Resource, ships: Patp[]) { - return this.proxyAction({ removeMembers: { resource, ships } }); - } - - addTag(resource: Resource, tag: Tag, ships: Patp[]) { - return this.proxyAction({ addTag: { resource, tag, ships } }); - } - - removeTag(resource: Resource, tag: Tag, ships: Patp[]) { - return this.proxyAction({ removeTag: { resource, tag, ships } }); - } - - add(resource: Resource, ships: Patp[]) { - return this.proxyAction({ addMembers: { resource, ships } }); - } - - removeGroup(resource: Resource) { - return this.storeAction({ removeGroup: { resource } }); - } - - changePolicy(resource: Resource, diff: Enc) { - return this.proxyAction({ changePolicy: { resource, diff } }); - } - - join(ship: string, name: string) { - const resource = makeResource(ship, name); - - return this.viewAction({ join: { resource, ship } }); - } - - create(name: string, policy: Enc, title: string, description: string) { - return this.viewThread('group-create', { - create: { - name, - policy, - title, - description - } - }); - } - - deleteGroup(ship: string, name: string) { - const resource = makeResource(ship, name); - - return this.viewThread('group-delete', { - remove: resource - }); - } - - leaveGroup(ship: string, name: string) { - const resource = makeResource(ship, name); - return this.viewThread('group-leave', { - leave: resource - }); - } - - invite(ship: string, name: string, ships: Patp[], description: string) { - const resource = makeResource(ship, name); - return this.viewThread('group-invite', { - invite: { - resource, - ships, - description - } - }); - } - - hide(resource: string) { - return this.viewAction({ hide: resource }); - } - - private proxyAction(action: GroupAction) { - return this.action('group-push-hook', 'group-update-0', action); - } - - private storeAction(action: GroupAction) { - return this.action('group-store', 'group-update-0', action); - } - - private viewThread(thread: string, action: any) { - return this.spider('group-view-action', 'json', thread, action); - } - - private viewAction(action: any) { - return this.action('group-view', 'group-view-action', action); - } -} diff --git a/pkg/interface/src/logic/api/hark.ts b/pkg/interface/src/logic/api/hark.ts deleted file mode 100644 index 6c83e7478..000000000 --- a/pkg/interface/src/logic/api/hark.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { Association, GraphNotifDescription, IndexedNotification, NotifIndex } from '@urbit/api'; -import { BigInteger } from 'big-integer'; -import { getParentIndex } from '../lib/notification'; -import { dateToDa, decToUd } from '../lib/util'; -import { reduce } from '../reducers/hark-update'; -import { doOptimistically } from '../state/base'; -import useHarkState from '../state/hark'; -import { StoreState } from '../store/type'; -import BaseApi from './base'; - -function getHarkSize() { - return useHarkState.getState().notifications.size ?? 0; -} - -export class HarkApi extends BaseApi { - private harkAction(action: any): Promise { - return this.action('hark-store', 'hark-action', action); - } - - private graphHookAction(action: any) { - return this.action('hark-graph-hook', 'hark-graph-hook-action', action); - } - - private groupHookAction(action: any) { - return this.action('hark-group-hook', 'hark-group-hook-action', action); - } - - private actOnNotification(frond: string, intTime: BigInteger | undefined, index: NotifIndex) { - const time = intTime ? decToUd(intTime.toString()) : null; - return this.harkAction({ - [frond]: { - time, - index - } - }); - } - - async setMentions(mentions: boolean) { - await this.graphHookAction({ - 'set-mentions': mentions - }); - } - - setWatchOnSelf(watchSelf: boolean) { - return this.graphHookAction({ - 'set-watch-on-self': watchSelf - }); - } - - setDoNotDisturb(dnd: boolean) { - return this.harkAction({ - 'set-dnd': dnd - }); - } - - async archive(intTime: BigInteger, index: NotifIndex) { - const time = intTime ? decToUd(intTime.toString()) : null; - const action = { - archive: { - time, - index - } - }; - await doOptimistically(useHarkState, action, this.harkAction.bind(this), [reduce]); - } - - read(time: BigInteger, index: NotifIndex) { - return this.harkAction({ - 'read-note': index - }); - } - - readIndex(index: NotifIndex) { - return this.harkAction({ - 'read-index': index - }); - } - - unread(time: BigInteger, index: NotifIndex) { - return this.actOnNotification('unread-note', time, index); - } - - readGroup(group: string) { - return this.harkAction({ - 'read-group': group - }); - } - - readGraph(graph: string) { - return this.harkAction({ - 'read-graph': graph - }); - } - - dismissReadCount(graph: string, index: string) { - return this.harkAction({ - 'read-count': { - graph: { - graph, - index - } - } - }); - } - - markCountAsRead(association: Association, parent: string, description: GraphNotifDescription) { - 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) { - return this.harkAction({ - 'read-each': { - index: - { graph: - { graph: association.resource, - group: association.group, - description, - module: mod, - index: parent - } - }, - target: child - } - }); - } - - dec(index: NotifIndex, ref: string) { - return this.harkAction({ - dec: { - index, - ref - } - }); - } - - seen() { - return this.harkAction({ seen: null }); - } - readAll() { - return this.harkAction({ 'read-all': null }); - } - - mute(notif: IndexedNotification) { - if('graph' in notif.index && 'graph' in notif.notification.contents) { - const { index } = notif; - const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph); - if(!parentIndex) { - return Promise.resolve(); - } - return this.ignoreGraph(index.graph.graph, parentIndex); - } - if('group' in notif.index) { - const { group } = notif.index.group; - return this.ignoreGroup(group); - } - return Promise.resolve(); - } - - unmute(notif: IndexedNotification) { - if('graph' in notif.index && 'graph' in notif.notification.contents) { - const { index } = notif; - const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph); - if(!parentIndex) { - return Promise.resolve(); - } - return this.listenGraph(index.graph.graph, parentIndex); - } - if('group' in notif.index) { - return this.listenGroup(notif.index.group.group); - } - return Promise.resolve(); - } - - ignoreGroup(group: string) { - return this.groupHookAction({ - ignore: group - }); - } - - ignoreGraph(graph: string, index: string) { - return this.graphHookAction({ - ignore: { - graph, - index - } - }); - } - - listenGroup(group: string) { - return this.groupHookAction({ - listen: group - }); - } - - listenGraph(graph: string, index: string) { - return this.graphHookAction({ - listen: { - graph, - index - } - }); - } - - async getMore(): Promise { - const offset = getHarkSize(); - const count = 3; - await this.getSubset(offset, count, false); - return offset === getHarkSize(); - } - - async getSubset(offset:number, count:number, isArchive: boolean) { - const where = isArchive ? 'archive' : 'inbox'; - const data = await this.scry('hark-store', `/recent/${where}/${offset}/${count}`); - this.store.handleEvent({ data }); - } - - async getTimeSubset(start?: Date, end?: Date) { - const s = start ? dateToDa(start) : '-'; - const e = end ? dateToDa(end) : '-'; - const result = await this.scry('hark-hook', `/recent/${s}/${e}`); - this.store.handleEvent({ - data: result - }); - } -} diff --git a/pkg/interface/src/logic/api/index.ts b/pkg/interface/src/logic/api/index.ts new file mode 100644 index 000000000..5adf2451b --- /dev/null +++ b/pkg/interface/src/logic/api/index.ts @@ -0,0 +1,8 @@ +import Urbit from '@urbit/http-api'; +const api = new Urbit('', ''); +api.ship = window.ship; +// api.verbose = true; +// @ts-ignore TODO window typings +window.api = api; + +export default api; diff --git a/pkg/interface/src/logic/api/invite.ts b/pkg/interface/src/logic/api/invite.ts deleted file mode 100644 index 1588879fd..000000000 --- a/pkg/interface/src/logic/api/invite.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Serial } from '@urbit/api'; -import { StoreState } from '../store/type'; -import BaseApi from './base'; - -export default class InviteApi extends BaseApi { - accept(app: string, uid: Serial) { - return this.inviteAction({ - accept: { - term: app, - uid - } - }); - } - - decline(app: string, uid: Serial) { - return this.inviteAction({ - decline: { - term: app, - uid - } - }); - } - - private inviteAction(action) { - return this.action('invite-store', 'invite-action', action); - } -} diff --git a/pkg/interface/src/logic/api/launch.ts b/pkg/interface/src/logic/api/launch.ts deleted file mode 100644 index 013928188..000000000 --- a/pkg/interface/src/logic/api/launch.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { StoreState } from '../store/type'; -import BaseApi from './base'; - -export default class LaunchApi extends BaseApi { - add(name: string, tile = { basic : { title: '', linkedUrl: '', iconUrl: '' } }) { - return this.launchAction({ add: { name, tile } }); - } - - remove(name: string) { - return this.launchAction({ remove: name }); - } - - changeFirstTime(firstTime = true) { - return this.launchAction({ 'change-first-time': firstTime }); - } - - changeIsShown(name: string, isShown = true) { - return this.launchAction({ 'change-is-shown': { name, isShown } }); - } - - weather(location: string) { - return this.action('weather', 'json', location); - } - - private launchAction(data) { - return this.action('launch', 'launch-action', data); - } -} - diff --git a/pkg/interface/src/logic/api/local.ts b/pkg/interface/src/logic/api/local.ts deleted file mode 100644 index 240e2abd6..000000000 --- a/pkg/interface/src/logic/api/local.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { StoreState } from '../store/type'; -import BaseApi from './base'; - -export default class LocalApi extends BaseApi { - getBaseHash() { - this.scry('file-server', '/clay/base/hash').then((baseHash) => { - this.store.handleEvent({ data: { baseHash } }); - }); - } - - getRuntimeLag() { - return this.scry('launch', '/runtime-lag').then((runtimeLag) => { - this.store.handleEvent({ data: { runtimeLag } }); - }); - } -} diff --git a/pkg/interface/src/logic/api/metadata.ts b/pkg/interface/src/logic/api/metadata.ts deleted file mode 100644 index 97cb1471e..000000000 --- a/pkg/interface/src/logic/api/metadata.ts +++ /dev/null @@ -1,108 +0,0 @@ - -import { Association, Metadata, MetadataUpdatePreview, Path } from '@urbit/api'; -import { uxToHex } from '../lib/util'; -import { StoreState } from '../store/type'; -import BaseApi from './base'; - -export default class MetadataApi extends BaseApi { - metadataAdd(appName: string, resource: Path, group: Path, title: string, description: string, dateCreated: string, color: string, moduleName: string) { - const creator = `~${this.ship}`; - return this.metadataAction({ - add: { - group, - resource: { - resource, - 'app-name': appName - }, - metadata: { - title, - description, - color, - 'date-created': dateCreated, - creator, - config: { graph: moduleName }, - picture: '', - hidden: false, - preview: false, - vip: '' - } - } - }); - } - - remove(appName: string, resource: string, group: string) { - return this.metadataAction({ - remove: { - group, - resource: { - resource, - 'app-name': appName - } - } - }); - } - - update(association: Association, newMetadata: Partial) { - const metadata = { ...association.metadata, ...newMetadata }; - metadata.color = uxToHex(metadata.color); - return this.metadataAction({ - add: { - group: association.group, - resource: { - resource: association.resource, - 'app-name': association['app-name'] - }, - metadata - } - }); - } - - preview(group: string) { - return new Promise((resolve, reject) => { - const tempChannel: any = new (window as any).channel(); - let done = false; - - setTimeout(() => { - if(done) { - return; - } - done = true; - tempChannel.delete(); - reject(new Error('offline')); - }, 15000); - - tempChannel.subscribe(window.ship, 'metadata-pull-hook', `/preview${group}`, - (err) => { - console.error(err); - reject(err); - tempChannel.delete(); - }, - (ev: any) => { - if ('metadata-hook-update' in ev) { - done = true; - tempChannel.delete(); - const upd = ev['metadata-hook-update'].preview as MetadataUpdatePreview; - resolve(upd); - } else { - done = true; - tempChannel.delete(); - reject(new Error('no-permissions')); - } - }, - (quit) => { - tempChannel.delete(); - if(!done) { - reject(new Error('offline')); - } - }, - (a) => { - console.log(a); - } - ); - }); - } - - private metadataAction(data) { - return this.action('metadata-push-hook', 'metadata-update-1', data); - } -} diff --git a/pkg/interface/src/logic/api/s3.ts b/pkg/interface/src/logic/api/s3.ts deleted file mode 100644 index 14b998a14..000000000 --- a/pkg/interface/src/logic/api/s3.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { StoreState } from '../store/type'; -import BaseApi from './base'; - -export default class S3Api extends BaseApi { - setCurrentBucket(bucket: string) { - return this.s3Action({ 'set-current-bucket': bucket }); - } - - addBucket(bucket: string) { - return this.s3Action({ 'add-bucket': bucket }); - } - - removeBucket(bucket: string) { - return this.s3Action({ 'remove-bucket': bucket }); - } - - setEndpoint(endpoint: string) { - return this.s3Action({ 'set-endpoint': endpoint }); - } - - setAccessKeyId(accessKeyId: string) { - return this.s3Action({ 'set-access-key-id': accessKeyId }); - } - - setSecretAccessKey(secretAccessKey: string) { - return this.s3Action({ 'set-secret-access-key': secretAccessKey }); - } - - private s3Action(data: any) { - return this.action('s3-store', 's3-action', data); - } -} - diff --git a/pkg/interface/src/logic/api/settings.ts b/pkg/interface/src/logic/api/settings.ts deleted file mode 100644 index 47be601ad..000000000 --- a/pkg/interface/src/logic/api/settings.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { - Bucket, Key, - - SettingsUpdate, Value -} from '@urbit/api/settings'; -import { StoreState } from '../store/type'; -import BaseApi from './base'; - -export default class SettingsApi extends BaseApi { - private storeAction(action: SettingsUpdate): Promise { - return this.action('settings-store', 'settings-event', action); - } - - putBucket(key: Key, bucket: Bucket) { - return this.storeAction({ - 'put-bucket': { - 'bucket-key': key, - 'bucket': bucket - } - }); - } - - delBucket(key: Key) { - return this.storeAction({ - 'del-bucket': { - 'bucket-key': key - } - }); - } - - putEntry(buc: Key, key: Key, val: Value) { - return this.storeAction({ - 'put-entry': { - 'bucket-key': buc, - 'entry-key': key, - 'value': val - } - }); - } - - delEntry(buc: Key, key: Key) { - return this.storeAction({ - 'put-entry': { - 'bucket-key': buc, - 'entry-key': key - } - }); - } - - async getAll() { - const { all } = await this.scry('settings-store', '/all'); - this.store.handleEvent({ data: - { 'settings-data': { all } } - }); - } - - async getBucket(bucket: Key) { - const data: Record = await this.scry('settings-store', `/bucket/${bucket}`); - this.store.handleEvent({ data: { 'settings-data': { - 'bucket-key': bucket, - 'bucket': data.bucket - } } }); - } - - async getEntry(bucket: Key, entry: Key) { - const data: Record = await this.scry('settings-store', `/entry/${bucket}/${entry}`); - this.store.handleEvent({ data: { 'settings-data': { - 'bucket-key': bucket, - 'entry-key': entry, - 'entry': data.entry - } } }); - } -} diff --git a/pkg/interface/src/logic/api/term.ts b/pkg/interface/src/logic/api/term.ts deleted file mode 100644 index b5e6755a2..000000000 --- a/pkg/interface/src/logic/api/term.ts +++ /dev/null @@ -1,41 +0,0 @@ -import BaseApi from './base'; -import { StoreState } from '../store/type'; - -export type Bolt = - | string - | { aro: 'd' | 'l' | 'r' | 'u' } - | { bac: null } - | { del: null } - | { hit: { r: number, c: number } } - | { ret: null } - -export type Belt = - | Bolt - | { mod: { mod: 'ctl' | 'met' | 'hyp', key: Bolt } } - | { txt: Array }; - -export type Task = - | { belt: Belt } - | { blew: { w: number, h: number } } - | { flow: { term: string, apps: Array<{ who: string, app: string }> } } - | { hail: null } - | { hook: null } - -export default class TermApi extends BaseApi { - public sendBelt(session: string, belt: Belt) { - if (session === '') { - //TODO remove? reduntant, probably minimal perf gains - return this.action('herm', 'belt', belt); - } else { - return this.sendTask(session, { 'belt': belt }); - } - } - - public sendTask(session: string, task: Task) { - return this.action('herm', 'herm-task', { 'session': session, ...task }); - } - - public getSessions(): Promise> { - return this.scry>('herm', '/sessions'); - } -} diff --git a/pkg/interface/src/logic/lib/contact.ts b/pkg/interface/src/logic/lib/contact.ts new file mode 100644 index 000000000..b2f7e6c50 --- /dev/null +++ b/pkg/interface/src/logic/lib/contact.ts @@ -0,0 +1,25 @@ +import airlock from '~/logic/api'; +import _ from 'lodash'; +import { fetchIsAllowed } from '@urbit/api'; + +export async function disallowedShipsForOurContact( + ships: string[] +): Promise { + return _.compact( + await Promise.all( + ships.map(async (s) => { + const ship = `~${s}`; + if (s === window.ship) { + return null; + } + const allowed = await airlock.scry(fetchIsAllowed( + `~${window.ship}`, + 'personal', + ship, + true + )); + return allowed ? null : ship; + }) + ) + ); +} diff --git a/pkg/interface/src/logic/lib/gcpManager.ts b/pkg/interface/src/logic/lib/gcpManager.ts index 9710e82d9..d74dc79d7 100644 --- a/pkg/interface/src/logic/lib/gcpManager.ts +++ b/pkg/interface/src/logic/lib/gcpManager.ts @@ -12,14 +12,10 @@ // intrinsic expiry. // // -import GlobalApi from '../api/global'; import useStorageState from '../state/storage'; class GcpManager { - #api: GlobalApi | null = null; - - configure(api: GlobalApi) { - this.#api = api; + configure() { } #running = false; @@ -30,10 +26,6 @@ class GcpManager { console.warn('GcpManager already running'); return; } - if (!this.#api) { - console.error('GcpManager must have api set'); - return; - } this.#running = true; this.refreshLoop(); } @@ -63,7 +55,7 @@ class GcpManager { private refreshLoop() { if (!this.#configured) { - this.#api!.gcp.isConfigured() + useStorageState.getState().gcp.isConfigured() .then((configured) => { if (configured === undefined) { throw new Error('can\'t check whether GCP is configured?'); @@ -82,7 +74,7 @@ class GcpManager { }); return; } - this.#api!.gcp.getToken() + useStorageState.getState().gcp.getToken() .then(() => { const token = useStorageState.getState().gcp.token; if (token) { diff --git a/pkg/interface/src/logic/lib/graph.ts b/pkg/interface/src/logic/lib/graph.ts new file mode 100644 index 000000000..194c308ac --- /dev/null +++ b/pkg/interface/src/logic/lib/graph.ts @@ -0,0 +1,27 @@ +import { Graph } from '@urbit/api'; +import { BigInteger } from 'big-integer'; +import _ from 'lodash'; +import useMetadataState from '~/logic/state/metadata'; + +export function getNodeFromGraph(graph: Graph, index: BigInteger[]) { + return _.reduce( + index.slice(1), + (acc, val) => { + return acc?.children?.get(val); + }, + graph.get(index[0]) + ); +} + +export function getPostRoute( + graph: string, + index: BigInteger[], + thread = false +) { + const association = useMetadataState.getState().associations.graph[graph]; + const segment = thread ? 'thread' : 'replies'; + + return `/~landscape${association.group}/feed/${segment}/${index + .map(i => i.toString()) + .join('/')}`; +} diff --git a/pkg/interface/src/logic/lib/migrateSettings.ts b/pkg/interface/src/logic/lib/migrateSettings.ts deleted file mode 100644 index d35f6212b..000000000 --- a/pkg/interface/src/logic/lib/migrateSettings.ts +++ /dev/null @@ -1,68 +0,0 @@ -import useLocalState from '~/logic/state/local'; -import useSettingsState from '~/logic/state/settings'; -import { BackgroundConfig, RemoteContentPolicy } from '~/types'; -import GlobalApi from '../api/global'; - -const getBackgroundString = (bg: BackgroundConfig) => { - if (bg?.type === 'url') { - return bg.url; - } else if (bg?.type === 'color') { - return bg.color; - } else { - return ''; - } -}; - -export function useMigrateSettings(api: GlobalApi) { - const local = useLocalState(); - const { display, remoteContentPolicy, calm } = useSettingsState(); - - return async () => { - const promises: Promise[] = []; - - if (local.hideAvatars !== calm.hideAvatars) { - promises.push( - api.settings.putEntry('calm', 'hideAvatars', local.hideAvatars) - ); - } - - if (local.hideNicknames !== calm.hideNicknames) { - promises.push( - api.settings.putEntry('calm', 'hideNicknames', local.hideNicknames) - ); - } - - if ( - local?.background?.type && - display.background !== getBackgroundString(local.background) - ) { - promises.push( - api.settings.putEntry( - 'display', - 'background', - getBackgroundString(local.background) - ) - ); - promises.push( - api.settings.putEntry( - 'display', - 'backgroundType', - local.background?.type - ) - ); - } - - Object.keys(local.remoteContentPolicy).forEach((_key) => { - const key = _key as keyof RemoteContentPolicy; - const localVal = local.remoteContentPolicy[key]; - if (localVal !== remoteContentPolicy[key]) { - promises.push( - api.settings.putEntry('remoteContentPolicy', key, localVal) - ); - } - }); - - await Promise.all(promises); - localStorage.removeItem('localReducer'); - }; -} diff --git a/pkg/interface/src/logic/lib/useDrag.ts b/pkg/interface/src/logic/lib/useDrag.ts index e8c346ab4..58e14a9a2 100644 --- a/pkg/interface/src/logic/lib/useDrag.ts +++ b/pkg/interface/src/logic/lib/useDrag.ts @@ -1,4 +1,4 @@ -import { DragEvent, useCallback, useEffect, useState } from 'react'; +import { DragEvent, useCallback, useEffect, useState, useMemo } from 'react'; function validateDragEvent(e: DragEvent): FileList | File[] | true | null { const files: File[] = []; @@ -43,7 +43,7 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi } setDragging(true); }, - [setDragging] + [] ); const onDrop = useCallback( @@ -56,7 +56,7 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi e.preventDefault(); dragged(files, e); }, - [setDragging, dragged] + [dragged] ); const onDragOver = useCallback( @@ -77,7 +77,7 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi setDragging(false); } }, - [setDragging] + [] ); useEffect(() => { @@ -92,12 +92,12 @@ export function useFileDrag(dragged: (f: FileList | File[], e: DragEvent) => voi }; }, []); - const bind = { + const bind = useMemo(() => ({ onDragLeave, onDragOver, onDrop, onDragEnter - }; + }), [onDragEnter, onDragOver, onDrop, onDragEnter]); - return { bind, dragging }; + return useMemo(() => ({ bind, dragging }), [bind, dragging]); } diff --git a/pkg/interface/src/logic/lib/useLocalStorageState.ts b/pkg/interface/src/logic/lib/useLocalStorageState.ts index d64b9f6d0..e07db8d37 100644 --- a/pkg/interface/src/logic/lib/useLocalStorageState.ts +++ b/pkg/interface/src/logic/lib/useLocalStorageState.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useMemo, useEffect, useState } from 'react'; function retrieve(key: string, initial: T): T { const s = localStorage.getItem(key); @@ -12,26 +12,16 @@ function retrieve(key: string, initial: T): T { return initial; } -interface SetStateFunc { - (t: T): T; -} -// See microsoft/typescript#37663 for filed bug -type SetState = T extends any ? SetStateFunc : never; export function useLocalStorageState(key: string, initial: T): any { - const [state, _setState] = useState(() => retrieve(key, initial)); + const [state, setState] = useState(() => retrieve(key, initial)); useEffect(() => { - _setState(retrieve(key, initial)); + setState(retrieve(key, initial)); }, [key]); - const setState = useCallback( - (s: SetState) => { - const updated = typeof s === 'function' ? s(state) : s; - _setState(updated); - localStorage.setItem(key, JSON.stringify(updated)); - }, - [_setState, key, state] - ); + useEffect(() => { + localStorage.setItem(key, JSON.stringify(state)); + }, [state]); - return [state, setState] as const; + return useMemo(() => [state, setState] as const, [state, setState]); } diff --git a/pkg/interface/src/logic/lib/util.tsx b/pkg/interface/src/logic/lib/util.tsx index 12e638bf5..fda3afa91 100644 --- a/pkg/interface/src/logic/lib/util.tsx +++ b/pkg/interface/src/logic/lib/util.tsx @@ -523,3 +523,19 @@ export const favicon = () => { }); return svg; }; + +export function binaryIndexOf(arr: BigInteger[], target: BigInteger): number | undefined { + let leftBound = 0; + let rightBound = arr.length - 1; + while(leftBound <= rightBound) { + const halfway = Math.floor((leftBound + rightBound) / 2); + if(arr[halfway].greater(target)) { + leftBound = halfway + 1; + } else if (arr[halfway].lesser(target)) { + rightBound = halfway - 1; + } else { + return halfway; + } + } + return undefined; +} diff --git a/pkg/interface/src/logic/reducers/connection.ts b/pkg/interface/src/logic/reducers/connection.ts deleted file mode 100644 index 529376c04..000000000 --- a/pkg/interface/src/logic/reducers/connection.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Cage } from '~/types/cage'; -import { StoreState } from '../store/type'; - -type LocalState = Pick; - -export default class ConnectionReducer { - reduce(json: Cage, state: S) { - if('connection' in json && json.connection) { - console.log(`Conn: ${json.connection}`); - state.connection = json.connection; - } - } -} diff --git a/pkg/interface/src/logic/reducers/contact-update.ts b/pkg/interface/src/logic/reducers/contact-update.ts index 6776dc08a..7f4f7ea3b 100644 --- a/pkg/interface/src/logic/reducers/contact-update.ts +++ b/pkg/interface/src/logic/reducers/contact-update.ts @@ -1,7 +1,9 @@ import { ContactUpdate, deSig } from '@urbit/api'; import _ from 'lodash'; -import { reduceState } from '../state/base'; -import useContactState, { ContactState } from '../state/contact'; +import { BaseState } from '../state/base'; +import { ContactState as State } from '../state/contact'; + +type ContactState = State & BaseState; const initial = (json: ContactUpdate, state: ContactState): ContactState => { const data = _.get(json, 'initial', false); @@ -71,23 +73,18 @@ const setPublic = (json: ContactUpdate, state: ContactState): ContactState => { return state; }; -export const ContactReducer = (json) => { - const data: ContactUpdate = _.get(json, 'contact-update', false); - if (data) { - reduceState(useContactState, data, [ - initial, - add, - remove, - edit, - setPublic - ]); - } - - // TODO: better isolation - const res = _.get(json, 'resource', false); - if (res) { - useContactState.setState({ - nackedContacts: useContactState.getState().nackedContacts.add(`~${res.ship}`) - }); +export const reduceNacks = (json, state: ContactState): ContactState => { + const data = json?.resource; + if(data) { + state.nackedContacts.add(`~${data.res}`); } + return state; }; + +export const reduce = [ + initial, + add, + remove, + edit, + setPublic +]; diff --git a/pkg/interface/src/logic/reducers/gcp-reducer.ts b/pkg/interface/src/logic/reducers/gcp-reducer.ts deleted file mode 100644 index f809c1a9f..000000000 --- a/pkg/interface/src/logic/reducers/gcp-reducer.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Cage } from '~/types/cage'; -import type { GcpToken } from '../../types/gcp-state'; -import { reduceState } from '../state/base'; -import useStorageState, { StorageState } from '../state/storage'; - -export default class GcpReducer { - reduce(json: Cage) { - reduceState(useStorageState, json, [ - reduceToken - ]); - } -} - -const reduceToken = (json: Cage, state: StorageState): StorageState => { - const data = json['gcp-token']; - if (data) { - setToken(data, state); - } - return state; -}; - -const setToken = (data: any, state: StorageState): StorageState => { - if (isToken(data)) { - state.gcp.token = data; - } - return state; -}; - -const isToken = (token: any): token is GcpToken => { - return (typeof(token.accessKey) === 'string' && - typeof(token.expiresIn) === 'number'); -}; diff --git a/pkg/interface/src/logic/reducers/graph-update.ts b/pkg/interface/src/logic/reducers/graph-update.ts index da427a901..5c0aaae09 100644 --- a/pkg/interface/src/logic/reducers/graph-update.ts +++ b/pkg/interface/src/logic/reducers/graph-update.ts @@ -7,8 +7,10 @@ import BigIntArrayOrderedMap, { import bigInt, { BigInteger } from 'big-integer'; import produce from 'immer'; import _ from 'lodash'; -import { reduceState } from '../state/base'; -import useGraphState, { GraphState } from '../state/graph'; +import { BaseState, reduceState } from '../state/base'; +import useGraphState, { GraphState as State } from '../state/graph'; + +type GraphState = State & BaseState; const mapifyChildren = (children) => { return new BigIntOrderedMap().gas( @@ -445,6 +447,12 @@ const removePosts = (json, state: GraphState): GraphState => { return state; }; +export const reduceDm = [ + acceptOrRejectDm, + pendings, + setScreen +]; + export const GraphReducer = (json) => { const data = _.get(json, 'graph-update', false); @@ -471,13 +479,4 @@ export const GraphReducer = (json) => { if (thread) { reduceState(useGraphState, thread, [addNodesThread]); } - const dm = _.get(json, 'dm-hook-action', false); - if(dm) { - console.log(dm); - reduceState(useGraphState, dm, [ - acceptOrRejectDm, - pendings, - setScreen - ]); - } }; diff --git a/pkg/interface/src/logic/reducers/group-update.ts b/pkg/interface/src/logic/reducers/group-update.ts index eb4d242ae..a008b64c6 100644 --- a/pkg/interface/src/logic/reducers/group-update.ts +++ b/pkg/interface/src/logic/reducers/group-update.ts @@ -9,8 +9,10 @@ import { import _ from 'lodash'; import { Cage } from '~/types/cage'; import { resourceAsPath } from '../lib/util'; -import { reduceState } from '../state/base'; -import useGroupState, { GroupState } from '../state/group'; +import { BaseState } from '../state/base'; +import { GroupState as State } from '../state/group'; + +type GroupState = BaseState & State; function decodeGroup(group: Enc): Group { const members = new Set(group.members); @@ -54,21 +56,7 @@ function decodeTags(tags: Enc): Tags { export default class GroupReducer { reduce(json: Cage) { - const data = json.groupUpdate; - if (data) { - reduceState(useGroupState, data, [ - initial, - addMembers, - addTag, - removeMembers, - initialGroup, - removeTag, - addGroup, - removeGroup, - changePolicy, - expose - ]); - } + return; } } const initial = (json: GroupUpdate, state: GroupState): GroupState => { @@ -175,24 +163,6 @@ const removeTag = (json: GroupUpdate, state: GroupState): GroupState => { return state; }; -const changePolicy = (json: GroupUpdate, state: GroupState): GroupState => { - if ('changePolicy' in json && state) { - const { resource, diff } = json.changePolicy; - const resourcePath = resourceAsPath(resource); - const policy = state.groups[resourcePath].policy; - if ('open' in policy && 'open' in diff) { - openChangePolicy(diff.open, policy); - } else if ('invite' in policy && 'invite' in diff) { - inviteChangePolicy(diff.invite, policy); - } else if ('replace' in diff) { - state.groups[resourcePath].policy = diff.replace; - } else { - console.log('bad policy diff'); - } - } - return state; -}; - const expose = (json: GroupUpdate, state: GroupState): GroupState => { if( 'expose' in json && state) { const { resource } = json.expose; @@ -243,3 +213,33 @@ const openChangePolicy = (diff: OpenPolicyDiff, policy: OpenPolicy) => { console.log('bad policy change'); } }; + +const changePolicy = (json: GroupUpdate, state: GroupState): GroupState => { + if ('changePolicy' in json && state) { + const { resource, diff } = json.changePolicy; + const resourcePath = resourceAsPath(resource); + const policy = state.groups[resourcePath].policy; + if ('open' in policy && 'open' in diff) { + openChangePolicy(diff.open, policy); + } else if ('invite' in policy && 'invite' in diff) { + inviteChangePolicy(diff.invite, policy); + } else if ('replace' in diff) { + state.groups[resourcePath].policy = diff.replace; + } else { + console.log('bad policy diff'); + } + } + return state; +}; +export const reduce = [ + initial, + addMembers, + addTag, + removeMembers, + initialGroup, + removeTag, + addGroup, + removeGroup, + changePolicy, + expose +]; diff --git a/pkg/interface/src/logic/reducers/group-view.ts b/pkg/interface/src/logic/reducers/group-view.ts index fae75a0a6..2f694efa7 100644 --- a/pkg/interface/src/logic/reducers/group-view.ts +++ b/pkg/interface/src/logic/reducers/group-view.ts @@ -1,6 +1,7 @@ -import { GroupUpdate } from '@urbit/api/groups'; -import { reduceState } from '../state/base'; -import useGroupState, { GroupState } from '../state/group'; +import { BaseState } from '../state/base'; +import { GroupState as State } from '../state/group'; + +type GroupState = State & BaseState; const initial = (json: any, state: GroupState): GroupState => { const data = json.initial; @@ -41,14 +42,9 @@ const hide = (json: any, state: GroupState) => { return state; }; -export const GroupViewReducer = (json: any) => { - const data = json['group-view-update']; - if (data) { - reduceState(useGroupState, data, [ - progress, - hide, - started, - initial - ]); - } -}; +export const reduce = [ + progress, + hide, + started, + initial +]; diff --git a/pkg/interface/src/logic/reducers/hark-update.ts b/pkg/interface/src/logic/reducers/hark-update.ts index beddf4aee..958af7998 100644 --- a/pkg/interface/src/logic/reducers/hark-update.ts +++ b/pkg/interface/src/logic/reducers/hark-update.ts @@ -8,8 +8,10 @@ import _ from 'lodash'; import { compose } from 'lodash/fp'; import { makePatDa } from '~/logic/lib/util'; import { describeNotification, getReferent } from '../lib/hark'; -import { reduceState } from '../state/base'; -import useHarkState, { HarkState } from '../state/hark'; +import { BaseState } from '../state/base'; +import { HarkState as State } from '../state/hark'; + +type HarkState = State & BaseState; function calculateCount(json: any, state: HarkState) { state.notificationsCount = Object.keys(state.unreadNotes).length; @@ -150,6 +152,9 @@ function unreads(json: any, state: HarkState): HarkState { data.forEach(({ index, stats }) => { const { unreads, notifications, last } = stats; updateNotificationStats(state, index, 'last', () => last); + if(index.graph.graph === '/ship/~hastuc-dibtux/test-book-7531') { + console.log(index, stats); + } _.each(notifications, ({ time, index }) => { if(!time) { addNotificationToUnread(state, index); @@ -182,7 +187,8 @@ function clearState(state: HarkState): HarkState { graph: {}, group: {} }, - notificationsCount: 0 + notificationsCount: 0, + unreadNotes: {} }; Object.assign(state, initialState); @@ -195,6 +201,9 @@ function updateUnreadCount(state: HarkState, index: NotifIndex, count: (c: numbe } const property = [index.graph.graph, index.graph.index, 'unreads']; const curr = _.get(state.unreads.graph, property, 0); + if(typeof curr !== 'number') { + return state; + } const newCount = count(curr); _.set(state.unreads.graph, property, newCount); return state; @@ -263,7 +272,7 @@ function added(json: any, state: HarkState): HarkState { const [fresh] = _.partition(state.unreadNotes, ({ index: idx }) => !notifIdxEqual(index, idx)); state.unreadNotes = [...fresh, { index, notification }]; - if ('Notification' in window && !useHarkState.getState().doNotDisturb) { + if ('Notification' in window && !state.doNotDisturb) { const description = describeNotification(data); const referent = getReferent(data); new Notification(`${description} ${referent}`, { @@ -412,37 +421,17 @@ export function reduce(data, state) { return reducer(state); } -export const HarkReducer = (json: any) => { - const data = _.get(json, 'harkUpdate', false); - if (data) { - console.log(data); - reduceState(useHarkState, data, [reduce]); - } - 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 - ]); - } -}; +export const reduceGraph = [ + graphInitial, + graphIgnore, + graphListen, + graphWatchSelf, + graphMentions +]; + +export const reduceGroup = [ + groupInitial, + groupListen, + groupIgnore +]; diff --git a/pkg/interface/src/logic/reducers/invite-update.ts b/pkg/interface/src/logic/reducers/invite-update.ts index 8ab979b86..6f08ec77f 100644 --- a/pkg/interface/src/logic/reducers/invite-update.ts +++ b/pkg/interface/src/logic/reducers/invite-update.ts @@ -1,24 +1,9 @@ import { InviteUpdate } from '@urbit/api/invite'; import _ from 'lodash'; -import { Cage } from '~/types/cage'; -import { reduceState } from '../state/base'; -import useInviteState, { InviteState } from '../state/invite'; +import { BaseState } from '../state/base'; +import { InviteState as State } from '../state/invite'; -export default class InviteReducer { - reduce(json: Cage) { - const data = json['invite-update']; - if (data) { - reduceState(useInviteState, data, [ - initial, - create, - deleteInvite, - invite, - accepted, - decline - ]); - } - } -} +type InviteState = State & BaseState; const initial = (json: InviteUpdate, state: InviteState): InviteState => { const data = _.get(json, 'initial', false); @@ -67,3 +52,12 @@ const decline = (json: InviteUpdate, state: InviteState): InviteState => { } return state; }; + +export const reduce = [ + initial, + create, + deleteInvite, + invite, + accepted, + decline +]; diff --git a/pkg/interface/src/logic/reducers/launch-update.ts b/pkg/interface/src/logic/reducers/launch-update.ts index 204673343..59216c238 100644 --- a/pkg/interface/src/logic/reducers/launch-update.ts +++ b/pkg/interface/src/logic/reducers/launch-update.ts @@ -1,55 +1,9 @@ import _ from 'lodash'; -import { Cage } from '~/types/cage'; -import { LaunchUpdate, WeatherState } from '~/types/launch-update'; -import { reduceState } from '../state/base'; -import useLaunchState, { LaunchState } from '../state/launch'; +import { LaunchUpdate } from '~/types/launch-update'; +import { LaunchState as State } from '../state/launch'; +import { BaseState } from '../state/base'; -export default class LaunchReducer { - reduce(json: Cage) { - const data = _.get(json, 'launch-update', false); - if (data) { - reduceState(useLaunchState, data, [ - initial, - changeFirstTime, - changeOrder, - changeFirstTime, - changeIsShown - ]); - } - - const weatherData: WeatherState | boolean | Record = _.get(json, 'weather', false); - if (weatherData) { - useLaunchState.getState().set((state) => { - // @ts-ignore investigate zustand types - state.weather = weatherData; - }); - } - - const locationData = _.get(json, 'location', false); - if (locationData) { - useLaunchState.getState().set((state) => { - // @ts-ignore investigate zustand types - state.userLocation = locationData; - }); - } - - const baseHash = _.get(json, 'baseHash', false); - if (baseHash) { - useLaunchState.getState().set((state) => { - // @ts-ignore investigate zustand types - state.baseHash = baseHash; - }); - } - - const runtimeLag = _.get(json, 'runtimeLag', null); - if (runtimeLag !== null) { - useLaunchState.getState().set(state => { - // @ts-ignore investigate zustand types - state.runtimeLag = runtimeLag; - }); - } - } -} +type LaunchState = State & BaseState; export const initial = (json: LaunchUpdate, state: LaunchState): LaunchState => { const data = _.get(json, 'initial', false); @@ -87,3 +41,11 @@ export const changeIsShown = (json: LaunchUpdate, state: LaunchState): LaunchSta } return state; }; + +export const reduce = [ + initial, + changeFirstTime, + changeOrder, + changeFirstTime, + changeIsShown +]; diff --git a/pkg/interface/src/logic/reducers/metadata-update.ts b/pkg/interface/src/logic/reducers/metadata-update.ts index 9049a19f3..5cf6671af 100644 --- a/pkg/interface/src/logic/reducers/metadata-update.ts +++ b/pkg/interface/src/logic/reducers/metadata-update.ts @@ -1,32 +1,17 @@ import { MetadataUpdate } from '@urbit/api/metadata'; import _ from 'lodash'; import { Cage } from '~/types/cage'; -import { reduceState } from '../state/base'; -import useMetadataState, { MetadataState } from '../state/metadata'; +import { BaseState } from '../state/base'; +import { MetadataState as State } from '../state/metadata'; + +type MetadataState = State & BaseState; export default class MetadataReducer { reduce(json: Cage) { - const data = json['metadata-update']; - if (data) { - reduceState(useMetadataState, data, [ - associations, - add, - update, - remove, - groupInitial - ]); - } + return; } } -const groupInitial = (json: MetadataUpdate, state: MetadataState): MetadataState => { - const data = _.get(json, 'initial-group', false); - if(data) { - associations(data, state); - } - return state; -}; - const associations = (json: MetadataUpdate, state: MetadataState): MetadataState => { const data = _.get(json, 'associations', false); if (data) { @@ -69,6 +54,14 @@ const add = (json: MetadataUpdate, state: MetadataState): MetadataState => { return state; }; +const groupInitial = (json: MetadataUpdate, state: MetadataState): MetadataState => { + const data = _.get(json, 'initial-group', false); + if(data) { + associations(data, state); + } + return state; +}; + const update = (json: MetadataUpdate, state: MetadataState): MetadataState => { const data = _.get(json, 'update-metadata', false); if (data) { @@ -103,3 +96,12 @@ const remove = (json: MetadataUpdate, state: MetadataState): MetadataState => { } return state; }; + +export const reduce = [ + associations, + add, + update, + remove, + groupInitial +]; + diff --git a/pkg/interface/src/logic/reducers/s3-update.ts b/pkg/interface/src/logic/reducers/s3-update.ts index b67ff3d60..381bbe81c 100644 --- a/pkg/interface/src/logic/reducers/s3-update.ts +++ b/pkg/interface/src/logic/reducers/s3-update.ts @@ -1,26 +1,9 @@ import _ from 'lodash'; -import { Cage } from '~/types/cage'; import { S3Update } from '~/types/s3-update'; -import { reduceState } from '../state/base'; -import useStorageState, { StorageState } from '../state/storage'; +import { BaseState } from '../state/base'; +import { StorageState as State } from '../state/storage'; -export default class S3Reducer { - reduce(json: Cage) { - const data = _.get(json, 's3-update', false); - if (data) { - reduceState(useStorageState, data, [ - credentials, - configuration, - currentBucket, - addBucket, - removeBucket, - endpoint, - accessKeyId, - secretAccessKey - ]); - } - } -} +type StorageState = State & BaseState; const credentials = (json: S3Update, state: StorageState): StorageState => { const data = _.get(json, 'credentials', false); @@ -89,3 +72,14 @@ const secretAccessKey = (json: S3Update, state: StorageState): StorageState => { } return state; }; + +export const reduce = [ + credentials, + configuration, + currentBucket, + addBucket, + removeBucket, + endpoint, + accessKeyId, + secretAccessKey +]; diff --git a/pkg/interface/src/logic/reducers/settings-update.ts b/pkg/interface/src/logic/reducers/settings-update.ts index 1a5e165bb..7895c767c 100644 --- a/pkg/interface/src/logic/reducers/settings-update.ts +++ b/pkg/interface/src/logic/reducers/settings-update.ts @@ -1,88 +1,81 @@ import { SettingsUpdate } from '@urbit/api/settings'; import _ from 'lodash'; -import useSettingsState, { SettingsState } from '~/logic/state/settings'; -import { reduceState } from '../state/base'; +import { SettingsState as State } from '~/logic/state/settings'; +import { BaseState } from '../state/base'; -export default class SettingsReducer { - reduce(json: any) { - let data = json['settings-event']; - if (data) { - reduceState(useSettingsState, data, [ - this.putBucket, - this.delBucket, - this.putEntry, - this.delEntry - ]); - } - data = json['settings-data']; - if (data) { - reduceState(useSettingsState, data, [ - this.getAll, - this.getBucket, - this.getEntry - ]); - } - } +type SettingsState = State & BaseState; - putBucket(json: SettingsUpdate, state: SettingsState): SettingsState { - const data = _.get(json, 'put-bucket', false); - if (data) { - state[data['bucket-key']] = data.bucket; - } - return state; - } - - delBucket(json: SettingsUpdate, state: SettingsState): SettingsState { - const data = _.get(json, 'del-bucket', false); - if (data) { - delete state[data['bucket-key']]; - } - return state; - } - - putEntry(json: SettingsUpdate, state: any): SettingsState { - const data: Record = _.get(json, 'put-entry', false); - if (data) { - if (!state[data['bucket-key']]) { - state[data['bucket-key']] = {}; - } - state[data['bucket-key']][data['entry-key']] = data.value; - } - return state; - } - - delEntry(json: SettingsUpdate, state: any): SettingsState { - const data = _.get(json, 'del-entry', false); - if (data) { - delete state[data['bucket-key']][data['entry-key']]; - } - return state; - } - - getAll(json: any, state: SettingsState): SettingsState { - const data = _.get(json, 'all'); - if(data) { - _.mergeWith(state, data, (obj, src) => _.isArray(src) ? src : undefined); - } - return state; - } - - getBucket(json: any, state: SettingsState): SettingsState { - const key = _.get(json, 'bucket-key', false); - const bucket = _.get(json, 'bucket', false); - if (key && bucket) { - state[key] = bucket; - } - return state; - } - - getEntry(json: any, state: any) { - const bucketKey = _.get(json, 'bucket-key', false); - const entryKey = _.get(json, 'entry-key', false); - const entry = _.get(json, 'entry', false); - if (bucketKey && entryKey && entry) { - state[bucketKey][entryKey] = entry; - } - return state; +function putBucket(json: SettingsUpdate, state: SettingsState): SettingsState { + const data = _.get(json, 'put-bucket', false); + if (data) { + state[data['bucket-key']] = data.bucket; } + return state; } + +function delBucket(json: SettingsUpdate, state: SettingsState): SettingsState { + const data = _.get(json, 'del-bucket', false); + if (data) { + delete state[data['bucket-key']]; + } + return state; +} + +function putEntry(json: SettingsUpdate, state: any): SettingsState { + const data: Record = _.get(json, 'put-entry', false); + if (data) { + if (!state[data['bucket-key']]) { + state[data['bucket-key']] = {}; + } + state[data['bucket-key']][data['entry-key']] = data.value; + } + return state; +} + +function delEntry(json: SettingsUpdate, state: any): SettingsState { + const data = _.get(json, 'del-entry', false); + if (data) { + delete state[data['bucket-key']][data['entry-key']]; + } + return state; +} + +function getAll(json: any, state: SettingsState): SettingsState { + const data = _.get(json, 'all'); + if(data) { + _.mergeWith(state, data, (obj, src) => _.isArray(src) ? src : undefined); + } + return state; +} + +function getBucket(json: any, state: SettingsState): SettingsState { + const key = _.get(json, 'bucket-key', false); + const bucket = _.get(json, 'bucket', false); + if (key && bucket) { + state[key] = bucket; + } + return state; +} + +function getEntry(json: any, state: any) { + const bucketKey = _.get(json, 'bucket-key', false); + const entryKey = _.get(json, 'entry-key', false); + const entry = _.get(json, 'entry', false); + if (bucketKey && entryKey && entry) { + state[bucketKey][entryKey] = entry; + } + return state; +} + +export const reduceUpdate = [ + putBucket, + delBucket, + putEntry, + delEntry +]; + +export const reduceScry = [ + getAll, + getBucket, + getEntry +]; diff --git a/pkg/interface/src/logic/state/base.ts b/pkg/interface/src/logic/state/base.ts index 543cb7e40..cf7e3a0a6 100644 --- a/pkg/interface/src/logic/state/base.ts +++ b/pkg/interface/src/logic/state/base.ts @@ -1,8 +1,11 @@ import { applyPatches, Patch, produceWithPatches, setAutoFreeze, enablePatches } from 'immer'; import { compose } from 'lodash/fp'; import _ from 'lodash'; -import create, { UseStore } from 'zustand'; +import create, { GetState, SetState, UseStore } from 'zustand'; import { persist } from 'zustand/middleware'; +import Urbit, { SubscriptionRequestInterface } from '@urbit/http-api'; +import { Poke } from '@urbit/api'; +import airlock from '~/logic/api'; setAutoFreeze(false); enablePatches(); @@ -44,6 +47,18 @@ export const reduceState = < }); }; +export const reduceStateN = < + S extends {}, + U +>( + state: S & BaseState, + data: U, + reducers: ((data: U, state: S & BaseState) => S & BaseState)[] +): void => { + const reducer = compose(reducers.map(r => sta => r(data, sta))); + state.set(reducer); +}; + export const optReduceState = ( state: UseStore>, data: U, @@ -74,17 +89,34 @@ export interface BaseState { patches: { [id: string]: Patch[]; }; - set: (fn: (state: BaseState) => void) => void; + set: (fn: (state: StateType & BaseState) => void) => void; addPatch: (id: string, ...patch: Patch[]) => void; removePatch: (id: string) => void; - optSet: (fn: (state: BaseState) => void) => string; + optSet: (fn: (state: StateType & BaseState) => void) => string; + initialize: (api: Urbit) => void; +} + +export function createSubscription(app: string, path: string, e: (data: any) => void): SubscriptionRequestInterface { + const request = { + app, + path, + event: e, + err: () => {}, + quit: () => {} + }; + // TODO: err, quit handling (resubscribe?) + return request; } export const createState = ( name: string, - properties: T, - blacklist: (keyof BaseState | keyof T)[] = [] + properties: T | ((set: SetState>, get: GetState>) => T), + blacklist: (keyof BaseState | keyof T)[] = [], + subscriptions: ((set: SetState>, get: GetState>) => SubscriptionRequestInterface)[] = [] ): UseStore> => create>(persist>((set, get) => ({ + initialize: (api: Urbit) => { + subscriptions.forEach(sub => api.subscribe(sub(set, get))); + }, // @ts-ignore investigate zustand types set: fn => stateSetter(fn, set, get), optSet: (fn) => { @@ -105,7 +137,7 @@ export const createState = ( return { ...applyPatches(state, applying), patches: _.omit(state.patches, id) }; }); }, - ...properties + ...(typeof properties === 'function' ? (properties as any)(set, get) : properties) }), { blacklist, name: stateStorageKey(name), @@ -125,3 +157,17 @@ export async function doOptimistically(state: UseStore(state: UseStore>, poke: Poke, reduce: ((a: A, fn: S & BaseState) => S & BaseState)[]) { + let num: string | undefined = undefined; + try { + num = optReduceState(state, poke.json, reduce); + await airlock.poke(poke); + 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 1f48833cf..4646edab5 100644 --- a/pkg/interface/src/logic/state/contact.ts +++ b/pkg/interface/src/logic/state/contact.ts @@ -1,37 +1,49 @@ -import { Contact, Patp, Rolodex } from '@urbit/api'; +import { Contact, deSig, Patp, Rolodex } from '@urbit/api'; import { useCallback } from 'react'; -import { BaseState, createState } from './base'; +import _ from 'lodash'; +import { reduce, reduceNacks } from '../reducers/contact-update'; +import { + createState, + createSubscription, + reduceStateN +} from './base'; -export interface ContactState extends BaseState { +export interface ContactState { contacts: Rolodex; isContactPublic: boolean; nackedContacts: Set; - // fetchIsAllowed: (entity, name, ship, personal) => Promise; } // @ts-ignore investigate zustand types -const useContactState = createState('Contact', { - contacts: {}, - nackedContacts: new Set(), - isContactPublic: false - // fetchIsAllowed: async ( - // entity, - // name, - // ship, - // personal - // ): Promise => { - // const isPersonal = personal ? 'true' : 'false'; - // const api = useApi(); - // return api.scry({ - // app: 'contact-store', - // path: `/is-allowed/${entity}/${name}/${ship}/${isPersonal}` - // }); - // }, -}, ['nackedContacts']); +const useContactState = createState( + 'Contact', + { + contacts: {}, + nackedContacts: new Set(), + isContactPublic: false + }, + ['nackedContacts'], + [ + (set, get) => + createSubscription('contact-pull-hook', '/nacks', (e) => { + const data = e?.resource; + if (data) { + reduceStateN(get(), data, [reduceNacks]); + } + }), + (set, get) => + createSubscription('contact-store', '/all', (e) => { + const data = _.get(e, 'contact-update', false); + if (data) { + reduceStateN(get(), data, reduce); + } + }) + ] +); export function useContact(ship: string) { return useContactState( - useCallback(s => s.contacts[ship] as Contact | null, [ship]) + useCallback(s => s.contacts[`~${deSig(ship)}`] as Contact | null, [ship]) ); } diff --git a/pkg/interface/src/logic/state/graph.ts b/pkg/interface/src/logic/state/graph.ts index 7b59d006d..4618186b3 100644 --- a/pkg/interface/src/logic/state/graph.ts +++ b/pkg/interface/src/logic/state/graph.ts @@ -1,11 +1,16 @@ import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap'; import { patp2dec } from 'urbit-ob'; +import shallow from 'zustand/shallow'; -import { Association, deSig, GraphNode, Graphs, FlatGraphs, resourceFromPath, ThreadGraphs } from '@urbit/api'; +import { Association, deSig, GraphNode, Graphs, FlatGraphs, resourceFromPath, ThreadGraphs, getGraph, getShallowChildren } from '@urbit/api'; import { useCallback } from 'react'; -import { BaseState, createState } from './base'; +import { createState, createSubscription, reduceStateN } from './base'; +import airlock from '~/logic/api'; +import { addDmMessage, addPost, Content, getDeepOlderThan, getFirstborn, getNewest, getNode, getOlderSiblings, getYoungerSiblings, markPending, Post, addNode, GraphNodePoke } from '@urbit/api/graph'; +import { GraphReducer, reduceDm } from '../reducers/graph-update'; +import _ from 'lodash'; -export interface GraphState extends BaseState { +export interface GraphState { graphs: Graphs; graphKeys: Set; looseNodes: { @@ -19,18 +24,20 @@ export interface GraphState extends BaseState { pendingDms: Set; screening: boolean; graphTimesentMap: Record; - // getKeys: () => Promise; - // getTags: () => Promise; - // getTagQueries: () => Promise; - // getGraph: (ship: string, resource: string) => Promise; - // getNewest: (ship: string, resource: string, count: number, index?: string) => Promise; - // getOlderSiblings: (ship: string, resource: string, count: number, index?: string) => Promise; - // getYoungerSiblings: (ship: string, resource: string, count: number, index?: string) => Promise; - // getGraphSubset: (ship: string, resource: string, start: string, end: string) => Promise; - // getNode: (ship: string, resource: string, index: string) => Promise; + getDeepOlderThan: (ship: string, name: string, count: number, start?: string) => Promise; + getNewest: (ship: string, resource: string, count: number, index?: string) => Promise; + getOlderSiblings: (ship: string, resource: string, count: number, index?: string) => Promise; + getYoungerSiblings: (ship: string, resource: string, count: number, index?: string) => Promise; + getNode: (ship: string, resource: string, index: string) => Promise; + getFirstborn: (ship: string, resource: string, index: string) => Promise; + getGraph: (ship: string, name: string) => Promise; + addDmMessage: (ship: string, contents: Content[]) => Promise; + addPost: (ship: string, name: string, post: Post) => Promise; + + addNode: (ship: string, name: string, post: GraphNodePoke) => Promise; } // @ts-ignore investigate zustand types -const useGraphState = createState('Graph', { +const useGraphState = createState('Graph', (set, get) => ({ graphs: {}, flatGraphs: {}, threadGraphs: {}, @@ -39,7 +46,101 @@ const useGraphState = createState('Graph', { pendingIndices: {}, graphTimesentMap: {}, pendingDms: new Set(), - screening: false + screening: false, + addDmMessage: async (ship: string, contents: Content[]) => { + const promise = airlock.poke(addDmMessage(window.ship, ship, contents)); + const { json } = addDmMessage(window.ship, ship, contents); + markPending(json['add-nodes'].nodes); + json['add-nodes'].resource.ship = json['add-nodes'].resource.ship.slice(1); + GraphReducer({ + 'graph-update': json + }); + await promise; + }, + addPost: async (ship, name, post) => { + const promise = airlock.thread(addPost(ship, name, post)); + const { body } = addPost(ship, name, post); + markPending(body['add-nodes'].nodes); + body['add-nodes'].resource.ship = body['add-nodes'].resource.ship.slice(1); + GraphReducer({ + 'graph-update': body, + 'graph-update-flat': body, + 'graph-update-thread': body + }); + await promise; + }, + addNode: async (ship, name, node) => { + const promise = airlock.thread(addNode(ship, name, node)); + const { body } = addNode(ship, name, node); + markPending(body['add-nodes'].nodes); + body['add-nodes'].resource.ship = body['add-nodes'].resource.ship.slice(1); + GraphReducer({ + 'graph-update': body, + 'graph-update-flat': body, + 'graph-update-thread': body + }); + await promise; + }, + getDeepOlderThan: async (ship, name, count, start) => { + const data = await airlock.scry(getDeepOlderThan(ship, name, count, start)); + + data['graph-update'].fetch = true; + const node = data['graph-update']; + GraphReducer({ + 'graph-update': node, + 'graph-update-flat': node + }); + }, + + getFirstborn: async (ship, name,index) => { + const data = await airlock.scry(getFirstborn(ship, name, index)); + data['graph-update'].fetch = true; + const node = data['graph-update']; + GraphReducer({ + 'graph-update-thread': { + index, + ...node + }, + 'graph-update': node + }); + }, + getNode: async (ship: string, name: string, index: string) => { + const data = await airlock.scry(getNode(ship, name, index)); + data['graph-update'].fetch = true; + const node = data['graph-update']; + GraphReducer({ + 'graph-update-loose': node + }); + }, + getOlderSiblings: async (ship: string, name: string, count: number, index: string) => { + const data = await airlock.scry(getOlderSiblings(ship, name, count, index)); + data['graph-update'].fetch = true; + GraphReducer(data); + }, + getYoungerSiblings: async (ship: string, name: string, count: number, index: string) => { + const data = await airlock.scry(getYoungerSiblings(ship, name, count, index)); + data['graph-update'].fetch = true; + GraphReducer(data); + }, + getNewest: async ( + ship: string, + name: string, + count: number, + index = '' + ) => { + const data = await airlock.scry(getNewest(ship, name, count, index)); + data['graph-update'].fetch = true; + GraphReducer(data); + }, + getGraph: async (ship, name) => { + const data = await airlock.scry(getGraph(ship, name)); + GraphReducer(data); + }, + getShallowChildren: async (ship: string, name: string, index = '') => { + const data = await airlock.scry(getShallowChildren(ship, name, index)); + data['graph-update'].fetch = true; + GraphReducer(data); + } // getKeys: async () => { // const api = useApi(); // const keys = await api.scry({ @@ -72,19 +173,6 @@ const useGraphState = createState('Graph', { // }); // graphReducer(graph); // }, - // getNewest: async ( - // ship: string, - // resource: string, - // count: number, - // index: string = '' - // ) => { - // const api = useApi(); - // const data = await api.scry({ - // app: 'graph-store', - // path: `/newest/${ship}/${resource}/${count}${index}` - // }); - // graphReducer(data); - // }, // getOlderSiblings: async ( // ship: string, // resource: string, @@ -139,7 +227,7 @@ const useGraphState = createState('Graph', { // }); // graphReducer(node); // }, -}, [ +}), [ 'graphs', 'graphKeys', 'looseNodes', @@ -147,6 +235,21 @@ const useGraphState = createState('Graph', { 'flatGraphs', 'threadGraphs', 'pendingDms' +], [ + (set, get) => createSubscription('graph-store', '/updates', (e) => { + GraphReducer(e); + }), + (set, get) => createSubscription('graph-store', '/keys', (e) => { + GraphReducer(e); + }), + + (set, get) => createSubscription('dm-hook', '/updates', (e) => { + const j = _.get(e, 'dm-hook-action', false); + if(j) { + reduceStateN(get(), j, reduceDm); + } + }) + ]); export function useGraph(ship: string, name: string) { @@ -176,7 +279,11 @@ export function useGraphTimesentMap(ship: string, name: string) { useCallback(s => s.graphTimesentMap[`${deSig(ship)}/${name}`], [ship, name]) ); } +const emptyObject = {}; +export function useGraphTimesent(key: string) { + return useGraphState(useCallback(s => s.graphTimesentMap[key] || emptyObject, [key]), shallow); +} export function useGraphForAssoc(association: Association) { const { resource } = association; const { ship, name } = resourceFromPath(resource); diff --git a/pkg/interface/src/logic/state/group.ts b/pkg/interface/src/logic/state/group.ts index 5f7324124..6d5cfba80 100644 --- a/pkg/interface/src/logic/state/group.ts +++ b/pkg/interface/src/logic/state/group.ts @@ -1,27 +1,56 @@ import { Association, Group, JoinRequests } from '@urbit/api'; import { useCallback } from 'react'; -import { BaseState, createState } from './base'; +import { reduce } from '../reducers/group-update'; +import _ from 'lodash'; +import { reduce as reduceView } from '../reducers/group-view'; +import { + createState, + createSubscription, + reduceStateN +} from './base'; -export interface GroupState extends BaseState { +export interface GroupState { groups: { [group: string]: Group; - } + }; pendingJoin: JoinRequests; } // @ts-ignore investigate zustand types -const useGroupState = createState('Group', { - groups: {}, - pendingJoin: {} -}, ['groups']); +const useGroupState = createState( + 'Group', + { + groups: {}, + pendingJoin: {} + }, + ['groups'], + [ + (set, get) => + createSubscription('group-store', '/groups', (e) => { + if ('groupUpdate' in e) { + reduceStateN(get(), e.groupUpdate, reduce); + } + }), + (set, get) => createSubscription('group-view', '/all', (e) => { + const data = _.get(e, 'group-view-update', false); + if (data) { + reduceStateN(get(), data, reduceView); + } + }) + ] +); export function useGroup(group: string) { - return useGroupState(useCallback(s => s.groups[group] as Group | undefined, [group])); + return useGroupState( + useCallback(s => s.groups[group] as Group | undefined, [group]) + ); } export function useGroupForAssoc(association: Association) { return useGroupState( - useCallback(s => s.groups[association.group] as Group | undefined, [association]) + useCallback(s => s.groups[association.group] as Group | undefined, [ + association + ]) ); } diff --git a/pkg/interface/src/logic/state/hark.ts b/pkg/interface/src/logic/state/hark.ts index b162c7899..5f6487998 100644 --- a/pkg/interface/src/logic/state/hark.ts +++ b/pkg/interface/src/logic/state/hark.ts @@ -1,18 +1,28 @@ -import { NotificationGraphConfig, Timebox, Unreads } from '@urbit/api'; +import { + archive, + NotificationGraphConfig, + NotifIndex, + readNote, + Timebox, + Unreads +} from '@urbit/api'; import { patp2dec } from 'urbit-ob'; +import _ from 'lodash'; import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap'; +import api from '~/logic/api'; import { useCallback } from 'react'; -// import { harkGraphHookReducer, harkGroupHookReducer, harkReducer } from "~/logic/subscription/hark"; -import { createState } from './base'; +import { createState, createSubscription, pokeOptimisticallyN, reduceState, reduceStateN } from './base'; +import { reduce, reduceGraph, reduceGroup } from '../reducers/hark-update'; +import { BigInteger } from 'big-integer'; export const HARK_FETCH_MORE_COUNT = 3; export interface HarkState { archivedNotifications: BigIntOrderedMap; doNotDisturb: boolean; - // getMore: () => Promise; - // getSubset: (offset: number, count: number, isArchive: boolean) => Promise; + getMore: () => Promise; + getSubset: (offset: number, count: number, isArchive: boolean) => Promise; // getTimeSubset: (start?: Date, end?: Date) => Promise; notifications: BigIntOrderedMap; unreadNotes: Timebox; @@ -20,59 +30,92 @@ export interface HarkState { notificationsGraphConfig: NotificationGraphConfig; // TODO unthread this everywhere notificationsGroupConfig: string[]; unreads: Unreads; + archive: (index: NotifIndex, time?: BigInteger) => Promise; + readNote: (index: NotifIndex) => Promise; } -const useHarkState = createState('Hark', { - archivedNotifications: new BigIntOrderedMap(), - doNotDisturb: false, - unreadNotes: [], - // getMore: async (): Promise => { - // const state = get(); - // const offset = state.notifications.size || 0; - // await state.getSubset(offset, HARK_FETCH_MORE_COUNT, false); - // // TODO make sure that state has mutated at this point. - // return offset === (state.notifications.size || 0); - // }, - // getSubset: async (offset, count, isArchive): Promise => { - // const api = useApi(); - // const where = isArchive ? 'archive' : 'inbox'; - // const result = await api.scry({ - // app: 'hark-store', - // path: `/recent/${where}/${offset}/${count}` - // }); - // harkReducer(result); - // return; - // }, - // getTimeSubset: async (start, end): Promise => { - // const api = useApi(); - // const s = start ? dateToDa(start) : '-'; - // const e = end ? dateToDa(end) : '-'; - // const result = await api.scry({ - // app: 'hark-hook', - // path: `/recent/${s}/${e}` - // }); - // harkGroupHookReducer(result); - // harkGraphHookReducer(result); - // return; - // }, - notifications: new BigIntOrderedMap(), - notificationsCount: 0, - notificationsGraphConfig: { - watchOnSelf: false, - mentions: false, - watching: [] - }, - notificationsGroupConfig: [], - unreads: { - graph: {}, - group: {} - } -}, ['unreadNotes', 'notifications', 'archivedNotifications', 'unreads', 'notificationsCount']); +const useHarkState = createState( + 'Hark', + (set, get) => ({ + archivedNotifications: new BigIntOrderedMap(), + doNotDisturb: false, + unreadNotes: [], + archive: async (index: NotifIndex, time?: BigInteger) => { + const poke = archive(index, time); + await pokeOptimisticallyN(useHarkState, poke, [reduce]); + }, + readNote: async (index) => { + await pokeOptimisticallyN(useHarkState, readNote(index), [reduce]); + }, + getMore: async (): Promise => { + const state = get(); + const offset = state.notifications.size || 0; + await state.getSubset(offset, HARK_FETCH_MORE_COUNT, false); + const newState = get(); + return offset === (newState?.notifications?.size || 0); + }, + getSubset: async (offset, count, isArchive): Promise => { + const where = isArchive ? 'archive' : 'inbox'; + const { harkUpdate } = await api.scry({ + app: 'hark-store', + path: `/recent/${where}/${offset}/${count}` + }); + reduceState(useHarkState, harkUpdate, [reduce]); + }, + + notifications: new BigIntOrderedMap(), + notificationsCount: 0, + notificationsGraphConfig: { + watchOnSelf: false, + mentions: false, + watching: [] + }, + notificationsGroupConfig: [], + unreads: { + graph: {}, + group: {} + } + }), + [ + 'unreadNotes', + 'notifications', + 'archivedNotifications', + 'unreads', + 'notificationsCount' + ], + [ + (set, get) => createSubscription('hark-store', '/updates', (j) => { + const d = _.get(j, 'harkUpdate', false); + if (d) { + reduceStateN(get(), d, [reduce]); + } + }), + (set, get) => createSubscription('hark-graph-hook', '/updates', (j) => { + const graphHookData = _.get(j, 'hark-graph-hook-update', false); + if (graphHookData) { + reduceStateN(get(), graphHookData, reduceGraph); + } + }), + (set, get) => createSubscription('hark-group-hook', '/updates', (j) => { + const data = _.get(j, 'hark-group-hook-update', false); + if (data) { + reduceStateN(get(), data, reduceGroup); + } + }) + ] +); export function useHarkDm(ship: string) { - return useHarkState(useCallback((s) => { - return s.unreads.graph[`/ship/~${window.ship}/dm-inbox`]?.[`/${patp2dec(ship)}`]; - }, [ship])); + return useHarkState( + useCallback( + (s) => { + return s.unreads.graph[`/ship/~${window.ship}/dm-inbox`]?.[ + `/${patp2dec(ship)}` + ]; + }, + [ship] + ) + ); } export default useHarkState; diff --git a/pkg/interface/src/logic/state/invite.ts b/pkg/interface/src/logic/state/invite.ts index 54bd3906b..d477236b1 100644 --- a/pkg/interface/src/logic/state/invite.ts +++ b/pkg/interface/src/logic/state/invite.ts @@ -1,13 +1,31 @@ import { Invites } from '@urbit/api'; -import { BaseState, createState } from './base'; +import { reduce } from '../reducers/invite-update'; +import _ from 'lodash'; +import { + createState, + createSubscription, + reduceStateN +} from './base'; -export interface InviteState extends BaseState { +export interface InviteState { invites: Invites; } -// @ts-ignore investigate zustand types -const useInviteState = createState('Invite', { - invites: {} -}); +const useInviteState = createState( + 'Invite', + { + invites: {} + }, + ['invites'], + [ + (set, get) => + createSubscription('invite-store', '/all', (e) => { + const d = _.get(e, 'invite-update', false); + if (d) { + reduceStateN(get(), d, reduce); + } + }) + ] +); export default useInviteState; diff --git a/pkg/interface/src/logic/state/join.ts b/pkg/interface/src/logic/state/join.ts new file mode 100644 index 000000000..0a66d89f2 --- /dev/null +++ b/pkg/interface/src/logic/state/join.ts @@ -0,0 +1,8 @@ +import { useOsDark } from './local'; +import { useTheme } from './settings'; + +export function useDark() { + const osDark = useOsDark(); + const theme = useTheme(); + return theme === 'dark' || (osDark && theme === 'auto'); +} diff --git a/pkg/interface/src/logic/state/launch.ts b/pkg/interface/src/logic/state/launch.ts index 4cefc57e2..fa69a3d80 100644 --- a/pkg/interface/src/logic/state/launch.ts +++ b/pkg/interface/src/logic/state/launch.ts @@ -1,27 +1,74 @@ import { Tile, WeatherState } from '~/types/launch-update'; -import { BaseState, createState } from './base'; +import { + createState, + createSubscription, + reduceStateN +} from './base'; +import airlock from '~/logic/api'; +import { reduce } from '../reducers/launch-update'; +import _ from 'lodash'; -export interface LaunchState extends BaseState { +export interface LaunchState { firstTime: boolean; tileOrdering: string[]; tiles: { [app: string]: Tile; - }, - weather: WeatherState | null | Record | boolean, + }; + weather: WeatherState | null | Record | boolean; userLocation: string | null; baseHash: string | null; runtimeLag: boolean; -}; + getRuntimeLag: () => Promise; + getBaseHash: () => Promise; +} // @ts-ignore investigate zustand types -const useLaunchState = createState('Launch', { - firstTime: true, - tileOrdering: [], - tiles: {}, - weather: null, - userLocation: null, - baseHash: null, - runtimeLag: false, -}); +const useLaunchState = createState( + 'Launch', + (set, get) => ({ + firstTime: true, + tileOrdering: [], + tiles: {}, + weather: null, + userLocation: null, + baseHash: null, + runtimeLag: false, + getBaseHash: async () => { + const baseHash = await airlock.scry({ + app: 'file-server', + path: '/clay/base/hash' + }); + set({ baseHash }); + }, + getRuntimeLag: async () => { + const runtimeLag = await airlock.scry({ + app: 'launch', + path: '/runtime-lag' + }); + set({ runtimeLag }); + } + }), + ['weather'], + [ + (set, get) => + createSubscription('weather', '/all', (e) => { + const w = _.get(e, 'weather', false); + if (w) { + set({ weather: w }); + } + const l = _.get(e, 'location', false); + if (l) { + set({ userLocation: l }); + } + }), + (set, get) => + createSubscription('launch', '/all', (e) => { + const d = _.get(e, 'launch-update', false); + if (d) { + reduceStateN(get(), d, reduce); + } + }) + ] +); export default useLaunchState; diff --git a/pkg/interface/src/logic/state/local.tsx b/pkg/interface/src/logic/state/local.tsx index fe9becd8c..ed682b1d2 100644 --- a/pkg/interface/src/logic/state/local.tsx +++ b/pkg/interface/src/logic/state/local.tsx @@ -4,6 +4,10 @@ import React from 'react'; import create, { State } from 'zustand'; import { persist } from 'zustand/middleware'; import { BackgroundConfig, LeapCategories, RemoteContentPolicy, TutorialProgress, tutorialProgress } from '~/types/local-update'; +import airlock from '~/logic/api'; +import { bootstrapApi } from '../api/bootstrap'; + +export type SubscriptionStatus = 'connected' | 'disconnected' | 'reconnecting'; export interface LocalState { theme: 'light' | 'dark' | 'auto'; @@ -25,7 +29,9 @@ export interface LocalState { omniboxShown: boolean; suspendedFocus?: HTMLElement; toggleOmnibox: () => void; - set: (fn: (state: LocalState) => void) => void + set: (fn: (state: LocalState) => void) => void; + subscription: SubscriptionStatus; + restartSubscription: () => Promise; } type LocalStateZus = LocalState & State; @@ -82,6 +88,26 @@ const useLocalState = create(persist((set, get) => ({ state.suspendedFocus.blur(); } })), + subscription: 'connected', + restartSubscription: async () => { + try { + set({ subscription: 'reconnecting' }); + await airlock.eventSource(); + set({ subscription: 'connected' }); + } catch (e) { + set({ subscription: 'disconnected' }); + } + }, + bootstrap: async () => { + try { + set({ subscription: 'reconnecting' }); + airlock.reset(); + await bootstrapApi(); + set({ subscription: 'connected' }); + } catch (e) { + set({ subscription: 'disconnected' }); + } + }, // @ts-ignore investigate zustand types set: fn => set(produce(fn)) }), { @@ -104,4 +130,9 @@ function withLocalState s.dark; +export function useOsDark() { + return useLocalState(selOsDark); +} + export { useLocalState as default, withLocalState }; diff --git a/pkg/interface/src/logic/state/metadata.ts b/pkg/interface/src/logic/state/metadata.ts index 4983d6d58..096c2a791 100644 --- a/pkg/interface/src/logic/state/metadata.ts +++ b/pkg/interface/src/logic/state/metadata.ts @@ -1,70 +1,117 @@ -import { Association, Associations } from '@urbit/api'; +import { Association, Associations, MetadataUpdatePreview } from '@urbit/api'; import _ from 'lodash'; -import { useCallback } from 'react'; -import { BaseState, createState } from './base'; +import { useCallback, useEffect, useState } from 'react'; +import { + createState, + createSubscription, + reduceStateN +} from './base'; +import airlock from '~/logic/api'; +import { reduce } from '../reducers/metadata-update'; export const METADATA_MAX_PREVIEW_WAIT = 150000; -export interface MetadataState extends BaseState { +export interface MetadataState { associations: Associations; - // preview: (group: string) => Promise; + getPreview: (group: string) => Promise; + previews: { + [group: string]: MetadataUpdatePreview + } } +// @ts-ignore investigate zustand types +const useMetadataState = createState( + 'Metadata', + (set, get) => ({ + associations: { + groups: {}, + graph: {} + }, + previews: {}, + getPreview: async (group: string): Promise => { + const state = get(); + if(group in state.previews) { + return state.previews[group]; + } + try { + const preview = await airlock.subscribeOnce('metadata-pull-hook', `/preview${group}`, 20 * 1000); + if('metadata-hook-update' in preview) { + const newState = get(); + newState.set((s) => { + s.previews[group] = preview['metadata-hook-update'].preview; + }); + return preview['metadata-hook-update'].preview; + } else { + throw 'no-permissions'; + } + } catch (e) { + if(e === 'timeout') { + throw 'offline'; + } + throw e; + } + } + }), + [], + [ + (set, get) => + createSubscription('metadata-store', '/all', (j) => { + const d = _.get(j, 'metadata-update', false); + if (d) { + reduceStateN(get(), d, reduce); + } + }) + ] +); + export function useAssocForGraph(graph: string) { - return useMetadataState(useCallback(s => s.associations.graph[graph] as Association | undefined, [graph])); + return useMetadataState( + useCallback(s => s.associations.graph[graph] as Association | undefined, [ + graph + ]) + ); } export function useAssocForGroup(group: string) { - return useMetadataState(useCallback(s => s.associations.groups[group] as Association | undefined, [group])); + return useMetadataState( + useCallback( + s => s.associations.groups[group] as Association | undefined, + [group] + ) + ); +} + +const selPreview = (s: MetadataState) => [s.previews, s.getPreview] as const; + +export function usePreview(group: string) { + const [error, setError] = useState(null); + const [previews, getPreview] = useMetadataState(selPreview); + useEffect(() => { + let mounted = true; + (async () => { + try { + await getPreview(group); + } catch (e) { + if(mounted) { + setError(e); + } + } + })(); + + return () => { + mounted = false; + }; + }, [group]); + + const preview = previews[group]; + + return { error, preview }; } 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 => { - // return new Promise((resolve, reject) => { - // const api = useApi(); - // let done = false; - - // setTimeout(() => { - // if (done) { - // return; - // } - // done = true; - // reject(new Error('offline')); - // }, METADATA_MAX_PREVIEW_WAIT); - - // api.subscribe({ - // app: 'metadata-pull-hook', - // path: `/preview${group}`, - // // TODO type this message? - // event: (message) => { - // if ('metadata-hook-update' in message) { - // done = true; - // const update = message['metadata-hook-update'].preview as MetadataUpdatePreview; - // resolve(update); - // } else { - // done = true; - // reject(new Error('no-permissions')); - // } - // // TODO how to delete this subscription? Perhaps return the susbcription ID as the second parameter of all the handlers - // }, - // err: (error) => { - // console.error(error); - // reject(error); - // }, - // quit: () => { - // if (!done) { - // reject(new Error('offline')); - // } - // } - // }); - // }); - // }, -}); export default useMetadataState; diff --git a/pkg/interface/src/logic/state/settings.ts b/pkg/interface/src/logic/state/settings.ts index 19c3f9f96..9ee68a260 100644 --- a/pkg/interface/src/logic/state/settings.ts +++ b/pkg/interface/src/logic/state/settings.ts @@ -1,8 +1,21 @@ import f from 'lodash/fp'; -import { RemoteContentPolicy, LeapCategories, leapCategories } from '~/types/local-update'; +import _ from 'lodash'; +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, + createSubscription, + reduceStateN +} from '~/logic/state/base'; import { useCallback } from 'react'; +import { reduceUpdate } from '../reducers/settings-update'; +import airlock from '~/logic/api'; +import { getAll } from '@urbit/api'; export interface ShortcutMapping { cycleForward: string; @@ -13,7 +26,7 @@ export interface ShortcutMapping { readGroup: string; } -export interface SettingsState extends BaseState { +export interface SettingsState { display: { backgroundType: 'none' | 'url' | 'color'; background?: string; @@ -29,6 +42,7 @@ export interface SettingsState extends BaseState { }; keyboard: ShortcutMapping; remoteContentPolicy: RemoteContentPolicy; + getAll: () => Promise; leap: { categories: LeapCategories[]; }; @@ -38,54 +52,82 @@ export interface SettingsState extends BaseState { }; } -export const selectSettingsState = -(keys: K[]) => f.pick(keys); +export const selectSettingsState = )>(keys: K[]) => + f.pick & SettingsState, K>(keys); 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', - background: undefined, - dark: false, - theme: 'auto' - }, - calm: { - hideNicknames: false, - hideAvatars: false, - hideUnreads: false, - hideGroups: false, - hideUtilities: false - }, - remoteContentPolicy: { - imageShown: true, - oembedShown: true, - audioShown: true, - videoShown: true - }, - leap: { - categories: leapCategories - }, - tutorial: { - seen: true, - joined: undefined - }, - keyboard: { - cycleForward: 'ctrl+\'', - cycleBack: 'ctrl+;', - navForward: 'ctrl+]', - navBack: 'ctrl+[', - hideSidebar: 'ctrl+\\', - readGroup: 'shift+Escape' - } -}); +const useSettingsState = createState( + 'Settings', + (set, get) => ({ + display: { + backgroundType: 'none', + background: undefined, + dark: false, + theme: 'auto' + }, + calm: { + hideNicknames: false, + hideAvatars: false, + hideUnreads: false, + hideGroups: false, + hideUtilities: false + }, + remoteContentPolicy: { + imageShown: true, + oembedShown: true, + audioShown: true, + videoShown: true + }, + leap: { + categories: leapCategories + }, + tutorial: { + seen: true, + joined: undefined + }, + keyboard: { + cycleForward: 'ctrl+\'', + cycleBack: 'ctrl+;', + navForward: 'ctrl+]', + navBack: 'ctrl+[', + hideSidebar: 'ctrl+\\', + readGroup: 'shift+Escape' + }, + getAll: async () => { + const { all } = await airlock.scry(getAll); + get().set((s) => { + Object.assign(s, all); + }); + } + }), + [], + [ + (set, get) => + createSubscription('settings-store', '/all', (e) => { + const data = _.get(e, 'settings-event', false); + if (data) { + reduceStateN(get(), data, reduceUpdate); + } + }) + ] +); -export function useShortcut(name: T, cb: (e: KeyboardEvent) => void) { +export function useShortcut( + name: T, + cb: (e: KeyboardEvent) => void +) { const key = useSettingsState(useCallback(s => s.keyboard[name], [name])); return usePlainShortcut(key, cb); } +const selTheme = (s: SettingsState) => s.display.theme; + +export function useTheme() { + return useSettingsState(selTheme); +} + export default useSettingsState; diff --git a/pkg/interface/src/logic/state/storage.ts b/pkg/interface/src/logic/state/storage.ts index 7ad79ceee..391738a5e 100644 --- a/pkg/interface/src/logic/state/storage.ts +++ b/pkg/interface/src/logic/state/storage.ts @@ -1,34 +1,72 @@ -import { BaseState, createState } from './base'; +import { reduce } from '../reducers/s3-update'; +import _ from 'lodash'; +import airlock from '~/logic/api'; +import { createState, createSubscription, reduceStateN } from './base'; export interface GcpToken { accessKey: string; expiresIn: number; } -export interface StorageState extends BaseState { +export interface StorageState { gcp: { configured?: boolean; token?: GcpToken; - }, + isConfigured: () => Promise; + getToken: () => Promise; + }; s3: { configuration: { buckets: Set; currentBucket: string; }; credentials: any | null; // TODO better type - } + }; } // @ts-ignore investigate zustand types -const useStorageState = createState('Storage', { - gcp: {}, - s3: { - configuration: { - buckets: new Set(), - currentBucket: '' +const useStorageState = createState( + 'Storage', + (set, get) => ({ + gcp: { + isConfigured: () => { + return airlock.thread({ + inputMark: 'noun', + outputMark: 'json', + threadName: 'gcp-is-configured', + body: {} + }); + }, + getToken: async () => { + const token = await airlock.thread({ + inputMark: 'noun', + outputMark: 'gcp-token', + threadName: 'gcp-get-token', + body: {} + }); + get().set((state) => { + state.gcp.token = token; + }); + } }, - credentials: null - } -}, ['s3']); + s3: { + configuration: { + buckets: new Set(), + currentBucket: '' + }, + credentials: null + } + }), + ['s3', 'gcp'], + [ + (set, get) => + createSubscription('s3-store', '/all', (e) => { + const d = _.get(e, 's3-update', false); + if (d) { + reduceStateN(get(), d, reduce); + } + }) + ] +); export default useStorageState; diff --git a/pkg/interface/src/logic/store/base.ts b/pkg/interface/src/logic/store/base.ts deleted file mode 100644 index f798f228f..000000000 --- a/pkg/interface/src/logic/store/base.ts +++ /dev/null @@ -1,43 +0,0 @@ -export default class BaseStore { - state: S; - setState: (s: Partial) => void = (s) => {}; - constructor() { - this.state = this.initialState(); - } - - initialState() { - return {} as S; - } - - setStateHandler(setState: (s: Partial) => void) { - this.setState = setState; - } - - clear() { - this.handleEvent({ - data: { clear: true } - }); - } - - handleEvent(data) { - const json = data.data; - - if (json === null) { - return; - } - - if ('clear' in json && json.clear) { - this.setState(this.initialState()); - return; - } - - this.reduce(json, this.state); - if('connection' in json) { - this.setState(this.state); - } - } - - reduce(data, state) { - // extend me! - } -} diff --git a/pkg/interface/src/logic/store/store.ts b/pkg/interface/src/logic/store/store.ts deleted file mode 100644 index cfc943d7f..000000000 --- a/pkg/interface/src/logic/store/store.ts +++ /dev/null @@ -1,67 +0,0 @@ -import _ from 'lodash'; -import { unstable_batchedUpdates } from 'react-dom'; -import { Cage } from '~/types/cage'; -import ConnectionReducer from '../reducers/connection'; -import { ContactReducer } from '../reducers/contact-update'; -import GcpReducer from '../reducers/gcp-reducer'; -import { GraphReducer } from '../reducers/graph-update'; -import GroupReducer from '../reducers/group-update'; -import { GroupViewReducer } from '../reducers/group-view'; -import { HarkReducer } from '../reducers/hark-update'; -import InviteReducer from '../reducers/invite-update'; -import LaunchReducer from '../reducers/launch-update'; -import MetadataReducer from '../reducers/metadata-update'; -import S3Reducer from '../reducers/s3-update'; -import SettingsReducer from '../reducers/settings-update'; -import BaseStore from './base'; -import { StoreState } from './type'; - -export default class GlobalStore extends BaseStore { - inviteReducer = new InviteReducer(); - metadataReducer = new MetadataReducer(); - s3Reducer = new S3Reducer(); - groupReducer = new GroupReducer(); - launchReducer = new LaunchReducer(); - connReducer = new ConnectionReducer(); - settingsReducer = new SettingsReducer(); - gcpReducer = new GcpReducer(); - - pastActions: Record = {} - - constructor() { - super(); - (window as any).debugStore = this.debugStore.bind(this); - } - - debugStore(tag: string, ...stateKeys: string[]) { - console.log(this.pastActions[tag]); - console.log(_.pick(this.state, stateKeys)); - } - - initialState(): StoreState { - return { - connection: 'connected' - }; - } - - reduce(data: Cage, state: StoreState) { - unstable_batchedUpdates(() => { - // debug shim - const tag = Object.keys(data)[0]; - const oldActions = this.pastActions[tag] || []; - this.pastActions[tag] = [data[tag], ...oldActions.slice(0, 14)]; - this.inviteReducer.reduce(data); - this.metadataReducer.reduce(data); - this.s3Reducer.reduce(data); - this.groupReducer.reduce(data); - GroupViewReducer(data); - this.launchReducer.reduce(data); - this.connReducer.reduce(data, this.state); - GraphReducer(data); - HarkReducer(data); - ContactReducer(data); - this.settingsReducer.reduce(data); - this.gcpReducer.reduce(data); - }); - } -} diff --git a/pkg/interface/src/logic/store/type.ts b/pkg/interface/src/logic/store/type.ts deleted file mode 100644 index 568cd2041..000000000 --- a/pkg/interface/src/logic/store/type.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ConnectionStatus } from '~/types/connection'; - -export interface StoreState { - // local state - connection: ConnectionStatus; -} diff --git a/pkg/interface/src/logic/subscription/base.ts b/pkg/interface/src/logic/subscription/base.ts deleted file mode 100644 index 8100f0a61..000000000 --- a/pkg/interface/src/logic/subscription/base.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Path } from '@urbit/api'; -import BaseApi from '../api/base'; -import BaseStore from '../store/base'; - -export default class BaseSubscription { - private errorCount = 0; - constructor(public store: BaseStore, public api: BaseApi, public channel: any) { - this.channel.setOnChannelError(this.onChannelError.bind(this)); - this.channel.setOnChannelOpen(this.onChannelOpen.bind(this)); - } - - clearQueue() { - this.channel.clearQueue(); - } - - delete() { - this.channel.delete(); - } - - // Exists to allow subclasses to hook - restart() { - this.handleEvent({ data: { connection: 'reconnecting' } }); - this.start(); - } - - onChannelOpen(e: any) { - this.errorCount = 0; - this.handleEvent({ data: { connection: 'connected' } }); - } - - onChannelError(err) { - console.error('event source error: ', err); - this.errorCount++; - if(this.errorCount >= 5) { - console.error('bailing out, too many retries'); - this.handleEvent({ data: { connection: 'disconnected' } }); - return; - } - this.handleEvent({ data: { connection: 'reconnecting' } }); - setTimeout(() => { - this.restart(); - }, Math.pow(2,this.errorCount - 1) * 750); - } - - subscribe(path: Path, app: string) { - return this.api.subscribe(path, 'PUT', this.api.ship, app, - this.handleEvent.bind(this), - (err) => { - console.log(err); - this.subscribe(path, app); - }, - () => { - this.subscribe(path, app); - }); - } - - unsubscribe(id: number) { - this.api.unsubscribe(id); - } - - start() { - // extend - } - - handleEvent(diff) { - // extend - this.store.handleEvent(diff); - } -} - diff --git a/pkg/interface/src/logic/subscription/global.ts b/pkg/interface/src/logic/subscription/global.ts deleted file mode 100644 index 6e928b6f2..000000000 --- a/pkg/interface/src/logic/subscription/global.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Path } from '@urbit/api'; -import { StoreState } from '../store/type'; -import BaseSubscription from './base'; - -export default class GlobalSubscription extends BaseSubscription { - openSubscriptions: any = {}; - - start() { - this.subscribe('/all', 'metadata-store'); - this.subscribe('/all', 'invite-store'); - this.subscribe('/all', 'launch'); - this.subscribe('/all', 'weather'); - this.subscribe('/groups', 'group-store'); - this.clearQueue(); - - this.subscribe('/updates', 'dm-hook'); - this.subscribe('/all', 'contact-store'); - this.subscribe('/all', 's3-store'); - this.subscribe('/keys', 'graph-store'); - this.subscribe('/updates', 'hark-store'); - this.subscribe('/updates', 'hark-graph-hook'); - this.subscribe('/updates', 'hark-group-hook'); - this.subscribe('/all', 'settings-store'); - this.subscribe('/all', 'group-view'); - this.subscribe('/nacks', 'contact-pull-hook'); - this.clearQueue(); - - this.subscribe('/updates', 'graph-store'); - } - - subscribe(path: Path, app: string) { - if (`${app}${path}` in this.openSubscriptions) { - return; - } - - const id = super.subscribe(path, app); - this.openSubscriptions[`${app}${path}`] = { app, path, id }; - } - - unsubscribe(id) { - for (const key in Object.keys(this.openSubscriptions)) { - const val = this.openSubscriptions[key]; - if (id === val.id) { - delete this.openSubscriptions[`${val.app}${val.path}`]; - super.unsubscribe(id); - } - } - } - - restart() { - this.openSubscriptions = {}; - super.restart(); - } -} diff --git a/pkg/interface/src/stories/GraphContentTall.stories.tsx b/pkg/interface/src/stories/GraphContentTall.stories.tsx index a5969e0d1..0255d53cc 100644 --- a/pkg/interface/src/stories/GraphContentTall.stories.tsx +++ b/pkg/interface/src/stories/GraphContentTall.stories.tsx @@ -12,8 +12,6 @@ export default { component: GraphContent } as Meta; -const fakeApi = {} as any; - const Template: Story = args => ( = args => ( m="3" maxWidth="100%" {...args} - api={fakeApi} showOurContact /> diff --git a/pkg/interface/src/stories/GraphContentWide.stories.tsx b/pkg/interface/src/stories/GraphContentWide.stories.tsx index 669be2256..6aae7a6d5 100644 --- a/pkg/interface/src/stories/GraphContentWide.stories.tsx +++ b/pkg/interface/src/stories/GraphContentWide.stories.tsx @@ -12,8 +12,6 @@ export default { component: GraphContent } as Meta; -const fakeApi = {} as any; - const Template: Story = (args) => { return ( = (args) => { width="100%" position="relative" > - + ); }; diff --git a/pkg/interface/src/stories/PendingDm.stories.tsx b/pkg/interface/src/stories/PendingDm.stories.tsx index 3579e2e95..13b98ebd7 100644 --- a/pkg/interface/src/stories/PendingDm.stories.tsx +++ b/pkg/interface/src/stories/PendingDm.stories.tsx @@ -8,10 +8,9 @@ export default { title: 'Notifications/PendingDm', component: PendingDm } as Meta; -const fakeApi = {} as any; export const Default = () => ( - + ); diff --git a/pkg/interface/src/types/term-update.ts b/pkg/interface/src/types/term-update.ts deleted file mode 100644 index 2e9f09ba8..000000000 --- a/pkg/interface/src/types/term-update.ts +++ /dev/null @@ -1,33 +0,0 @@ - -export type TermUpdate = - | Blit; - -export type Tint = - | null - | 'r' | 'g' | 'b' | 'c' | 'm' | 'y' | 'k' | 'w' - | { r: number, g: number, b: number }; - -export type Deco = null | 'br' | 'un' | 'bl'; - -export type Stye = { - deco: Deco[], - back: Tint, - fore: Tint -}; - -export type Stub = { - stye: Stye, - text: string[] -} - -export type Blit = - | { bel: null } // make a noise - | { clr: null } // clear the screen - | { hop: number | { r: number, c: number } } // set cursor col/pos - | { klr: Stub[] } // put styled - | { put: string[] } // put text at cursor - | { nel: null } // newline - | { sag: { path: string, file: string } } // save to jamfile - | { sav: { path: string, file: string } } // save to file - | { url: string } // activate url - | { wyp: null } // wipe cursor line diff --git a/pkg/interface/src/views/App.js b/pkg/interface/src/views/App.js index 587378a58..7c8f028af 100644 --- a/pkg/interface/src/views/App.js +++ b/pkg/interface/src/views/App.js @@ -1,6 +1,7 @@ import dark from '@tlon/indigo-dark'; import light from '@tlon/indigo-light'; import Mousetrap from 'mousetrap'; +import shallow from 'zustand/shallow'; import 'mousetrap-global-bind'; import * as React from 'react'; import Helmet from 'react-helmet'; @@ -8,18 +9,15 @@ import 'react-hot-loader'; import { hot } from 'react-hot-loader/root'; 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 { 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 useGraphState from '~/logic/state/graph'; import { ShortcutContextProvider } from '~/logic/lib/shortcutContext'; -import GlobalStore from '~/logic/store/store'; -import GlobalSubscription from '~/logic/subscription/global'; import ErrorBoundary from '~/views/components/ErrorBoundary'; import { TutorialModal } from '~/views/landscape/components/TutorialModal'; import './apps/chat/css/custom.css'; @@ -29,6 +27,8 @@ import './css/fonts.css'; import './css/indigo-static.css'; import { Content } from './landscape/components/Content'; import './landscape/css/custom.css'; +import { bootstrapApi } from '~/logic/api/bootstrap'; +import useLaunchState from '../logic/state/launch'; const Root = withState(styled.div` font-family: ${p => p.theme.fonts.sans}; @@ -74,24 +74,14 @@ class App extends React.Component { constructor(props) { super(props); this.ship = window.ship; - this.store = new GlobalStore(); - this.store.setStateHandler(this.setState.bind(this)); - this.state = this.store.state; - - // eslint-disable-next-line - this.appChannel = new window.channel(); - this.api = new GlobalApi(this.ship, this.appChannel, this.store); - gcpManager.configure(this.api); - this.subscription = - new GlobalSubscription(this.store, this.api, this.appChannel); this.updateTheme = this.updateTheme.bind(this); this.updateMobile = this.updateMobile.bind(this); } componentDidMount() { - this.subscription.start(); - this.api.graph.getShallowChildren(`~${window.ship}`, 'dm-inbox'); + bootstrapApi(); + this.props.getShallowChildren(`~${window.ship}`, 'dm-inbox'); const theme = this.getTheme(); this.themeWatcher = window.matchMedia('(prefers-color-scheme: dark)'); this.mobileWatcher = window.matchMedia(`(max-width: ${theme.breakpoints[0]})`); @@ -103,9 +93,9 @@ class App extends React.Component { this.updateMobile(this.mobileWatcher); this.updateTheme(this.themeWatcher); }, 500); - this.api.local.getBaseHash(); - this.api.local.getRuntimeLag(); // TODO consider polling periodically - this.api.settings.getAll(); + this.props.getBaseHash(); + this.props.getRuntimeLag(); // TODO consider polling periodically + this.props.getAll(); gcpManager.start(); Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => { e.preventDefault(); @@ -139,10 +129,9 @@ class App extends React.Component { } render() { - const { state } = this; const theme = this.getTheme(); - const ourContact = this.props.contacts[`~${this.ship}`] || null; + const { ourContact } = this.props; return ( @@ -153,21 +142,18 @@ class App extends React.Component { - + @@ -175,9 +161,8 @@ class App extends React.Component { @@ -188,10 +173,38 @@ class App extends React.Component { ); } } +const WarmApp = process.env.NODE_ENV === 'production' ? App : hot(App); + +const selContacts = s => s.contacts[`~${window.ship}`]; +const selLocal = s => [s.set, s.omniboxShown, s.toggleOmnibox]; +const selSettings = s => [s.display, s.getAll]; +const selGraph = s => s.getShallowChildren; +const selLaunch = s => [s.getRuntimeLag, s.getBaseHash]; + +const WithApp = React.forwardRef((props, ref) => { + const ourContact = useContactState(selContacts); + const [display, getAll] = useSettingsState(selSettings, shallow); + const [setLocal, omniboxShown, toggleOmnibox] = useLocalState(selLocal); + const getShallowChildren = useGraphState(selGraph); + const [getRuntimeLag, getBaseHash] = useLaunchState(selLaunch, shallow); + + return ( + + ); +}); + +WarmApp.whyDidYouRender = true; + +export default WithApp; -export default withState(process.env.NODE_ENV === 'production' ? App : hot(App), [ - [useGroupState], - [useContactState], - [useSettingsState, ['display']], - [useLocalState] -]); diff --git a/pkg/interface/src/views/apps/chat/ChatResource.tsx b/pkg/interface/src/views/apps/chat/ChatResource.tsx index 03949d0da..5a80838bf 100644 --- a/pkg/interface/src/views/apps/chat/ChatResource.tsx +++ b/pkg/interface/src/views/apps/chat/ChatResource.tsx @@ -1,4 +1,4 @@ -import { Content, createPost, Post } from '@urbit/api'; +import { Content, createPost, fetchIsAllowed, markCountAsRead, Post, removePosts } from '@urbit/api'; import { Association } from '@urbit/api/metadata'; import { BigInteger } from 'big-integer'; import React, { @@ -7,15 +7,16 @@ import React, { useMemo, useState } from 'react'; -import GlobalApi from '~/logic/api/global'; import { isWriter, resourceFromPath } from '~/logic/lib/group'; import { getPermalinkForGraph } from '~/logic/lib/permalinks'; import useGraphState, { useGraphForAssoc } from '~/logic/state/graph'; import { useGroupForAssoc } from '~/logic/state/group'; import useHarkState from '~/logic/state/hark'; -import { StoreState } from '~/logic/store/type'; import { Loading } from '~/views/components/Loading'; import { ChatPane } from './components/ChatPane'; +import airlock from '~/logic/api'; +import { disallowedShipsForOurContact } from '~/logic/lib/contact'; +import shallow from 'zustand/shallow'; const getCurrGraphSize = (ship: string, name: string) => { const { graphs } = useGraphState.getState(); @@ -23,14 +24,13 @@ const getCurrGraphSize = (ship: string, name: string) => { return graph?.size ?? 0; }; -type ChatResourceProps = StoreState & { +type ChatResourceProps = { association: Association; - api: GlobalApi; baseUrl: string; }; const ChatResource = (props: ChatResourceProps): ReactElement => { - const { association, api } = props; + const { association } = props; const { resource } = association; const [toShare, setToShare] = useState(); const group = useGroupForAssoc(association)!; @@ -39,15 +39,24 @@ const ChatResource = (props: ChatResourceProps): ReactElement => { const unreadCount = (unreads.graph?.[resource]?.['/']?.unreads as number) || 0; const canWrite = group ? isWriter(group, resource) : false; + const [ + getNewest, + getOlderSiblings, + getYoungerSiblings, + addPost + ] = useGraphState( + s => [s.getNewest, s.getOlderSiblings, s.getYoungerSiblings, s.addPost], + shallow + ); useEffect(() => { const count = Math.min(400, 100 + unreadCount); const { ship, name } = resourceFromPath(resource); - props.api.graph.getNewest(ship, name, count); + getNewest(ship, name, count); setToShare(undefined); (async function () { if (group.hidden) { - const members = await props.api.contacts.disallowedShipsForOurContact( + const members = await disallowedShipsForOurContact( Array.from(group.members) ); if (members.length > 0) { @@ -55,12 +64,12 @@ const ChatResource = (props: ChatResourceProps): ReactElement => { } } else { const { ship: groupHost } = resourceFromPath(association.group); - const shared = await props.api.contacts.fetchIsAllowed( + const shared = await airlock.scry(fetchIsAllowed( `~${window.ship}`, 'personal', groupHost, true - ); + )); if (!shared) { setToShare(association.group); } @@ -77,7 +86,7 @@ const ChatResource = (props: ChatResourceProps): ReactElement => { ); return `${url}\n~${msg.author}: `; }, - [association] + [association.resource] ); const isAdmin = useMemo( @@ -86,18 +95,21 @@ const ChatResource = (props: ChatResourceProps): ReactElement => { ); const fetchMessages = useCallback(async (newer: boolean) => { - const { api } = props; const pageSize = 100; const [, , ship, name] = resource.split('/'); const graphSize = graph?.size ?? 0; const expectedSize = graphSize + pageSize; + if(graphSize === 0) { + // already loading the graph + return false; + } if (newer) { const index = graph.peekLargest()?.[0]; if (!index) { - return true; + return false; } - await api.graph.getYoungerSiblings( + await getYoungerSiblings( ship, name, pageSize, @@ -107,32 +119,34 @@ const ChatResource = (props: ChatResourceProps): ReactElement => { } else { const index = graph.peekSmallest()?.[0]; if (!index) { - return true; + return false; } - await api.graph.getOlderSiblings(ship, name, pageSize, `/${index.toString()}`); - const done = expectedSize !== getCurrGraphSize(ship.slice(1), name); + await getOlderSiblings(ship, name, pageSize, `/${index.toString()}`); + const currSize = getCurrGraphSize(ship.slice(1), name); + console.log(currSize); + const done = expectedSize !== currSize; return done; } }, [graph, resource]); const onSubmit = useCallback((contents: Content[]) => { const { ship, name } = resourceFromPath(resource); - api.graph.addPost(ship, name, createPost(window.ship, contents)); - }, [resource]); + addPost(ship, name, createPost(window.ship, contents)); + }, [resource, addPost]); const onDelete = useCallback((msg: Post) => { const { ship, name } = resourceFromPath(resource); - api.graph.removePosts(ship, name, [msg.index]); + airlock.poke(removePosts(ship, name, [msg.index])); }, [resource]); const dismissUnread = useCallback(() => { - api.hark.markCountAsRead(association, '/', 'message'); - }, [association]); + airlock.poke(markCountAsRead(association.resource)); + }, [association.resource]); const getPermalink = useCallback( (index: BigInteger) => getPermalinkForGraph(association.group, resource, `/${index.toString()}`), - [association] + [association.resource] ); if (!graph) { @@ -144,7 +158,6 @@ const ChatResource = (props: ChatResourceProps): ReactElement => { id={resource.slice(7)} graph={graph} unreadCount={unreadCount} - api={api} canWrite={canWrite} onReply={onReply} onDelete={onDelete} diff --git a/pkg/interface/src/views/apps/chat/DmResource.tsx b/pkg/interface/src/views/apps/chat/DmResource.tsx index e3cbbe25c..c3d447646 100644 --- a/pkg/interface/src/views/apps/chat/DmResource.tsx +++ b/pkg/interface/src/views/apps/chat/DmResource.tsx @@ -1,21 +1,20 @@ -import { cite, Content, Post } from '@urbit/api'; +import { cite, Content, markCountAsRead, Post } from '@urbit/api'; import React, { useCallback, useEffect } from 'react'; import _ from 'lodash'; import bigInt from 'big-integer'; import { Box, Row, Col, Text } from '@tlon/indigo-react'; import { Link } from 'react-router-dom'; import { patp2dec } from 'urbit-ob'; -import GlobalApi from '~/logic/api/global'; import { useContact } from '~/logic/state/contact'; import useGraphState, { useDM } from '~/logic/state/graph'; import { useHarkDm } from '~/logic/state/hark'; import useSettingsState, { selectCalmState } from '~/logic/state/settings'; import { ChatPane } from './components/ChatPane'; -import { patpToUd } from '~/logic/lib/util'; +import airlock from '~/logic/api'; +import shallow from 'zustand/shallow'; interface DmResourceProps { ship: string; - api: GlobalApi; } const getCurrDmSize = (ship: string) => { @@ -50,7 +49,7 @@ function quoteReply(post: Post) { } export function DmResource(props: DmResourceProps) { - const { ship, api } = props; + const { ship } = props; const dm = useDM(ship); const hark = useHarkDm(ship); const unreadCount = (hark?.unreads as number) ?? 0; @@ -59,12 +58,22 @@ export function DmResource(props: DmResourceProps) { const showNickname = !hideNicknames && Boolean(contact); const nickname = showNickname ? contact!.nickname : cite(ship) ?? ship; + const [ + getYoungerSiblings, + getOlderSiblings, + getNewest, + addDmMessage + ] = useGraphState( + s => [s.getYoungerSiblings, s.getOlderSiblings, s.getNewest, s.addDmMessage], + shallow + ); + useEffect(() => { - api.graph.getNewest( + getNewest( `~${window.ship}`, 'dm-inbox', 100, - `/${patpToUd(ship)}` + `/${patp2dec(ship)}` ); }, [ship]); @@ -77,11 +86,11 @@ export function DmResource(props: DmResourceProps) { if (!index) { return true; } - await api.graph.getYoungerSiblings( + await getYoungerSiblings( `~${window.ship}`, 'dm-inbox', pageSize, - `/${patpToUd(ship)}/${index.toString()}` + `/${patp2dec(ship)}/${index.toString()}` ); return expectedSize !== getCurrDmSize(ship); } else { @@ -89,30 +98,27 @@ export function DmResource(props: DmResourceProps) { if (!index) { return true; } - await api.graph.getOlderSiblings( + await getOlderSiblings( `~${window.ship}`, 'dm-inbox', pageSize, - `/${patpToUd(ship)}/${index.toString()}` + `/${patp2dec(ship)}/${index.toString()}` ); return expectedSize !== getCurrDmSize(ship); } }, - [ship, dm, api] + [ship, dm] ); const dismissUnread = useCallback(() => { - api.hark.dismissReadCount( - `/ship/~${window.ship}/dm-inbox`, - `/${patp2dec(ship)}` - ); + airlock.poke(markCountAsRead(`/ship/~${window.ship}/dm-inbox`, `/${patp2dec(ship)}`)); }, [ship]); const onSubmit = useCallback( (contents: Content[]) => { - api.graph.addDmMessage(ship, contents); + addDmMessage(ship, contents); }, - [ship] + [ship, addDmMessage] ); return ( @@ -156,7 +162,6 @@ export function DmResource(props: DmResourceProps) { undefined} - isAdmin + isAdmin={false} onSubmit={onSubmit} /> diff --git a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx index 330fe1bb6..c8194620b 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatInput.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatInput.tsx @@ -1,7 +1,6 @@ import { BaseImage, Box, Icon, LoadingSpinner, Row } from '@tlon/indigo-react'; -import { Contact, Content } from '@urbit/api'; +import { Contact, Content, evalCord } from '@urbit/api'; import React, { Component, ReactNode } from 'react'; -import GlobalApi from '~/logic/api/global'; import { Sigil } from '~/logic/lib/sigil'; import tokenizeMessage from '~/logic/lib/tokenizeMessage'; import { IuseStorage } from '~/logic/lib/useStorage'; @@ -9,9 +8,9 @@ import { MOBILE_BROWSER_REGEX, uxToHex } from '~/logic/lib/util'; import { withLocalState } from '~/logic/state/local'; import withStorage from '~/views/components/withStorage'; import ChatEditor from './ChatEditor'; +import airlock from '~/logic/api'; type ChatInputProps = IuseStorage & { - api: GlobalApi; ourContact?: Contact; onUnmount(msg: string): void; placeholder: string; @@ -59,13 +58,13 @@ export class ChatInput extends Component { async submit(text) { const { props, state } = this; - const { onSubmit, api } = this.props; + const { onSubmit } = this.props; this.setState({ inCodeMode: false }); props.deleteMessage(); if(state.inCodeMode) { - const output = await api.graph.eval(text) as string[]; + const output = await airlock.thread(evalCord(text)); onSubmit([{ code: { output, expression: text } }]); } else { onSubmit(tokenizeMessage(text)); diff --git a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx index 7c607ee50..a321e3956 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatMessage.tsx @@ -9,7 +9,6 @@ import React, { useMemo, useState } from 'react'; import VisibilitySensor from 'react-visibility-sensor'; -import GlobalApi from '~/logic/api/global'; import { useIdlingState } from '~/logic/lib/idling'; import { Sigil } from '~/logic/lib/sigil'; import { useCopy } from '~/logic/lib/useCopy'; @@ -17,7 +16,7 @@ import { cite, daToUnix, useHovering, useShowNickname, uxToHex } from '~/logic/lib/util'; import { useContact } from '~/logic/state/contact'; -import useLocalState from '~/logic/state/local'; +import { useDark } from '~/logic/state/join'; import useSettingsState, { selectCalmState } from '~/logic/state/settings'; import { Dropdown } from '~/views/components/Dropdown'; import ProfileOverlay from '~/views/components/ProfileOverlay'; @@ -54,17 +53,13 @@ export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => ( ); -export const MessageAuthor = ({ +export const MessageAuthor = React.memo(({ timestamp, msg, - api, showOurContact, ...props }) => { - const osDark = useLocalState(state => state.dark); - - const theme = useSettingsState(s => s.display.theme); - const dark = theme === 'dark' || (theme === 'auto' && osDark); + const dark = useDark(); let contact: Contact | null = useContact(`~${msg.author}`); const date = daToUnix(bigInt(msg.index.split('/').reverse()[0])); @@ -138,7 +133,7 @@ export const MessageAuthor = ({ cursor='pointer' position='relative' > - + {img} @@ -180,15 +175,15 @@ export const MessageAuthor = ({ ); -}; +}); +MessageAuthor.displayName = 'MessageAuthor'; type MessageProps = { timestamp: string; timestampHover: boolean; } - & Pick + & Pick export const Message = React.memo(({ timestamp, msg, - api, timestampHover, transcluded, showOurContact @@ -219,7 +214,6 @@ export const Message = React.memo(({ width="100%" contents={msg.contents} transcluded={transcluded} - api={api} showOurContact={showOurContact} /> @@ -390,7 +384,6 @@ interface ChatMessageProps { style?: unknown; isLastMessage?: boolean; dismissUnread?: () => void; - api: GlobalApi; highlighted?: boolean; renderSigil?: boolean; hideHover?: boolean; @@ -399,6 +392,7 @@ interface ChatMessageProps { showOurContact: boolean; onDelete?: () => void; } +const emptyCallback = () => {}; function ChatMessage(props: ChatMessageProps) { let { highlighted } = props; @@ -411,7 +405,6 @@ function ChatMessage(props: ChatMessageProps) { style, isLastMessage, isAdmin, - api, showOurContact, hideHover, dismissUnread = () => null, @@ -424,10 +417,10 @@ function ChatMessage(props: ChatMessageProps) { ); } - const onReply = props?.onReply ?? (() => {}); - const onDelete = props?.onDelete ?? (() => {}); - const transcluded = props?.transcluded ?? 0; - const renderSigil = props.renderSigil ?? (Boolean(nextMsg && msg.author !== nextMsg.author) || + const onReply = props?.onReply || emptyCallback; + const onDelete = props?.onDelete || emptyCallback; + const transcluded = props?.transcluded || 0; + const renderSigil = props.renderSigil || (Boolean(nextMsg && msg.author !== nextMsg.author) || !nextMsg ); @@ -470,7 +463,6 @@ function ChatMessage(props: ChatMessageProps) { timestamp, isPending, showOurContact, - api, highlighted, hideHover, transcluded, @@ -484,11 +476,10 @@ function ChatMessage(props: ChatMessageProps) { msg={msg} timestamp={timestamp} timestampHover={!renderSigil} - api={api} transcluded={transcluded} showOurContact={showOurContact} /> - ), [renderSigil, msg, timestamp, api, transcluded, showOurContact]); + ), [renderSigil, msg, timestamp, transcluded, showOurContact]); const unreadContainerStyle = { height: isLastRead ? '2rem' : '0' @@ -519,9 +510,9 @@ function ChatMessage(props: ChatMessageProps) { ); } -export default React.forwardRef((props: Omit, ref: any) => ( +export default React.memo(React.forwardRef((props: Omit, ref: any) => ( -)); +))); export const MessagePlaceholder = ({ height, diff --git a/pkg/interface/src/views/apps/chat/components/ChatPane.tsx b/pkg/interface/src/views/apps/chat/components/ChatPane.tsx index 120ec21ca..bd2f34090 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatPane.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatPane.tsx @@ -3,11 +3,10 @@ import { Content, Graph, Post } from '@urbit/api'; import bigInt, { BigInteger } from 'big-integer'; import _ from 'lodash'; import React, { ReactElement, useCallback, useEffect, useRef, useState } from 'react'; -import GlobalApi from '~/logic/api/global'; import { useFileDrag } from '~/logic/lib/useDrag'; import { useLocalStorageState } from '~/logic/lib/useLocalStorageState'; import { useOurContact } from '~/logic/state/contact'; -import useGraphState from '~/logic/state/graph'; +import { useGraphTimesent } from '~/logic/state/graph'; import ShareProfile from '~/views/apps/chat/components/ShareProfile'; import { Loading } from '~/views/components/Loading'; import SubmitDragger from '~/views/components/SubmitDragger'; @@ -29,7 +28,6 @@ interface ChatPaneProps { * User able to write to chat */ canWrite: boolean; - api: GlobalApi; /** * Get contents of reply message */ @@ -67,7 +65,6 @@ interface ChatPaneProps { export function ChatPane(props: ChatPaneProps): ReactElement { const { - api, graph, unreadCount, canWrite, @@ -80,7 +77,7 @@ export function ChatPane(props: ChatPaneProps): ReactElement { promptShare = [], fetchMessages } = props; - const graphTimesentMap = useGraphState(state => state.graphTimesentMap); + const graphTimesentMap = useGraphTimesent(id); const ourContact = useOurContact(); const chatInput = useRef(); @@ -91,7 +88,7 @@ export function ChatPane(props: ChatPaneProps): ReactElement { } (chatInput.current as NakedChatInput)?.uploadFiles(files); }, - [chatInput.current] + [chatInput] ); const { bind, dragging } = useFileDrag(onFileDrag); @@ -136,10 +133,10 @@ export function ChatPane(props: ChatPaneProps): ReactElement { } return ( + // @ts-ignore bind typings setShowBanner(false)} /> @@ -150,20 +147,18 @@ export function ChatPane(props: ChatPaneProps): ReactElement { graphSize={graph.size} unreadCount={unreadCount} showOurContact={promptShare.length === 0 && !showBanner} - pendingSize={Object.keys(graphTimesentMap[id] || {}).length} + pendingSize={Object.keys(graphTimesentMap).length} onReply={onReply} onDelete={onDelete} dismissUnread={dismissUnread} fetchMessages={fetchMessages} isAdmin={isAdmin} getPermalink={getPermalink} - api={api} scrollTo={scrollTo ? bigInt(scrollTo) : undefined} /> {canWrite && ( ); } + +ChatPane.whyDidYouRender = true; diff --git a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx index 9913c7419..377aeac0a 100644 --- a/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx +++ b/pkg/interface/src/views/apps/chat/components/ChatWindow.tsx @@ -5,9 +5,8 @@ import { } from '@urbit/api'; import bigInt, { BigInteger } from 'big-integer'; import React, { Component } from 'react'; -import GlobalApi from '~/logic/api/global'; import VirtualScroller from '~/views/components/VirtualScroller'; -import ChatMessage, { MessagePlaceholder } from './ChatMessage'; +import ChatMessage from './ChatMessage'; import UnreadNotice from './UnreadNotice'; const IDLE_THRESHOLD = 64; @@ -18,7 +17,6 @@ type ChatWindowProps = { graphSize: number; station?: unknown; fetchMessages: (newer: boolean) => Promise; - api: GlobalApi; scrollTo?: BigInteger; onReply: (msg: Post) => void; onDelete: (msg: Post) => void; @@ -59,7 +57,7 @@ class ChatWindow extends Component< this.state = { fetchPending: false, idle: true, - initialized: false, + initialized: true, unreadIndex: bigInt.zero }; @@ -74,14 +72,10 @@ class ChatWindow extends Component< componentDidMount() { this.calculateUnreadIndex(); - setTimeout(() => { - this.setState({ initialized: true }, () => { - if(this.props.scrollTo) { - this.virtualList!.scrollLocked = false; - this.virtualList!.scrollToIndex(this.props.scrollTo); - } - }); - }, this.INITIALIZATION_MAX_TIME); + if(this.props.scrollTo) { + this.virtualList!.scrollLocked = false; + this.virtualList!.scrollToIndex(this.props.scrollTo); + } } calculateUnreadIndex() { @@ -181,7 +175,6 @@ class ChatWindow extends Component< renderer = React.forwardRef(({ index, scrollWindow }: RendererProps, ref) => { const { - api, showOurContact, graph, onReply, @@ -193,7 +186,6 @@ class ChatWindow extends Component< const permalink = getPermalink(index); const messageProps = { showOurContact, - api, onReply, onDelete, permalink, @@ -209,15 +201,6 @@ class ChatWindow extends Component< ); } - if (!this.state.initialized) { - return ( - - ); - } const isPending: boolean = 'pending' in msg && Boolean(msg.pending); const isLastMessage = index.eq( graph.peekLargest()?.[0] ?? bigInt.zero diff --git a/pkg/interface/src/views/apps/chat/components/ShareProfile.tsx b/pkg/interface/src/views/apps/chat/components/ShareProfile.tsx index b24cb50b3..98b9d3d37 100644 --- a/pkg/interface/src/views/apps/chat/components/ShareProfile.tsx +++ b/pkg/interface/src/views/apps/chat/components/ShareProfile.tsx @@ -1,22 +1,18 @@ import { BaseImage, Box, Row, Text } from '@tlon/indigo-react'; -import { Contact } from '@urbit/api'; +import { allowGroup, allowShips, Contact, share } from '@urbit/api'; import React, { ReactElement } from 'react'; -import GlobalApi from '~/logic/api/global'; import { Sigil } from '~/logic/lib/sigil'; import { uxToHex } from '~/logic/lib/util'; +import airlock from '~/logic/api'; interface ShareProfileProps { our?: Contact; - api: GlobalApi; recipients: string | string[]; onShare: () => void; } const ShareProfile = (props: ShareProfileProps): ReactElement | null => { - const { - api, - recipients - } = props; + const { recipients } = props; const image = (props?.our?.avatar) ? ( @@ -46,13 +42,13 @@ const ShareProfile = (props: ShareProfileProps): ReactElement | null => { const onClick = async () => { if(typeof recipients === 'string') { const [,,ship,name] = recipients.split('/'); - await api.contacts.allowGroup(ship,name); + await airlock.poke(allowGroup(ship, name)); if(ship !== `~${window.ship}`) { - await api.contacts.share(ship); + await airlock.poke(share(ship)); } } else if(recipients.length > 0) { - await api.contacts.allowShips(recipients); - await Promise.all(recipients.map(r => api.contacts.share(r))); + await airlock.poke(allowShips(recipients)); + await Promise.all(recipients.map(r => airlock.poke(share(r)))); } props.onShare(); }; diff --git a/pkg/interface/src/views/apps/graph/App.tsx b/pkg/interface/src/views/apps/graph/App.tsx index 9cb22a715..c84654708 100644 --- a/pkg/interface/src/views/apps/graph/App.tsx +++ b/pkg/interface/src/views/apps/graph/App.tsx @@ -1,23 +1,17 @@ import { Center, Text } from '@tlon/indigo-react'; -import { GraphConfig } from '@urbit/api'; +import { GraphConfig, joinGraph } from '@urbit/api'; import React, { ReactElement } from 'react'; import { Route, Switch, useHistory } from 'react-router-dom'; -import GlobalApi from '~/logic/api/global'; import { deSig } from '~/logic/lib/util'; import useGraphState from '~/logic/state/graph'; import useMetadataState from '~/logic/state/metadata'; +import airlock from '~/logic/api'; -interface GraphAppProps { - api: GlobalApi; -} - -const GraphApp = (props: GraphAppProps): ReactElement => { +const GraphApp = (): ReactElement => { const associations= useMetadataState(state => state.associations); const graphKeys = useGraphState(state => state.graphKeys); const history = useHistory(); - const { api } = props; - return ( { const autoJoin = () => { try { - api.graph.joinGraph( + airlock.thread(joinGraph( `~${deSig(props.match.params.ship)}`, props.match.params.name - ); + )); } catch(err) { setTimeout(autoJoin, 2000); } diff --git a/pkg/interface/src/views/apps/launch/App.tsx b/pkg/interface/src/views/apps/launch/App.tsx index f84d4592d..fe0eb7b6a 100644 --- a/pkg/interface/src/views/apps/launch/App.tsx +++ b/pkg/interface/src/views/apps/launch/App.tsx @@ -4,7 +4,6 @@ import f from 'lodash/fp'; import React, { ReactElement, useEffect, useMemo, useState } from 'react'; import { Helmet } from 'react-helmet'; import styled from 'styled-components'; -import GlobalApi from '~/logic/api/global'; import { hasTutorialGroup, @@ -32,6 +31,10 @@ import ModalButton from './components/ModalButton'; import Tiles from './components/tiles'; import Tile from './components/tiles/tile'; import './css/custom.css'; +import { join } from '@urbit/api/groups'; +import { putEntry } from '@urbit/api/settings'; +import { joinGraph } from '@urbit/api/graph'; +import airlock from '~/logic/api'; const ScrollbarLessBox = styled(Box)` scrollbar-width: none !important; @@ -45,7 +48,6 @@ const tutSelector = f.pick(['tutorialProgress', 'nextTutStep', 'hideGroups']); interface LaunchAppProps { connection: string; - api: GlobalApi; } export const LaunchApp = (props: LaunchAppProps): ReactElement | null => { @@ -66,14 +68,13 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => { const waiter = useWaitForProps({ ...props, associations }); const hashBox = ( { @@ -85,8 +86,10 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => { }} > {hashText || baseHash} @@ -101,17 +104,17 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => { modal: function modal(dismiss) { const onDismiss = (e) => { e.stopPropagation(); - props.api.settings.putEntry('tutorial', 'seen', true); + airlock.poke(putEntry('tutorial', 'seen', true)); dismiss(); }; const onContinue = async (e) => { e.stopPropagation(); if (!hasTutorialGroup({ associations })) { - await props.api.groups.join(TUTORIAL_HOST, TUTORIAL_GROUP); - await props.api.settings.putEntry('tutorial', 'joined', Date.now()); + await airlock.poke(join(TUTORIAL_HOST, TUTORIAL_GROUP)); + await airlock.poke(putEntry('tutorial', 'joined', Date.now())); await waiter(hasTutorialGroup); await Promise.all( - [TUTORIAL_BOOK, TUTORIAL_CHAT, TUTORIAL_LINKS].map(graph => props.api.graph.joinGraph(TUTORIAL_HOST, graph))); + [TUTORIAL_BOOK, TUTORIAL_CHAT, TUTORIAL_LINKS].map(graph => airlock.thread(joinGraph(TUTORIAL_HOST, graph)))); await waiter((p) => { return `/ship/${TUTORIAL_HOST}/${TUTORIAL_CHAT}` in p.associations.graph && @@ -215,9 +218,7 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => { - + { text="New Group" style={{ gridColumnStart: 1 }} > - + { color="black" text="Join Group" > - + } {!hideGroups && () } - {hashBox} + {hashBox} - {hashBox} ); }; diff --git a/pkg/interface/src/views/apps/launch/components/tiles.tsx b/pkg/interface/src/views/apps/launch/components/tiles.tsx index fe3f27248..5833333b3 100644 --- a/pkg/interface/src/views/apps/launch/components/tiles.tsx +++ b/pkg/interface/src/views/apps/launch/components/tiles.tsx @@ -1,5 +1,4 @@ import React, { ReactElement } from 'react'; -import GlobalApi from '~/logic/api/global'; import useLaunchState from '~/logic/state/launch'; import { WeatherState } from '~/types'; import BasicTile from './tiles/basic'; @@ -7,16 +6,10 @@ import ClockTile from './tiles/clock'; import CustomTile from './tiles/custom'; import WeatherTile from './tiles/weather'; -export interface TileProps { - api: GlobalApi; -} - -const Tiles = (props: TileProps): ReactElement => { +const Tiles = (): ReactElement => { const weather = useLaunchState(state => state.weather) as WeatherState; const tileOrdering = useLaunchState(state => state.tileOrdering); const tileState = useLaunchState(state => state.tiles); - console.log('tileOrdering', tileOrdering); - console.log('tileState', tileState); const tiles = tileOrdering.filter((key) => { const tile = tileState[key]; @@ -35,11 +28,7 @@ const Tiles = (props: TileProps): ReactElement => { } else if ('custom' in tile.type) { if (key === 'weather') { return ( - + ); } else if (key === 'clock') { const location = weather && 'nearest-area' in weather ? weather['nearest-area'][0] : ''; 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 ce049c150..194499b2b 100644 --- a/pkg/interface/src/views/apps/launch/components/tiles/weather.tsx +++ b/pkg/interface/src/views/apps/launch/components/tiles/weather.tsx @@ -1,12 +1,11 @@ import { BaseInput, Box, Icon, Text } from '@tlon/indigo-react'; import moment from 'moment'; import React from 'react'; -import GlobalApi from '~/logic/api/global'; import withState from '~/logic/lib/withState'; import useLaunchState from '~/logic/state/launch'; import ErrorBoundary from '~/views/components/ErrorBoundary'; import Tile from './tile'; - +import airlock from '~/logic/api'; export const weatherStyleMap = { Clear: 'rgba(67, 169, 255, 0.4)', @@ -34,12 +33,11 @@ export const weatherStyleMap = { const imperialCountries = [ 'United States of America', 'Myanmar', - 'Liberia', + 'Liberia' ]; interface WeatherTileProps { weather: any; - api: GlobalApi; location: string; } @@ -49,6 +47,14 @@ interface WeatherTileState { error: boolean; } +function update(location: string) { + return { + mark: 'json', + json: location, + app: 'weather' + }; +} + class WeatherTile extends React.Component { constructor(props: WeatherTileProps) { super(props); @@ -64,7 +70,7 @@ class WeatherTile extends React.Component { navigator.geolocation.getCurrentPosition((res) => { const location = `${res.coords.latitude},${res.coords.longitude}`; this.setState({ location }); - this.props.api.launch.weather(location); + airlock.poke(update(location)); this.setState({ manualEntry: !this.state.manualEntry }); }); } @@ -73,13 +79,13 @@ class WeatherTile extends React.Component { event.preventDefault(); const location = (document.getElementById('location') as HTMLInputElement).value; this.setState({ location }); - this.props.api.launch.weather(location); + airlock.poke(update(location)); this.setState({ manualEntry: !this.state.manualEntry }); } // set appearance based on weather colorFromCondition(data) { - let weatherDesc = data['current-condition'][0].weatherDesc[0].value; + const weatherDesc = data['current-condition'][0].weatherDesc[0].value; return weatherStyleMap[weatherDesc] || weatherStyleMap.default; } @@ -258,7 +264,7 @@ class WeatherTile extends React.Component { } if ('currently' in data) { // Old weather source - this.props.api.launch.weather(this.props.location); + airlock.poke(update(this.props.location)); } if ('current-condition' in data && 'weather' in data) { @@ -287,7 +293,7 @@ class WeatherTile extends React.Component { onClick={() => this.setState({ manualEntry: !this.state.manualEntry }) } - > + > {'->'} diff --git a/pkg/interface/src/views/apps/links/LinkResource.tsx b/pkg/interface/src/views/apps/links/LinkResource.tsx index eb69ca2e9..437c0286f 100644 --- a/pkg/interface/src/views/apps/links/LinkResource.tsx +++ b/pkg/interface/src/views/apps/links/LinkResource.tsx @@ -4,10 +4,8 @@ import { Association } from '@urbit/api/metadata'; import bigInt from 'big-integer'; import React, { useEffect } from 'react'; import { Link, Route, Switch } from 'react-router-dom'; -import GlobalApi from '~/logic/api/global'; import useGraphState from '~/logic/state/graph'; import useMetadataState from '~/logic/state/metadata'; -import { StoreState } from '~/logic/store/type'; import { Comments } from '~/views/components/Comments'; import useGroupState from '../../../logic/state/group'; import { LinkItem } from './components/LinkItem'; @@ -16,16 +14,14 @@ import LinkWindow from './LinkWindow'; const emptyMeasure = () => {}; -type LinkResourceProps = StoreState & { +type LinkResourceProps = { association: Association; - api: GlobalApi; baseUrl: string; }; export function LinkResource(props: LinkResourceProps) { const { association, - api, baseUrl } = props; @@ -45,9 +41,10 @@ export function LinkResource(props: LinkResourceProps) { const graphs = useGraphState(state => state.graphs); const graph = graphs[resourcePath] || null; const graphTimesentMap = useGraphState(state => state.graphTimesentMap); + const getGraph = useGraphState(s => s.getGraph); useEffect(() => { - api.graph.getGraph(ship, name); + getGraph(ship, name); }, [association]); const resourceUrl = `${baseUrl}/resource/link${rid}`; @@ -63,7 +60,7 @@ export function LinkResource(props: LinkResourceProps) { path={relativePath('')} render={(props) => { return ( - // @ts-ignore + // @ts-ignore state helper weirdness ); @@ -114,7 +110,6 @@ export function LinkResource(props: LinkResourceProps) { association={association} group={group as Group} path={resource?.group} - api={api} mt={3} measure={emptyMeasure} /> @@ -124,7 +119,6 @@ export function LinkResource(props: LinkResourceProps) { comments={node} resource={resourcePath} association={association} - api={api} editCommentId={editCommentId} history={props.history} baseUrl={`${resourceUrl}/index/${props.match.params.index}`} diff --git a/pkg/interface/src/views/apps/links/LinkWindow.tsx b/pkg/interface/src/views/apps/links/LinkWindow.tsx index 7bd564462..5190cc4dd 100644 --- a/pkg/interface/src/views/apps/links/LinkWindow.tsx +++ b/pkg/interface/src/views/apps/links/LinkWindow.tsx @@ -4,7 +4,6 @@ import bigInt from 'big-integer'; import React, { Component, ReactNode } from 'react'; -import GlobalApi from '~/logic/api/global'; import { isWriter } from '~/logic/lib/group'; import VirtualScroller from '~/views/components/VirtualScroller'; import { LinkItem } from './components/LinkItem'; @@ -19,7 +18,6 @@ interface LinkWindowProps { baseUrl: string; group: Group; path: string; - api: GlobalApi; pendingSize: number; mb?: number; } @@ -47,7 +45,7 @@ class LinkWindow extends Component { renderItem = React.forwardRef(({ index }: RendererProps, ref) => { const { props } = this; - const { association, graph, api } = props; + const { association, graph } = props; const [, , ship, name] = association.resource.split('/'); // @ts-ignore Uint8Array vs. BigInt mismatch? const node = graph.get(index); @@ -60,7 +58,7 @@ class LinkWindow extends Component { ...props, node }; - {/* @ts-ignore calling @liam-fitzgerald on Uint8Array props */} + { /* @ts-ignore calling @liam-fitzgerald on Uint8Array props */ } if (this.canWrite() && index.eq(first ?? bigInt.zero)) { return ( @@ -77,7 +75,6 @@ class LinkWindow extends Component { { typeof post !== 'string' && } @@ -96,7 +93,7 @@ class LinkWindow extends Component { }); render() { - const { graph, api, association } = this.props; + const { graph, association } = this.props; const first = graph.peekLargest()?.[0]; const [, , ship, name] = association.resource.split('/'); if (!first) { @@ -114,7 +111,6 @@ class LinkWindow extends Component { ) : ( diff --git a/pkg/interface/src/views/apps/links/components/LinkItem.tsx b/pkg/interface/src/views/apps/links/components/LinkItem.tsx index fecd771e2..037fa1748 100644 --- a/pkg/interface/src/views/apps/links/components/LinkItem.tsx +++ b/pkg/interface/src/views/apps/links/components/LinkItem.tsx @@ -1,8 +1,7 @@ import { Action, Anchor, Box, Col, Icon, Row, Rule, Text } from '@tlon/indigo-react'; -import { Association, GraphNode, Group, TextContent, UrlContent } from '@urbit/api'; +import { Association, GraphNode, Group, markEachAsRead, removePosts, TextContent, UrlContent } from '@urbit/api'; import React, { ReactElement, RefObject, useCallback, useEffect, useRef } from 'react'; import { Link, Redirect } from 'react-router-dom'; -import GlobalApi from '~/logic/api/global'; import { roleForShip } from '~/logic/lib/group'; import { getPermalinkForGraph, referenceToPermalink } from '~/logic/lib/permalinks'; import { useCopy } from '~/logic/lib/useCopy'; @@ -11,12 +10,12 @@ import Author from '~/views/components/Author'; import { Dropdown } from '~/views/components/Dropdown'; import RemoteContent from '~/views/components/RemoteContent'; import { PermalinkEmbed } from '../../permalinks/embed'; +import airlock from '~/logic/api'; interface LinkItemProps { node: GraphNode; association: Association; resource: string; - api: GlobalApi; group: Group; path: string; baseUrl: string; @@ -28,9 +27,7 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject { - api.hark.markEachAsRead(props.association, '/', `/${index}`, 'link', 'link'); - }, [association, index]); + airlock.poke(markEachAsRead(resource, '/', `/${index}`)); + }, [resource, index]); useEffect(() => { function onBlur() { // FF will only update on next tick setTimeout(() => { - console.log(remoteRef.current); if(document.activeElement instanceof HTMLIFrameElement // @ts-ignore forwardref prop passing && remoteRef?.current?.containerRef?.contains(document.activeElement)) { @@ -96,15 +92,15 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject { if (confirm('Are you sure you want to delete this link?')) { - api.graph.removePosts(`~${ship}`, name, [node.post.index]); + airlock.poke(removePosts(`~${ship}`, name, [node.post.index])); } }; const appPath = `/ship/~${resource}`; - const unreads = useHarkState(state => state.unreads); - const commColor = (unreads.graph?.[appPath]?.[`/${index}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray'; + const unreads = useHarkState(state => state.unreads?.[appPath]); + const commColor = (unreads?.[`/${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?.['/']?.unreads ?? new Set()).has(node.post.index); return ( - + ) : ( <> diff --git a/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx b/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx index 11ab0bfa1..aa97d8d4f 100644 --- a/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx +++ b/pkg/interface/src/views/apps/links/components/LinkSubmit.tsx @@ -1,15 +1,14 @@ import { BaseInput, Box, Button, LoadingSpinner, Text } from '@tlon/indigo-react'; import { hasProvider } from 'oembed-parser'; import React, { useCallback, useState, DragEvent, useEffect } from 'react'; -import GlobalApi from '~/logic/api/global'; -import { createPost } from '~/logic/api/graph'; import { parsePermalink, permalinkToReference } from '~/logic/lib/permalinks'; import { useFileDrag } from '~/logic/lib/useDrag'; import useStorage from '~/logic/lib/useStorage'; import SubmitDragger from '~/views/components/SubmitDragger'; +import useGraphState from '~/logic/state/graph'; +import { createPost } from '@urbit/api'; interface LinkSubmitProps { - api: GlobalApi; name: string; ship: string; parentIndex?: any; @@ -18,6 +17,7 @@ interface LinkSubmitProps { const LinkSubmit = (props: LinkSubmitProps) => { const { canUpload, uploadDefault, uploading, promptUpload } = useStorage(); + const addPost = useGraphState(s => s.addPost); const [submitFocused, setSubmitFocused] = useState(false); const [urlFocused, setUrlFocused] = useState(false); @@ -26,6 +26,28 @@ const LinkSubmit = (props: LinkSubmitProps) => { const [disabled, setDisabled] = useState(false); const [linkValid, setLinkValid] = useState(false); + const doPost = () => { + const url = linkValue; + const text = linkTitle ? linkTitle : linkValue; + const contents = url.startsWith('web+urbitgraph:/') + ? [{ text }, permalinkToReference(parsePermalink(url)!)] + : [{ text }, { url }]; + + setDisabled(true); + const parentIndex = props.parentIndex || ''; + const post = createPost(`~${window.ship}`, contents, parentIndex); + + addPost( + `~${props.ship}`, + props.name, + post + ); + setDisabled(false); + setLinkValue(''); + setLinkTitle(''); + setLinkValid(false); + }; + const validateLink = (link) => { const URLparser = new RegExp( /((?:([\w\d\.-]+)\:\/\/?){1}(?:(www)\.?){0,1}(((?:[\w\d-]+\.)*)([\w\d-]+\.[\w\d]+))){1}(?:\:(\d+)){0,1}((\/(?:(?:[^\/\s\?]+\/)*))(?:([^\?\/\s#]+?(?:.[^\?\s]+){0,1}){0,1}(?:\?([^\s#]+)){0,1})){0,1}(?:#([^#\s]+)){0,1}/ @@ -76,29 +98,6 @@ const LinkSubmit = (props: LinkSubmitProps) => { setLinkValid(validateLink(linkValue)); }, [linkValue]); - const doPost = () => { - const url = linkValue; - const text = linkTitle ? linkTitle : linkValue; - const contents = url.startsWith('web+urbitgraph:/') - ? [{ text }, permalinkToReference(parsePermalink(url)!)] - : [{ text }, { url }]; - - setDisabled(true); - const parentIndex = props.parentIndex || ''; - const post = createPost(contents, parentIndex); - - props.api.graph.addPost( - `~${props.ship}`, - props.name, - post - ).then(() => { - setDisabled(false); - setLinkValue(''); - setLinkTitle(''); - setLinkValid(false); - }); - }; - const onFileDrag = useCallback( (files: FileList | File[], e: DragEvent): void => { if (!canUpload) { @@ -111,6 +110,13 @@ const LinkSubmit = (props: LinkSubmitProps) => { const { bind, dragging } = useFileDrag(onFileDrag); + const onLinkChange = () => { + const link = validateLink(linkValue); + setLinkValid(link); + }; + + useEffect(onLinkChange, [linkValue]); + const onPaste = useCallback( (event: ClipboardEvent) => { if (!event.clipboardData || !event.clipboardData.files.length) { diff --git a/pkg/interface/src/views/apps/notifications/PendingDm.tsx b/pkg/interface/src/views/apps/notifications/PendingDm.tsx index 2073bc127..286f57e92 100644 --- a/pkg/interface/src/views/apps/notifications/PendingDm.tsx +++ b/pkg/interface/src/views/apps/notifications/PendingDm.tsx @@ -2,20 +2,21 @@ import React, { useCallback } from 'react'; import { Box, Row, Text } from '@tlon/indigo-react'; import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction'; import Author from '~/views/components/Author'; -import GlobalApi from '~/logic/api/global'; import { useHistory } from 'react-router'; +import { acceptDm, declineDm } from '@urbit/api/graph'; +import airlock from '~/logic/api'; -export function PendingDm(props: { ship: string; api: GlobalApi }) { - const { ship, api } = props; +export function PendingDm(props: { ship: string; }) { + const { ship } = props; const { push } = useHistory(); const onAccept = useCallback(async () => { - await api.graph.acceptDm(ship); + await airlock.poke(acceptDm(ship)); push(`/~landscape/messages/dm/${ship}`); - }, [ship, push, api]); + }, [ship, push]); const onDecline = useCallback(async () => { - await api.graph.declineDm(ship); - }, [ship, api]); + await airlock.poke(declineDm(ship)); + }, [ship]); return ( { } return ( - + ); }; @@ -227,9 +226,8 @@ export function GraphNotification(props: { read: boolean; time: number; timebox: BigInteger; - api: GlobalApi; }) { - const { contents, index, read, time, api, timebox } = props; + const { contents, index, read, time, timebox } = props; const history = useHistory(); const authors = _.uniq(_.map(contents, 'author')); @@ -261,7 +259,7 @@ export function GraphNotification(props: { first.index ) ); - }, [api, timebox, index, read, history.push, authors, dm]); + }, [timebox, index, read, history.push, authors, dm]); const authorsInHeader = dm || diff --git a/pkg/interface/src/views/apps/notifications/group.tsx b/pkg/interface/src/views/apps/notifications/group.tsx index 43ece56c7..4f7bfb68e 100644 --- a/pkg/interface/src/views/apps/notifications/group.tsx +++ b/pkg/interface/src/views/apps/notifications/group.tsx @@ -7,7 +7,6 @@ import { import bigInt from 'big-integer'; import _ from 'lodash'; import React, { ReactElement } from 'react'; -import GlobalApi from '~/logic/api/global'; import { useAssocForGroup } from '~/logic/state/metadata'; import { Header } from './header'; @@ -35,10 +34,8 @@ function getGroupUpdateParticipants(update: GroupUpdate): string[] { interface GroupNotificationProps { index: GroupNotifIndex; contents: GroupNotificationContents; - read: boolean; time: number; timebox: bigInt.BigInteger; - api: GlobalApi; } export function GroupNotification(props: GroupNotificationProps): ReactElement { diff --git a/pkg/interface/src/views/apps/notifications/inbox.tsx b/pkg/interface/src/views/apps/notifications/inbox.tsx index c18084915..acdb03c4c 100644 --- a/pkg/interface/src/views/apps/notifications/inbox.tsx +++ b/pkg/interface/src/views/apps/notifications/inbox.tsx @@ -4,6 +4,8 @@ import { JoinRequests, Notifications, + seen, + Timebox, unixToDa } from '@urbit/api'; @@ -11,8 +13,7 @@ import { BigInteger } from 'big-integer'; import _ from 'lodash'; import f from 'lodash/fp'; import moment from 'moment'; -import React, { useCallback, useEffect, useRef } from 'react'; -import GlobalApi from '~/logic/api/global'; +import React, { useEffect, useRef } from 'react'; import { getNotificationKey } from '~/logic/lib/hark'; import { useLazyScroll } from '~/logic/lib/useLazyScroll'; import useLaunchState from '~/logic/state/launch'; @@ -20,6 +21,7 @@ import { daToUnix } from '~/logic/lib/util'; import useHarkState from '~/logic/state/hark'; import { Invites } from './invites'; import { Notification } from './notification'; +import airlock from '~/logic/api'; type DatedTimebox = [BigInteger, Timebox]; @@ -42,19 +44,17 @@ function filterNotification(groups: string[]) { export default function Inbox(props: { archive: Notifications; showArchive?: boolean; - api: GlobalApi; filter: string[]; pendingJoin: JoinRequests; }) { - const { api } = props; useEffect(() => { - let seen = false; + let hasSeen = false; setTimeout(() => { - seen = true; + hasSeen = true; }, 3000); return () => { - if (seen) { - api.hark.seen(); + if (hasSeen) { + airlock.poke(seen()); } }; }, []); @@ -65,6 +65,8 @@ export default function Inbox(props: { s => Object.keys(s.unreads.graph).length > 0 ); + const getMore = useHarkState(s => s.getMore); + const notificationState = useHarkState(state => state.notifications); const unreadNotes = useHarkState(s => s.unreadNotes); const archivedNotifications = useHarkState(state => state.archivedNotifications); @@ -95,16 +97,12 @@ export default function Inbox(props: { const scrollRef = useRef(null); - const loadMore = useCallback(async () => { - return api.hark.getMore(); - }, [api]); - const { isDone, isLoading } = useLazyScroll( scrollRef, ready, 0.2, _.flatten(notifications).length, - loadMore + getMore ); const date = unixToDa(Date.now()); @@ -118,15 +116,14 @@ export default function Inbox(props: { )} - - + + {[...notificationsByDayMap.keys()].sort().reverse().map((day, index) => { const timeboxes = notificationsByDayMap.get(day)!; return timeboxes.length > 0 && ( ); })} @@ -159,8 +156,7 @@ function sortIndexedNotification( function DaySection({ timeboxes, - unread = false, - api + unread = false }) { const lent = timeboxes.map(([,nots]) => nots.length).reduce(f.add, 0); if (lent === 0 || timeboxes.length === 0) { @@ -173,7 +169,6 @@ function DaySection({ _.map(nots.sort(sortIndexedNotification), (not, j: number) => ( state.invites); const pendingDms = useGraphState(s => s.pendingDms) ?? []; @@ -55,7 +52,7 @@ export function Invites(props: InvitesProps): ReactElement { return ( <> {[...pendingDms].map(ship => ( - + ))} {Object.keys(invitesAndStatus) .sort(alphabeticalOrder) @@ -67,7 +64,6 @@ export function Invites(props: InvitesProps): ReactElement { ); @@ -76,7 +72,6 @@ export function Invites(props: InvitesProps): ReactElement { return ( api.groups.hide(resource) - , [resource, api]); + const onHide = useCallback( + async () => { + await airlock.poke(hideGroup(resource)); +}, + [resource] + ); return ( s.mobile); + const [archive, readNote] = useHarkState(s => [s.archive, s.readNote], shallow); + const onArchive = useCallback(async (e) => { e.stopPropagation(); if (!notification) { return; } - return api.hark.archive(time, notification.index); + await archive(notification.index, time); }, [time, notification]); const onClick = (e: any) => { if (!notification || read) { return; } - return api.hark.read(time, notification.index); + return readNote(notification.index); }; const { hovering, bind } = useHovering(); @@ -107,8 +105,7 @@ export function Notification(props: NotificationProps) { const wrapperProps = { notification, read: !unread, - time: props.time, - api: props.api + time: props.time }; if ('graph' in notification.index) { @@ -118,7 +115,6 @@ export function Notification(props: NotificationProps) { return ( diff --git a/pkg/interface/src/views/apps/notifications/notifications.tsx b/pkg/interface/src/views/apps/notifications/notifications.tsx index bfef8998e..26d9fe1e4 100644 --- a/pkg/interface/src/views/apps/notifications/notifications.tsx +++ b/pkg/interface/src/views/apps/notifications/notifications.tsx @@ -1,55 +1,26 @@ import { Box, Col, Icon, Row, Text } from '@tlon/indigo-react'; -import React, { ReactElement, useCallback, useRef, useState } from 'react'; +import React, { ReactElement, useCallback, useRef } from 'react'; import Helmet from 'react-helmet'; import { Link, Route, Switch } from 'react-router-dom'; import useGroupState from '~/logic/state/group'; import useHarkState from '~/logic/state/hark'; -import useMetadataState from '~/logic/state/metadata'; -import { PropFunc } from '~/types/util'; import { Body } from '~/views/components/Body'; import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction'; import { useTutorialModal } from '~/views/components/useTutorialModal'; import Inbox from './inbox'; +import airlock from '~/logic/api'; +import { readAll } from '@urbit/api'; const baseUrl = '/~notifications'; -const HeaderLink = React.forwardRef(( - props: PropFunc & { view?: string; current: string }, - ref -): ReactElement => { - const { current, view, ...textProps } = props; - const to = view ? `${baseUrl}/${view}` : baseUrl; - const active = view ? current === view : !current; - - return ( - - - - ); -}); - -interface NotificationFilter { - groups: string[]; -} - export default function NotificationsScreen(props: any): ReactElement { const relativePath = (p: string) => baseUrl + p; - const [filter, setFilter] = useState({ groups: [] }); - const associations = useMetadataState(state => state.associations); const pendingJoin = useGroupState(s => s.pendingJoin); - const onSubmit = async ({ groups } : NotificationFilter) => { - setFilter({ groups }); - }; const onReadAll = useCallback(async () => { - await props.api.hark.readAll(); + await airlock.poke(readAll()); }, []); - const groupFilterDesc = - filter.groups.length === 0 - ? 'All' - : filter.groups - .map(g => associations.groups?.[g]?.metadata?.title) - .join(', '); + const anchorRef = useRef(null); useTutorialModal('notifications', true, anchorRef); const notificationsCount = useHarkState(state => state.notificationsCount); @@ -101,7 +72,7 @@ export default function NotificationsScreen(props: any): ReactElement { {!view && } diff --git a/pkg/interface/src/views/apps/permalinks/TranscludedNode.tsx b/pkg/interface/src/views/apps/permalinks/TranscludedNode.tsx index 6d888956a..110ae36c0 100644 --- a/pkg/interface/src/views/apps/permalinks/TranscludedNode.tsx +++ b/pkg/interface/src/views/apps/permalinks/TranscludedNode.tsx @@ -4,7 +4,6 @@ import { Anchor, Box, Col, Icon, Row, Text } from '@tlon/indigo-react'; import { Association, GraphConfig, GraphNode, Group, Post, ReferenceContent, TextContent, UrlContent } from '@urbit/api'; import bigInt from 'big-integer'; import React from 'react'; -import GlobalApi from '~/logic/api/global'; import { referenceToPermalink } from '~/logic/lib/permalinks'; import { getSnippet } from '~/logic/lib/publish'; import { useGroupForAssoc } from '~/logic/state/group'; @@ -19,9 +18,8 @@ function TranscludedLinkNode(props: { node: GraphNode; assoc: Association; transcluded: number; - api: GlobalApi; }) { - const { node, api, assoc, transcluded } = props; + const { node, assoc, transcluded } = props; const idx = node?.post?.index?.slice(1)?.split('/') ?? []; if (typeof node?.post === 'string') { @@ -43,7 +41,7 @@ function TranscludedLinkNode(props: { const [{ text }, link] = node.post.contents as [TextContent, UrlContent | ReferenceContent]; if('reference' in link) { const permalink = referenceToPermalink(link).link; - return ; + return ; } return ( @@ -83,7 +81,6 @@ function TranscludedLinkNode(props: { case 2: return ( @@ -213,12 +206,11 @@ function TranscludedPublishNode(props: { export function TranscludedPost(props: { post: Post; - api: GlobalApi; transcluded: number; commentsCount?: number; group: Group; }) { - const { transcluded, post, group, commentsCount, api } = props; + const { transcluded, post, group, commentsCount } = props; if (typeof post === 'string') { return ( @@ -249,7 +241,6 @@ export function TranscludedPost(props: { /> @@ -318,7 +307,6 @@ export function TranscludedNode(props: { case 'post': return ( ; } diff --git a/pkg/interface/src/views/apps/permalinks/embed.tsx b/pkg/interface/src/views/apps/permalinks/embed.tsx index 56c184944..ae637ae59 100644 --- a/pkg/interface/src/views/apps/permalinks/embed.tsx +++ b/pkg/interface/src/views/apps/permalinks/embed.tsx @@ -3,13 +3,12 @@ import { Association, GraphNode, resourceFromPath, GraphConfig } from '@urbit/ap import React, { useCallback, useEffect, useState } from 'react'; import _ from 'lodash'; import { useHistory, useLocation } from 'react-router-dom'; -import GlobalApi from '~/logic/api/global'; import { getPermalinkForGraph, GraphPermalink as IGraphPermalink, parsePermalink } from '~/logic/lib/permalinks'; import { getModuleIcon, GraphModule } from '~/logic/lib/util'; 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 { GroupLink } from '~/views/components/GroupLink'; import { TranscludedNode } from './TranscludedNode'; @@ -55,12 +54,11 @@ function Placeholder(type) { ); } -function GroupPermalink(props: { group: string; api: GlobalApi }) { - const { group, api } = props; +function GroupPermalink(props: { group: string; }) { + const { group } = props; return ( s.getNode); const association = useMetadataState( useCallback(s => s.associations.graph[graph] as Association | null, [ graph @@ -104,7 +102,7 @@ function GraphPermalink( } try { setLoading(true); - await api.graph.getNode(ship, name, index); + await getNode(ship, name, index); setLoading(false); } catch (e) { console.log(e); @@ -157,7 +155,6 @@ function GraphPermalink( {loading && association && !errored && Placeholder((association.metadata.config as GraphConfig).graph)} {showTransclusion && index && !loading && ( ; + return ; case 'graph': return ( diff --git a/pkg/interface/src/views/apps/permalinks/graphIndex.tsx b/pkg/interface/src/views/apps/permalinks/graphIndex.tsx index 71af7e357..01f29369f 100644 --- a/pkg/interface/src/views/apps/permalinks/graphIndex.tsx +++ b/pkg/interface/src/views/apps/permalinks/graphIndex.tsx @@ -73,7 +73,6 @@ function getLinkPermalink( const res = _.reduce( idx, (acc, val, i) => { - console.log(acc); if (i === 0) { return { ...acc, pathname: `${acc.pathname}/index/${val}` }; } else if (i === 1) { diff --git a/pkg/interface/src/views/apps/profile/components/EditProfile.tsx b/pkg/interface/src/views/apps/profile/components/EditProfile.tsx index 4b591a64c..a6ac69669 100644 --- a/pkg/interface/src/views/apps/profile/components/EditProfile.tsx +++ b/pkg/interface/src/views/apps/profile/components/EditProfile.tsx @@ -22,6 +22,8 @@ import { ProfileImages, ProfileStatus } from './Profile'; +import airlock from '~/logic/api'; +import { editContact, setPublic } from '@urbit/api'; const formSchema = Yup.object({ nickname: Yup.string(), @@ -78,7 +80,7 @@ export function ProfileHeaderImageEdit(props: any): ReactElement { } export function EditProfile(props: any): ReactElement { - const { contact, ship, api } = props; + const { contact, ship } = props; const isPublic = useContactState(state => state.isContactPublic); const [hideCover, setHideCover] = useState(false); @@ -94,7 +96,7 @@ export function EditProfile(props: any): ReactElement { const newValue = key !== 'color' ? values[key] : uxToHex(values[key]); if (newValue !== contact[key]) { if (key === 'isPublic') { - api.contacts.setPublic(newValue) + airlock.poke(setPublic(true)); return; } else if (key === 'groups') { const toRemove: string[] = _.difference( @@ -105,19 +107,18 @@ export function EditProfile(props: any): ReactElement { newValue, contact?.groups || [] ); - toRemove.forEach(e => - api.contacts.edit(ship, { 'remove-group': resourceFromPath(e) }) - ) + toRemove.forEach(e => + airlock.poke(editContact(ship, { 'remove-group': resourceFromPath(e) })) + ); toAdd.forEach(e => - api.contacts.edit(ship, { 'add-group': resourceFromPath(e) }) - ) + airlock.poke(editContact(ship, { 'add-group': resourceFromPath(e) })) + ); } else if (key !== 'last-updated' && key !== 'isPublic') { - api.contacts.edit(ship, { [key]: newValue }); + airlock.poke(editContact(ship, { [key]: newValue })); return; } } }); - // actions.setStatus({ success: null }); history.push(`/~profile/${ship}`); } catch (e) { console.error(e); diff --git a/pkg/interface/src/views/apps/profile/components/Profile.tsx b/pkg/interface/src/views/apps/profile/components/Profile.tsx index 0dc60937e..a4fad437f 100644 --- a/pkg/interface/src/views/apps/profile/components/Profile.tsx +++ b/pkg/interface/src/views/apps/profile/components/Profile.tsx @@ -1,4 +1,5 @@ import { BaseImage, Box, Center, Row, Text } from '@tlon/indigo-react'; +import { retrieve } from '@urbit/api'; import React, { ReactElement, useEffect, useRef } from 'react'; import { useHistory } from 'react-router-dom'; import { Sigil } from '~/logic/lib/sigil'; @@ -10,6 +11,7 @@ import { SetStatusBarModal } from '~/views/components/SetStatusBarModal'; import { useTutorialModal } from '~/views/components/useTutorialModal'; import { EditProfile } from './EditProfile'; import { ViewProfile } from './ViewProfile'; +import airlock from '~/logic/api'; export function ProfileHeader(props: any): ReactElement { return ( @@ -120,7 +122,7 @@ export function ProfileStatus(props: any): ReactElement { } export function ProfileActions(props: any): ReactElement { - const { ship, isPublic, contact, api } = props; + const { ship, isPublic, contact } = props; const history = useHistory(); return ( @@ -147,7 +149,6 @@ export function ProfileActions(props: any): ReactElement { isControl py={2} ml={3} - api={api} ship={`~${window.ship}`} contact={contact} /> @@ -176,7 +177,7 @@ export function Profile(props: any): ReactElement | null { useEffect(() => { if (hasLoaded && !contact && !nacked) { - props.api.contacts.retrieve(ship); + airlock.poke(retrieve(ship)); } }, [hasLoaded, contact]); @@ -191,13 +192,11 @@ export function Profile(props: any): ReactElement | null { ) : ( )} diff --git a/pkg/interface/src/views/apps/profile/components/SetStatus.tsx b/pkg/interface/src/views/apps/profile/components/SetStatus.tsx index af66a6395..672cf018f 100644 --- a/pkg/interface/src/views/apps/profile/components/SetStatus.tsx +++ b/pkg/interface/src/views/apps/profile/components/SetStatus.tsx @@ -3,15 +3,15 @@ import { StatelessTextInput as Input } from '@tlon/indigo-react'; +import { editContact } from '@urbit/api'; import React, { ChangeEvent, useCallback, - useEffect, - - useRef, useState + useEffect, useRef, useState } from 'react'; +import airlock from '~/logic/api'; export function SetStatus(props: any) { - const { contact, ship, api, callback } = props; + const { contact, ship, callback } = props; const inputRef = useRef(null); const [_status, setStatus] = useState(''); const onStatusChange = useCallback( @@ -26,7 +26,7 @@ export function SetStatus(props: any) { }, [contact]); const editStatus = () => { - api.contacts.edit(ship, { status: _status }); + airlock.poke(editContact(ship, { status: _status })); inputRef.current.blur(); if (callback) { callback(); diff --git a/pkg/interface/src/views/apps/profile/components/ViewProfile.tsx b/pkg/interface/src/views/apps/profile/components/ViewProfile.tsx index 8afcfc319..2480a6769 100644 --- a/pkg/interface/src/views/apps/profile/components/ViewProfile.tsx +++ b/pkg/interface/src/views/apps/profile/components/ViewProfile.tsx @@ -13,7 +13,7 @@ import { export function ViewProfile(props: any): ReactElement { const { hideNicknames } = useSettingsState(selectCalmState); - const { api, contact, nacked, ship } = props; + const { contact, nacked, ship } = props; const isPublic = useContactState(state => state.isContactPublic); @@ -25,7 +25,6 @@ export function ViewProfile(props: any): ReactElement { ship={ship} isPublic={isPublic} contact={contact} - api={props.api} /> @@ -47,7 +46,7 @@ export function ViewProfile(props: any): ReactElement {
- + {contact?.bio ? contact.bio : ''}
@@ -58,7 +57,6 @@ export function ViewProfile(props: any): ReactElement { {contact?.groups.slice().sort(lengthOrder).map((g, i) => ( {}} diff --git a/pkg/interface/src/views/apps/profile/profile.tsx b/pkg/interface/src/views/apps/profile/profile.tsx index 3e1faf914..c493ecfd8 100644 --- a/pkg/interface/src/views/apps/profile/profile.tsx +++ b/pkg/interface/src/views/apps/profile/profile.tsx @@ -43,7 +43,6 @@ export default function ProfileScreen(props: any) { ship={ship} hasLoaded={Object.keys(contacts).length !== 0} contact={contact} - api={props.api} isEdit={isEdit} /> diff --git a/pkg/interface/src/views/apps/publish/PublishResource.tsx b/pkg/interface/src/views/apps/publish/PublishResource.tsx index 2b455aa2b..4d2eaaf81 100644 --- a/pkg/interface/src/views/apps/publish/PublishResource.tsx +++ b/pkg/interface/src/views/apps/publish/PublishResource.tsx @@ -2,13 +2,10 @@ import { Box } from '@tlon/indigo-react'; import { Association } from '@urbit/api'; import React, { useEffect, useRef } from 'react'; import { useLocation } from 'react-router-dom'; -import GlobalApi from '~/logic/api/global'; -import { StoreState } from '~/logic/store/type'; import { NotebookRoutes } from './components/NotebookRoutes'; -type PublishResourceProps = StoreState & { +type PublishResourceProps = { association: Association; - api: GlobalApi; baseUrl: string; history?: any; match?: any; @@ -16,7 +13,7 @@ type PublishResourceProps = StoreState & { }; export function PublishResource(props: PublishResourceProps) { - const { association, api, baseUrl } = props; + const { association, baseUrl } = props; const rid = association.resource; const [, , ship, book] = rid.split('/'); const location = useLocation(); @@ -32,7 +29,6 @@ export function PublishResource(props: PublishResourceProps) { return ( { const [rev] = getLatestRevision(p.note); return rev === newRev; diff --git a/pkg/interface/src/views/apps/publish/components/Note.tsx b/pkg/interface/src/views/apps/publish/components/Note.tsx index f609c63af..9b9a62ad1 100644 --- a/pkg/interface/src/views/apps/publish/components/Note.tsx +++ b/pkg/interface/src/views/apps/publish/components/Note.tsx @@ -1,20 +1,18 @@ -import { Action, Anchor, Box, Col, Row, Text } from '@tlon/indigo-react'; -import { Association, Graph, GraphNode, Group } from '@urbit/api'; +import { Action, Box, Col, Row, Text } from '@tlon/indigo-react'; +import { Association, Graph, GraphNode, Group, markEachAsRead, removePosts } from '@urbit/api'; import bigInt from 'big-integer'; import React, { useEffect, useState } from 'react'; import { Link, RouteComponentProps } from 'react-router-dom'; -import GlobalApi from '~/logic/api/global'; import { roleForShip } from '~/logic/lib/group'; import { getPermalinkForGraph } from '~/logic/lib/permalinks'; import { getComments, getLatestRevision } from '~/logic/lib/publish'; import { useCopy } from '~/logic/lib/useCopy'; -import { useQuery } from '~/logic/lib/useQuery'; import Author from '~/views/components/Author'; import { Comments } from '~/views/components/Comments'; import { Spinner } from '~/views/components/Spinner'; import { GraphContent } from '~/views/landscape/components/Graph/GraphContent'; import { NoteNavigation } from './NoteNavigation'; -import { Redirect } from 'react-router-dom'; +import airlock from '~/logic/api'; interface NoteProps { ship: string; @@ -22,16 +20,15 @@ interface NoteProps { note: GraphNode; association: Association; notebook: Graph; - api: GlobalApi; rootUrl: string; baseUrl: string; group: Group; } -export function NoteContent({ post, api }) { +export function NoteContent({ post }) { return ( - + ); } @@ -39,12 +36,12 @@ export function NoteContent({ post, api }) { export function Note(props: NoteProps & RouteComponentProps) { const [deleting, setDeleting] = useState(false); - const { association, notebook, note, ship, book, api, rootUrl, baseUrl, group } = props; + const { association, notebook, note, ship, book, rootUrl, baseUrl, group } = props; const deletePost = async () => { setDeleting(true); const indices = [note.post.index]; - await api.graph.removePosts(ship, book, indices); + await airlock.poke(removePosts(ship, book, indices)); props.history.push(rootUrl); }; @@ -56,14 +53,13 @@ export function Note(props: NoteProps & RouteComponentProps) { ); } - const { query } = useQuery(); const comments = getComments(note); - const [revNum, title, body, post] = getLatestRevision(note); + const [, title, , post] = getLatestRevision(note); const index = note.post.index.split('/'); const noteId = bigInt(index[1]); useEffect(() => { - api.hark.markEachAsRead(props.association, '/',`/${index[1]}/1/1`, 'note', 'publish'); + airlock.poke(markEachAsRead(props.association.resource, '/',`/${index[1]}/1/1`)); }, [props.association, props.note]); const adminLinks: JSX.Element[] = []; @@ -124,7 +120,7 @@ export function Note(props: NoteProps & RouteComponentProps) { - + state.unreads); + const unreads = useHarkState(state => state.unreads.graph?.[appPath]); // @ts-ignore hark will have to choose between sets and numbers - const isUnread = unreads.graph?.[appPath]?.['/']?.unreads?.has(`/${noteId}/1/1`); + const isUnread = (unreads?.['/'].unreads ?? new Set()).has(`/${noteId}/1/1`); const snippet = getSnippet(body); - const commColor = (unreads.graph?.[appPath]?.[`/${noteId}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray'; + const commColor = (unreads?.[`/${noteId}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray'; const cursorStyle = post.pending ? 'default' : 'pointer'; diff --git a/pkg/interface/src/views/apps/publish/components/NoteRoutes.tsx b/pkg/interface/src/views/apps/publish/components/NoteRoutes.tsx index 80fc38bea..e8611a7bf 100644 --- a/pkg/interface/src/views/apps/publish/components/NoteRoutes.tsx +++ b/pkg/interface/src/views/apps/publish/components/NoteRoutes.tsx @@ -2,7 +2,6 @@ import { Association, Graph, GraphNode, Group } from '@urbit/api'; import bigInt from 'big-integer'; import React from 'react'; import { Route, RouteComponentProps, Switch } from 'react-router-dom'; -import GlobalApi from '~/logic/api/global'; import { EditPost } from './EditPost'; import Note from './Note'; @@ -12,7 +11,6 @@ interface NoteRoutesProps { note: GraphNode; noteId: bigInt.BigInteger; notebook: Graph; - api: GlobalApi; association: Association; baseUrl?: string; rootUrl?: string; diff --git a/pkg/interface/src/views/apps/publish/components/Notebook.tsx b/pkg/interface/src/views/apps/publish/components/Notebook.tsx index ba6e40a86..66e04178d 100644 --- a/pkg/interface/src/views/apps/publish/components/Notebook.tsx +++ b/pkg/interface/src/views/apps/publish/components/Notebook.tsx @@ -1,11 +1,11 @@ import { Box, Button, Col, Row, Text } from '@tlon/indigo-react'; -import { Association, Graph } from '@urbit/api'; +import { Association, Graph, readGraph } from '@urbit/api'; import React, { ReactElement, useCallback } from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import GlobalApi from '~/logic/api/global'; import { useShowNickname } from '~/logic/lib/util'; import useContactState from '~/logic/state/contact'; import useGroupState from '~/logic/state/group'; +import airlock from '~/logic/api'; import { NotebookPosts } from './NotebookPosts'; interface NotebookProps { @@ -15,7 +15,6 @@ interface NotebookProps { association: Association; baseUrl: string; rootUrl: string; - api: GlobalApi; } export function Notebook(props: NotebookProps & RouteComponentProps): ReactElement | null { @@ -23,8 +22,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps): ReactEleme ship, book, association, - graph, - api + graph } = props; const groups = useGroupState(state => state.groups); @@ -37,7 +35,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps): ReactEleme const showNickname = useShowNickname(contact); const readBook = useCallback(() => { - api.hark.readGraph(association.resource); + airlock.poke(readGraph(association.resource)); }, [association.resource]); if (!group) { diff --git a/pkg/interface/src/views/apps/publish/components/NotebookRoutes.tsx b/pkg/interface/src/views/apps/publish/components/NotebookRoutes.tsx index ab28a3c6b..5bae22860 100644 --- a/pkg/interface/src/views/apps/publish/components/NotebookRoutes.tsx +++ b/pkg/interface/src/views/apps/publish/components/NotebookRoutes.tsx @@ -5,7 +5,6 @@ import { import bigInt from 'big-integer'; import React, { useEffect } from 'react'; import { Route, RouteComponentProps, Switch } from 'react-router-dom'; -import GlobalApi from '~/logic/api/global'; import useGraphState from '~/logic/state/graph'; import useGroupState from '~/logic/state/group'; import NewPost from './new-post'; @@ -13,7 +12,6 @@ import Notebook from './Notebook'; import { NoteRoutes } from './NoteRoutes'; interface NotebookRoutesProps { - api: GlobalApi; ship: string; book: string; baseUrl: string; @@ -24,10 +22,11 @@ interface NotebookRoutesProps { export function NotebookRoutes( props: NotebookRoutesProps & RouteComponentProps ) { - const { ship, book, api, baseUrl, rootUrl } = props; + const { ship, book, baseUrl, rootUrl } = props; + const getGraph = useGraphState(s => s.getGraph); useEffect(() => { - ship && book && api.graph.getGraph(ship, book); + ship && book && getGraph(ship, book); }, [ship, book]); const graphs = useGraphState(state => state.graphs); @@ -62,7 +61,6 @@ export function NotebookRoutes( render={routeProps => ( { - const { association, groups, api } = props; + const { association, groups } = props; const resource = resourceFromPath(association?.group); const onSubmit = async (values, actions) => { try { const ships = values.ships.map(e => `~${e}`); - await api.groups.addTag( + await airlock.poke(addTag( resource, { app: 'graph', resource: association.resource, tag: 'writers' }, ships - ); + )); actions.resetForm(); actions.setStatus({ success: null }); } catch (e) { diff --git a/pkg/interface/src/views/apps/publish/components/new-post.tsx b/pkg/interface/src/views/apps/publish/components/new-post.tsx index 7a666094a..ec61635a1 100644 --- a/pkg/interface/src/views/apps/publish/components/new-post.tsx +++ b/pkg/interface/src/views/apps/publish/components/new-post.tsx @@ -1,15 +1,13 @@ -import { Association } from '@urbit/api'; +import { addNodes, Association } from '@urbit/api'; import { Graph } from '@urbit/api/graph'; import { FormikHelpers } from 'formik'; import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import GlobalApi from '~/logic/api/global'; import { newPost } from '~/logic/lib/publish'; -import { useWaitForProps } from '~/logic/lib/useWaitForProps'; import { PostForm, PostFormSchema } from './NoteForm'; +import airlock from '~/logic/api'; interface NewPostProps { - api: GlobalApi; book: string; ship: string; graph: Graph; @@ -18,9 +16,7 @@ interface NewPostProps { } export default function NewPost(props: NewPostProps & RouteComponentProps) { - const { api, book, ship, history } = props; - - const waiter = useWaitForProps(props, 20000); + const { book, ship, history } = props; const onSubmit = async ( values: PostFormSchema, @@ -28,8 +24,8 @@ export default function NewPost(props: NewPostProps & RouteComponentProps) { ) => { const { title, body } = values; try { - const [noteId, nodes] = newPost(title, body); - await api.graph.addNodes(ship, book, nodes); + const [, nodes] = newPost(title, body); + await airlock.thread(addNodes(ship, book, nodes)); history.push(`${props.baseUrl}`); } catch (e) { console.error(e); 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 119a112b4..ed2ede7f2 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/BackgroundPicker.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/BackgroundPicker.tsx @@ -3,19 +3,13 @@ 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'; import { ImageInput } from '~/views/components/ImageInput'; export type BgType = 'none' | 'url' | 'color'; -export function BackgroundPicker({ api }: { - bgType: BgType; - bgUrl?: string; - api: GlobalApi; -}): ReactElement { +export function BackgroundPicker(): ReactElement { const rowSpace = { my: 0, alignItems: 'center' }; const colProps = { my: 3, @@ -33,7 +27,6 @@ export function BackgroundPicker({ api }: { Set an image background ; selected: string; - api: GlobalApi; }): ReactElement { const _buckets = Array.from(buckets); @@ -28,28 +27,28 @@ export function BucketList({ const onSubmit = useCallback( (values: { newBucket: string }, actions: FormikHelpers) => { - api.s3.addBucket(values.newBucket); + airlock.poke(addBucket(values.newBucket)); actions.resetForm({ values: { newBucket: '' } }); }, - [api] + [] ); const onSelect = useCallback( (bucket: string) => { return function () { - api.s3.setCurrentBucket(bucket); + airlock.poke(setCurrentBucket(bucket)); }; }, - [api] + [] ); const onDelete = useCallback( (bucket: string) => { return function () { - api.s3.removeBucket(bucket); + airlock.poke(removeBucket(bucket)); }; }, - [api] + [] ); return ( 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 ff7ccc55a..4b205ef03 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/CalmPref.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/CalmPref.tsx @@ -3,13 +3,14 @@ import { Text } from '@tlon/indigo-react'; -import { Form } from 'formik'; +import { putEntry } from '@urbit/api/settings'; import React, { useCallback } from 'react'; -import GlobalApi from '~/logic/api/global'; +import { Form } from 'formik'; import useSettingsState, { SettingsState } from '~/logic/state/settings'; import { BackButton } from './BackButton'; import _ from 'lodash'; import { FormikOnBlur } from '~/views/components/FormikOnBlur'; +import airlock from '~/logic/api'; interface FormSchema { hideAvatars: boolean; @@ -35,17 +36,14 @@ const settingsSel = (s: SettingsState): FormSchema => ({ audioShown: !s.remoteContentPolicy.audioShown }); -export function CalmPrefs(props: { - api: GlobalApi; -}) { - const { api } = props; +export function CalmPrefs() { const initialValues = useSettingsState(settingsSel); const onSubmit = useCallback(async (v: FormSchema) => { _.forEach(v, (bool, key) => { const bucket = ['imageShown', 'videoShown', 'audioShown', 'oembedShown'].includes(key) ? 'remoteContentPolicy' : 'calm'; if(initialValues[key] !== bool) { - api.settings.putEntry(bucket, key, bool); + airlock.poke(putEntry(bucket, key, bool)); } }); }, [initialValues]); 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 a6c3745e5..13905cc7c 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/DisplayForm.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/DisplayForm.tsx @@ -1,28 +1,27 @@ import { - Col, - - Label, - ManagedRadioButtonField as Radio, Text + Col, + Label, + ManagedRadioButtonField as Radio, + Text } from '@tlon/indigo-react'; -import { Form, Formik } from 'formik'; -import React from 'react'; +import { Form } from 'formik'; +import { putEntry } from '@urbit/api/settings'; +import React, { useMemo } from 'react'; import * as Yup from 'yup'; -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 { FormikOnBlur } from '~/views/components/FormikOnBlur'; import { BackButton } from './BackButton'; +import airlock from '~/logic/api'; import { BackgroundPicker, BgType } from './BackgroundPicker'; const formSchema = Yup.object().shape({ bgType: Yup.string() .oneOf(['none', 'color', 'url'], 'invalid') .required('Required'), - background: Yup.string(), - theme: Yup.string() - .oneOf(['light', 'dark', 'auto']) - .required('Required') + bgColor: Yup.string().when('bgType', (bgType, schema) => bgType === 'color' ? schema.required() : schema), + bgUrl: Yup.string().when('bgType', (bgType, schema) => bgType === 'url' ? schema.required() : schema), + theme: Yup.string().oneOf(['light', 'dark', 'auto']).required('Required') }); interface FormSchema { @@ -32,85 +31,70 @@ interface FormSchema { theme: string; } -interface DisplayFormProps { - api: GlobalApi; -} - const settingsSel = selectSettingsState(['display']); -export default function DisplayForm(props: DisplayFormProps) { - const { api } = props; - +export default function DisplayForm() { const { - display: { - background, - backgroundType, - theme - } + display: { background, backgroundType, theme } } = useSettingsState(settingsSel); - let bgColor, bgUrl; - if (backgroundType === 'url') { - bgUrl = background; - } - if (backgroundType === 'color') { - bgColor = background; - } - const bgType = backgroundType || 'none'; + const initialValues: FormSchema = useMemo(() => { + let bgColor, bgUrl; + if (backgroundType === 'url') { + bgUrl = background; + } + if (backgroundType === 'color') { + bgColor = background; + } + return { + bgType: backgroundType, + bgColor: bgColor || '', + bgUrl, + theme + }; + }, [backgroundType, background, theme]); return ( { const promises = [] as Promise[]; - promises.push(api.settings.putEntry('display', 'backgroundType', values.bgType)); promises.push( - api.settings.putEntry('display', 'background', - values.bgType === 'color' - ? `#${uxToHex(values.bgColor || '0x0')}` - : values.bgType === 'url' - ? values.bgUrl || '' - : false - )); - - promises.push(api.settings.putEntry('display', 'theme', values.theme)); - await Promise.all(promises); - - actions.setStatus({ success: null }); + airlock.poke(putEntry('display', 'backgroundType', values.bgType)) + ); + promises.push( + airlock.poke( + putEntry( + 'display', + 'background', + values.bgType === 'color' + ? `#${uxToHex(values.bgColor || '0x0')}` + : values.bgType === 'url' + ? values.bgUrl || '' + : false + ) + ) + ); + promises.push(airlock.poke(putEntry('display', 'theme', values.theme))); }} > -
- - - - - Display Preferences - - - Customize visual interfaces across your Landscape - - - - - - - - - Save - + + + + + + Display Preferences + + Customize visual interfaces across your Landscape - + + + + + + +
); } diff --git a/pkg/interface/src/views/apps/settings/components/lib/DmSettings.tsx b/pkg/interface/src/views/apps/settings/components/lib/DmSettings.tsx index 201a56021..a1c8febf9 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/DmSettings.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/DmSettings.tsx @@ -6,17 +6,17 @@ import { } from '@tlon/indigo-react'; import { Form, Formik } from 'formik'; import React, { useCallback } from 'react'; -import GlobalApi from '~/logic/api/global'; import useGraphState from '~/logic/state/graph'; import { AsyncButton } from '~/views/components/AsyncButton'; +import airlock from '~/logic/api'; +import { setScreen } from '@urbit/api/graph'; -export function DmSettings(props: { api: GlobalApi }) { - const { api } = props; +export function DmSettings() { const screening = useGraphState(s => s.screening); const initialValues = { accept: !screening }; const onSubmit = useCallback( async (values, actions) => { - await api.graph.setScreen(!values.accept); + await airlock.poke(setScreen(!values.accept)); actions.setStatus({ success: null }); }, [screening] 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 590ae05fe..9b0d28a85 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/LeapSettings.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/LeapSettings.tsx @@ -3,10 +3,10 @@ import { ManagedCheckboxField, Text } from '@tlon/indigo-react'; -import { Form, useField, useFormikContext } from 'formik'; +import { Form, useFormikContext } from 'formik'; +import { putEntry } from '@urbit/api/settings'; import _ from 'lodash'; import React from 'react'; -import GlobalApi from '~/logic/api/global'; import useSettingsState, { selectSettingsState } from '~/logic/state/settings'; import { LeapCategories, @@ -15,9 +15,10 @@ import { import { FormikOnBlur } from '~/views/components/FormikOnBlur'; import { ShuffleFields } from '~/views/components/ShuffleFields'; import { BackButton } from './BackButton'; +import airlock from '~/logic/api'; const labels: Record = { - mychannel: 'My Channel', + mychannel: 'My Channels', updates: 'Notifications', profile: 'Profile', messages: 'Messages', @@ -32,8 +33,6 @@ function CategoryCheckbox(props: { index: number }) { const { index } = props; const { values } = useFormikContext(); const cats = values.categories; - const catNameId = `categories[${index}].category`; - const [field] = useField(catNameId); const { category } = cats[index]; const label = labels[category]; @@ -45,9 +44,8 @@ function CategoryCheckbox(props: { index: number }) { const settingsSel = selectSettingsState(['leap', 'set']); -export function LeapSettings(props: { api: GlobalApi; }) { - const { api } = props; - const { leap, set: setSettingsState } = useSettingsState(settingsSel); +export function LeapSettings() { + const { leap } = useSettingsState(settingsSel); const categories = leap.categories as LeapCategories[]; const missing = _.difference(leapCategories, categories); @@ -66,7 +64,7 @@ export function LeapSettings(props: { api: GlobalApi; }) { (acc, { display, category }) => (display ? [...acc, category] : acc), [] as LeapCategories[] ); - await api.settings.putEntry('leap', 'categories', result); + await airlock.poke(putEntry('leap', 'categories', result)); }; return ( 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 5bda6924b..600653e25 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/NotificationPref.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/NotificationPref.tsx @@ -1,21 +1,19 @@ import { Button, Col, - - - - ManagedToggleSwitchField as Toggle, Text } from '@tlon/indigo-react'; import { Form, FormikHelpers } from 'formik'; import _ from 'lodash'; 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 { FormikOnBlur } from '~/views/components/FormikOnBlur'; import { BackButton } from './BackButton'; import { GroupChannelPicker } from './GroupChannelPicker'; +import airlock from '~/logic/api'; +import { ignoreGraph, ignoreGroup, listenGraph, listenGroup, setDoNotDisturb, setMentions } from '@urbit/api'; +import { setWatchOnSelf } from '@urbit/api'; interface FormSchema { mentions: boolean; @@ -29,10 +27,7 @@ interface FormSchema { } } -export function NotificationPreferences(props: { - api: GlobalApi; -}) { - const { api } = props; +export function NotificationPreferences() { const dnd = useHarkState(state => state.doNotDisturb); const graphConfig = useHarkState(state => state.notificationsGraphConfig); const groupConfig = useHarkState(s => s.notificationsGroupConfig); @@ -46,22 +41,22 @@ export function NotificationPreferences(props: { try { const promises: Promise[] = []; if (values.mentions !== graphConfig.mentions) { - promises.push(api.hark.setMentions(values.mentions)); + promises.push(airlock.poke(setMentions(values.mentions))); } if (values.watchOnSelf !== graphConfig.watchOnSelf) { - promises.push(api.hark.setWatchOnSelf(values.watchOnSelf)); + promises.push(airlock.poke(setWatchOnSelf(values.watchOnSelf))); } if (values.dnd !== dnd && !_.isUndefined(values.dnd)) { - promises.push(api.hark.setDoNotDisturb(values.dnd)); + promises.push(airlock.poke(setDoNotDisturb(values.dnd))); } _.forEach(values.graph, (listen: boolean, graph: string) => { if(listen !== isWatching(graphConfig, graph)) { - promises.push(api.hark[listen ? 'listenGraph' : 'ignoreGraph'](graph, '/')); + promises.push(airlock.poke((listen ? listenGraph : ignoreGraph)(graph, '/'))); } }); _.forEach(values.groups, (listen: boolean, group: string) => { if(listen !== groupConfig.includes(group)) { - promises.push(api.hark[listen ? 'listenGroup' : 'ignoreGroup'](group)); + promises.push(airlock.poke((listen ? listenGroup : ignoreGroup)(group))); } }); @@ -71,7 +66,7 @@ export function NotificationPreferences(props: { console.error(e); actions.setStatus({ error: e.message }); } - }, [api, graphConfig, dnd]); + }, [graphConfig, dnd]); const [notificationsAllowed, setNotificationsAllowed] = useState('Notification' in window && Notification.permission !== 'default'); diff --git a/pkg/interface/src/views/apps/settings/components/lib/S3Form.tsx b/pkg/interface/src/views/apps/settings/components/lib/S3Form.tsx index cf07907be..700f4bfe5 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/S3Form.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/S3Form.tsx @@ -5,11 +5,12 @@ import { } from '@tlon/indigo-react'; import { Formik, FormikHelpers } from 'formik'; import React, { ReactElement, useCallback } from 'react'; -import GlobalApi from '~/logic/api/global'; import useStorageState from '~/logic/state/storage'; import { AsyncButton } from '~/views/components/AsyncButton'; import { BackButton } from './BackButton'; import { BucketList } from './BucketList'; +import airlock from '~/logic/api'; +import { setAccessKeyId, setEndpoint, setSecretAccessKey } from '@urbit/api'; interface FormSchema { s3bucket: string; @@ -19,30 +20,23 @@ interface FormSchema { s3secretAccessKey: string; } -interface S3FormProps { - api: GlobalApi; -} - -export default function S3Form(props: S3FormProps): ReactElement { - const { api } = props; +export default function S3Form(_props: {}): ReactElement { const s3 = useStorageState(state => state.s3); const onSubmit = useCallback(async (values: FormSchema, actions: FormikHelpers) => { - if (values.s3secretAccessKey !== s3.credentials?.secretAccessKey) { - await api.s3.setSecretAccessKey(values.s3secretAccessKey); - } + if (values.s3secretAccessKey !== s3.credentials?.secretAccessKey) { + await airlock.poke(setSecretAccessKey(values.s3secretAccessKey)); + } - if (values.s3endpoint !== s3.credentials?.endpoint) { - await api.s3.setEndpoint(values.s3endpoint); - } + if (values.s3endpoint !== s3.credentials?.endpoint) { + await airlock.poke(setEndpoint(values.s3endpoint)); + } - if (values.s3accessKeyId !== s3.credentials?.accessKeyId) { - await api.s3.setAccessKeyId(values.s3accessKeyId); - } - actions.setStatus({ success: null }); - }, - [api, s3] - ); + if (values.s3accessKeyId !== s3.credentials?.accessKeyId) { + await airlock.poke(setAccessKeyId(values.s3accessKeyId)); + } + actions.setStatus({ success: null }); + }, [s3]); return ( <> @@ -100,14 +94,13 @@ export default function S3Form(props: S3FormProps): ReactElement { S3 Buckets - Your 'active' bucket will be the one used when Landscape uploads a + Your 'active' bucket will be the one used when Landscape uploads a file diff --git a/pkg/interface/src/views/apps/settings/components/lib/Security.tsx b/pkg/interface/src/views/apps/settings/components/lib/Security.tsx index 1cb273557..eb2a3dca7 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/Security.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/Security.tsx @@ -4,14 +4,9 @@ import { StatelessCheckboxField, Text } from '@tlon/indigo-react'; import React, { useState } from 'react'; -import GlobalApi from '~/logic/api/global'; import { BackButton } from './BackButton'; -interface SecuritySettingsProps { - api: GlobalApi; -} - -export default function SecuritySettings({ api }: SecuritySettingsProps) { +export default function SecuritySettings() { const [allSessions, setAllSessions] = useState(false); return ( <> diff --git a/pkg/interface/src/views/apps/settings/components/lib/ShortcutSettings.tsx b/pkg/interface/src/views/apps/settings/components/lib/ShortcutSettings.tsx index 539724913..1ffa0471b 100644 --- a/pkg/interface/src/views/apps/settings/components/lib/ShortcutSettings.tsx +++ b/pkg/interface/src/views/apps/settings/components/lib/ShortcutSettings.tsx @@ -3,8 +3,8 @@ import _ from 'lodash'; import { Box, Col, Text } from '@tlon/indigo-react'; import { Formik, Form, useField } from 'formik'; +import { putEntry } from '@urbit/api/settings'; -import GlobalApi from '~/logic/api/global'; import { getChord } from '~/logic/lib/util'; import useSettingsState, { selectSettingsState, @@ -12,10 +12,7 @@ import useSettingsState, { } from '~/logic/state/settings'; import { AsyncButton } from '~/views/components/AsyncButton'; import { BackButton } from './BackButton'; - -interface ShortcutSettingsProps { - api: GlobalApi; -} +import airlock from '~/logic/api'; const settingsSel = selectSettingsState(['keyboard']); @@ -64,9 +61,7 @@ export function ChordInput(props: { id: string; label: string }) { ); } -export default function ShortcutSettings(props: ShortcutSettingsProps) { - const { api } = props; - +export default function ShortcutSettings() { const { keyboard } = useSettingsState(settingsSel); return ( @@ -75,8 +70,8 @@ export default function ShortcutSettings(props: ShortcutSettingsProps) { onSubmit={async (values: ShortcutMapping, actions) => { const promises = _.map(values, (value, key) => { return keyboard[key] !== value - ? api.settings.putEntry('keyboard', key, value) - : Promise.resolve(); + ? airlock.poke(putEntry('keyboard', key, value)) + : Promise.resolve(0); }); await Promise.all(promises); actions.setStatus({ success: null }); diff --git a/pkg/interface/src/views/apps/settings/settings.tsx b/pkg/interface/src/views/apps/settings/settings.tsx index a0251df3e..0b9498aa5 100644 --- a/pkg/interface/src/views/apps/settings/settings.tsx +++ b/pkg/interface/src/views/apps/settings/settings.tsx @@ -132,13 +132,13 @@ return; graphConfig={props.notificationsGraphConfig} /> )} - {hash === 'display' && } - {hash === 'dm' && } - {hash === 'shortcuts' && } - {hash === 's3' && } - {hash === 'leap' && } - {hash === 'calm' && } - {hash === 'security' && } + {hash === 'display' && } + {hash === 'dm' && } + {hash === 'shortcuts' && } + {hash === 's3' && } + {hash === 'leap' && } + {hash === 'calm' && } + {hash === 'security' && } {hash === 'debug' && } diff --git a/pkg/interface/src/views/apps/term/app.tsx b/pkg/interface/src/views/apps/term/app.tsx index 0ab8f8ef1..69a2f8222 100644 --- a/pkg/interface/src/views/apps/term/app.tsx +++ b/pkg/interface/src/views/apps/term/app.tsx @@ -6,8 +6,8 @@ import React, { import Helmet from 'react-helmet'; import useTermState from '~/logic/state/term'; -import useSettingsState from "~/logic/state/settings"; -import useLocalState from "~/logic/state/local"; +import useSettingsState from '~/logic/state/settings'; +import useLocalState from '~/logic/state/local'; import { Terminal, ITerminalOptions, ITheme } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; @@ -15,15 +15,16 @@ import { saveAs } from 'file-saver'; import { Box, Col } from '@tlon/indigo-react'; -import '../../../../node_modules/xterm/css/xterm.css' -import GlobalApi from '~/logic/api/global'; -import { Belt } from '~/logic/api/term'; -import { Blit, Stye, Stub, Tint, Deco } from '~/types/term-update'; +import '../../../../node_modules/xterm/css/xterm.css'; +import api from '~/logic/api/index'; +import { + Belt, Blit, Stye, Stub, Tint, Deco, + pokeTask, pokeBelt +} from '@urbit/api/term'; import bel from '~/logic/lib/bel'; type TermAppProps = { - api: GlobalApi; ship: string; notificationsCount: number; } @@ -37,16 +38,16 @@ const makeTheme = (dark: boolean): ITheme => { fg = 'black'; bg = 'white'; } - //TODO indigo colors. + // TODO indigo colors. // we can't pluck these from ThemeContext because they have transparency. // technically xterm supports transparency, but it degrades performance. return { foreground: fg, background: bg, - brightBlack: '#7f7f7f', //NOTE slogs - cursor: fg, - } -} + brightBlack: '#7f7f7f', // NOTE slogs + cursor: fg + }; +}; const termConfig: ITerminalOptions = { logLevel: 'warn', @@ -58,18 +59,18 @@ const termConfig: ITerminalOptions = { scrollback: 10000, // fontFamily: '"Source Code Pro","Roboto mono","Courier New",monospace', - //NOTE theme colors configured dynamically + // NOTE theme colors configured dynamically // bellStyle: 'sound', bellSound: bel, // // allows text selection by holding modifier (option, or shift) - macOptionClickForcesSelection: true, -} + macOptionClickForcesSelection: true +}; const csi = (cmd: string, ...args: number[]) => { return '\x1b[' + args.join(';') + cmd; -} +}; const tint = (t: Tint) => { switch (t) { @@ -84,7 +85,7 @@ const tint = (t: Tint) => { case 'w': return '7'; default: return `8;2;${t.r%256};${t.g%256};${t.b%256}`; } -} +}; const stye = (s: Stye) => { let out = ''; @@ -93,6 +94,7 @@ const stye = (s: Stye) => { // if (s.deco.length > 0) { out += s.deco.reduce((decs: number[], deco: Deco) => { + /* eslint-disable max-statements-per-line */ switch (deco) { case null: decs.push(0); return decs; case 'br': decs.push(1); return decs; @@ -106,7 +108,9 @@ const stye = (s: Stye) => { // background color // if (s.back !== null) { - if (out !== '') out += ';'; + if (out !== '') { + out += ';'; + } out += '4'; out += tint(s.back); } @@ -114,39 +118,38 @@ const stye = (s: Stye) => { // foreground color // if (s.fore !== null) { - if (out !== '') out += ';'; + if (out !== '') { + out += ';'; + } out += '3'; out += tint(s.fore); } - if (out === '') return out; + if (out === '') { + return out; + } return '\x1b[' + out + 'm'; -} +}; const showBlit = (term: Terminal, blit: Blit) => { let out = ''; if ('bel' in blit) { out += '\x07'; - } - else if ('clr' in blit) { + } else if ('clr' in blit) { term.clear(); out += csi('u'); - } - else if ('hop' in blit) { + } else if ('hop' in blit) { if (typeof blit.hop === 'number') { out += csi('H', term.rows, blit.hop + 1); - } - else { + } else { out += csi('H', term.rows - blit.hop.r, blit.hop.c + 1); } out += csi('s'); // save cursor position - } - else if ('put' in blit) { + } else if ('put' in blit) { out += blit.put.join(''); out += csi('u'); - } - else if ('klr' in blit) { + } else if ('klr' in blit) { out += blit.klr.reduce((lin: string, p: Stub) => { lin += stye(p.stye); lin += p.text.join(''); @@ -154,32 +157,27 @@ const showBlit = (term: Terminal, blit: Blit) => { return lin; }, ''); out += csi('u'); - } - else if ('nel' in blit) { + } else if ('nel' in blit) { out += '\n'; - } - else if ('sag' in blit || 'sav' in blit) { + } else if ('sag' in blit || 'sav' in blit) { const sav = ('sag' in blit) ? blit.sag : blit.sav; - let name = sav.path.split('/').slice(-2).join('.'); - let buff = new Buffer(sav.file, 'base64'); - let blob = new Blob([buff], {type: 'application/octet-stream'}); + const name = sav.path.split('/').slice(-2).join('.'); + const buff = Buffer.from(sav.file, 'base64'); + const blob = new Blob([buff], { type: 'application/octet-stream' }); saveAs(blob, name); - } - else if ('url' in blit) { + } else if ('url' in blit) { window.open(blit.url); - } - else if ('wyp' in blit) { + } else if ('wyp' in blit) { out += '\r' + csi('K'); out += csi('u'); - } - else { + } else { console.log('weird blit', blit); } term.write(out); }; -//NOTE should generally only be passed the default terminal session +// NOTE should generally only be passed the default terminal session const showSlog = (term: Terminal, slog: string) => { // set scroll region to exclude the bottom line, // scroll up one line, @@ -199,7 +197,7 @@ const showSlog = (term: Terminal, slog: string) => { }; const readInput = (term: Terminal, e: string): Belt[] => { - let belts: Belt[] = []; + const belts: Belt[] = []; let strap = ''; while (e.length > 0) { @@ -220,14 +218,11 @@ const readInput = (term: Terminal, e: string): Belt[] => { // if (0 === c) { term.write('\x07'); // bel - } - else if (8 === c || 127 === c) { + } else if (8 === c || 127 === c) { belts.push({ bac: null }); - } - else if (13 === c) { + } else if (13 === c) { belts.push({ ret: null }); - } - else if (c <= 26) { + } else if (c <= 26) { belts.push({ mod: { mod: 'ctl', key: String.fromCharCode(96 + c) } }); } @@ -239,13 +234,14 @@ const readInput = (term: Terminal, e: string): Belt[] => { if (91 === c || 79 === c) { // [ or O e = e.slice(1); c = e.charCodeAt(0); + /* eslint-disable max-statements-per-line */ switch (c) { case 65: belts.push({ aro: 'u' }); break; case 66: belts.push({ aro: 'd' }); break; case 67: belts.push({ aro: 'r' }); break; case 68: belts.push({ aro: 'l' }); break; // - case 77: + case 77: { const m = e.charCodeAt(1) - 31; if (1 === m) { const c = e.charCodeAt(2) - 32; @@ -254,20 +250,17 @@ const readInput = (term: Terminal, e: string): Belt[] => { } e = e.slice(3); break; + } // default: term.write('\x07'); break; // bel } - } - else if (c >= 97 && c <= 122) { // a <= c <= z + } else if (c >= 97 && c <= 122) { // a <= c <= z belts.push({ mod: { mod: 'met', key: e[0] } }); - } - else if (c === 46) { // . + } else if (c === 46) { // . belts.push({ mod: { mod: 'met', key: '.' } }); - } - else if (c === 8 || c === 127) { + } else if (c === 8 || c === 127) { belts.push({ mod: { mod: 'met', key: { bac: null } } }); - } - else { + } else { term.write('\x07'); break; // bel } } @@ -279,13 +272,11 @@ const readInput = (term: Terminal, e: string): Belt[] => { strap = ''; } return belts; -} +}; export default function TermApp(props: TermAppProps) { - const { api } = props; - const container = useRef(null); - //TODO allow switching of selected + // TODO allow switching of selected const { sessions, selected, slogstream, set } = useTermState(); const session = useTermState(useCallback( @@ -293,7 +284,7 @@ export default function TermApp(props: TermAppProps) { [selected, sessions] )); - const osDark = useLocalState((state) => state.dark); + const osDark = useLocalState(state => state.dark); const theme = useSettingsState(s => s.display.theme); const dark = theme === 'dark' || (theme === 'auto' && osDark); @@ -302,54 +293,60 @@ export default function TermApp(props: TermAppProps) { let available = false; const slog = new EventSource('/~_~/slog', { withCredentials: true }); - slog.onopen = e => { + slog.onopen = (e) => { console.log('slog: opened stream'); available = true; - } + }; - slog.onmessage = e => { - let session = useTermState.getState().sessions['']; + slog.onmessage = (e) => { + const session = useTermState.getState().sessions['']; if (!session) { console.log('default session mia!', 'slog:', slog); return; } showSlog(session.term, e.data); - } + }; - slog.onerror = e => { + slog.onerror = (e) => { console.error('slog: eventsource error:', e); if (available) { window.setTimeout(() => { - if (slog.readyState !== EventSource.CLOSED) return; + if (slog.readyState !== EventSource.CLOSED) { + return; + } console.log('slog: reconnecting...'); setupSlog(); }, 10000); } - } + }; - set(state => { state.slogstream = slog }); + set((state) => { + state.slogstream = slog; +}); }, [sessions]); const onInput = useCallback((ses: string, e: string) => { const term = useTermState.getState().sessions[ses].term; const belts = readInput(term, e); - belts.map(b => { //NOTE passing api.term.sendBelt makes `this` undefined! - api.term.sendBelt(ses, b); + belts.map((b) => { // NOTE passing api.poke(pokeBelt makes `this` undefined! + api.poke(pokeBelt(ses, b)); }); - }, [sessions, api.term]); + }, [sessions]); const onResize = useCallback(() => { - //TODO debounce, if it ever becomes a problem + // TODO debounce, if it ever becomes a problem session?.fit.fit(); }, [session]); // on-init, open slogstream // useEffect(() => { - if (!slogstream) setupSlog(); + if (!slogstream) { + setupSlog(); + } window.addEventListener('resize', onResize); return () => { - //TODO clean up subs? + // TODO clean up subs? window.removeEventListener('resize', onResize); }; }, [onResize, setupSlog]); @@ -358,7 +355,7 @@ export default function TermApp(props: TermAppProps) { // useEffect(() => { const theme = makeTheme(dark); - for (let ses in sessions) { + for (const ses in sessions) { sessions[ses].term.setOption('theme', theme); } if (container.current) { @@ -375,7 +372,7 @@ export default function TermApp(props: TermAppProps) { if (!ses) { // set up terminal // - let term = new Terminal(termConfig); + const term = new Terminal(termConfig); term.setOption('theme', makeTheme(dark)); const fit = new FitAddon(); term.loadAddon(fit); @@ -386,20 +383,20 @@ export default function TermApp(props: TermAppProps) { // set up event handlers // - term.onData((e) => onInput(selected, e)); - term.onBinary((e) => onInput(selected, e)); + term.onData(e => onInput(selected, e)); + term.onBinary(e => onInput(selected, e)); term.onResize((e) => { - api.term.sendTask(selected, { blew: { w: e.cols, h: e.rows } }); + api.poke(pokeTask(selected, { blew: { w: e.cols, h: e.rows } })); }); ses = { term, fit }; // open subscription // - //TODO start default session alongside other landscape subscriptions, + // TODO start default session alongside other landscape subscriptions, // once subscription refactor is in. - api.subscribe('/session/'+selected, 'PUT', api.ship, 'herm', - (e) => { + api.subscribe({ app: 'herm', path: '/session/'+selected, + event: (e) => { const ses = useTermState.getState().sessions[selected]; if (!ses) { console.log('on blit: no such session', selected, sessions, useTermState.getState().sessions); @@ -407,14 +404,10 @@ export default function TermApp(props: TermAppProps) { } showBlit(ses.term, e.data); }, - (err) => { // fail - console.log('sub error', selected, err); - //TODO resubscribe - }, - () => { // quit - //TODO resubscribe + quit: () => { // quit + // TODO show user a message } - ); + }); } if (container.current && !container.current.contains(ses.term.element || null)) { @@ -423,12 +416,14 @@ export default function TermApp(props: TermAppProps) { ses.term.focus(); } - set(state => { state.sessions[selected] = ses; }); + set((state) => { + state.sessions[selected] = ses; + }); return () => { - //TODO unload term from container + // TODO unload term from container // but term.dispose is too powerful? maybe just empty the container? - } + }; }, [set, session, container]); return ( @@ -451,7 +446,7 @@ export default function TermApp(props: TermAppProps) { border={['0','1']} p='1' ref={container} - > + >
diff --git a/pkg/interface/src/views/components/Author.tsx b/pkg/interface/src/views/components/Author.tsx index 76d52f99e..3eac067dd 100644 --- a/pkg/interface/src/views/components/Author.tsx +++ b/pkg/interface/src/views/components/Author.tsx @@ -1,12 +1,11 @@ import { BaseImage, Box, Row, Text } from '@tlon/indigo-react'; import moment from 'moment'; import React, { ReactElement, ReactNode } from 'react'; -import GlobalApi from '~/logic/api/global'; import { Sigil } from '~/logic/lib/sigil'; import { useCopy } from '~/logic/lib/useCopy'; -import { cite, deSig, useShowNickname, uxToHex } from '~/logic/lib/util'; -import useContactState from '~/logic/state/contact'; -import useLocalState from '~/logic/state/local'; +import { cite, useShowNickname, uxToHex } from '~/logic/lib/util'; +import { useContact } from '~/logic/state/contact'; +import { useDark } from '~/logic/state/join'; import useSettingsState, { selectCalmState } from '~/logic/state/settings'; import { PropFunc } from '~/types'; import ProfileOverlay from './ProfileOverlay'; @@ -18,14 +17,13 @@ export interface AuthorProps { showImage?: boolean; children?: ReactNode; unread?: boolean; - api?: GlobalApi; size?: number; lineHeight?: string | number; isRelativeTime?: boolean; } // eslint-disable-next-line max-lines-per-function -export default function Author(props: AuthorProps & PropFunc): ReactElement { +function Author(props: AuthorProps & PropFunc): ReactElement { const { ship = '', date, @@ -43,16 +41,9 @@ export default function Author(props: AuthorProps & PropFunc): React const size = props.size || 16; const sigilPadding = props.sigilPadding || 2; - const osDark = useLocalState(state => state.dark); + const dark = useDark(); - const theme = useSettingsState(s => s.display.theme); - const dark = theme === 'dark' || (theme === 'auto' && osDark); - - let contact; - const contacts = useContactState(state => state.contacts); - if (contacts) { - contact = `~${deSig(ship)}` in contacts ? contacts[`~${deSig(ship)}`] : null; - } + const contact = useContact(ship); const color = contact?.color ? `#${uxToHex(contact?.color)}` : dark ? '#000000' : '#FFFFFF'; const showNickname = useShowNickname(contact); const { hideAvatars } = useSettingsState(selectCalmState); @@ -88,7 +79,7 @@ export default function Author(props: AuthorProps & PropFunc): React cursor='pointer' > {showImage && ( - + {img} )} @@ -126,3 +117,5 @@ export default function Author(props: AuthorProps & PropFunc): React ); } + +export default React.memo(Author); diff --git a/pkg/interface/src/views/components/CommentItem.tsx b/pkg/interface/src/views/components/CommentItem.tsx index 68401d2f9..832155954 100644 --- a/pkg/interface/src/views/components/CommentItem.tsx +++ b/pkg/interface/src/views/components/CommentItem.tsx @@ -1,10 +1,9 @@ import { Action, Box, Row, Text } from '@tlon/indigo-react'; -import { Group } from '@urbit/api'; +import { Group, removePosts } from '@urbit/api'; import { GraphNode } from '@urbit/api/graph'; import bigInt from 'big-integer'; import React, { useCallback, useEffect, useRef } from 'react'; import { Link } from 'react-router-dom'; -import GlobalApi from '~/logic/api/global'; import { roleForShip } from '~/logic/lib/group'; import { getPermalinkForGraph } from '~/logic/lib/permalinks'; import { getLatestCommentRevision } from '~/logic/lib/publish'; @@ -12,6 +11,7 @@ import { useCopy } from '~/logic/lib/useCopy'; import useMetadataState from '~/logic/state/metadata'; import Author from '~/views/components/Author'; import { GraphContent } from '../landscape/components/Graph/GraphContent'; +import airlock from '~/logic/api'; interface CommentItemProps { pending?: boolean; @@ -20,14 +20,13 @@ interface CommentItemProps { unread: boolean; name: string; ship: string; - api: GlobalApi; group: Group; highlighted: boolean; } export function CommentItem(props: CommentItemProps) { let { highlighted } = props; - const { ship, name, api, comment, group } = props; + const { ship, name, comment, group } = props; const association = useMetadataState( useCallback(s => s.associations.graph[`/ship/${ship}/${name}`], [ship,name]) ); @@ -46,11 +45,11 @@ export function CommentItem(props: CommentItemProps) { } } - await api.graph.removePosts(ship, name, [ + await airlock.poke(removePosts(ship, name, [ comment.post?.index, revs?.post?.index, ...indices - ]); + ])); }; const ourMention = post?.contents?.some((e) => { @@ -134,7 +133,6 @@ return false; mb={1} backgroundColor={highlighted ? 'washedBlue' : 'white'} transcluded={0} - api={api} contents={post.contents} showOurContact /> diff --git a/pkg/interface/src/views/components/Comments.tsx b/pkg/interface/src/views/components/Comments.tsx index c9e0a6060..d8106aed2 100644 --- a/pkg/interface/src/views/components/Comments.tsx +++ b/pkg/interface/src/views/components/Comments.tsx @@ -1,10 +1,8 @@ import { Col } from '@tlon/indigo-react'; -import { Association, GraphNode, Group } from '@urbit/api'; +import { createPost, createBlankNodeWithChildPost, Association, GraphNode, Group, markCountAsRead, addPost } from '@urbit/api'; import bigInt from 'big-integer'; import { FormikHelpers } from 'formik'; import React, { useEffect, useMemo } from 'react'; -import GlobalApi from '~/logic/api/global'; -import { createBlankNodeWithChildPost, createPost } from '~/logic/api/graph'; import { isWriter } from '~/logic/lib/group'; import { getUnreadCount } from '~/logic/lib/hark'; import { referenceToPermalink } from '~/logic/lib/permalinks'; @@ -15,6 +13,8 @@ import useHarkState from '~/logic/state/hark'; import { PropFunc } from '~/types/util'; import CommentInput from './CommentInput'; import { CommentItem } from './CommentItem'; +import airlock from '~/logic/api'; +import useGraphState from '~/logic/state/graph'; interface CommentsProps { comments: GraphNode; @@ -22,7 +22,6 @@ interface CommentsProps { name: string; ship: string; baseUrl: string; - api: GlobalApi; group: Group; } @@ -32,12 +31,12 @@ export function Comments(props: CommentsProps & PropFunc) { comments, ship, name, - api, history, baseUrl, group, ...rest } = props; + const addNode = useGraphState(s => s.addNode); const { query } = useQuery(); const selectedComment = useMemo(() => { @@ -57,11 +56,12 @@ export function Comments(props: CommentsProps & PropFunc) { try { const content = tokenizeMessage(comment); const node = createBlankNodeWithChildPost( + window.ship, comments?.post?.index, '1', content ); - await api.graph.addNode(ship, name, node); + addNode(ship, name, node); actions.resetForm(); actions.setStatus({ success: null }); } catch (e) { @@ -76,15 +76,16 @@ export function Comments(props: CommentsProps & PropFunc) { ) => { try { const commentNode = comments.children.get(bigInt(editCommentId))!; - const [idx, _] = getLatestCommentRevision(commentNode); + const [idx] = getLatestCommentRevision(commentNode); const content = tokenizeMessage(comment); const post = createPost( + `~${window.ship}`, content, commentNode.post.index, parseInt((idx + 1).toString(), 10).toString() ); - await api.graph.addPost(ship, name, post); + await airlock.thread(addPost(ship, name, post)); history.push(baseUrl); } catch (e) { console.error(e); @@ -95,7 +96,7 @@ export function Comments(props: CommentsProps & PropFunc) { let commentContent = null; if (editCommentId) { const commentNode = comments.children.get(bigInt(editCommentId)); - const [_, post] = getLatestCommentRevision(commentNode); + const [,post] = getLatestCommentRevision(commentNode); commentContent = post.contents.reduce((val, curr) => { if ('text' in curr) { val = val + curr.text; @@ -118,9 +119,9 @@ export function Comments(props: CommentsProps & PropFunc) { useEffect(() => { return () => { - api.hark.markCountAsRead(association, parentIndex, 'comment'); + airlock.poke(markCountAsRead(association.resource)); }; - }, [comments.post.index]); + }, [comments.post?.index]); const unreads = useHarkState(state => state.unreads); const readCount = children.length - getUnreadCount(unreads, association.resource, parentIndex); @@ -146,7 +147,6 @@ export function Comments(props: CommentsProps & PropFunc) { highlighted={highlighted} comment={comment} key={idx.toString()} - api={api} name={name} ship={ship} unread={i >= readCount} diff --git a/pkg/interface/src/views/components/FormikOnBlur.tsx b/pkg/interface/src/views/components/FormikOnBlur.tsx index 3e1249052..4ab064734 100644 --- a/pkg/interface/src/views/components/FormikOnBlur.tsx +++ b/pkg/interface/src/views/components/FormikOnBlur.tsx @@ -17,10 +17,13 @@ export function FormikOnBlur< ) { setSubmitting(true); const { values } = formikBag; - formikBag.submitForm().then(() => { - formikBag.resetForm({ values }); - setSubmitting(false); - }); + formikBag.validateForm(values) + .then(valid => valid ? + formikBag.submitForm().then(() => { + formikBag.resetForm({ values }); + setSubmitting(false); + }) : null + ); } }, [ formikBag.errors, diff --git a/pkg/interface/src/views/components/GroupLink.tsx b/pkg/interface/src/views/components/GroupLink.tsx index eea1a2db1..998a525ee 100644 --- a/pkg/interface/src/views/components/GroupLink.tsx +++ b/pkg/interface/src/views/components/GroupLink.tsx @@ -1,50 +1,30 @@ import { Box, Col, Icon, Row, Text } from '@tlon/indigo-react'; -import { MetadataUpdatePreview } from '@urbit/api'; -import React, { ReactElement, useEffect, useLayoutEffect, useState } from 'react'; +import React, { ReactElement, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; -import GlobalApi from '~/logic/api/global'; import { useModal } from '~/logic/lib/useModal'; -import { useVirtual } from '~/logic/lib/virtualContext'; -import useMetadataState from '~/logic/state/metadata'; +import useMetadataState, { usePreview } from '~/logic/state/metadata'; import { PropFunc } from '~/types'; import { JoinGroup } from '../landscape/components/JoinGroup'; import { MetadataIcon } from '../landscape/components/MetadataIcon'; export function GroupLink( props: { - api: GlobalApi; resource: string; detailed?: boolean; } & PropFunc ): ReactElement { - const { resource, api, ...rest } = props; + const { resource, ...rest } = props; const name = resource.slice(6); - const [preview, setPreview] = useState(null); - const associations = useMetadataState(state => state.associations); - const { save, restore } = useVirtual(); + const joined = useMetadataState( + useCallback(s => resource in s.associations.groups, [resource]) + ); const history = useHistory(); - const joined = resource in associations.groups; const { modal, showModal } = useModal({ - modal: - }); + modal: + }); - useEffect(() => { - (async () => { - const prev = await api.metadata.preview(resource); - save(); - setPreview(prev); - })(); - - return () => { - save(); - setPreview(null); - }; - }, [resource]); - - useLayoutEffect(() => { - restore(); - }, [preview]); + const { preview } = usePreview(resource); return ( { return ( (resource in p.groups && @@ -199,32 +200,31 @@ export function useInviteAccept( function InviteActions(props: { status?: JoinRequest; resource: string; - api: GlobalApi; app?: string; uid?: string; }) { - const { status, resource, api, app, uid } = props; - const inviteAccept = useInviteAccept(resource, api, app, uid); + const { status, resource, app, uid } = props; + const inviteAccept = useInviteAccept(resource, app, uid); const set = useGroupState(s => s.set); const inviteDecline = useCallback(async () => { if (!(app && uid)) { return; } - await api.invite.decline(app, uid); + await airlock.poke(decline(app, uid)); }, [app, uid]); const hideJoin = useCallback(async (e) => { if(status?.progress === 'done') { - set(s => { + set((s) => { // @ts-ignore investigate zustand types - delete s.pendingJoin[resource] + delete s.pendingJoin[resource]; }); e.stopPropagation(); return; } - await api.groups.hide(resource); - }, [api, resource, status]); + await airlock.poke(hideGroup(resource)); + }, [resource, status]); if (status) { return ( @@ -276,7 +276,7 @@ const responsiveStyle = ({ gapXY = 0 as number | number[] }) => { }; const ResponsiveRow = styled(Row)(responsiveStyle); export function GroupInvite(props: GroupInviteProps): ReactElement { - const { resource, api, preview, invite, status, app, uid } = props; + const { resource, preview, invite, status, app, uid } = props; const dm = isDm(resource); const history = useHistory(); @@ -297,7 +297,7 @@ export function GroupInvite(props: GroupInviteProps): ReactElement { }; return ( - +
@@ -309,7 +309,6 @@ export function GroupInvite(props: GroupInviteProps): ReactElement { ; export function JoinSkeleton(props: JoinSkeletonProps): ReactElement { - const { api, resource, children, status, ...rest } = props; + const { resource, children, status, ...rest } = props; return ( <> {children} - + diff --git a/pkg/interface/src/views/components/Invite/index.tsx b/pkg/interface/src/views/components/Invite/index.tsx index 3abd042a6..b743ae058 100644 --- a/pkg/interface/src/views/components/Invite/index.tsx +++ b/pkg/interface/src/views/components/Invite/index.tsx @@ -1,9 +1,7 @@ -import { - JoinRequest, MetadataUpdatePreview -} from '@urbit/api'; +import { JoinRequest } from '@urbit/api'; import { Invite } from '@urbit/api/invite'; -import React, { useEffect, useState } from 'react'; -import GlobalApi from '~/logic/api/global'; +import React from 'react'; +import { usePreview } from '~/logic/state/metadata'; import { GroupInvite } from './Group'; interface InviteItemProps { @@ -12,25 +10,12 @@ interface InviteItemProps { pendingJoin?: JoinRequest; app?: string; uid?: string; - api: GlobalApi; } export function InviteItem(props: InviteItemProps) { - const [preview, setPreview] = useState(null); - const { pendingJoin, invite, resource, uid, app, api } = props; + const { pendingJoin, invite, resource, uid, app } = props; - useEffect(() => { - if (!app || app === 'groups') { - (async () => { - setPreview(await api.metadata.preview(resource)); - })(); - return () => { - setPreview(null); - }; - } else { - return () => {}; - } - }, [invite]); + const { preview } = usePreview(resource); if (pendingJoin?.hidden) { return null; @@ -39,7 +24,6 @@ export function InviteItem(props: InviteItemProps) { return ( ) { + const { ship, first = false, ...rest } = props; const contact = useContact(`~${deSig(ship)}`); const showNickname = useShowNickname(contact); const name = showNickname ? contact?.nickname : cite(ship); return ( - + void; }; interface OverlaySigilState { @@ -27,13 +19,8 @@ interface OverlaySigilState { export const OverlaySigil = (props: OverlaySigilProps) => { const { - api, className, color, - contact, - group, - history, - onDismiss, scrollWindow, ship, ...rest @@ -88,7 +75,6 @@ export const OverlaySigil = (props: OverlaySigilProps) => { style={{ visibility: visible ? 'visible' : 'hidden' }} > [s.calm.hideAvatars, s.calm.hideNicknames]; + const ProfileOverlay = (props: ProfileOverlayProps) => { const { ship, @@ -54,8 +56,7 @@ const ProfileOverlay = (props: ProfileOverlayProps) => { const history = useHistory(); const outerRef = useRef(null); const innerRef = useRef(null); - const hideAvatars = useSettingsState(state => state.calm.hideAvatars); - const hideNicknames = useSettingsState(state => state.calm.hideNicknames); + const [hideAvatars, hideNicknames] = useSettingsState(selSettings, shallow); const isOwn = useMemo(() => window.ship === ship, [ship]); const { copyDisplay, doCopy, didCopy } = useCopy(`~${ship}`); @@ -129,7 +130,7 @@ const ProfileOverlay = (props: ProfileOverlayProps) => { return ( - + {children} { open && ( @@ -202,7 +203,6 @@ const ProfileOverlay = (props: ProfileOverlayProps) => { {isOwn ? ( diff --git a/pkg/interface/src/views/components/ProfileStatus.tsx b/pkg/interface/src/views/components/ProfileStatus.tsx index ef35a3288..cdaff0d91 100644 --- a/pkg/interface/src/views/components/ProfileStatus.tsx +++ b/pkg/interface/src/views/components/ProfileStatus.tsx @@ -4,10 +4,12 @@ import { StatelessTextInput as Input, Text } from '@tlon/indigo-react'; +import { editContact } from '@urbit/api'; import React, { useCallback, useEffect, useState } from 'react'; +import airlock from '~/logic/api'; export const ProfileStatus = (props) => { - const { contact, ship, api, callback } = props; + const { contact, ship, callback } = props; const [_status, setStatus] = useState(''); const [notice, setNotice] = useState(' '); @@ -23,7 +25,7 @@ export const ProfileStatus = (props) => { }, [contact]); const editStatus = () => { - api.contacts.edit(ship, { status: _status }); + airlock.poke(editContact(ship, { status: _status })); setNotice('Success!'); setTimeout(() => { diff --git a/pkg/interface/src/views/components/ReconnectButton.tsx b/pkg/interface/src/views/components/ReconnectButton.tsx index 5921780b4..d7a866b42 100644 --- a/pkg/interface/src/views/components/ReconnectButton.tsx +++ b/pkg/interface/src/views/components/ReconnectButton.tsx @@ -1,18 +1,33 @@ import { Button, LoadingSpinner, Text } from '@tlon/indigo-react'; import React from 'react'; +import useLocalState from '~/logic/state/local'; +import api from '~/logic/api'; -const ReconnectButton = ({ connection, subscription }) => { - const connectedStatus = connection || 'connected'; - const reconnect = subscription.restart.bind(subscription); +const ReconnectButton = () => { + const { set, subscription } = useLocalState(); + const reconnect = () => { + (async () => { + try { + await api.eventSource(); + set((state) => { + state.subscription = 'connected'; + }); + } catch (e) { + set((state) => { + state.subscription = 'connected'; + }); + } + })(); + }; - if (connectedStatus === 'disconnected') { + if (subscription === 'disconnected') { return ( ); - } else if (connectedStatus === 'reconnecting') { + } else if (subscription === 'reconnecting') { return (