Merge branch 'release/next-userspace' into m/next-gen-term-real

This commit is contained in:
fang 2021-06-23 13:24:24 +02:00
commit cbed01f0ac
No known key found for this signature in database
GPG Key ID: EB035760C1BBA972
205 changed files with 3404 additions and 5007 deletions

View File

@ -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

View File

@ -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]
--

View File

@ -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
--

View File

@ -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

View File

@ -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)]
==
--
::

View File

@ -0,0 +1,12 @@
/- *post
|_ i=indexed-post
++ grad %noun
++ grow
|%
++ noun i
--
++ grab
|%
++ noun indexed-post
--
--

View File

@ -10,5 +10,7 @@
::
[%warm-cache-all ~]
[%cool-cache-all ~]
[%warm-static-conversion from=term to=term]
[%cool-static-conversion from=term to=term]
==
--

View File

@ -1,4 +1,10 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
cd pkg/interface && npx lint-staged
cd pkg/interface
command -v npx > /dev/null || {
exit 0
}
npx lint-staged

Binary file not shown.

View File

@ -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",

View File

@ -9,4 +9,6 @@ for i in $(find . -type d -maxdepth 1) ; do
npm ci
cd ..
fi
done
done
cd http-api
npm run build

View File

@ -1,74 +0,0 @@
import { Path, Patp } from '@urbit/api';
import _ from 'lodash';
import BaseStore from '../store/base';
export default class BaseApi<S extends object = {}> {
bindPaths: Path[] = [];
constructor(public ship: Patp, public channel: any, public store: BaseStore<S>) {}
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<any> {
return new Promise((resolve, reject) => {
this.channel.poke(
ship,
appl,
mark,
data,
(json) => {
resolve(json);
},
(err) => {
reject(err);
}
);
});
}
scry<T>(app: string, path: Path): Promise<T> {
return fetch(`/~/scry/${app}${path}.json`).then(r => r.json() as Promise<T>);
}
async spider<T>(inputMark: string, outputMark: string, threadName: string, body: any): Promise<T> {
const res = await fetch(`/spider/${inputMark}/${threadName}/${outputMark}.json`, {
method: 'POST',
body: JSON.stringify(body)
});
return res.json();
}
}

View File

@ -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);
});
};

View File

@ -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<StoreState> {
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<any>(
'contact-store',
`/is-allowed/${entity}/${name}/${ship}/${isPersonal}`
);
}
async disallowedShipsForOurContact(ships: string[]): Promise<string[]> {
return _.compact(
await Promise.all(
ships.map(
async (s) => {
const ship = `~${s}`;
if(s === window.ship) {
return null;
}
const allowed = await this.fetchIsAllowed(
`~${window.ship}`,
'personal',
ship,
true
);
return allowed ? null : ship;
}
)
)
);
}
retrieve(ship: string) {
const resource = { ship, name: '' };
return this.action('contact-pull-hook', 'pull-hook-action', {
add: {
resource,
ship
}
});
}
private storeAction(action: any): Promise<any> {
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<any> {
return this.action('contact-push-hook', 'contact-update-0', action);
}
}

View File

@ -1,19 +0,0 @@
import type { StoreState } from '../store/type';
import BaseApi from './base';
export default class GcpApi extends BaseApi<StoreState> {
// Does not touch the store; use the value manually.
async isConfigured(): Promise<boolean> {
return this.spider('noun', 'json', 'gcp-is-configured', {});
}
// Does not return the token; read it out of the store.
async getToken(): Promise<void> {
return this.spider('noun', 'gcp-token', 'gcp-get-token', {})
.then((token) => {
this.store.handleEvent({
data: token
});
});
}
}

View File

@ -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<StoreState> {
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);
}
}

View File

@ -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<GraphNode>
};
};
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<StoreState> {
joiningGraphs = new Set<string>();
private storeAction(action: any): Promise<any> {
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<any> {
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<GroupPolicy>,
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<string[] | undefined> {
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<Resource> {
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<void> {
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<any>('graph-store', '/keys')
.then((keys) => {
this.store.handleEvent({
data: keys
});
});
}
getTags() {
return this.scry<any>('graph-store', '/tags')
.then((tags) => {
this.store.handleEvent({
data: tags
});
});
}
getTagQueries() {
return this.scry<any>('graph-store', '/tag-queries')
.then((tagQueries) => {
this.store.handleEvent({
data: tagQueries
});
});
}
getGraph(ship: string, resource: string) {
return this.scry<any>('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<any>('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<any>('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<any>('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<any>('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<any>('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<any>('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<any>(
'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<any>(
'graph-store',
`/node/${ship}/${resource}${idx}`
);
data['graph-update'].fetch = true;
const node = data['graph-update'];
this.store.handleEvent({
data: {
'graph-update-loose': node
}
});
}
}

View File

@ -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<StoreState> {
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<GroupPolicyDiff>) {
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<GroupPolicy>, 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);
}
}

View File

@ -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<StoreState> {
private harkAction(action: any): Promise<any> {
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<boolean> {
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
});
}
}

View File

@ -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;

View File

@ -1,27 +0,0 @@
import { Serial } from '@urbit/api';
import { StoreState } from '../store/type';
import BaseApi from './base';
export default class InviteApi extends BaseApi<StoreState> {
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);
}
}

View File

@ -1,29 +0,0 @@
import { StoreState } from '../store/type';
import BaseApi from './base';
export default class LaunchApi extends BaseApi<StoreState> {
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);
}
}

View File

