mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-19 04:41:37 +03:00
Merge branch 'release/next-userspace' into m/next-gen-term-real
This commit is contained in:
commit
cbed01f0ac
@ -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
|
||||
|
@ -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]
|
||||
--
|
||||
|
@ -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
|
||||
--
|
||||
|
@ -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
|
||||
|
2
pkg/arvo/mar/graph/cache/hook.hoon
vendored
2
pkg/arvo/mar/graph/cache/hook.hoon
vendored
@ -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)]
|
||||
==
|
||||
--
|
||||
::
|
||||
|
12
pkg/arvo/mar/transform-add-nodes.hoon
Normal file
12
pkg/arvo/mar/transform-add-nodes.hoon
Normal file
@ -0,0 +1,12 @@
|
||||
/- *post
|
||||
|_ i=indexed-post
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun i
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun indexed-post
|
||||
--
|
||||
--
|
@ -10,5 +10,7 @@
|
||||
::
|
||||
[%warm-cache-all ~]
|
||||
[%cool-cache-all ~]
|
||||
[%warm-static-conversion from=term to=term]
|
||||
[%cool-static-conversion from=term to=term]
|
||||
==
|
||||
--
|
||||
|
@ -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
|
5431
pkg/interface/package-lock.json
generated
5431
pkg/interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
43
pkg/interface/src/logic/api/bootstrap.ts
Normal file
43
pkg/interface/src/logic/api/bootstrap.ts
Normal 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);
|
||||
});
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
8
pkg/interface/src/logic/api/index.ts
Normal file
8
pkg/interface/src/logic/api/index.ts
Normal 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;
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 } });
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
} } });
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
25
pkg/interface/src/logic/lib/contact.ts
Normal file
25
pkg/interface/src/logic/lib/contact.ts
Normal 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;
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
@ -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) {
|
||||
|
27
pkg/interface/src/logic/lib/graph.ts
Normal file
27
pkg/interface/src/logic/lib/graph.ts
Normal 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('/')}`;
|
||||
}
|
@ -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');
|
||||
};
|
||||
}
|
@ -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]);
|
||||
}
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
];
|
||||
|
@ -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');
|
||||
};
|
@ -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
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
@ -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
|
||||
];
|
||||
|
@ -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
|
||||
];
|
||||
|
@ -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
|
||||
];
|
||||
|
||||
|
@ -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
|
||||
];
|
||||
|
@ -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
|
||||
];
|
||||
|
@ -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
|
||||
];
|
||||
|
||||
|
@ -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
|
||||
];
|
||||
|
@ -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
|
||||
];
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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])
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
8
pkg/interface/src/logic/state/join.ts
Normal file
8
pkg/interface/src/logic/state/join.ts
Normal 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');
|
||||
}
|
@ -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;
|
||||
|
@ -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 };
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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!
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import { ConnectionStatus } from '~/types/connection';
|
||||
|
||||
export interface StoreState {
|
||||
// local state
|
||||
connection: ConnectionStatus;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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
|
@ -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]
|
||||
]);
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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));
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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] : '';
|
||||
|
@ -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>
|
||||
|
@ -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}`}
|
||||
|
@ -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>
|
||||
|
@ -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} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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 ||
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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']}
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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 }} />;
|
||||
}
|
||||
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
@ -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();
|
||||
|
@ -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={() => {}}
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user