@ -1,16 +0,0 @@
import { StoreState } from '../store/type';
import BaseApi from './base';
export default class LocalApi extends BaseApi<StoreState> {
getBaseHash() {
this.scry<string>('file-server', '/clay/base/hash').then((baseHash) => {
this.store.handleEvent({ data: { baseHash } });
});
}
getRuntimeLag() {
return this.scry<boolean>('launch', '/runtime-lag').then((runtimeLag) => {
this.store.handleEvent({ data: { runtimeLag } });
});
}
}

View File

@ -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<StoreState> {
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<Metadata>) {
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<MetadataUpdatePreview>((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);
}
}

View File

@ -1,33 +0,0 @@
import { StoreState } from '../store/type';
import BaseApi from './base';
export default class S3Api extends BaseApi<StoreState> {
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);
}
}

View File

@ -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<StoreState> {
private storeAction(action: SettingsUpdate): Promise<any> {
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<string, unknown> = 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<string, unknown> = await this.scry('settings-store', `/entry/${bucket}/${entry}`);
this.store.handleEvent({ data: { 'settings-data': {
'bucket-key': bucket,
'entry-key': entry,
'entry': data.entry
} } });
}
}

View File

@ -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<string> };
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<StoreState> {
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<Array<string>> {
return this.scry<Array<string>>('herm', '/sessions');
}
}

View File

@ -0,0 +1,25 @@
import airlock from '~/logic/api';
import _ from 'lodash';
import { fetchIsAllowed } from '@urbit/api';
export async function disallowedShipsForOurContact(
ships: string[]
): Promise<string[]> {
return _.compact(
await Promise.all(
ships.map(async (s) => {
const ship = `~${s}`;
if (s === window.ship) {
return null;
}
const allowed = await airlock.scry(fetchIsAllowed(
`~${window.ship}`,
'personal',
ship,
true
));
return allowed ? null : ship;
})
)
);
}

View File

@ -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) {

View File

@ -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('/')}`;
}

View File

@ -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<any>[] = [];
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');
};
}

View File

@ -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]);
}

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useMemo, useEffect, useState } from 'react';
function retrieve<T>(key: string, initial: T): T {
const s = localStorage.getItem(key);
@ -12,26 +12,16 @@ function retrieve<T>(key: string, initial: T): T {
return initial;
}
interface SetStateFunc<T> {
(t: T): T;
}
// See microsoft/typescript#37663 for filed bug
type SetState<T> = T extends any ? SetStateFunc<T> : never;
export function useLocalStorageState<T>(key: string, initial: T): 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<T>) => {
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]);
}

View File

@ -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;
}

View File

@ -1,13 +0,0 @@
import { Cage } from '~/types/cage';
import { StoreState } from '../store/type';
type LocalState = Pick<StoreState, 'connection'>;
export default class ConnectionReducer<S extends LocalState> {
reduce(json: Cage, state: S) {
if('connection' in json && json.connection) {
console.log(`Conn: ${json.connection}`);
state.connection = json.connection;
}
}
}

View File

@ -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<State>;
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<ContactState, ContactUpdate>(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
];

View File

@ -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<StorageState, any>(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');
};

View File

@ -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<State>;
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<GraphState, any>(useGraphState, thread, [addNodesThread]);
}
const dm = _.get(json, 'dm-hook-action', false);
if(dm) {
console.log(dm);
reduceState<GraphState, any>(useGraphState, dm, [
acceptOrRejectDm,
pendings,
setScreen
]);
}
};

View File

@ -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> & State;
function decodeGroup(group: Enc<Group>): Group {
const members = new Set(group.members);
@ -54,21 +56,7 @@ function decodeTags(tags: Enc<Tags>): Tags {
export default class GroupReducer {
reduce(json: Cage) {
const data = json.groupUpdate;
if (data) {
reduceState<GroupState, GroupUpdate>(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
];

View File

@ -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<State>;
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<GroupState, GroupUpdate>(useGroupState, data, [
progress,
hide,
started,
initial
]);
}
};
export const reduce = [
progress,
hide,
started,
initial
];

View File

@ -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<State>;
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<HarkState, any>(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<HarkState, any>(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
];

View File

@ -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<InviteState, InviteUpdate>(useInviteState, data, [
initial,
create,
deleteInvite,
invite,
accepted,
decline
]);
}
}
}
type InviteState = State & BaseState<State>;
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
];

View File

@ -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<LaunchState, LaunchUpdate>(useLaunchState, data, [
initial,
changeFirstTime,
changeOrder,
changeFirstTime,
changeIsShown
]);
}
const weatherData: WeatherState | boolean | Record<string, never> = _.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<State>;
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
];

View File

@ -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<State>;
export default class MetadataReducer {
reduce(json: Cage) {
const data = json['metadata-update'];
if (data) {
reduceState<MetadataState, MetadataUpdate>(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
];

View File

@ -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<StorageState, S3Update>(useStorageState, data, [
credentials,
configuration,
currentBucket,
addBucket,
removeBucket,
endpoint,
accessKeyId,
secretAccessKey
]);
}
}
}
type StorageState = State & BaseState<State>;
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
];

View File

@ -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<SettingsState, SettingsUpdate>(useSettingsState, data, [
this.putBucket,
this.delBucket,
this.putEntry,
this.delEntry
]);
}
data = json['settings-data'];
if (data) {
reduceState<SettingsState, SettingsUpdate>(useSettingsState, data, [
this.getAll,
this.getBucket,
this.getEntry
]);
}
}
type SettingsState = State & BaseState<State>;
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<string, string> = _.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<string, string> = _.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
];

View File

@ -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<S>,
data: U,
reducers: ((data: U, state: S & BaseState<S>) => S & BaseState<S>)[]
): void => {
const reducer = compose(reducers.map(r => sta => r(data, sta)));
state.set(reducer);
};
export const optReduceState = <S, U>(
state: UseStore<S & BaseState<S>>,
data: U,
@ -74,17 +89,34 @@ export interface BaseState<StateType extends {}> {
patches: {
[id: string]: Patch[];
};
set: (fn: (state: BaseState<StateType>) => void) => void;
set: (fn: (state: StateType & BaseState<StateType>) => void) => void;
addPatch: (id: string, ...patch: Patch[]) => void;
removePatch: (id: string) => void;
optSet: (fn: (state: BaseState<StateType>) => void) => string;
optSet: (fn: (state: StateType & BaseState<StateType>) => 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 = <T extends {}>(
name: string,
properties: T,
blacklist: (keyof BaseState<T> | keyof T)[] = []
properties: T | ((set: SetState<T & BaseState<T>>, get: GetState<T & BaseState<T>>) => T),
blacklist: (keyof BaseState<T> | keyof T)[] = [],
subscriptions: ((set: SetState<T & BaseState<T>>, get: GetState<T & BaseState<T>>) => SubscriptionRequestInterface)[] = []
): UseStore<T & BaseState<T>> => create<T & BaseState<T>>(persist<T & BaseState<T>>((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 = <T extends {}>(
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<A, S extends {}>(state: UseStore<S & Base
}
}
}
export async function pokeOptimisticallyN<A, S extends {}>(state: UseStore<S & BaseState<S>>, poke: Poke<any>, reduce: ((a: A, fn: S & BaseState<S>) => S & BaseState<S>)[]) {
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);
}
}
}

View File

@ -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<ContactState> {
export interface ContactState {
contacts: Rolodex;
isContactPublic: boolean;
nackedContacts: Set<Patp>;
// fetchIsAllowed: (entity, name, ship, personal) => Promise<boolean>;
}
// @ts-ignore investigate zustand types
const useContactState = createState<ContactState>('Contact', {
contacts: {},
nackedContacts: new Set(),
isContactPublic: false
// fetchIsAllowed: async (
// entity,
// name,
// ship,
// personal
// ): Promise<boolean> => {
// 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<ContactState>(
'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])
);
}

View File

@ -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<GraphState> {
export interface GraphState {
graphs: Graphs;
graphKeys: Set<string>;
looseNodes: {
@ -19,18 +24,20 @@ export interface GraphState extends BaseState<GraphState> {
pendingDms: Set<string>;
screening: boolean;
graphTimesentMap: Record<number, string>;
// getKeys: () => Promise<void>;
// getTags: () => Promise<void>;
// getTagQueries: () => Promise<void>;
// getGraph: (ship: string, resource: string) => Promise<void>;
// getNewest: (ship: string, resource: string, count: number, index?: string) => Promise<void>;
// getOlderSiblings: (ship: string, resource: string, count: number, index?: string) => Promise<void>;
// getYoungerSiblings: (ship: string, resource: string, count: number, index?: string) => Promise<void>;
// getGraphSubset: (ship: string, resource: string, start: string, end: string) => Promise<void>;
// getNode: (ship: string, resource: string, index: string) => Promise<void>;
getDeepOlderThan: (ship: string, name: string, count: number, start?: string) => Promise<void>;
getNewest: (ship: string, resource: string, count: number, index?: string) => Promise<void>;
getOlderSiblings: (ship: string, resource: string, count: number, index?: string) => Promise<void>;
getYoungerSiblings: (ship: string, resource: string, count: number, index?: string) => Promise<void>;
getNode: (ship: string, resource: string, index: string) => Promise<void>;
getFirstborn: (ship: string, resource: string, index: string) => Promise<void>;
getGraph: (ship: string, name: string) => Promise<void>;
addDmMessage: (ship: string, contents: Content[]) => Promise<void>;
addPost: (ship: string, name: string, post: Post) => Promise<void>;
addNode: (ship: string, name: string, post: GraphNodePoke) => Promise<void>;
}
// @ts-ignore investigate zustand types
const useGraphState = createState<GraphState>('Graph', {
const useGraphState = createState<GraphState>('Graph', (set, get) => ({
graphs: {},
flatGraphs: {},
threadGraphs: {},
@ -39,7 +46,101 @@ const useGraphState = createState<GraphState>('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<GraphState>('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<GraphState>('Graph', {
// });
// graphReducer(node);
// },
}, [
}), [
'graphs',
'graphKeys',
'looseNodes',
@ -147,6 +235,21 @@ const useGraphState = createState<GraphState>('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);

View File

@ -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<GroupState> {
export interface GroupState {
groups: {
[group: string]: Group;
}
};
pendingJoin: JoinRequests;
}
// @ts-ignore investigate zustand types
const useGroupState = createState<GroupState>('Group', {
groups: {},
pendingJoin: {}
}, ['groups']);
const useGroupState = createState<GroupState>(
'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
])
);
}

View File

@ -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<Timebox>;
doNotDisturb: boolean;
// getMore: () => Promise<boolean>;
// getSubset: (offset: number, count: number, isArchive: boolean) => Promise<void>;
getMore: () => Promise<boolean>;
getSubset: (offset: number, count: number, isArchive: boolean) => Promise<void>;
// getTimeSubset: (start?: Date, end?: Date) => Promise<void>;
notifications: BigIntOrderedMap<Timebox>;
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<void>;
readNote: (index: NotifIndex) => Promise<void>;
}
const useHarkState = createState<HarkState>('Hark', {
archivedNotifications: new BigIntOrderedMap<Timebox>(),
doNotDisturb: false,
unreadNotes: [],
// getMore: async (): Promise<boolean> => {
// 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<void> => {
// 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<void> => {
// 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<Timebox>(),
notificationsCount: 0,
notificationsGraphConfig: {
watchOnSelf: false,
mentions: false,
watching: []
},
notificationsGroupConfig: [],
unreads: {
graph: {},
group: {}
}
}, ['unreadNotes', 'notifications', 'archivedNotifications', 'unreads', 'notificationsCount']);
const useHarkState = createState<HarkState>(
'Hark',
(set, get) => ({
archivedNotifications: new BigIntOrderedMap<Timebox>(),
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<boolean> => {
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<void> => {
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<Timebox>(),
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;

View File

@ -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<InviteState> {
export interface InviteState {
invites: Invites;
}
// @ts-ignore investigate zustand types
const useInviteState = createState<InviteState>('Invite', {
invites: {}
});
const useInviteState = createState<InviteState>(
'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;

View File

@ -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');
}

View File

@ -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<LaunchState> {
export interface LaunchState {
firstTime: boolean;
tileOrdering: string[];
tiles: {
[app: string]: Tile;
},
weather: WeatherState | null | Record<string, never> | boolean,
};
weather: WeatherState | null | Record<string, never> | boolean;
userLocation: string | null;
baseHash: string | null;
runtimeLag: boolean;
};
getRuntimeLag: () => Promise<void>;
getBaseHash: () => Promise<void>;
}
// @ts-ignore investigate zustand types
const useLaunchState = createState<LaunchState>('Launch', {
firstTime: true,
tileOrdering: [],
tiles: {},
weather: null,
userLocation: null,
baseHash: null,
runtimeLag: false,
});
const useLaunchState = createState<LaunchState>(
'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;

View File

@ -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<void>;
}
type LocalStateZus = LocalState & State;
@ -82,6 +88,26 @@ const useLocalState = create<LocalStateZus>(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<P, S extends keyof LocalState, C extends React.Component
});
}
const selOsDark = (s: LocalState) => s.dark;
export function useOsDark() {
return useLocalState(selOsDark);
}
export { useLocalState as default, withLocalState };

View File

@ -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<MetadataState> {
export interface MetadataState {
associations: Associations;
// preview: (group: string) => Promise<MetadataUpdatePreview>;
getPreview: (group: string) => Promise<MetadataUpdatePreview
>;
previews: {
[group: string]: MetadataUpdatePreview
}
}
// @ts-ignore investigate zustand types
const useMetadataState = createState<MetadataState>(
'Metadata',
(set, get) => ({
associations: {
groups: {},
graph: {}
},
previews: {},
getPreview: async (group: string): Promise<MetadataUpdatePreview> => {
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<MetadataState>('Metadata', {
associations: { groups: {}, graph: {}, contacts: {}, chat: {}, link: {}, publish: {} }
// preview: async (group): Promise<MetadataUpdatePreview> => {
// return new Promise<MetadataUpdatePreview>((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;

View File

@ -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<SettingsState> {
export interface SettingsState {
display: {
backgroundType: 'none' | 'url' | 'color';
background?: string;
@ -29,6 +42,7 @@ export interface SettingsState extends BaseState<SettingsState> {
};
keyboard: ShortcutMapping;
remoteContentPolicy: RemoteContentPolicy;
getAll: () => Promise<void>;
leap: {
categories: LeapCategories[];
};
@ -38,54 +52,82 @@ export interface SettingsState extends BaseState<SettingsState> {
};
}
export const selectSettingsState =
<K extends keyof SettingsState>(keys: K[]) => f.pick<SettingsState, K>(keys);
export const selectSettingsState = <K extends keyof (SettingsState & BaseState<SettingsState>)>(keys: K[]) =>
f.pick<BaseState<SettingsState> & 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<SettingsState>('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<SettingsState>(
'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<T extends keyof ShortcutMapping>(name: T, cb: (e: KeyboardEvent) => void) {
export function useShortcut<T extends keyof ShortcutMapping>(
name: T,
cb: (e: KeyboardEvent) => void
) {
const key = useSettingsState(useCallback(s => s.keyboard[name], [name]));
return usePlainShortcut(key, cb);
}
const selTheme = (s: SettingsState) => s.display.theme;
export function useTheme() {
return useSettingsState(selTheme);
}
export default useSettingsState;

View File

@ -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<StorageState> {
export interface StorageState {
gcp: {
configured?: boolean;
token?: GcpToken;
},
isConfigured: () => Promise<boolean>;
getToken: () => Promise<void>;
};
s3: {
configuration: {
buckets: Set<string>;
currentBucket: string;
};
credentials: any | null; // TODO better type
}
};
}
// @ts-ignore investigate zustand types
const useStorageState = createState<StorageState>('Storage', {
gcp: {},
s3: {
configuration: {
buckets: new Set(),
currentBucket: ''
const useStorageState = createState<StorageState>(
'Storage',
(set, get) => ({
gcp: {
isConfigured: () => {
return airlock.thread({
inputMark: 'noun',
outputMark: 'json',
threadName: 'gcp-is-configured',
body: {}
});
},
getToken: async () => {
const token = await airlock.thread<GcpToken>({
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;

View File

@ -1,43 +0,0 @@
export default class BaseStore<S extends object> {
state: S;
setState: (s: Partial<S>) => void = (s) => {};
constructor() {
this.state = this.initialState();
}
initialState() {
return {} as S;
}
setStateHandler(setState: (s: Partial<S>) => 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!
}
}

View File

@ -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<StoreState> {
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<string, any> = {}
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);
});
}
}

View File

@ -1,6 +0,0 @@
import { ConnectionStatus } from '~/types/connection';
export interface StoreState {
// local state
connection: ConnectionStatus;
}

View File

@ -1,70 +0,0 @@
import { Path } from '@urbit/api';
import BaseApi from '../api/base';
import BaseStore from '../store/base';
export default class BaseSubscription<S extends object> {
private errorCount = 0;
constructor(public store: BaseStore<S>, public api: BaseApi<S>, 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);
}
}

View File

@ -1,54 +0,0 @@
import { Path } from '@urbit/api';
import { StoreState } from '../store/type';
import BaseSubscription from './base';
export default class GlobalSubscription extends BaseSubscription<StoreState> {
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();
}
}

View File

@ -12,8 +12,6 @@ export default {
component: GraphContent
} as Meta;
const fakeApi = {} as any;
const Template: Story<GraphContentProps> = args => (
<Box
maxWidth="500px"
@ -27,7 +25,6 @@ const Template: Story<GraphContentProps> = args => (
m="3"
maxWidth="100%"
{...args}
api={fakeApi}
showOurContact
/>
</Box>

View File

@ -12,8 +12,6 @@ export default {
component: GraphContent
} as Meta;
const fakeApi = {} as any;
const Template: Story<GraphContentProps> = (args) => {
return (
<Box
@ -23,7 +21,7 @@ const Template: Story<GraphContentProps> = (args) => {
width="100%"
position="relative"
>
<GraphContent width="100%" {...args} api={fakeApi} showOurContact />
<GraphContent width="100%" {...args} showOurContact />
</Box>
);
};

View File

@ -8,10 +8,9 @@ export default {
title: 'Notifications/PendingDm',
component: PendingDm
} as Meta;
const fakeApi = {} as any;
export const Default = () => (
<Box width="95%" p="1" backgroundColor="white">
<PendingDm api={fakeApi} ship="~hastuc-dibtux" />
<PendingDm ship="~hastuc-dibtux" />
</Box>
);

View File

@ -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

View File

@ -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 (
<ThemeProvider theme={theme}>
<ShortcutContextProvider>
@ -153,21 +142,18 @@ class App extends React.Component {
</Helmet>
<Root>
<Router>
<TutorialModal api={this.api} />
<TutorialModal />
<ErrorBoundary>
<StatusBarWithRouter
props={this.props}
ourContact={ourContact}
api={this.api}
connection={this.state.connection}
connection={'foo'}
subscription={this.subscription}
ship={this.ship}
/>
</ErrorBoundary>
<ErrorBoundary>
<Omnibox
associations={state.associations}
api={this.api}
show={this.props.omniboxShown}
toggle={this.props.toggleOmnibox}
/>
@ -175,9 +161,8 @@ class App extends React.Component {
<ErrorBoundary>
<Content
ship={this.ship}
api={this.api}
subscription={this.subscription}
connection={this.state.connection}
connection={'aa'}
/>
</ErrorBoundary>
</Router>
@ -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
ref={ref}
ourContact={ourContact}
display={display}
getAll={getAll}
set={setLocal}
getShallowChildren={getShallowChildren}
getRuntimeLag={getRuntimeLag}
getBaseHash={getBaseHash}
toggleOmnibox={toggleOmnibox}
omniboxShown={omniboxShown}
/>
);
});
WarmApp.whyDidYouRender = true;
export default WithApp;
export default withState(process.env.NODE_ENV === 'production' ? App : hot(App), [
[useGroupState],
[useContactState],
[useSettingsState, ['display']],
[useLocalState]
]);

View File

@ -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<string[] | string | undefined>();
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}

View File

@ -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) {
</Row>
</Row>
<ChatPane
api={api}
canWrite
id={ship}
graph={dm}
@ -165,7 +170,7 @@ export function DmResource(props: DmResourceProps) {
fetchMessages={fetchMessages}
dismissUnread={dismissUnread}
getPermalink={() => undefined}
isAdmin
isAdmin={false}
onSubmit={onSubmit}
/>
</Col>

View File

@ -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<ChatInputProps, ChatInputState> {
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<string[]>(evalCord(text));
onSubmit([{ code: { output, expression: text } }]);
} else {
onSubmit(tokenizeMessage(text));

View File

@ -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) => (
</Row>
);
export const MessageAuthor = ({
export const MessageAuthor = React.memo<any>(({
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'
>
<ProfileOverlay cursor='auto' ship={msg.author} api={api}>
<ProfileOverlay cursor='auto' ship={msg.author}>
{img}
</ProfileOverlay>
</Box>
@ -180,15 +175,15 @@ export const MessageAuthor = ({
</Box>
</Box>
);
};
});
MessageAuthor.displayName = 'MessageAuthor';
type MessageProps = { timestamp: string; timestampHover: boolean; }
& Pick<ChatMessageProps, 'msg' | 'api' | 'transcluded' | 'showOurContact'>
& Pick<ChatMessageProps, 'msg' | 'transcluded' | 'showOurContact'>
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}
/>
</Box>
@ -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<ChatMessageProps, 'innerRef'>, ref: any) => (
export default React.memo(React.forwardRef((props: Omit<ChatMessageProps, 'innerRef'>, ref: any) => (
<ChatMessage {...props} innerRef={ref} />
));
)));
export const MessagePlaceholder = ({
height,

View File

@ -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<NakedChatInput>();
@ -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
<Col {...bind} height="100%" overflow="hidden" position="relative">
<ShareProfile
our={ourContact}
api={api}
recipients={showBanner ? promptShare : []}
onShare={() => 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 && (
<ChatInput
ref={chatInput}
api={props.api}
onSubmit={onSubmit}
ourContact={(promptShare.length === 0 && ourContact) || undefined}
onUnmount={appendUnsent}
@ -175,3 +170,5 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
</Col>
);
}
ChatPane.whyDidYouRender = true;

View File

@ -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<boolean>;
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<
</Text>
);
}
if (!this.state.initialized) {
return (
<MessagePlaceholder
key={index.toString()}
height='64px'
index={index}
/>
);
}
const isPending: boolean = 'pending' in msg && Boolean(msg.pending);
const isLastMessage = index.eq(
graph.peekLargest()?.[0] ?? bigInt.zero

View File

@ -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();
};

View File

@ -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 (
<Switch>
<Route exact path="/~graph/join/ship/:ship/:name/:module?"
@ -30,10 +24,10 @@ const GraphApp = (props: GraphAppProps): ReactElement => {
const autoJoin = () => {
try {
api.graph.joinGraph(
airlock.thread(joinGraph(
`~${deSig(props.match.params.ship)}`,
props.match.params.name
);
));
} catch(err) {
setTimeout(autoJoin, 2000);
}

View File

@ -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 = (
<Box
position={['relative', 'absolute']}
left={0}
bottom={0}
position="sticky"
left={3}
bottom={3}
mt={3}
backgroundColor="white"
ml={3}
mb={3}
borderRadius={2}
overflow='hidden'
width="fit-content"
fontSize={0}
cursor="pointer"
onClick={() => {
@ -85,8 +86,10 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
}}
>
<Box
height="100%"
backgroundColor={runtimeLag ? 'yellow' : 'washedGray'}
p={2}
width="fit-content"
>
<Text mono bold>{hashText || baseHash}</Text>
</Box>
@ -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 => {
</Row>
</Box>
</Tile>
<Tiles
api={props.api}
/>
<Tiles />
<ModalButton
icon="Plus"
bg="washedGray"
@ -225,7 +226,7 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
text="New Group"
style={{ gridColumnStart: 1 }}
>
<NewGroup {...props} />
<NewGroup />
</ModalButton>
<ModalButton
icon="BootNode"
@ -233,16 +234,15 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
color="black"
text="Join Group"
>
<JoinGroup {...props} />
<JoinGroup />
</ModalButton>
</>}
{!hideGroups &&
(<Groups />)
}
</Box>
<Box alignSelf="flex-start" display={['block', 'none']}>{hashBox}</Box>
{hashBox}
</ScrollbarLessBox>
<Box display={['none', 'block']}>{hashBox}</Box>
</>
);
};

View File

@ -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 (
<WeatherTile
key={key}
// @ts-ignore withState not passing props
api={props.api}
/>
<WeatherTile key={key} />
);
} else if (key === 'clock') {
const location = weather && 'nearest-area' in weather ? weather['nearest-area'][0] : '';

View File

@ -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<WeatherTileProps, WeatherTileState> {
constructor(props: WeatherTileProps) {
super(props);
@ -64,7 +70,7 @@ class WeatherTile extends React.Component<WeatherTileProps, WeatherTileState> {
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<WeatherTileProps, WeatherTileState> {
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<WeatherTileProps, WeatherTileState> {
}
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<WeatherTileProps, WeatherTileState> {
onClick={() =>
this.setState({ manualEntry: !this.state.manualEntry })
}
>
>
{'->'}
</Text>
</Text>

View File

@ -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
<LinkWindow
key={rid}
association={resource}
@ -73,7 +70,6 @@ export function LinkResource(props: LinkResourceProps) {
group={group as Group}
path={resource.group}
pendingSize={Object.keys(graphTimesentMap[resourcePath] || {}).length}
api={api}
mb={3}
/>
);
@ -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}`}

View File

@ -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<LinkWindowProps, {}> {
renderItem = React.forwardRef<HTMLDivElement>(({ 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<LinkWindowProps, {}> {
...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 (
<React.Fragment key={index.toString()}>
@ -77,7 +75,6 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
<LinkSubmit
name={name}
ship={ship.slice(1)}
api={api}
/>
</Col>
{ typeof post !== 'string' && <LinkItem {...linkProps} /> }
@ -96,7 +93,7 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
});
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<LinkWindowProps, {}> {
<LinkSubmit
name={name}
ship={ship.slice(1)}
api={api}
/>
) : (
<Text>

View File

@ -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<H
association,
node,
resource,
api,
group,
path,
...rest
} = props;
@ -42,14 +39,13 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<H
const index = node.post.index.split('/')[1];
const markRead = useCallback(() => {
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<H
const deleteLink = () => {
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 (
<Box
@ -133,7 +129,7 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<H
{ 'reference' in contents[1] ? (
<>
<Rule />
<PermalinkEmbed full link={referenceToPermalink(contents[1]).link} api={api} transcluded={0} />
<PermalinkEmbed full link={referenceToPermalink(contents[1]).link} transcluded={0} />
</>
) : (
<>

View File

@ -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) {

View File

@ -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 (
<Box

View File

@ -6,7 +6,6 @@ import _ from 'lodash';
import React, { useCallback } from 'react';
import { Link, useHistory } from 'react-router-dom';
import styled from 'styled-components';
import GlobalApi from '~/logic/api/global';
import { pluralize } from '~/logic/lib/util';
import useGroupState from '~/logic/state/group';
import {
@ -111,7 +110,7 @@ export const GraphNodeContent = ({ post, mod, index, hidden, association }) => {
}
return (
<TruncBox truncate={8}>
<GraphContent api={{} as any} contents={post.contents} showOurContact />
<GraphContent contents={post.contents} showOurContact />
</TruncBox>
);
};
@ -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 ||

View File

@ -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 {

View File

@ -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: {
</Text>
</Box>
)}
<Invites pendingJoin={props.pendingJoin} api={api} />
<DaySection unread key="unread" timeboxes={[[date,unreadNotes]]} api={api} />
<Invites pendingJoin={props.pendingJoin} />
<DaySection unread key="unread" timeboxes={[[date,unreadNotes]]} />
{[...notificationsByDayMap.keys()].sort().reverse().map((day, index) => {
const timeboxes = notificationsByDayMap.get(day)!;
return timeboxes.length > 0 && (
<DaySection
key={day}
timeboxes={timeboxes}
api={api}
/>
);
})}
@ -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) => (
<Notification
key={getNotificationKey(date, not)}
api={api}
notification={not}
unread={unread}
time={!unread ? date : undefined}

View File

@ -6,7 +6,6 @@ import {
AppInvites,
JoinRequest
} from '@urbit/api';
import GlobalApi from '~/logic/api/global';
import { alphabeticalOrder, resourceAsPath } from '~/logic/lib/util';
import useInviteState from '~/logic/state/invite';
import useGraphState from '~/logic/state/graph';
@ -14,7 +13,6 @@ import { PendingDm } from './PendingDm';
import InviteItem from '~/views/components/Invite';
interface InvitesProps {
api: GlobalApi;
pendingJoin?: any;
}
@ -25,7 +23,6 @@ interface InviteRef {
}
export function Invites(props: InvitesProps): ReactElement {
const { api } = props;
const invites = useInviteState(state => state.invites);
const pendingDms = useGraphState(s => s.pendingDms) ?? [];
@ -55,7 +52,7 @@ export function Invites(props: InvitesProps): ReactElement {
return (
<>
{[...pendingDms].map(ship => (
<PendingDm key={ship} api={api} ship={`~${ship}`} />
<PendingDm key={ship} ship={`~${ship}`} />
))}
{Object.keys(invitesAndStatus)
.sort(alphabeticalOrder)
@ -67,7 +64,6 @@ export function Invites(props: InvitesProps): ReactElement {
<InviteItem
key={resource}
resource={resource}
api={api}
pendingJoin={join}
/>
);
@ -76,7 +72,6 @@ export function Invites(props: InvitesProps): ReactElement {
return (
<InviteItem
key={resource}
api={api}
invite={invite}
app={app}
uid={uid}

View File

@ -1,16 +1,11 @@
import { Box, Row, SegmentedProgressBar, Text } from '@tlon/indigo-react';
import {
joinError, joinProgress,
JoinRequest
} from '@urbit/api';
import { joinError, joinProgress, JoinRequest, hideGroup } from '@urbit/api';
import React, { useCallback } from 'react';
import GlobalApi from '~/logic/api/global';
import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
import airlock from '~/logic/api';
interface JoiningStatusProps {
status: JoinRequest;
api: GlobalApi;
resource: string;
}
@ -23,13 +18,17 @@ const description: string[] = [
];
export function JoiningStatus(props: JoiningStatusProps) {
const { status, resource, api } = props;
const { status, resource } = props;
const current = joinProgress.indexOf(status.progress);
const desc = description?.[current] || '';
const isError = joinError.indexOf(status.progress as any) !== -1;
const onHide = useCallback(() => api.groups.hide(resource)
, [resource, api]);
const onHide = useCallback(
async () => {
await airlock.poke(hideGroup(resource));
},
[resource]
);
return (
<Row
display={['flex-column', 'flex']}

View File

@ -1,15 +1,11 @@
import { Box, Button, Icon, Row } from '@tlon/indigo-react';
import {
GraphNotificationContents,
GroupNotificationContents,
IndexedNotification
} from '@urbit/api';
import { BigInteger } from 'big-integer';
import React, { ReactNode, useCallback } from 'react';
import GlobalApi from '~/logic/api/global';
import { getNotificationKey } from '~/logic/lib/hark';
import { useHovering } from '~/logic/lib/util';
import useLocalState from '~/logic/state/local';
@ -17,38 +13,40 @@ import { StatelessAsyncAction } from '~/views/components/StatelessAsyncAction';
import { SwipeMenu } from '~/views/components/SwipeMenu';
import { GraphNotification } from './graph';
import { GroupNotification } from './group';
import useHarkState from '~/logic/state/hark';
import shallow from 'zustand/shallow';
export interface NotificationProps {
notification: IndexedNotification;
time: BigInteger;
api: GlobalApi;
unread: boolean;
}
export function NotificationWrapper(props: {
api: GlobalApi;
time?: BigInteger;
read?: boolean;
notification?: IndexedNotification;
children: ReactNode;
}) {
const { api, time, notification, children, read = false } = props;
const { time, notification, children, read = false } = props;
const isMobile = useLocalState(s => 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 (
<NotificationWrapper {...wrapperProps}>
<GraphNotification
api={props.api}
index={index}
contents={c}
read={!unread}
@ -134,10 +130,8 @@ export function Notification(props: NotificationProps) {
return (
<NotificationWrapper {...wrapperProps}>
<GroupNotification
api={props.api}
index={index}
contents={c}
read={!unread}
timebox={props.time}
time={time}
/>

View File

@ -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<typeof Text> & { view?: string; current: string },
ref
): ReactElement => {
const { current, view, ...textProps } = props;
const to = view ? `${baseUrl}/${view}` : baseUrl;
const active = view ? current === view : !current;
return (
<Link to={to}>
<Text ref={ref} px={2} {...textProps} gray={!active} />
</Link>
);
});
interface NotificationFilter {
groups: string[];
}
export default function NotificationsScreen(props: any): ReactElement {
const relativePath = (p: string) => baseUrl + p;
const [filter, setFilter] = useState<NotificationFilter>({ 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<HTMLElement | null>(null);
useTutorialModal('notifications', true, anchorRef);
const notificationsCount = useHarkState(state => state.notificationsCount);
@ -101,7 +72,7 @@ export default function NotificationsScreen(props: any): ReactElement {
{!view && <Inbox
pendingJoin={pendingJoin}
{...props}
filter={filter.groups}
filter={[]}
/>}
</Col>
</Body>

View File

@ -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 <PermalinkEmbed transcluded={transcluded + 1} api={api} link={permalink} association={assoc} />;
return <PermalinkEmbed transcluded={transcluded + 1} link={permalink} association={assoc} />;
}
return (
@ -83,7 +81,6 @@ function TranscludedLinkNode(props: {
case 2:
return (
<TranscludedComment
api={api}
transcluded={transcluded}
node={node}
assoc={assoc}
@ -97,10 +94,9 @@ function TranscludedLinkNode(props: {
function TranscludedComment(props: {
node: GraphNode;
assoc: Association;
api: GlobalApi;
transcluded: number;
}) {
const { assoc, node, api, transcluded } = props;
const { assoc, node, transcluded } = props;
if (typeof node?.post === 'string') {
return (
@ -134,7 +130,6 @@ function TranscludedComment(props: {
/>
<Box pl="44px" pt='2'>
<GraphContent
api={api}
transcluded={transcluded}
contents={comment.post.contents}
showOurContact={false}
@ -147,10 +142,9 @@ function TranscludedComment(props: {
function TranscludedPublishNode(props: {
node: GraphNode;
assoc: Association;
api: GlobalApi;
transcluded: number;
}) {
const { node, assoc, transcluded, api } = props;
const { node, assoc, transcluded } = props;
const group = useGroupForAssoc(assoc)!;
if (typeof node?.post === 'string') {
@ -201,7 +195,6 @@ function TranscludedPublishNode(props: {
return (
<TranscludedComment
transcluded={transcluded}
api={api}
node={node}
assoc={assoc}
/>
@ -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: {
/>
<Box pl='44px' pt='3' pr='3'>
<MentionText
api={api}
transcluded={transcluded}
content={post.contents}
group={group}
@ -270,10 +261,9 @@ export function TranscludedNode(props: {
assoc: Association;
node: GraphNode;
transcluded: number;
api: GlobalApi;
showOurContact?: boolean;
}) {
const { node, showOurContact, assoc, transcluded, api } = props;
const { node, showOurContact, assoc, transcluded } = props;
const group = useGroupForAssoc(assoc)!;
if (
@ -306,7 +296,6 @@ export function TranscludedNode(props: {
msg={node.post}
fontSize={0}
showOurContact={showOurContact}
api={api}
mt='0'
/>
</Row>
@ -318,7 +307,6 @@ export function TranscludedNode(props: {
case 'post':
return (
<TranscludedPost
api={props.api}
post={node.post}
commentsCount={Object.keys(node.children.root).length}
group={group}

View File

@ -50,7 +50,6 @@ function FallbackRoutes(props: { query: URLSearchParams }) {
if (query.has('ext')) {
const ext = query.get('ext')!;
const url = `/perma${ext.slice(16)}`;
console.log(url);
return <Redirect to={{ pathname: url }} />;
}

View File

@ -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 (
<GroupLink
resource={group}
api={api}
pl={2}
border={1}
borderRadius={2}
@ -71,14 +69,13 @@ function GroupPermalink(props: { group: string; api: GlobalApi }) {
function GraphPermalink(
props: IGraphPermalink & {
api: GlobalApi;
transcluded: number;
pending?: boolean;
showOurContact?: boolean;
full?: boolean;
}
) {
const { full = false, showOurContact, pending, graph, group, index, api, transcluded } = props;
const { full = false, showOurContact, pending, graph, group, index, transcluded } = props;
const history = useHistory();
const location = useLocation();
const { ship, name } = resourceFromPath(graph);
@ -90,6 +87,7 @@ function GraphPermalink(
);
const [errored, setErrored] = useState(false);
const [loading, setLoading] = useState(false);
const getNode = useGraphState(s => 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 && (
<TranscludedNode
api={api}
transcluded={transcluded + 1}
node={node}
assoc={association!}
@ -232,7 +229,6 @@ function PermalinkDetails(props: {
export function PermalinkEmbed(props: {
link: string;
association?: Association;
api: GlobalApi;
transcluded: number;
showOurContact?: boolean;
full?: boolean;
@ -246,13 +242,12 @@ export function PermalinkEmbed(props: {
switch (permalink.type) {
case 'group':
return <GroupPermalink group={permalink.group} api={props.api} />;
return <GroupPermalink group={permalink.group} />;
case 'graph':
return (
<GraphPermalink
transcluded={props.transcluded}
{...permalink}
api={props.api}
full={props.full}
showOurContact={props.showOurContact}
/>

View File

@ -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) {

View File

@ -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);

View File

@ -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 (
<Row>
@ -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 {
<EditProfile
ship={ship}
contact={contact}
api={props.api}
/>
) : (
<ViewProfile
nacked={nacked}
ship={ship}
api={props.api}
contact={contact}
/>
)}

View File

@ -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();

View File

@ -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}
/>
<ProfileStatus contact={contact} />
</ProfileControls>
@ -47,7 +46,7 @@ export function ViewProfile(props: any): ReactElement {
</Row>
<Col pb={2} mt={3} alignItems='center' justifyContent='center' width='100%'>
<Center flexDirection='column' maxWidth='32rem'>
<RichText api={props.api} width='100%' disableRemoteContent>
<RichText width='100%' disableRemoteContent>
{contact?.bio ? contact.bio : ''}
</RichText>
</Center>
@ -58,7 +57,6 @@ export function ViewProfile(props: any): ReactElement {
<Col>
{contact?.groups.slice().sort(lengthOrder).map((g, i) => (
<GroupLink
api={api}
key={i}
resource={g}
measure={() => {}}

View File

@ -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}
/>
</Box>

Some files were not shown because too many files have changed in this diff Show More