Merge branch 'release/next-userspace' into lf/keybinds

This commit is contained in:
Matilde Park 2021-05-13 13:15:07 -04:00
commit 62ba7462d2
192 changed files with 2092 additions and 1433 deletions

View File

@ -2,7 +2,7 @@ name: glob
on:
push:
branches:
- 'release/next-js'
- 'release/next-userspace'
jobs:
glob:
runs-on: ubuntu-latest

View File

@ -6,13 +6,13 @@ on:
jobs:
merge-to-next-js:
runs-on: ubuntu-latest
name: "Merge master to release/next-js"
name: "Merge master to release/next-userspace"
steps:
- uses: actions/checkout@v2
- uses: devmasx/merge-branch@v1.3.1
with:
type: now
target_branch: release/next-js
target_branch: release/next-userspace
github_token: ${{ secrets.JANEWAY_BOT_TOKEN }}
merge-to-group-timer:

View File

@ -309,9 +309,9 @@ the new binary, and restarting the pier with it.
#### Continuous deployment
A subset of release branches are deployed continuously to the network. Thus far
this only includes `release/next-js`, which deploys livenet-compatible
JavaScript changes to select QA ships. Any push to master will automatically
merge master into `release/next-js` to keep the streams at parity.
this only includes `release/next-userspace`, which deploys livenet-compatible
changes to select QA ships. Any push to master will automatically
merge master into `release/next-userspace` to keep the streams at parity.
### Announce the update

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:90dd5f5b2821f1a057c053b141e1143f019193c8dd7da41b39b0a3799e0fda5a
size 10999106
oid sha256:d7b7cf24e56ab078cf1dcb82e4e7744f188c5221c08772d6cfb15f59ce81aaa5
size 11198219

View File

@ -1394,8 +1394,6 @@
^+ this
?: =(~ dom)
~|(%acme-empty-certificate-order !!)
?: ?=(?(%earl %pawn) (clan:title our.bow))
this
=. ..emit (queue-next-order 1 | dom)
=. ..emit cancel-current-order
:: notify %dill

View File

@ -738,7 +738,8 @@
::
?. (is-chat-graph target)
[[(note:sh-out "no such chat")]~ put-ses]
=. viewing (~(put in viewing) target)
=. audience target
=. viewing (~(put in viewing) target)
=^ cards state
?: (~(has by bound) target)
[~ state]

View File

@ -70,10 +70,11 @@
::
++ transform-proxy-update
|= vas=vase
^- (unit vase)
^- (quip card (unit vase))
:: TODO: should check if user is allowed to %add, %remove, %edit
:: contact
=/ =update:store !<(update:store vas)
:- ~
?- -.update
%initial ~
%add `vas

View File

@ -43,8 +43,8 @@
::
++ transform-proxy-update
|= vas=vase
^- (unit vase)
`vas
^- (quip card (unit vase))
``vas
::
++ resource-for-update
|= =vase

View File

@ -188,8 +188,11 @@
?: ?=([%'~landscape' %js %session ~] site.req-line)
%+ require-authorization-simple:app
inbound-request
%- js-response:gen
(as-octt:mimes:html "window.ship = '{+:(scow %p our.bowl)}';")
%. %- as-octs:mimes:html
(rap 3 'window.ship = "' (rsh 3 (scot %p our.bowl)) '";' ~)
%* . js-response:gen
cache %.n
==
::
=/ [payload=simple-payload:http public=?] (get-file req-line is-file)
?: public payload
@ -222,6 +225,7 @@
[~ %js] (js-response:gen file)
[~ %css] (css-response:gen file)
[~ %png] (png-response:gen file)
[~ %ico] (ico-response:gen file)
::
[~ %html]
%. file
@ -238,11 +242,9 @@
[not-found:gen %.n]
:_ public.u.content
=/ mime-type=@t (rsh 3 (crip <p.u.data>))
:: Should maybe inspect to see how long cache should hold
::
=/ headers
:~ content-type+mime-type
max-1-da:gen
max-1-wk:gen
'service-worker-allowed'^'/'
==
[[200 headers] `q.u.data]

View File

@ -5,7 +5,7 @@
/- glob
/+ default-agent, verb, dbug
|%
++ hash 0v7.k043v.fjsi2.bpm4g.0ekbj.566c4 :: DO NOT MOVE FROM LINE 8
++ hash 0v2.rvlfs.f97fq.hjrpe.d3h68.n54sj
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
+$ all-states
$% state-0

View File

@ -1,6 +1,6 @@
/- *group, metadata=metadata-store
/+ store=graph-store, mdl=metadata, res=resource, graph, group, default-agent,
dbug, verb, push-hook
dbug, verb, push-hook, agentio
::
~% %graph-push-hook-top ..part ~
|%
@ -25,6 +25,25 @@
$% state-zero
state-one
==
::
:: 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] tube:clay)
transform-marks=(map mark tube:clay)
==
::
+$ inflated-state
$: state-one
cache
==
::
+$ cache-action
$% [%graph-to-mark (pair resource:res (unit mark))]
[%perm-marks (pair (pair mark @tas) tube:clay)]
[%transform-marks (pair mark tube:clay)]
==
--
::
%- agent:dbug
@ -33,7 +52,8 @@
%- (agent:push-hook config)
^- agent
=-
=| state-one
~% %graph-push-hook-agent ..scry.hook-core ~
=| inflated-state
=* state -
|_ =bowl:gall
+* this .
@ -41,10 +61,11 @@
grp ~(. group bowl)
gra ~(. graph bowl)
met ~(. mdl bowl)
hc ~(. hook-core bowl)
hc ~(. hook-core bowl +.state)
io ~(. agentio bowl)
::
++ on-init on-init:def
++ on-save !>(state)
++ on-save !>(-.state)
++ on-load
|= =vase
=+ !<(old=versioned-state vase)
@ -53,9 +74,26 @@
=? old ?=(%0 -.old)
[%1 ~]
?> ?=(%1 -.old)
`this(state old)
`this(-.state old, +.state *cache)
::
++ on-poke
|= [=mark =vase]
^- (quip card _this)
?. =(mark %graph-cache-hook)
[~ this]
=/ 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)]
::
++ on-poke on-poke:def
++ on-agent on-agent:def
++ on-watch on-watch:def
++ on-leave on-leave:def
@ -72,32 +110,64 @@
::
++ on-fail on-fail:def
++ transform-proxy-update
~/ %transform-proxy-update
|= vas=vase
^- (unit vase)
^- (quip card (unit vase))
=/ =update:store !<(update:store vas)
=* rid resource.q.update
=. p.update now.bowl
?- -.q.update
%add-nodes
?. (is-allowed-add:hc rid nodes.q.update)
~
=/ mark (get-mark:gra rid)
?~ mark `vas
|^
=/ transform
!< $-([index:store post:store atom ?] [index:store post:store])
%. !>(*indexed-post:store)
.^(tube:clay (scry:hc %cc %home /[u.mark]/transform-add-nodes))
=/ [* result=(list [index:store node:store])]
%+ roll
(flatten-node-map ~(tap by nodes.q.update))
(transform-list transform)
=. nodes.q.update
%- ~(gas by *(map index:store node:store))
result
[~ !>(update)]
=| cards=(list card)
=^ allowed cards (is-allowed-add:hc rid nodes.q.update)
?. allowed
[cards ~]
=/ mark-cached (~(has by graph-to-mark) rid)
=/ mark
?: mark-cached
(~(got by graph-to-mark) rid)
(get-mark:gra rid)
?~ mark
[cards `vas]
=< $
~% %transform-add-nodes ..transform-proxy-update ~
|%
++ $
^- (quip card (unit vase))
=/ transform-cached (~(has by transform-marks) u.mark)
=/ =tube:clay
?: transform-cached
(~(got by transform-marks) u.mark)
.^(tube:clay (scry:hc %cc %home /[u.mark]/transform-add-nodes))
=/ transform
!< $-([index:store post:store atom ?] [index:store post:store])
%. !>(*indexed-post:store)
tube
=/ [* result=(list [index:store node:store])]
%+ roll
(flatten-node-map ~(tap by nodes.q.update))
(transform-list transform)
=. nodes.q.update
%- ~(gas by *(map index:store node:store))
result
:_ [~ !>(update)]
%+ weld cards
%- zing
:~ ?: mark-cached ~
:_ ~
%+ poke-self:pass:io %graph-cache-hook
!> ^- cache-action
[%graph-to-mark rid mark]
::
?: transform-cached ~
:_ ~
%+ poke-self:pass:io %graph-cache-hook
!> ^- cache-action
[%transform-marks u.mark tube]
==
::
++ flatten-node-map
~/ %flatten-node-map
|= lis=(list [index:store node:store])
^- (list [index:store node:store])
|^
@ -129,6 +199,7 @@
--
::
++ transform-list
~/ %transform-list
|= transform=$-([index:store post:store atom ?] [index:store post:store])
|= $: [=index:store =node:store]
[indices=(set index:store) lis=(list [index:store node:store])]
@ -151,27 +222,32 @@
--
::
%remove-posts
?. (is-allowed-remove:hc resource.q.update indices.q.update)
=| cards=(list card)
=^ allowed cards
(is-allowed-remove:hc rid indices.q.update)
:- cards
?. allowed
~
`vas
::
%add-graph ~
%remove-graph ~
%add-signatures ~
%remove-signatures ~
%archive-graph ~
%unarchive-graph ~
%add-tag ~
%remove-tag ~
%keys ~
%tags ~
%tag-queries ~
%run-updates ~
%add-graph [~ ~]
%remove-graph [~ ~]
%add-signatures [~ ~]
%remove-signatures [~ ~]
%archive-graph [~ ~]
%unarchive-graph [~ ~]
%add-tag [~ ~]
%remove-tag [~ ~]
%keys [~ ~]
%tags [~ ~]
%tag-queries [~ ~]
%run-updates [~ ~]
==
::
++ resource-for-update resource-for-update:gra
::
++ initial-watch
~/ %initial-watch
|= [=path =resource:res]
^- vase
|^
@ -211,11 +287,13 @@
==
--
::
^| ^= hook-core
|_ =bowl:gall
~% %graph-push-hook-helper ..card.hook-core ~
^= hook-core
|_ [=bowl:gall =cache]
+* grp ~(. group bowl)
met ~(. mdl bowl)
gra ~(. graph bowl)
io ~(. agentio bowl)
::
++ scry
|= [care=@t desk=@t =path]
@ -225,14 +303,38 @@
::
++ perm-mark
|= [=resource:res perm=@t vip=vip-metadata:metadata =indexed-post:store]
^- permissions:store
^- [permissions:store (list card)]
|^
=- (check vip)
!< check=$-(vip-metadata:metadata permissions:store)
%. !>(indexed-post)
=/ mark (get-mark:gra resource)
?~ mark |=(=vase !>([%no %no %no]))
.^(tube:clay (scry %cc %home /[u.mark]/(perm-mark-name perm)))
=/ mark-cached (~(has by graph-to-mark.cache) resource)
=/ mark
?: mark-cached
(~(got by graph-to-mark.cache) resource)
(get-mark:gra resource)
?~ mark
[[%no %no %no] ~]
=/ key [u.mark (perm-mark-name perm)]
=/ perms-cached (~(has by perm-marks.cache) key)
=/ =tube:clay
?: perms-cached
(~(got by perm-marks.cache) key)
.^(tube:clay (scry %cc %home /[u.mark]/(perm-mark-name perm)))
=/ check
!< $-(vip-metadata:metadata permissions:store)
(tube !>(indexed-post))
:- (check vip)
%- zing
:~ ?: mark-cached ~
:_ ~
%+ poke-self:pass:io %graph-cache-hook
!> ^- cache-action
[%graph-to-mark resource mark]
::
?: perms-cached ~
:_ ~
%+ poke-self:pass:io %graph-cache-hook
!> ^- cache-action
[%perm-marks [u.mark (perm-mark-name perm)] tube]
==
::
++ perm-mark-name
|= perm=@t
@ -252,15 +354,22 @@
reader.permissions
::
++ get-roles-writers-variation
~/ %get-roles-writers-variation
|= =resource:res
^- (unit [is-admin=? writers=(set ship) vip=vip-metadata:metadata])
=/ assoc=(unit association:metadata)
(peek-association:met %graph resource)
(peek-association:met %graph resource)
?~ assoc ~
=/ group=(unit group:grp)
(scry-group:grp group.u.assoc)
?~ group ~
=/ role=(unit (unit role-tag))
(role-for-ship:grp group.u.assoc src.bowl)
(role-for-ship-with-group:grp u.group group.u.assoc src.bowl)
=/ writers=(set ship)
(get-tagged-ships:grp group.u.assoc [%graph resource %writers])
%^ get-tagged-ships-with-group:grp
u.group
group.u.assoc
[%graph resource %writers]
?~ role ~
=/ is-admin=?
?=(?([~ %admin] [~ %moderator]) u.role)
@ -274,39 +383,58 @@
[(snag (dec (lent index)) index) p.post.node]
::
++ is-allowed-add
~/ %is-allowed-add
|= [=resource:res nodes=(map index:store node:store)]
^- ?
^- [? (list card)]
|^
%- (bond |.(%.n))
%- (bond |.([%.n ~]))
%+ biff (get-roles-writers-variation resource)
|= [is-admin=? writers=(set ship) vip=vip-metadata:metadata]
^- (unit ?)
^- (unit [? (list card)])
%- some
%+ levy ~(tap by nodes)
|= [=index:store =node:store]
=/ parent-index=index:store
(scag (dec (lent index)) index)
?: (~(has by nodes) parent-index) %.y
?: ?=(%| -.post.node)
%.n
?. =(author.p.post.node src.bowl)
%.n
=/ =permissions:store
%^ add-mark resource vip
(node-to-indexed-post node)
=/ =permission-level:store
(get-permission permissions is-admin writers)
?- permission-level
%yes %.y
%no %.n
::
%self
=/ parent-node=node:store
(got-node:gra resource parent-index)
?: ?=(%| -.post.parent-node)
%.n
=(author.p.post.parent-node src.bowl)
==
=/ a ~(tap by nodes)
=| cards=(list card)
|- ^- [? (list card)]
?~ a [& cards]
=/ c (check i.a is-admin writers vip)
?. -.c
[| (weld cards +.c)]
$(a t.a, cards (weld cards +.c))
::
++ check
|= $: [=index:store =node:store]
is-admin=?
writers=(set ship)
vip=vip-metadata:metadata
==
^- [? (list card)]
=/ parent-index=index:store
(scag (dec (lent index)) index)
?: (~(has by nodes) parent-index)
[%.y ~]
?: ?=(%| -.post.node)
[%.n ~]
?. =(author.p.post.node src.bowl)
[%.n ~]
=/ added
%^ add-mark resource vip
(node-to-indexed-post node)
=* permissions -.added
=* cards +.added
=/ =permission-level:store
(get-permission permissions is-admin writers)
:_ cards
?- permission-level
%yes %.y
%no %.n
::
%self
=/ parent-node=node:store
(got-node:gra resource parent-index)
?: ?=(%| -.post.parent-node)
%.n
=(author.p.post.parent-node src.bowl)
==
::
++ add-mark
|= [=resource:res vip=vip-metadata:metadata =indexed-post:store]
@ -314,29 +442,43 @@
--
::
++ is-allowed-remove
~/ %is-allowed-remove
|= [=resource:res indices=(set index:store)]
^- ?
^- [? (list card)]
|^
%- (bond |.(%.n))
%- (bond |.([%.n ~]))
%+ biff (get-roles-writers-variation resource)
|= [is-admin=? writers=(set ship) vip=vip-metadata:metadata]
%- some
%+ levy ~(tap by indices)
|= =index:store
^- ?
=/ =node:store
(got-node:gra resource index)
?: ?=(%| -.post.node) %.n
=/ =permissions:store
%^ remove-mark resource vip
(node-to-indexed-post node)
=/ =permission-level:store
(get-permission permissions is-admin writers)
?- permission-level
%yes %.y
%no %.n
%self =(author.p.post.node src.bowl)
==
%- some
=/ a ~(tap by indices)
=| cards=(list card)
|- ^- [? (list card)]
?~ a [& cards]
=/ c (check i.a is-admin writers vip)
?. -.c
[| (weld cards +.c)]
$(a t.a, cards (weld cards +.c))
::
++ check
|= [=index:store is-admin=? writers=(set ship) vip=vip-metadata:metadata]
^- [? (list card)]
=/ =node:store
(got-node:gra resource index)
?: ?=(%| -.post.node)
[%.n ~]
=/ removed
%^ remove-mark resource vip
(node-to-indexed-post node)
=* permissions -.removed
=* cards +.removed
=/ =permission-level:store
(get-permission permissions is-admin writers)
:_ cards
?- permission-level
%yes %.y
%no %.n
%self =(author.p.post.node src.bowl)
==
::
++ remove-mark
|= [=resource:res vip=vip-metadata:metadata =indexed-post:store]

View File

@ -18,9 +18,20 @@
++ orm orm:store
++ orm-log orm-log:store
+$ debug-input [%validate-graph =resource:store]
::
+$ cache
$: validators=(map mark dais:clay)
==
::
:: 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
==
--
::
=| state-5
=| inflated-state
=* state -
::
%- agent:dbug
@ -32,7 +43,7 @@
def ~(. (default-agent this %|) bowl)
::
++ on-init [~ this]
++ on-save !>(state)
++ on-save !>(-.state)
++ on-load
|= =old=vase
^- (quip card _this)
@ -108,7 +119,7 @@
(gas:orm-log ~ [now.bowl logged-update] ~)
==
::
%5 [cards this(state old)]
%5 [cards this(-.state old, +.state *cache)]
==
::
++ on-watch
@ -180,7 +191,9 @@
!(~(has by graphs) resource)
== ==
~| "validation of graph {<resource>} failed using mark {<mark>}"
?> (validate-graph graph mark)
=^ is-valid state
(validate-graph graph mark)
?> is-valid
=/ =logged-update:store
[time %add-graph resource graph mark overwrite]
=/ =update-log:store
@ -217,6 +230,10 @@
(~(got by graphs) resource)
~| "cannot add duplicate nodes to {<resource>}"
?< (check-for-duplicates graph ~(key by nodes))
~| "validation of nodes failed using mark {<mark>}"
=^ is-valid state
(check-validity ~(tap by nodes) mark)
?> is-valid
=/ =update-log:store (~(got by update-logs) resource)
=. update-log
(put:orm-log update-log time [time [%add-nodes resource nodes]])
@ -231,6 +248,17 @@
(add-node-list resource graph mark (sort-nodes nodes))
==
::
++ check-validity
|= [lis=(list (pair index:store node:store)) mark=(unit ^mark)]
^- [? _state]
|-
?~ lis [& state]
=^ is-valid state
(validate-graph (gas:orm ~ [(rear p.i.lis) q.i.lis]~) mark)
?. is-valid
[| state]
$(lis t.lis)
::
++ check-for-duplicates
|= [=graph:store nodes=(set index:store)]
^- ?
@ -288,8 +316,6 @@
==
^- graph:store
?< ?=(~ index)
~| "validation of node failed using mark {<mark>}"
?> (validate-graph (gas:orm ~ [i.index node]~) mark)
=* atom i.index
%^ put:orm
graph
@ -588,18 +614,24 @@
^- (quip card _state)
=/ [=graph:store mark=(unit mark:store)]
(~(got by graphs) resource.debug-input)
?> (validate-graph graph mark)
=^ is-valid state
(validate-graph graph mark)
?> is-valid
[~ state]
::
++ validate-graph
|= [=graph:store mark=(unit mark:store)]
^- ?
?~ mark %.y
^- [? _state]
?~ mark [%.y state]
=/ has-dais (~(has by validators) u.mark)
=/ =dais:clay
?: has-dais
(~(got by validators) u.mark)
.^ =dais:clay
%cb
/(scot %p our.bowl)/[q.byk.bowl]/(scot %da now.bowl)/[u.mark]
==
:_ state(validators (~(put by validators) u.mark dais))
|- ^- ?
?~ graph %.y
%+ roll (tap:orm graph)
@ -617,7 +649,9 @@
++ poke-import
|= arc=*
^- (quip card _state)
(import:store arc our.bowl)
=^ cards -.state
(import:store arc our.bowl)
[cards state]
--
::
++ on-peek

View File

@ -47,8 +47,9 @@
::
++ transform-proxy-update
|= vas=vase
^- (unit vase)
^- (quip card (unit vase))
=/ =update:store !<(update:store vas)
:- ~
?: ?=(%initial -.update)
~
|^

View File

@ -24,6 +24,6 @@
<div id="portal-root"></div>
<script src="/~landscape/js/channel.js"></script>
<script src="/~landscape/js/session.js"></script>
<script src="/~landscape/js/bundle/index.0a121973708299f6b966.js"></script>
<script src="/~landscape/js/bundle/index.a6842e8d167b4e66a4e0.js"></script>
</body>
</html>

View File

@ -191,9 +191,14 @@
^- (unit (unit cage))
?. (team:title our.bowl src.bowl) ~
?+ path [~ ~]
[%x %tiles ~] ``noun+!>([tiles tile-ordering])
[%x %first-time ~] ``noun+!>(first-time)
[%x %keys ~] ``noun+!>(~(key by tiles))
[%x %tiles ~] ``noun+!>([tiles tile-ordering])
[%x %first-time ~] ``noun+!>(first-time)
[%x %keys ~] ``noun+!>(~(key by tiles))
::
[%x %runtime-lag ~]
:^ ~ ~ %json
!> ^- json
b+.^(? //(scot %p our.bowl)//(scot %da now.bowl)/zen/lag)
==
::
++ on-arvo

View File

@ -59,8 +59,9 @@
::
++ transform-proxy-update
|= vas=vase
^- (unit vase)
^- (quip card (unit vase))
=/ =update:store !<(update:store vas)
:- ~
?. ?=(?(%add %remove) -.update)
~
=/ role=(unit (unit role-tag))

View File

@ -50,18 +50,17 @@
==
::
++ index
|= i=^index
|= ind=^index
^- json
?: =(~ i) s+'/'
=/ j=^tape ""
|-
?~ i [%s (crip j)]
=/ k=json (numb i.i)
?> ?=(%n -.k)
%_ $
i t.i
j (weld j (weld "/" (trip +.k)))
==
:- %s
?: =(~ ind)
'/'
%+ roll ind
|= [cur=@ acc=@t]
^- @t
=/ num (numb cur)
?> ?=(%n -.num)
(rap 3 acc '/' p.num ~)
::
++ uid
|= u=^uid

View File

@ -75,7 +75,12 @@
=/ grp=(unit group)
(scry-group rid)
?~ grp ~
=* group u.grp
(role-for-ship-with-group u.grp rid ship)
::
++ role-for-ship-with-group
|= [grp=group rid=resource =ship]
^- (unit (unit role-tag))
=* group grp
=* policy policy.group
=* tags tags.group
=/ admins=(set ^ship)
@ -106,8 +111,13 @@
^- (set ship)
=/ grp=(unit group)
(scry-group rid)
?~ grp ~
(~(get ju tags.u.grp) tag)
?~ grp ~
(get-tagged-ships-with-group u.grp rid tag)
::
++ get-tagged-ships-with-group
|= [grp=group rid=resource =tag]
^- (set ship)
(~(get ju tags.grp) tag)
::
++ is-managed
|= rid=resource

View File

@ -23,13 +23,13 @@
%+ turn ~(tap by associations)
|= [=md-resource [group=resource =^metadatum]]
^- [cord json]
:-
%- crip
;: weld
(trip (spat (en-path:resource group)))
(weld "/" (trip app-name.md-resource))
(trip (spat (en-path:resource resource.md-resource)))
==
:- %: rap 3
(spat (en-path:resource group))
'/'
app-name.md-resource
(spat (en-path:resource resource.md-resource))
~
==
%- pairs
:~ [%group s+(enjs-path:resource group)]
[%app-name s+app-name.md-resource]

View File

@ -118,11 +118,10 @@
::
:: If %.n please leave note as to why renegotiation necessary
::
:: - Fixing incorrectly held unversioned subscriptions
::
++ diplomatic
^- ?
%.n
%.y
::
++ default
|* [pull-hook=* =config]

View File

@ -26,6 +26,7 @@
::
/- *push-hook
/+ default-agent, resource, verb, versioning, agentio
~% %push-hook-top ..part ~
|%
+$ card card:agent:gall
::
@ -76,14 +77,13 @@
:: +diplomatic: only renegotiate if versions changed
::
:: If %.n please leave note as to why renegotiation necessary
::
:: - Fixing incorrectly held unversioned subscriptions
::
++ diplomatic
^- ?
%.n
%.y
::
++ push-hook
~/ %push-hook
|* =config
$_ ^|
|_ bowl:gall
@ -113,7 +113,7 @@
::
++ transform-proxy-update
|~ vase
*(unit vase)
*[(list card) (unit vase)]
:: +initial-watch: produce initial state for a subscription
::
:: .resource is the resource being subscribed to.
@ -175,6 +175,7 @@
=* state -
^- agent:gall
=<
~% %push-agent-lib ..poke-hook-action ~
|_ =bowl:gall
+* this .
og ~(. push-hook bowl)
@ -267,6 +268,7 @@
!>(state)
::
++ on-poke
~/ %on-poke
|= [=mark =vase]
^- (quip card:agent:gall agent:gall)
?: =(mark %push-hook-action)
@ -283,6 +285,7 @@
[cards this]
::
++ on-watch
~/ %on-watch
|= =path
^- (quip card:agent:gall agent:gall)
?: ?=([%version ~] path)
@ -320,6 +323,7 @@
--
::
++ on-agent
~/ %on-agent
|= [=wire =sign:agent:gall]
^- (quip card:agent:gall agent:gall)
?. ?=([%helper %push-hook @ *] wire)
@ -373,6 +377,7 @@
[%x %min-version ~] ``version+!>(version.config)
==
--
~% %push-helper-lib ..card ~
|_ =bowl:gall
+* og ~(. push-hook bowl)
ver ~(. versioning [bowl [update-mark version min-version]:config])
@ -380,6 +385,7 @@
pass pass:io
::
++ poke-hook-action
~/ %poke-hook-action
|= =action
^- (quip card:agent:gall _state)
|^
@ -448,6 +454,7 @@
[%pass wire %agent [our.bowl store-name.config] %watch store-path.config]
::
++ push-updates
~/ %push-updates
|= =cage
^- (list card:agent:gall)
%+ roll (resource-for-update q.cage)
@ -484,6 +491,7 @@
--
::
++ forward-update
~/ %forward-update
|= =cage
^- (list card:agent:gall)
=- lis
@ -494,35 +502,32 @@
^- [(list card:agent:gall) (unit vase)]
=/ =path
resource+(en-path:resource rid)
=/ =wire (make-wire path)
=* ship entity.rid
=. tf-vas
=/ out=(pair (list card:agent:gall) (unit vase))
?. =(our.bowl ship)
:: do not transform before forwarding
::
`vas
``vas
:: use cached transform
::
?^ tf-vas tf-vas
?^ tf-vas `tf-vas
:: transform before poking store
::
(transform-proxy-update:og vas)
~| "forwarding failed during transform. mark: {<p.cage>} resource: {<rid>}"
?> ?=(^ tf-vas)
=/ =dock
:- ship
?. =(our.bowl ship)
:: forward to host
::
dap.bowl
:: poke our store
~| "forwarding failed during transform. mark: {<p.cage>} rid: {<rid>}"
?> ?=(^ q.out)
:_ q.out
:_ (weld lis p.out)
=/ =wire (make-wire path)
=- [%pass wire %agent - %poke [current-version:ver u.q.out]]
:- ship
?. =(our.bowl ship)
:: forward to host
::
store-name.config
=/ cag=^cage
:- current-version:ver
u.tf-vas
:_ tf-vas
[[%pass wire %agent dock %poke cag] lis]
dap.bowl
:: poke our store
::
store-name.config
::
++ ver-from-path
|= =path
@ -532,6 +537,7 @@
(slav %ud i.extra)
::
++ resource-for-update
~/ %resource-for-update
|= =vase
^- (list resource)
%~ tap in

View File

@ -39,10 +39,10 @@
~! +:*handler
(handler inbound-request)
::
=/ redirect=cord
%- crip
"/~/login?redirect={(trip url.request.inbound-request)}"
[[307 ['location' redirect]~] ~]
=- [[307 ['location' -]~] ~]
%^ cat 3
'/~/login?redirect='
url.request.inbound-request
::
:: +require-authorization-simple:
:: redirect to the login page when unauthenticated
@ -56,10 +56,10 @@
~! this
simple-payload
::
=/ redirect=cord
%- crip
"/~/login?redirect={(trip url.request.inbound-request)}"
[[307 ['location' redirect]~] ~]
=- [[307 ['location' -]~] ~]
%^ cat 3
'/~/login?redirect='
url.request.inbound-request
::
++ give-simple-payload
|= [eyre-id=@ta =simple-payload:http]
@ -86,36 +86,52 @@
:_ `octs
[200 [['content-type' 'text/html'] ?:(cache [max-1-wk ~] ~)]]
::
++ js-response
|= =octs
^- simple-payload:http
[[200 [['content-type' 'text/javascript'] max-1-da ~]] `octs]
::
++ json-response
|= =json
^- simple-payload:http
[[200 ['content-type' 'application/json']~] `(json-to-octs json)]
::
++ css-response
=| cache=?
|= =octs
^- simple-payload:http
[[200 [['content-type' 'text/css'] max-1-da ~]] `octs]
:_ `octs
[200 [['content-type' 'text/css'] ?:(cache [max-1-wk ~] ~)]]
::
++ manx-response
|= man=manx
++ js-response
=| cache=?
|= =octs
^- simple-payload:http
[[200 ['content-type' 'text/html']~] `(manx-to-octs man)]
:_ `octs
[200 [['content-type' 'text/javascript'] ?:(cache [max-1-wk ~] ~)]]
::
++ png-response
=| cache=?
|= =octs
^- simple-payload:http
[[200 [['content-type' 'image/png'] max-1-wk ~]] `octs]
:_ `octs
[200 [['content-type' 'image/png'] ?:(cache [max-1-wk ~] ~)]]
::
++ ico-response
|= =octs
^- simple-payload:http
[[200 [['content-type' 'image/x-icon'] max-1-wk ~]] `octs]
::
++ woff2-response
=| cache=?
|= =octs
^- simple-payload:http
[[200 [['content-type' 'font/woff2'] max-1-wk ~]] `octs]
::
++ json-response
=| cache=_|
|= =json
^- simple-payload:http
:_ `(json-to-octs json)
[200 [['content-type' 'application/json'] ?:(cache [max-1-da ~] ~)]]
::
++ manx-response
=| cache=_|
|= man=manx
^- simple-payload:http
:_ `(manx-to-octs man)
[200 [['content-type' 'text/html'] ?:(cache [max-1-da ~] ~)]]
::
++ not-found
^- simple-payload:http
[[404 ~] ~]
@ -123,10 +139,10 @@
++ login-redirect
|= =request:http
^- simple-payload:http
=/ redirect=cord
%- crip
"/~/login?redirect={(trip url.request)}"
[[307 ['location' redirect]~] ~]
=- [[307 ['location' -]~] ~]
%^ cat 3
'/~/login?redirect='
url.request
::
++ redirect
|= redirect=cord

20
pkg/arvo/mar/graph/cache/hook.hoon vendored Normal file
View File

@ -0,0 +1,20 @@
/- metadata=metadata-store, res=resource
|%
+$ cache-action
$% [%graph-to-mark (pair resource:res (unit mark))]
[%perm-marks (pair (pair mark @tas) tube:clay)]
[%transform-marks (pair mark tube:clay)]
==
--
::
|_ act=cache-action
++ grad %noun
++ grow
|%
++ noun act
--
++ grab
|%
++ noun cache-action
--
--

12
pkg/arvo/mar/ico.hoon Normal file
View File

@ -0,0 +1,12 @@
|_ dat=@
++ grow
|%
++ mime [/image/x-icon (as-octs:mimes:html dat)]
--
++ grab
|%
++ mime |=([p=mite q=octs] q.q)
++ noun @
--
++ grad %mime
--

View File

@ -4964,18 +4964,18 @@
"dev": true
},
"elliptic": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz",
"integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==",
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz",
"integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==",
"dev": true,
"requires": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"bn.js": "^4.11.9",
"brorand": "^1.1.0",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
"hmac-drbg": "^1.0.1",
"inherits": "^2.0.4",
"minimalistic-assert": "^1.0.1",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"emoji-regex": {
@ -6643,9 +6643,9 @@
"dev": true
},
"immer": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz",
"integrity": "sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA=="
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.2.tgz",
"integrity": "sha512-mkcmzLtIfSp40vAqteRr1MbWNSoI7JE+/PB36FNPoSfJ9RQRmNKuTYCjKkyXyuq3Dgn07HuJBrwJd4ZSk2yUbw=="
},
"import-fresh": {
"version": "3.3.0",
@ -7192,9 +7192,9 @@
}
},
"lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash-es": {
"version": "4.17.20",
@ -9133,9 +9133,38 @@
}
},
"remark-breaks": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-2.0.1.tgz",
"integrity": "sha512-CZKI8xdPUnvMqPxYEIBBUg8C0B0kyn14lkW0abzhfh/P71YRIxCC3wvBh6AejQL602OxF6kNRl1x4HAZA07JyQ=="
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/remark-breaks/-/remark-breaks-2.0.2.tgz",
"integrity": "sha512-LsQnPPQ7Fzp9RTjj4IwdEmjPOr9bxe9zYKWhs9ZQOg9hMg8rOfeeqQ410cvVdIK87Famqza1CKRxNkepp2EvUA==",
"requires": {
"unist-util-visit": "^2.0.0"
},
"dependencies": {
"unist-util-is": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz",
"integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg=="
},
"unist-util-visit": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-2.0.3.tgz",
"integrity": "sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==",
"requires": {
"@types/unist": "^2.0.0",
"unist-util-is": "^4.0.0",
"unist-util-visit-parents": "^3.0.0"
}
},
"unist-util-visit-parents": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-3.1.1.tgz",
"integrity": "sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==",
"requires": {
"@types/unist": "^2.0.0",
"unist-util-is": "^4.0.0"
}
}
}
},
"remark-disable-tokenizers": {
"version": "1.1.0",
@ -10094,9 +10123,9 @@
"dev": true
},
"ssri": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
"integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
"dev": true,
"requires": {
"figgy-pudding": "^3.5.1"
@ -11068,9 +11097,9 @@
}
},
"url-parse": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz",
"integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==",
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz",
"integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==",
"dev": true,
"requires": {
"querystringify": "^2.1.1",
@ -12168,9 +12197,9 @@
}
},
"zustand": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-3.3.1.tgz",
"integrity": "sha512-o0rgrBsi29nCkPHdhtkAHisCIlmRUoXOV+1AmDMeCgkGG0i5edFSpGU0KiZYBvFmBYycnck4Z07JsLYDjSET9g=="
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-3.5.1.tgz",
"integrity": "sha512-7J56Ve814z4zap71iaKFD+t65LFI//jEq/Vf55BTSVqJZCm+w9rov8OMBg+YSwIPQk54bfoIWHTrOWuAbpEDMw=="
}
}
}

View File

@ -22,8 +22,8 @@
"css-loader": "^3.6.0",
"file-saver": "^2.0.5",
"formik": "^2.1.5",
"immer": "^8.0.1",
"lodash": "^4.17.20",
"immer": "^9.0.2",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"mousetrap": "^1.6.5",
"mousetrap-global-bind": "^1.1.0",
@ -42,7 +42,7 @@
"react-virtuoso": "^0.20.3",
"react-visibility-sensor": "^5.1.1",
"remark": "^12.0.0",
"remark-breaks": "^2.0.1",
"remark-breaks": "^2.0.2",
"remark-disable-tokenizers": "1.1.0",
"stacktrace-js": "^2.0.2",
"style-loader": "^1.3.0",
@ -56,7 +56,7 @@
"workbox-recipes": "^6.0.2",
"workbox-routing": "^6.0.2",
"yup": "^0.29.3",
"zustand": "^3.3.1"
"zustand": "^3.5.0"
},
"devDependencies": {
"@babel/core": "^7.12.10",

View File

@ -1,6 +1,9 @@
import { Patp } from '@urbit/api';
import { ContactEditField } from '@urbit/api/contacts';
import _ from 'lodash';
import {edit} from '../reducers/contact-update';
import {doOptimistically} from '../state/base';
import useContactState from '../state/contact';
import { StoreState } from '../store/type';
import BaseApi from './base';
@ -26,13 +29,14 @@ export default class ContactsApi extends BaseApi<StoreState> {
{add-group: {ship, name}}
{remove-group: {ship, name}}
*/
return this.storeAction({
const action = {
edit: {
ship,
'edit-field': editField,
timestamp: Date.now()
}
});
}
doOptimistically(useContactState, action, this.storeAction.bind(this), [edit])
}
allowShips(ships: Patp[]) {

View File

@ -1,4 +1,5 @@
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';
@ -9,7 +10,7 @@ export const createBlankNodeWithChildPost = (
parentIndex = '',
childIndex = '',
contents: Content[]
) => {
): GraphNode => {
const date = unixToDa(Date.now()).toString();
const nodeIndex = parentIndex + '/' + date;
@ -35,7 +36,7 @@ export const createBlankNodeWithChildPost = (
hash: null,
signatures: []
},
children: childGraph
children: childGraph as BigIntOrderedMap<GraphNode>
};
};

View File

@ -1,7 +1,10 @@
import { Association, GraphNotifDescription, IndexedNotification, NotifIndex } from '@urbit/api';
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
import { BigInteger } from 'big-integer';
import { getParentIndex } from '../lib/notification';
import { dateToDa, decToUd } from '../lib/util';
import {reduce} from '../reducers/hark-update';
import {doOptimistically, optReduceState} from '../state/base';
import useHarkState from '../state/hark';
import { StoreState } from '../store/type';
import BaseApi from './base';
@ -51,8 +54,15 @@ export class HarkApi extends BaseApi<StoreState> {
});
}
archive(time: BigInteger, index: NotifIndex) {
return this.actOnNotification('archive', time, index);
async archive(intTime: BigInteger, index: NotifIndex) {
const time = decToUd(intTime.toString());
const action = {
archive: {
time,
index
}
};
await doOptimistically(useHarkState, action, this.harkAction.bind(this), [reduce])
}
read(time: BigInteger, index: NotifIndex) {

View File

@ -7,4 +7,10 @@ export default class LocalApi extends BaseApi<StoreState> {
this.store.handleEvent({ data: { baseHash } });
});
}
getRuntimeLag() {
return this.scry<boolean>('launch', '/runtime-lag').then((runtimeLag) => {
this.store.handleEvent({ data: { runtimeLag } });
});
}
}

View File

@ -1,6 +1,8 @@
import { IndexedNotification, NotificationGraphConfig, Unreads } from '@urbit/api';
import { GraphNotifIndex, GroupNotifIndex, IndexedNotification, NotificationGraphConfig, Post, Unreads } from '@urbit/api';
import bigInt, { BigInteger } from 'big-integer';
import _ from 'lodash';
import f from 'lodash/fp';
import { pluralize } from './util';
export function getLastSeen(
unreads: Unreads,
@ -31,7 +33,7 @@ export function getNotificationCount(
): number {
const unread = unreads.graph?.[path] || {};
return Object.keys(unread)
.map(index => unread[index]?.notifications?.length || 0)
.map(index => _.get(unread[index], 'notifications.length', 0))
.reduce(f.add, 0);
}
@ -57,3 +59,54 @@ export function getNotificationKey(time: BigInteger, notification: IndexedNotifi
return `${base}-unknown`;
}
export function notificationReferent(not: IndexedNotification) {
if('graph' in not.index) {
return not.index.graph.graph;
} else {
return not.index.group.group;
}
}
export function describeNotification(notification: IndexedNotification) {
function group(idx: GroupNotifIndex) {
switch (idx.description) {
case 'add-members':
return 'joined';
case 'remove-members':
return 'left';
default:
return idx.description;
}
}
function graph(idx: GraphNotifIndex, plural: boolean, singleAuthor: boolean) {
const isDm = idx.graph.startsWith('dm--');
switch (idx.description) {
case 'post':
return singleAuthor ? 'replied to you' : 'Your post received replies';
case 'link':
return `New link${plural ? 's' : ''} in`;
case 'comment':
return `New comment${plural ? 's' : ''} on`;
case 'note':
return `New Note${plural ? 's' : ''} in`;
case 'edit-note':
return `updated ${pluralize('note', plural)} in`;
case 'mention':
return singleAuthor ? 'mentioned you in' : 'You were mentioned in';
case 'message':
if (isDm) {
return 'messaged you';
}
return `New message${plural ? 's' : ''} in`;
default:
return idx.description;
}
}
if('group' in notification.index) {
return group(notification.index.group);
} else if('graph' in notification.index) {
const contents = notification.notification?.contents?.graph ?? [] as Post[];
return graph(notification.index.graph, contents.length > 1, _.uniq(_.map(contents, 'author')).length === 1)
}
}

View File

@ -20,11 +20,12 @@ function getPermalinkForAssociatedGroup(group: string) {
type Permalink = GraphPermalink | GroupPermalink;
interface GroupPermalink {
export interface GroupPermalink {
type: 'group';
group: string;
link: string;
}
export interface GraphPermalink {
type: 'graph';
link: string;

View File

@ -58,13 +58,7 @@ const tokenizeMessage = (text) => {
|| (str === '`' && !isInCodeBlock)
) {
isInCodeBlock = true;
} else if (
(str.endsWith('`') && str !== '`')
|| (str === '`' && isInCodeBlock)
) {
isInCodeBlock = false;
}
}
if(isRef(str) && !isInCodeBlock) {
if (currTextLine.length > 0 || currTextBlock.length > 0) {
// If we're in the middle of a message, add it to the stack and reset
@ -105,6 +99,13 @@ const tokenizeMessage = (text) => {
} else {
currTextLine.push(str);
}
if (
(str.endsWith('`') && str !== '`')
|| (str === '`' && isInCodeBlock)
) {
isInCodeBlock = false;
}
});
}
currTextBlock.push(currTextLine.join(' '))

View File

@ -1,9 +1,9 @@
import { Box } from '@tlon/indigo-react';
import React, {
ReactNode,
useCallback,
useMemo,
useRef, useState
ReactNode,
useCallback,
useMemo,
useRef, useState
} from 'react';
import { PropFunc } from '~/types';
import { ModalOverlay } from '~/views/components/ModalOverlay';
@ -57,7 +57,7 @@ export function useModal(props: UseModalProps & PropFunc<typeof Box>): UseModalR
display="flex"
alignItems="stretch"
flexDirection="column"
spacing="2"
spacing={2}
dismiss={dismiss}
{...rest}
>

View File

@ -1,12 +1,15 @@
/* eslint-disable max-lines */
import { sigil as sigiljs, stringRenderer } from '@tlon/sigil-js';
import { Association, Contact } from '@urbit/api';
import anyAscii from 'any-ascii';
import bigInt, { BigInteger } from 'big-integer';
import { enableMapSet } from 'immer';
import _ from 'lodash';
import f from 'lodash/fp';
import { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { foregroundFromBackground } from '~/logic/lib/sigil';
import { IconRef, Workspace } from '~/types';
import useContactState from '../state/contact';
import useSettingsState from '../state/settings';
enableMapSet();
@ -58,10 +61,10 @@ export const getChord = (e: KeyboardEvent) => {
}
export function getResourcePath(workspace: Workspace, path: string, joined: boolean, mod: string) {
const base = workspace.type === 'group'
? `/~landscape${workspace.group}`
: workspace.type === 'home'
? `/~landscape/home`
const base = workspace.type === 'group'
? `/~landscape${workspace.group}`
: workspace.type === 'home'
? `/~landscape/home`
: `/~landscape/messages`;
return `${base}/${joined ? 'resource' : 'join'}/${mod}${path}`
}
@ -452,6 +455,13 @@ export const useHovering = (): useHoveringInterface => {
return useMemo(() => ({ hovering, bind }), [hovering, bind]);
};
export function withHovering<T>(Component: React.ComponentType<T>) {
return React.forwardRef((props, ref) => {
const { hovering, bind } = useHovering();
return <Component ref={ref} hovering={hovering} bind={bind} {...props} />
})
}
const DM_REGEX = /ship\/~([a-z]|-)*\/dm--/;
export function getItemTitle(association: Association): string {
if (DM_REGEX.test(association.resource)) {
@ -464,3 +474,22 @@ export function getItemTitle(association: Association): string {
return association.metadata.title ?? association.resource ?? '';
}
export const svgDataURL = (svg) => 'data:image/svg+xml;base64,' + btoa(svg);
export const svgBlobURL = (svg) => URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml' }));
export const favicon = () => {
let background = '#ffffff';
const contacts = useContactState.getState().contacts;
if (contacts.hasOwnProperty(`~${window.ship}`)) {
background = `#${uxToHex(contacts[`~${window.ship}`].color)}`;
}
const foreground = foregroundFromBackground(background);
const svg = sigiljs({
patp: window.ship,
renderer: stringRenderer,
size: 16,
colors: [background, foreground]
});
return svg;
}

View File

@ -19,12 +19,14 @@ const withStateo = <
});
};
const withState = <
StateType extends BaseState<StateType>,
stateKey extends keyof StateType
>(
interface StatePicker extends Array<any> {
0: UseStore<any>;
1?: string[];
}
const withState = (
Component: any,
stores: ([UseStore<StateType>, stateKey[]])[]
stores: StatePicker[]
) => {
return React.forwardRef((props, ref) => {
const stateProps: unknown = {};

View File

@ -1,4 +1,4 @@
import { ContactUpdate } from '@urbit/api';
import { ContactUpdate, deSig } from '@urbit/api';
import _ from 'lodash';
import { reduceState } from '../state/base';
import useContactState, { ContactState } from '../state/contact';
@ -52,9 +52,9 @@ const remove = (json: ContactUpdate, state: ContactState): ContactState => {
return state;
};
const edit = (json: ContactUpdate, state: ContactState): ContactState => {
export const edit = (json: ContactUpdate, state: ContactState): ContactState => {
const data = _.get(json, 'edit', false);
const ship = `~${data.ship}`;
const ship = `~${deSig(data.ship)}`;
if (
data &&
(ship in state.contacts)

View File

@ -1,3 +1,4 @@
import { GraphNode } from '@urbit/api';
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
import bigInt, { BigInteger } from 'big-integer';
import produce from 'immer';
@ -52,14 +53,14 @@ const keys = (json, state: GraphState): GraphState => {
const processNode = (node) => {
// is empty
if (!node.children) {
return produce(node, (draft) => {
return produce<GraphNode>(node, (draft: GraphNode) => {
draft.children = new BigIntOrderedMap();
});
}
// is graph
return produce(node, (draft) => {
draft.children = new BigIntOrderedMap()
return produce<GraphNode>(node, (draft: GraphNode) => {
draft.children = new BigIntOrderedMap<GraphNode>()
.gas(_.map(draft.children, (item, idx) =>
[bigInt(idx), processNode(item)] as [BigInteger, any]
));

View File

@ -7,8 +7,10 @@ import { BigInteger } from 'big-integer';
import _ from 'lodash';
import { compose } from 'lodash/fp';
import { makePatDa } from '~/logic/lib/util';
import { describeNotification } from '../lib/hark';
import { reduceState } from '../state/base';
import useHarkState, { HarkState } from '../state/hark';
import useMetadataState from '../state/metadata';
export const HarkReducer = (json: any) => {
const data = _.get(json, 'harkUpdate', false);
@ -35,7 +37,7 @@ export const HarkReducer = (json: any) => {
}
};
function reduce(data, state) {
export function reduce(data, state) {
const reducers = [
calculateCount,
unread,
@ -64,11 +66,19 @@ function calculateCount(json: any, state: HarkState) {
let count = 0;
_.forEach(state.unreads.graph, (graphs) => {
_.forEach(graphs, (graph) => {
count += (graph?.notifications || []).length;
if (typeof graph?.notifications === 'object') {
count += graph?.notifications.length;
} else {
count += 0;
}
});
});
_.forEach(state.unreads.group, (group) => {
count += (group?.notifications || []).length;
if (typeof group?.notifications === 'object') {
count += group?.notifications.length;
} else {
count += 0;
}
});
state.notificationsCount = count;
return state;
@ -187,7 +197,7 @@ function readSince(json: any, state: HarkState): HarkState {
function unreadSince(json: any, state: HarkState): HarkState {
const data = _.get(json, 'unread-count');
if(data) {
if (data) {
updateUnreadCount(state, data.index, u => u + 1);
}
return state;
@ -305,7 +315,7 @@ function removeNotificationFromUnread(state: HarkState, index: NotifIndex, time:
}
}
function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'unreads' | 'last', f: (x: number) => number) {
function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'unreads' | 'last', f: (x: number) => number, notify = false) {
if('graph' in index) {
const curr: any = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, statField], 0);
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, statField], f(curr));
@ -320,6 +330,20 @@ function added(json: any, state: HarkState): HarkState {
if (data) {
const { index, notification } = data;
const time = makePatDa(data.time);
if (!useHarkState.getState().doNotDisturb) {
const description = describeNotification(data);
const meta = useMetadataState.getState();
const referent = 'graph' in data.index ? meta.associations.graph[data.index.graph.graph]?.metadata?.title ?? data.index.graph : meta.associations.groups[data.index.group.group]?.metadata?.title ?? data.index.group;
new Notification(`${description} ${referent}`, {
tag: 'landscape',
image: '/img/favicon.png',
icon: '/img/favicon.png',
badge: '/img/favicon.png',
renotify: true
});
}
const timebox = state.notifications.get(time) || [];
addNotificationToUnread(state, index, time);

View File

@ -37,6 +37,13 @@ export default class LaunchReducer {
state.baseHash = baseHash;
});
}
const runtimeLag = _.get(json, 'runtimeLag', null);
if (runtimeLag !== null) {
useLaunchState.getState().set(state => {
state.runtimeLag = runtimeLag;
});
}
}
}

View File

@ -1,24 +1,39 @@
import produce, { setAutoFreeze } from 'immer';
import produce, { applyPatches, Patch, produceWithPatches, setAutoFreeze, enablePatches } from 'immer';
import { compose } from 'lodash/fp';
import create, { State, UseStore } from 'zustand';
import _ from 'lodash';
import create, { UseStore } from 'zustand';
import { persist } from 'zustand/middleware';
setAutoFreeze(false);
enablePatches();
export const stateSetter = <StateType>(
fn: (state: StateType) => void,
set
export const stateSetter = <T extends {}>(
fn: (state: Readonly<T & BaseState<T>>) => void,
set: (newState: T & BaseState<T>) => void
): void => {
set(produce(fn));
set(produce(fn) as any);
};
export const optStateSetter = <T extends {}>(
fn: (state: T & BaseState<T>) => void,
set: (newState: T & BaseState<T>) => void,
get: () => T & BaseState<T>
): string => {
const old = get();
const id = _.uniqueId()
const [state, ,patches] = produceWithPatches(old, fn) as readonly [(T & BaseState<T>), any, Patch[]];
set({ ...state, patches: { ...state.patches, [id]: patches }});
return id;
};
export const reduceState = <
StateType extends BaseState<StateType>,
UpdateType
S extends {},
U
>(
state: UseStore<StateType>,
data: UpdateType,
reducers: ((data: UpdateType, state: StateType) => StateType)[]
state: UseStore<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.getState().set((state) => {
@ -26,6 +41,18 @@ export const reduceState = <
});
};
export const optReduceState = <S, U>(
state: UseStore<S & BaseState<S>>,
data: U,
reducers: ((data: U, state: S & BaseState<S>) => BaseState<S> & S)[]
): string => {
const reducer = compose(reducers.map(r => sta => r(data, sta)));
return state.getState().optSet((state) => {
reducer(state);
});
};
export let stateStorageKeys: string[] = [];
export const stateStorageKey = (stateName: string) => {
@ -40,19 +67,56 @@ export const stateStorageKey = (stateName: string) => {
});
};
export interface BaseState<StateType> extends State {
set: (fn: (state: StateType) => void) => void;
export interface BaseState<StateType> {
rollback: (id: string) => void;
patches: {
[id: string]: Patch[];
};
set: (fn: (state: BaseState<StateType>) => void) => void;
addPatch: (id: string, ...patch: Patch[]) => void;
removePatch: (id: string) => void;
optSet: (fn: (state: BaseState<StateType>) => void) => string;
}
export const createState = <T extends {}>(
name: string,
properties: T,
blacklist: string[] = []
): UseStore<T & BaseState<T>> => create(persist((set, get) => ({
blacklist: (keyof BaseState<T> | keyof T)[] = []
): UseStore<T & BaseState<T>> => create<T & BaseState<T>>(persist<T & BaseState<T>>((set, get) => ({
set: fn => stateSetter(fn, set),
optSet: fn => {
return optStateSetter(fn, set, get);
},
patches: {},
addPatch: (id: string, ...patch: Patch[]) => {
set(({ patches }) => ({ patches: {...patches, [id]: patch }}));
},
removePatch: (id: string) => {
set(({ patches }) => ({ patches: _.omit(patches, id)}));
},
rollback: (id: string) => {
set(state => {
const applying = state.patches[id]
return {...applyPatches(state, applying), patches: _.omit(state.patches, id) }
});
},
...properties
}), {
blacklist,
name: stateStorageKey(name),
version: process.env.LANDSCAPE_SHORTHASH as any
}));
export async function doOptimistically<A, S extends {}>(state: UseStore<S & BaseState<S>>, action: A, call: (a: A) => Promise<any>, reduce: ((a: A, fn: S & BaseState<S>) => S & BaseState<S>)[]) {
let num: string | undefined = undefined;
try {
num = optReduceState(state, action, reduce);
await call(action);
state.getState().removePatch(num)
} catch (e) {
console.error(e);
if(num) {
state.getState().rollback(num);
}
}
}

View File

@ -1,11 +1,11 @@
import { NotificationGraphConfig, Timebox, Unreads } from '@urbit/api';
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
// import { harkGraphHookReducer, harkGroupHookReducer, harkReducer } from "~/logic/subscription/hark";
import { BaseState, createState } from './base';
import { createState } from './base';
export const HARK_FETCH_MORE_COUNT = 3;
export interface HarkState extends BaseState<HarkState> {
export interface HarkState {
archivedNotifications: BigIntOrderedMap<Timebox>;
doNotDisturb: boolean;
// getMore: () => Promise<boolean>;

View File

@ -10,7 +10,8 @@ export interface LaunchState extends BaseState<LaunchState> {
weather: WeatherState | null | Record<string, never> | boolean,
userLocation: string | null;
baseHash: string | null;
}
runtimeLag: boolean;
};
const useLaunchState = createState<LaunchState>('Launch', {
firstTime: true,
@ -18,7 +19,8 @@ const useLaunchState = createState<LaunchState>('Launch', {
tiles: {},
weather: null,
userLocation: null,
baseHash: null
baseHash: null,
runtimeLag: false,
});
export default useLaunchState;

View File

@ -13,6 +13,10 @@ interface LocalUpdateBaseHash {
baseHash: string;
}
interface LocalUpdateRuntimeLag {
runtimeLag: boolean;
}
interface LocalUpdateBackgroundConfig {
backgroundConfig: BackgroundConfig;
}
@ -51,6 +55,7 @@ export type BackgroundConfig = BackgroundConfigUrl | BackgroundConfigColor | und
export type LocalUpdate =
| LocalUpdateSetDark
| LocalUpdateBaseHash
| LocalUpdateRuntimeLag
| LocalUpdateBackgroundConfig
| LocalUpdateHideAvatars
| LocalUpdateHideNicknames

View File

@ -1,6 +1,5 @@
import dark from '@tlon/indigo-dark';
import light from '@tlon/indigo-light';
import { sigil as sigiljs, stringRenderer } from '@tlon/sigil-js';
import Mousetrap from 'mousetrap';
import 'mousetrap-global-bind';
import * as React from 'react';
@ -11,8 +10,7 @@ import { BrowserRouter as Router, withRouter } from 'react-router-dom';
import styled, { ThemeProvider } from 'styled-components';
import GlobalApi from '~/logic/api/global';
import gcpManager from '~/logic/lib/gcpManager';
import { foregroundFromBackground } from '~/logic/lib/sigil';
import { uxToHex } from '~/logic/lib/util';
import { favicon, svgDataURL } from '~/logic/lib/util';
import withState from '~/logic/lib/withState';
import useContactState from '~/logic/state/contact';
import useGroupState from '~/logic/state/group';
@ -88,7 +86,6 @@ class App extends React.Component {
this.updateTheme = this.updateTheme.bind(this);
this.updateMobile = this.updateMobile.bind(this);
this.faviconString = this.faviconString.bind(this);
}
componentDidMount() {
@ -105,6 +102,7 @@ class App extends React.Component {
this.updateTheme(this.themeWatcher);
}, 500);
this.api.local.getBaseHash();
this.api.local.getRuntimeLag(); //TODO consider polling periodically
this.api.settings.getAll();
gcpManager.start();
Mousetrap.bindGlobal(['command+/', 'ctrl+/'], (e) => {
@ -131,22 +129,6 @@ class App extends React.Component {
});
}
faviconString() {
let background = '#ffffff';
if (this.props.contacts.hasOwnProperty(`~${window.ship}`)) {
background = `#${uxToHex(this.props.contacts[`~${window.ship}`].color)}`;
}
const foreground = foregroundFromBackground(background);
const svg = sigiljs({
patp: window.ship,
renderer: stringRenderer,
size: 16,
colors: [background, foreground]
});
const dataurl = 'data:image/svg+xml;base64,' + btoa(svg);
return dataurl;
}
getTheme() {
const { props } = this;
return ((props.dark && props?.display?.theme == 'auto') ||
@ -164,7 +146,7 @@ class App extends React.Component {
<ShortcutContextProvider>
<Helmet>
{window.ship.length < 14
? <link rel="icon" type="image/svg+xml" href={this.faviconString()} />
? <link rel="icon" type="image/svg+xml" href={svgDataURL(favicon())} />
: null}
</Helmet>
<Root>

View File

@ -108,8 +108,18 @@ interface ChatEditorState {
message: string;
}
interface CodeMirrorShim {
setValue: (string) => void;
setOption: (option: string, property: any) => void;
focus: () => void;
execCommand: (string) => void;
getValue: () => string;
getInputField: () => HTMLInputElement;
element: HTMLElement;
}
export default class ChatEditor extends Component<ChatEditorProps, ChatEditorState> {
editor: ProxyHandler<unknown> | null;
editor: CodeMirrorShim;
constructor(props: ChatEditorProps) {
super(props);
@ -239,7 +249,7 @@ export default class ChatEditor extends Component<ChatEditorProps, ChatEditorSta
<Row
backgroundColor='white'
alignItems='center'
flexGrow='1'
flexGrow={1}
height='100%'
paddingTop={MOBILE_BROWSER_REGEX.test(navigator.userAgent) ? '16px' : '0'}
paddingBottom={MOBILE_BROWSER_REGEX.test(navigator.userAgent) ? '16px' : '0'}
@ -252,7 +262,7 @@ export default class ChatEditor extends Component<ChatEditorProps, ChatEditorSta
{MOBILE_BROWSER_REGEX.test(navigator.userAgent)
? <MobileBox
data-value={this.state.message}
fontSize="1"
fontSize={1}
lineHeight="tall"
onClick={(event) => {
if (this.editor) {
@ -262,7 +272,7 @@ export default class ChatEditor extends Component<ChatEditorProps, ChatEditorSta
>
<BaseTextArea
fontFamily={inCodeMode ? 'Source Code Pro' : 'Inter'}
fontSize="1"
fontSize={1}
lineHeight="tall"
rows={1}
style={{ width: '100%', background: 'transparent', color: 'currentColor' }}
@ -271,7 +281,7 @@ export default class ChatEditor extends Component<ChatEditorProps, ChatEditorSta
this.messageChange(null, null, event.target.value)
}
onKeyDown={event =>
this.messageChange(null, null, event.target.value)
this.messageChange(null, null, (event.target as any).value)
}
ref={(input) => {
if (!input)

View File

@ -29,7 +29,7 @@ interface ChatInputState {
currentInput: string;
}
class ChatInput extends Component<ChatInputProps, ChatInputState> {
export class ChatInput extends Component<ChatInputProps, ChatInputState> {
private chatEditor: React.RefObject<ChatEditor>;
constructor(props) {

View File

@ -1,9 +1,10 @@
/* eslint-disable max-lines-per-function */
import { BaseImage, Box, Col, Icon, Row, Rule, Text } from '@tlon/indigo-react';
import { Contact, Post } from '@urbit/api';
import { Contact, MentionContent, Post } from '@urbit/api';
import bigInt from 'big-integer';
import moment from 'moment';
import React, {
Ref,
useEffect,
useMemo, useState
} from 'react';
@ -20,13 +21,13 @@ import useLocalState from '~/logic/state/local';
import useSettingsState, { selectCalmState } from '~/logic/state/settings';
import { Dropdown } from '~/views/components/Dropdown';
import ProfileOverlay from '~/views/components/ProfileOverlay';
import { GraphContent} from '~/views/landscape/components/Graph/GraphContent';
import { GraphContent } from '~/views/landscape/components/Graph/GraphContent';
export const DATESTAMP_FORMAT = '[~]YYYY.M.D';
interface DayBreakProps {
when: string;
when: string | number;
shimTop?: boolean;
}
@ -55,7 +56,7 @@ export const DayBreak = ({ when, shimTop = false }: DayBreakProps) => (
);
export const UnreadMarker = React.forwardRef(
({ dismissUnread }: any, ref) => {
({ dismissUnread }: any, ref: Ref<HTMLDivElement>) => {
const [visible, setVisible] = useState(false);
const idling = useIdlingState();
@ -113,7 +114,7 @@ const MessageActionItem = (props) => {
);
};
const MessageActions = ({ api, onReply, onDelete, association, msg, isAdmin, permalink }) => {
const MessageActions = ({ onReply, onDelete, msg, isAdmin, permalink }) => {
const isOwn = () => msg.author === window.ship;
const { doCopy, copyDisplay } = useCopy(permalink, 'Copy Message Link');
@ -205,14 +206,14 @@ interface ChatMessageProps {
msg: Post;
previousMsg?: Post;
nextMsg?: Post;
isLastRead: boolean;
permalink: string;
isLastRead?: boolean;
permalink?: string;
transcluded?: number;
className?: string;
isPending: boolean;
isPending?: boolean;
style?: unknown;
isLastMessage?: boolean;
dismissUnread: () => void;
dismissUnread?: () => void;
api: GlobalApi;
highlighted?: boolean;
renderSigil?: boolean;
@ -220,27 +221,24 @@ interface ChatMessageProps {
innerRef: (el: HTMLDivElement | null) => void;
onReply?: (msg: Post) => void;
showOurContact: boolean;
onDelete?: () => void;
}
function ChatMessage(props: ChatMessageProps) {
let { highlighted } = props;
const {
msg,
previousMsg,
nextMsg,
isLastRead,
group,
association,
isLastRead = false,
className = '',
isPending,
isPending = false,
style,
isLastMessage,
api,
showOurContact,
fontSize,
hideHover,
dismissUnread,
permalink
dismissUnread = () => null,
permalink = ''
} = props;
if (typeof msg === 'string' || !msg) {
@ -257,7 +255,7 @@ function ChatMessage(props: ChatMessageProps) {
msg.number === 1
);
const ourMention = msg?.contents?.some((e) => {
const ourMention = msg?.contents?.some((e: MentionContent) => {
return e?.mention && e?.mention === window.ship;
});
@ -291,12 +289,10 @@ function ChatMessage(props: ChatMessageProps) {
const messageProps = {
msg,
timestamp,
association,
isPending,
showOurContact,
api,
highlighted,
fontSize,
hideHover,
transcluded,
onReply,
@ -494,8 +490,9 @@ export const Message = React.memo(({
position='absolute'
width='36px'
textAlign='right'
left='0'
top='3px'
left={0}
top='2px'
lineHeight="tall"
fontSize={0}
gray
>
@ -527,10 +524,10 @@ export const MessagePlaceholder = ({
}) => (
<Box
width='100%'
fontSize='2'
pl='3'
pt='4'
pr='3'
fontSize={2}
pl={3}
pt={4}
pr={3}
display='flex'
lineHeight='tall'
className={className}
@ -538,7 +535,7 @@ export const MessagePlaceholder = ({
{...props}
>
<Box
pr='3'
pr={3}
verticalAlign='top'
backgroundColor='white'
style={{ float: 'left' }}
@ -561,20 +558,20 @@ export const MessagePlaceholder = ({
>
<Box
className='hide-child'
paddingTop='4'
paddingTop={4}
style={{ visibility: index % 5 == 0 ? 'initial' : 'hidden' }}
>
<Text
display='inline-block'
verticalAlign='middle'
fontSize='0'
fontSize={0}
color='washedGray'
cursor='default'
>
<Text maxWidth='32rem' display='block'>
<Text
backgroundColor='washedGray'
borderRadius='2'
borderRadius={2}
display='block'
width='100%'
height='100%'
@ -585,12 +582,12 @@ export const MessagePlaceholder = ({
display='inline-block'
mono
verticalAlign='middle'
fontSize='0'
washedGray
fontSize={0}
color='washedGray'
>
<Text
background='washedGray'
borderRadius='2'
borderRadius={2}
display='block'
height='1em'
style={{ width: `${((index % 3) + 1) * 3}em` }}
@ -599,16 +596,16 @@ export const MessagePlaceholder = ({
<Text
mono
verticalAlign='middle'
fontSize='0'
ml='2'
washedGray
borderRadius='2'
fontSize={0}
ml={2}
color='washedGray'
borderRadius={2}
display={['none', 'inline-block']}
className='child'
>
<Text
backgroundColor='washedGray'
borderRadius='2'
borderRadius={2}
display='block'
width='100%'
height='100%'
@ -618,7 +615,7 @@ export const MessagePlaceholder = ({
<Text
display='block'
backgroundColor='washedGray'
borderRadius='2'
borderRadius={2}
height='1em'
style={{ width: `${(index % 5) * 20}%` }}
></Text>

View File

@ -11,7 +11,7 @@ import useGraphState from '~/logic/state/graph';
import ShareProfile from '~/views/apps/chat/components/ShareProfile';
import { Loading } from '~/views/components/Loading';
import SubmitDragger from '~/views/components/SubmitDragger';
import ChatInput from './ChatInput';
import ChatInput, { ChatInput as NakedChatInput } from './ChatInput';
import ChatWindow from './ChatWindow';
interface ChatPaneProps {
@ -82,14 +82,14 @@ export function ChatPane(props: ChatPaneProps): ReactElement {
} = props;
const graphTimesentMap = useGraphState(state => state.graphTimesentMap);
const ourContact = useOurContact();
const chatInput = useRef<ChatInput>();
const chatInput = useRef<NakedChatInput>();
const onFileDrag = useCallback(
(files: FileList | File[]) => {
if (!chatInput.current) {
return;
}
chatInput.current?.uploadFiles(files);
(chatInput.current as NakedChatInput)?.uploadFiles(files);
},
[chatInput.current]
);

View File

@ -16,7 +16,7 @@ type ChatWindowProps = {
unreadCount: number;
graph: Graph;
graphSize: number;
station: unknown;
station?: unknown;
fetchMessages: (newer: boolean) => Promise<boolean>;
api: GlobalApi;
scrollTo?: BigInteger;
@ -36,6 +36,11 @@ interface ChatWindowState {
unreadIndex: BigInteger;
}
interface RendererProps {
index: bigInt.BigInteger;
scrollWindow: any;
}
const virtScrollerStyle = { height: '100%' };
class ChatWindow extends Component<
@ -99,11 +104,12 @@ class ChatWindow extends Component<
});
}
dismissedInitialUnread(): void {
dismissedInitialUnread(): boolean {
const { unreadCount, graph } = this.props;
return this.state.unreadIndex.eq(bigInt.zero) ? unreadCount > graph.size :
this.state.unreadIndex.neq(graph.keys()?.[unreadCount]?.[0] ?? bigInt.zero);
return this.state.unreadIndex.eq(bigInt.zero)
? unreadCount > graph.size
: this.state.unreadIndex.neq(graph.keys()?.[unreadCount]?.[0] ?? bigInt.zero);
}
handleWindowBlur(): void {
@ -173,7 +179,7 @@ class ChatWindow extends Component<
}
}
renderer = React.forwardRef(({ index, scrollWindow }, ref) => {
renderer = React.forwardRef(({ index, scrollWindow }: RendererProps, ref) => {
const {
api,
showOurContact,

View File

@ -22,16 +22,16 @@ const UnreadNotice = (props): ReactElement | null => {
className='unread-notice'
>
<Center>
<Box backgroundColor='white' borderRadius='3' overflow='hidden'>
<Box backgroundColor='white' borderRadius={3} overflow='hidden'>
<Box
backgroundColor='washedBlue'
display='flex'
alignItems='center'
p='2'
fontSize='0'
p={2}
fontSize={0}
justifyContent='space-between'
borderRadius='3'
border='1'
borderRadius={3}
border={1}
borderColor='lightBlue'
>
<Text

View File

@ -1,4 +1,5 @@
import { Center, Text } from '@tlon/indigo-react';
import { GraphConfig } from '@urbit/api';
import React, { ReactElement } from 'react';
import { Route, Switch, useHistory } from 'react-router-dom';
import GlobalApi from '~/logic/api/global';
@ -40,8 +41,8 @@ const GraphApp = (props: GraphAppProps): ReactElement => {
if(!graphKeys.has(resource)) {
autoJoin();
} else if(Boolean(association) && 'graph' in association.config) {
history.push(`/~landscape/home/resource/${association.metadata.config.graph}${path}`);
} else if(Boolean(association) && 'graph' in association.metadata.config) {
history.push(`/~landscape/home/resource/${(association.metadata.config as GraphConfig).graph}${path}`);
}
return (
<Center width="100%" height="100%">

View File

@ -6,13 +6,13 @@ import { Helmet } from 'react-helmet';
import styled from 'styled-components';
import GlobalApi from '~/logic/api/global';
import {
hasTutorialGroup,
hasTutorialGroup,
TUTORIAL_BOOK,
TUTORIAL_CHAT, TUTORIAL_GROUP,
TUTORIAL_HOST,
TUTORIAL_BOOK,
TUTORIAL_CHAT, TUTORIAL_GROUP,
TUTORIAL_HOST,
TUTORIAL_LINKS
TUTORIAL_LINKS
} from '~/logic/lib/tutorialModal';
import { useModal } from '~/logic/lib/useModal';
import { useQuery } from '~/logic/lib/useQuery';
@ -50,7 +50,7 @@ interface LaunchAppProps {
export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
const { connection } = props;
const baseHash = useLaunchState(state => state.baseHash);
const { baseHash, runtimeLag } = useLaunchState(state => state);
const [hashText, setHashText] = useState(baseHash);
const [exitingTut, setExitingTut] = useState(false);
const seen = useSettingsState(s => s?.tutorial?.seen) ?? true;
@ -67,8 +67,8 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
const hashBox = (
<Box
position={['relative', 'absolute']}
left="0"
bottom="0"
left={0}
bottom={0}
backgroundColor="white"
ml={3}
mb={3}
@ -84,7 +84,10 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
}, 2000);
}}
>
<Box backgroundColor="washedGray" p={2}>
<Box
backgroundColor={runtimeLag ? 'yellow' : 'washedGray'}
p={2}
>
<Text mono bold>{hashText || baseHash}</Text>
</Box>
</Box>
@ -120,28 +123,28 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
dismiss();
};
return exitingTut ? (
<Col maxWidth="350px" p="3">
<Col maxWidth="350px" p={3}>
<Icon icon="Info" fill="black"></Icon>
<Text my="3" lineHeight="tall">
<Text my={3} lineHeight="tall">
You can always restart the tutorial by typing &ldquo;tutorial&rdquo; in Leap
</Text>
<Row gapX="2" justifyContent="flex-end">
<Row gapX={2} justifyContent="flex-end">
<Button primary onClick={onDismiss}>Ok</Button>
</Row>
</Col>
) : (
<Col maxWidth="350px" p="3">
<Col maxWidth="350px" p={3}>
<Box position="absolute" left="-16px" top="-16px">
<StarIcon width="32px" height="32px" color="blue" display="block" />
</Box>
<Text mb="3" lineHeight="tall" fontWeight="medium">Welcome</Text>
<Text mb="3" lineHeight="tall">
<Text mb={3} lineHeight="tall" fontWeight="medium">Welcome</Text>
<Text mb={3} lineHeight="tall">
You have been invited to use Landscape, an interface to chat
and interact with communities
<br />
Would you like a tour of Landscape?
</Text>
<Row gapX="2" justifyContent="flex-end">
<Row gapX={2} justifyContent="flex-end">
<Button
backgroundColor="washedGray"
onClick={() => setExitingTut(true)}
@ -181,7 +184,7 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
<ScrollbarLessBox height='100%' overflowY='scroll' display="flex" flexDirection="column">
{modal}
<Box
mx='2'
mx={2}
display='grid'
gridTemplateColumns='repeat(auto-fill, minmax(128px, 1fr))'
gridGap={3}
@ -208,7 +211,7 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
color="black"
icon="Home"
/>
<Text ml="2" mt='1px' color="black">My Channels</Text>
<Text ml={2} mt='1px' color="black">My Channels</Text>
</Row>
</Box>
</Tile>

View File

@ -47,8 +47,8 @@ export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
const groups = Object.values(associations?.groups || {})
.filter(e => e?.group in groupState)
.sort(sortGroupsAlph);
const graphUnreads = getGraphUnreads(associations || {}, unreads);
const graphNotifications = getGraphNotifications(associations || {}, unreads);
const graphUnreads = getGraphUnreads(associations || {} as Associations, unreads);
const graphNotifications = getGraphNotifications(associations || {} as Associations, unreads);
return (
<>
@ -56,9 +56,9 @@ export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
const path = group?.group;
const unreadCount = graphUnreads(path);
const notCount = graphNotifications(path);
return (
<Group
key={group.metadata.title}
updates={notCount}
first={index === 0}
unreads={unreadCount}
@ -82,7 +82,7 @@ interface GroupProps {
const selectJoined = (s: SettingsState) => s.tutorial.joined;
function Group(props: GroupProps) {
const { path, title, unreads, updates, first = false } = props;
const anchorRef = useRef<HTMLElement>(null);
const anchorRef = useRef<HTMLDivElement>(null);
const isTutorialGroup = path === `/ship/${TUTORIAL_HOST}/${TUTORIAL_GROUP}`;
useTutorialModal(
'start',
@ -104,7 +104,7 @@ function Group(props: GroupProps) {
(<Text>{days} day{days !== 1 && 's'} remaining</Text>)
}
{updates > 0 &&
(<Text mt="1" color="blue">{updates} update{updates !== 1 && 's'} </Text>)
(<Text mt={1} color="blue">{updates} update{updates !== 1 && 's'} </Text>)
}
{unreads > 0 &&
(<Text color="lightGray">{unreads}</Text>)

View File

@ -1,17 +1,18 @@
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';
import ClockTile from './tiles/clock';
import CustomTile from './tiles/custom';
import WeatherTile from './tiles/weather';
interface TileProps {
export interface TileProps {
api: GlobalApi;
}
const Tiles = (props: TileProps): ReactElement => {
const weather = useLaunchState(state => state.weather);
const weather = useLaunchState(state => state.weather) as WeatherState;
const tileOrdering = useLaunchState(state => state.tileOrdering);
const tileState = useLaunchState(state => state.tiles);
const tiles = tileOrdering.filter((key) => {
@ -26,7 +27,6 @@ const Tiles = (props: TileProps): ReactElement => {
<BasicTile
key={key}
title={basic.title}
iconUrl={basic.iconUrl}
linkedUrl={basic.linkedUrl}
/>
);

View File

@ -21,7 +21,7 @@ const BasicTile = (props: BasicTileProps): ReactElement => (
display='inline-block'
verticalAlign='top'
mt='5px'
mr='2'
mr={2}
/>
: null
}{props.title}

View File

@ -120,7 +120,11 @@ const SvgArc = ({ start, end, ...rest }) => {
return <path d={d} {...rest} />;
};
class ClockText extends React.Component<ClockTextProps, ClockTextState> {
interface ClockTextState {
time: number;
}
class ClockText extends React.Component<{}, ClockTextState> {
interval?: NodeJS.Timeout;
constructor(props) {
super(props);
@ -176,7 +180,34 @@ class ClockText extends React.Component<ClockTextProps, ClockTextState> {
}
}
class Clock extends React.PureComponent {
interface ClockProps {
data: {
latitude?: number;
longitude?: number;
}
}
interface ClockState {
time: number;
lat: number;
lon: number;
geolocationSuccess: boolean;
sunrise: number;
sunsetStart: number;
sunset: number;
sunriseEnd: number;
dusk: number;
dawn: number;
night: number;
nightEnd: number;
nauticalDawn: number;
nauticalDusk: number;
}
class Clock extends React.PureComponent<ClockProps, ClockState> {
angle: number;
referenceTime: moment.Moment;
interval: NodeJS.Timeout;
constructor(props) {
super(props);
this.angle = 0;
@ -188,7 +219,7 @@ class Clock extends React.PureComponent {
geolocationSuccess: false,
sunrise: 0,
sunsetStart: 0,
sunset:0,
sunset: 0,
sunriseEnd: 0,
dusk: 0,
dawn: 0,

View File

@ -13,7 +13,7 @@ export default class CustomTile extends React.PureComponent {
backgroundColor='white'
border='1px solid'
borderColor='lightGray'
borderRadius='2'
borderRadius={2}
>
<BaseImage
position='absolute'

View File

@ -1,7 +1,8 @@
import { Box } from '@tlon/indigo-react';
import React from 'react';
import { Box, BoxProps } from '@tlon/indigo-react';
import React, { RefObject } from 'react';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { PaddingProps } from 'styled-system';
import defaultApps from '~/logic/lib/default-apps';
const SquareBox = styled(Box)`
@ -20,7 +21,17 @@ const SquareBox = styled(Box)`
`;
const routeList = defaultApps.map(a => `/~${a}`);
const Tile = React.forwardRef((props, ref) => {
type TileProps = BoxProps & {
bg?: string;
to?: string;
href?: string;
p?: PaddingProps;
children: any;
gridColumnStart?: number;
color?: string;
}
const Tile = React.forwardRef((props: TileProps, ref: RefObject<HTMLDivElement>) => {
const { bg, to, href, p, boxShadow, gridColumnStart, ...rest } = props;
let childElement = (

View File

@ -1,6 +1,7 @@
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';
@ -36,8 +37,20 @@ const imperialCountries = [
'Liberia',
];
class WeatherTile extends React.Component {
constructor(props) {
interface WeatherTileProps {
weather: any;
api: GlobalApi;
location: string;
}
interface WeatherTileState {
location: string;
manualEntry: boolean;
error: boolean;
}
class WeatherTile extends React.Component<WeatherTileProps, WeatherTileState> {
constructor(props: WeatherTileProps) {
super(props);
this.state = {
location: '',
@ -50,11 +63,7 @@ class WeatherTile extends React.Component {
locationSubmit() {
navigator.geolocation.getCurrentPosition((res) => {
const location = `${res.coords.latitude},${res.coords.longitude}`;
this.setState({
location
}, (err) => {
console.log(err);
}, { maximumAge: Infinity, timeout: 10000 });
this.setState({ location });
this.props.api.launch.weather(location);
this.setState({ manualEntry: !this.state.manualEntry });
});
@ -62,10 +71,8 @@ class WeatherTile extends React.Component {
manualLocationSubmit(event) {
event.preventDefault();
const location = document.getElementById('location').value;
this.setState({ location }, (err) => {
console.log(err);
}, { maximumAge: Infinity, timeout: 10000 });
const location = (document.getElementById('location') as HTMLInputElement).value;
this.setState({ location });
this.props.api.launch.weather(location);
this.setState({ manualEntry: !this.state.manualEntry });
}
@ -91,7 +98,7 @@ class WeatherTile extends React.Component {
let secureCheck;
let error;
if (this.state.error === true) {
error = <Text display='block' color='red' pt='1'>Please try again.</Text>;
error = <Text display='block' color='red' pt={1}>Please try again.</Text>;
}
if (location.protocol === 'https:') {
secureCheck = (
@ -128,15 +135,15 @@ class WeatherTile extends React.Component {
{locationName ? ` Current location is near ${locationName}.` : ''}
</Text>
{error}
<Box mt='auto' display='flex' marginBlockEnd='0'>
<Box mt='auto' display='flex' marginBlockEnd={0}>
<BaseInput
id="location"
size="10"
size={10}
width='100%'
color='black'
fontSize='0'
fontSize={0}
backgroundColor='transparent'
border='0'
border={0}
type="text"
autoFocus
placeholder="GPS, ZIP, City"
@ -150,10 +157,10 @@ class WeatherTile extends React.Component {
backgroundColor='transparent'
color='black'
cursor='pointer'
flexShrink='0'
pl='1'
fontSize='0'
border='0'
flexShrink={0}
pl={1}
fontSize={0}
border={0}
type="submit"
onClick={this.manualLocationSubmit.bind(this)}
value="->"
@ -173,7 +180,7 @@ class WeatherTile extends React.Component {
onClick={() => this.setState({ manualEntry: !this.state.manualEntry })}
>
<Box>
<Icon icon='Weather' display='inline-block' verticalAlign='top' mt='3px' mr='2' />
<Icon icon='Weather' display='inline-block' verticalAlign='top' mt='3px' mr={2} />
<Text>Weather</Text>
</Box>
<Text style={{ cursor: 'pointer' }}>
@ -219,7 +226,7 @@ class WeatherTile extends React.Component {
title={`${locationName} Weather`}
>
<Text>
<Icon icon='Weather' display='inline' mr='2' style={{ position: 'relative', top: '.3em' }} />
<Icon icon='Weather' display='inline' mr={2} style={{ position: 'relative', top: '.3em' }} />
<Text
cursor='pointer'
onClick={() =>
@ -269,7 +276,7 @@ class WeatherTile extends React.Component {
flexDirection="column"
justifyContent="flex-start"
>
<Text><Icon icon='Weather' color='black' display='inline' mr='2' style={{ position: 'relative', top: '.3em' }} /> Weather</Text>
<Text><Icon icon='Weather' color='black' display='inline' mr={2} style={{ position: 'relative', top: '.3em' }} /> Weather</Text>
<Text width='100%' display='flex' flexDirection='column' mt={1}>
Loading, please check again later...
</Text>

View File

@ -125,7 +125,7 @@ export function LinkResource(props: LinkResourceProps) {
api={api}
editCommentId={editCommentId}
history={props.history}
baseUrl={`${resourceUrl}/${props.match.params.index}`}
baseUrl={`${resourceUrl}/index/${props.match.params.index}`}
group={group}
px={3}
/>

View File

@ -2,7 +2,7 @@ import { Box, Col, Text } from '@tlon/indigo-react';
import { Association, Graph, Group } from '@urbit/api';
import bigInt from 'big-integer';
import React, {
Component
Component, ReactNode
} from 'react';
import GlobalApi from '~/logic/api/global';
import { isWriter } from '~/logic/lib/group';
@ -31,6 +31,11 @@ const style = {
alignItems: 'center'
};
interface RendererProps {
index: BigInteger;
children?: ReactNode;
}
class LinkWindow extends Component<LinkWindowProps, {}> {
fetchLinks = async () => true;
@ -39,7 +44,7 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
return isWriter(group, association.resource);
}
renderItem = React.forwardRef<HTMLDivElement>(({ index, scrollWindow }, ref) => {
renderItem = React.forwardRef<HTMLDivElement>(({ index }: RendererProps, ref) => {
const { props } = this;
const { association, graph, api } = props;
const [, , ship, name] = association.resource.split('/');
@ -60,7 +65,7 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
ref={ref}
key={index.toString()}
mx="auto"
mt="4"
mt={4}
maxWidth="768px"
width="100%"
flexShrink={0}
@ -82,7 +87,7 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
}
return (
<Box ref={ref}>
<LinkItem key={index.toString()} {...linkProps} />;
<LinkItem key={index.toString()} {...linkProps} />
</Box>
);
});
@ -96,7 +101,7 @@ class LinkWindow extends Component<LinkWindowProps, {}> {
<Col
key={0}
mx="auto"
mt="4"
mt={4}
maxWidth="768px"
width="100%"
flexShrink={0}

View File

@ -1,6 +1,6 @@
import { Action, Anchor, Box, Col, Icon, Row, Rule, Text } from '@tlon/indigo-react';
import { Association, GraphNode, Group } from '@urbit/api';
import React, { ReactElement, useCallback, useEffect, useRef } from 'react';
import { Association, GraphNode, Group, 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';
@ -15,8 +15,13 @@ import { PermalinkEmbed } from '../../permalinks/embed';
interface LinkItemProps {
node: GraphNode;
association: Association;
resource: string; api: GlobalApi; group: Group; path: string; }
export const LinkItem = React.forwardRef((props: LinkItemProps, ref): ReactElement => {
resource: string;
api: GlobalApi;
group: Group;
path: string;
baseUrl: string;
}
export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<HTMLDivElement>): ReactElement => {
const {
association,
node,
@ -61,7 +66,7 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref): ReactEleme
const author = node.post.author;
const size = node.children ? node.children.size : 0;
const contents = node.post.contents;
const contents = node.post.contents as [TextContent, UrlContent];
const hostname = URLparser.exec(contents[1].url) ? URLparser.exec(contents[1].url)[4] : null;
const href = URLparser.exec(contents[1].url) ? contents[1].url : `http://${contents[1].url}`;
@ -130,8 +135,8 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref): ReactEleme
<>
<RemoteContent
ref={(r) => {
remoteRef.current = r;
}}
remoteRef.current = r;
}}
renderUrl={false}
url={href}
text={contents[0].text}
@ -166,14 +171,14 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref): ReactEleme
</>
)}
</Box>
<Row minWidth='0' flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white">
<Row minWidth={0} flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white">
<Author
showImage
isRelativeTime
ship={author}
date={node.post['time-sent']}
group={group}
lineHeight="1"
lineHeight={1}
/>
<Box ml="auto">
<Link
@ -208,7 +213,7 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref): ReactEleme
</Col>
}
>
<Icon ml="2" display="block" icon="Ellipsis" color="gray" />
<Icon ml={2} display="block" icon="Ellipsis" color="gray" />
</Dropdown>
</Row>

View File

@ -1,5 +1,5 @@
import { Anchor, Box, Col, Icon, Row, Text } from '@tlon/indigo-react';
import { Association, Group, Post } from '@urbit/api';
import { Association, GraphNotificationContents, GraphNotifIndex, Post } from '@urbit/api';
import { BigInteger } from 'big-integer';
import _ from 'lodash';
import React, { useCallback } from 'react';
@ -15,9 +15,6 @@ import {
useAssocForGraph,
useAssocForGroup
} from '~/logic/state/metadata';
import {
GraphNotificationContents, GraphNotifIndex
} from '~/types';
import Author from '~/views/components/Author';
import { GraphContent } from '~/views/landscape/components/Graph/GraphContent';
import { PermalinkEmbed } from '../permalinks/embed';
@ -84,9 +81,9 @@ const GraphUrl = ({ contents, api }) => {
);
}
return (
<Box borderRadius="2" p="2" bg="scales.black05">
<Box borderRadius={2} p={2} bg="scales.black05">
<Anchor underline={false} target="_blank" color="black" href={link.url}>
<Icon verticalAlign="bottom" mr="2" icon="ArrowExternal" />
<Icon verticalAlign="bottom" mr={2} icon="ArrowExternal" />
{text}
</Anchor>
</Box>
@ -97,17 +94,17 @@ function ContentSummary({ icon, name, author, to }) {
return (
<Link to={to}>
<Col
gapY="1"
gapY={1}
flexDirection={['column', 'row']}
alignItems={['flex-start', 'center']}
>
<Row
alignItems="center"
gapX="2"
p="1"
gapX={2}
p={1}
width="fit-content"
borderRadius="2"
border="1"
borderRadius={2}
border={1}
borderColor="lightGray"
>
<Icon display="block" icon={icon} />
@ -116,7 +113,7 @@ function ContentSummary({ icon, name, author, to }) {
</Text>
</Row>
<Row ml={[0, 1]} alignItems="center">
<Text lineHeight="1" fontWeight="medium" mr="1">
<Text lineHeight={1} fontWeight="medium" mr={1}>
by
</Text>
<Author
@ -193,11 +190,7 @@ interface PostsByAuthor {
}
const GraphNodes = (props: {
posts: Post[];
graph: string;
hideAuthors?: boolean;
group?: Group;
groupPath: string;
description: string;
index: string;
mod: string;
association: Association;
@ -208,7 +201,6 @@ const GraphNodes = (props: {
mod,
hidden,
index,
description,
hideAuthors = false,
association
} = props;
@ -242,7 +234,7 @@ const GraphNodes = (props: {
date={time}
/>
)}
<Col gapY="2" py={hideAuthors ? 0 : 2} width="100%">
<Col gapY={2} py={hideAuthors ? 0 : 2} width="100%">
{_.map(posts, post => (
<GraphNodeContent
key={post.index}
@ -328,18 +320,17 @@ export function GraphNotification(props: {
groupTitle={groupTitle}
content
/>
<Col onClick={onClick} gapY="2" flexGrow={1} width="100%" gridArea="main">
<Col onClick={onClick} gapY={2} flexGrow={1} width="100%" gridArea="main">
<GraphNodes
hideAuthors={hideAuthors}
posts={contents.slice(0, 4)}
mod={index.module}
description={index.description}
index={contents?.[0].index}
association={association}
hidden={groups[association?.group]?.hidden}
/>
{contents.length > 4 && (
<Text mb="2" gray>
<Text mb={2} gray>
+ {contents.length - 4} more
</Text>
)}

View File

@ -4,6 +4,7 @@ import {
GroupNotifIndex,
GroupUpdate
} from '@urbit/api';
import bigInt from 'big-integer';
import _ from 'lodash';
import React, { ReactElement } from 'react';
import GlobalApi from '~/logic/api/global';
@ -37,7 +38,7 @@ interface GroupNotificationProps {
archived: boolean;
read: boolean;
time: number;
timebox: BigInteger;
timebox: bigInt.BigInteger;
api: GlobalApi;
}

View File

@ -32,7 +32,7 @@ export function Header(
return (
<Row
flexDirection={['column-reverse', 'row']}
minHeight="4"
minHeight={4}
mb={content ? 2 : 0}
onClick={props.onClick}
flexWrap="wrap"
@ -40,7 +40,7 @@ export function Header(
gridArea="header"
overflow="hidden"
>
<Row gapX="1" overflow="hidden" alignItems="center">
<Row gapX={1} overflow="hidden" alignItems="center">
{authors.length > 0 && (
<>
<Author
@ -58,15 +58,15 @@ export function Header(
</>
)}
<Box whiteSpace="nowrap" overflow="hidden" textOverflow="ellipsis">
<Text lineHeight="tall" mr="1">
<Text lineHeight="tall" mr={1}>
{description} {channelTitle}
</Text>
</Box>
</Row>
<Row ml={[0, 1]} mb={[1, 0]} gapX="1" alignItems="center">
<Row ml={[0, 1]} mb={[1, 0]} gapX={1} alignItems="center">
{groupTitle && (
<>
<Text lineHeight="tall" fontSize="1" gray>
<Text lineHeight="tall" fontSize={1} gray>
{groupTitle}
</Text>
<Dot color="gray" />
@ -75,7 +75,7 @@ export function Header(
{time && (
<Timestamp
lineHeight="tall"
fontSize="1"
fontSize={1}
relative
stamp={moment(time)}
color="gray"

View File

@ -1,10 +1,10 @@
import { Box, Center, Col, LoadingSpinner, Text } from '@tlon/indigo-react';
import { Box, Center, Col, LoadingSpinner, Text, Icon } from '@tlon/indigo-react';
import {
IndexedNotification,
IndexedNotification,
JoinRequests, Notifications,
JoinRequests, Notifications,
Timebox
Timebox
} from '@urbit/api';
import { BigInteger } from 'big-integer';
import _ from 'lodash';
@ -14,6 +14,7 @@ import React, { useCallback, useEffect, useRef } from 'react';
import GlobalApi from '~/logic/api/global';
import { getNotificationKey } from '~/logic/lib/hark';
import { useLazyScroll } from '~/logic/lib/useLazyScroll';
import useLaunchState from '~/logic/state/launch';
import { daToUnix, MOMENT_CALENDAR_DATE } from '~/logic/lib/util';
import useHarkState from '~/logic/state/hark';
import { Invites } from './invites';
@ -57,6 +58,8 @@ export default function Inbox(props: {
};
}, []);
const runtimeLag = useLaunchState(state => state.runtimeLag);
const ready = useHarkState(
s => Object.keys(s.unreads.graph).length > 0
);
@ -113,7 +116,15 @@ export default function Inbox(props: {
);
return (
<Col p="1" ref={scrollRef} position="relative" height="100%" overflowY="auto" overflowX="hidden">
<Col p={1} ref={scrollRef} position="relative" height="100%" overflowY="auto" overflowX="hidden">
{runtimeLag && (
<Box bg="yellow" borderRadius={2} p={2} m={2}>
<Icon verticalAlign="middle" mr={2} icon="Tutorial" />
<Text verticalAlign="middle">
Update your binary to continue receiving updates.
</Text>
</Box>
)}
<Invites pendingJoin={props.pendingJoin} api={api} />
{[...notificationsByDayMap.keys()].sort().reverse().map((day, index) => {
const timeboxes = notificationsByDayMap.get(day)!;
@ -129,15 +140,15 @@ export default function Inbox(props: {
);
})}
{isDone ? (
<Center mt="2" borderTop={notifications.length !== 0 ? 1 : 0} borderTopColor="lightGray" width="100%" height="96px">
<Text gray fontSize="1">No more notifications</Text>
<Center mt={2} borderTop={notifications.length !== 0 ? 1 : 0} borderTopColor="lightGray" width="100%" height="96px">
<Text gray fontSize={1}>No more notifications</Text>
</Center>
) : isLoading ? (
<Center mt="2" borderTop={notifications.length !== 0 ? 1 : 0} borderTopColor="lightGray" width="100%" height="96px">
<Center mt={2} borderTop={notifications.length !== 0 ? 1 : 0} borderTopColor="lightGray" width="100%" height="96px">
<LoadingSpinner />
</Center>
) : (
<Box mt="2" height="96px" />
<Box mt={2} height="96px" />
)}
</Col>
@ -172,7 +183,7 @@ function DaySection({
{_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i: number) =>
_.map(nots.sort(sortIndexedNotification), (not, j: number) => (
<Notification
key={getNotificationKey(time, not)}
key={getNotificationKey(date, not)}
api={api}
notification={not}
archived={archive}

View File

@ -1,8 +1,8 @@
import { Box, Row, SegmentedProgressBar, Text } from '@tlon/indigo-react';
import {
joinError, joinProgress,
joinError, joinProgress,
JoinRequest
JoinRequest
} from '@urbit/api';
import React, { useCallback } from 'react';
import GlobalApi from '~/logic/api/global';
@ -34,8 +34,8 @@ export function JoiningStatus(props: JoiningStatusProps) {
<Row
display={['flex-column', 'flex']}
alignItems="center"
px="4"
gapX="4"
px={4}
gapX={4}
>
<Box flexGrow={1} maxWidth="400px">
<SegmentedProgressBar current={current + 1} segments={3} />

View File

@ -1,13 +1,9 @@
import { Box } from '@tlon/indigo-react';
import { Metadata } from '@urbit/api';
import React from 'react';
import { Header } from './header';
import { MetadataBody, NotificationProps } from './types';
function getInvolvedUsers(body: MetadataBody) {
return [];
}
function getDescription(body: MetadataBody) {
function getDescription(body: { metadata: Metadata}) {
const b = body.metadata;
if ('new' in b) {
return 'created';
@ -24,12 +20,12 @@ function getDescription(body: MetadataBody) {
}
}
export function MetadataNotification(props: NotificationProps<'metadata'>) {
export function MetadataNotification(props: any) {
const { unread } = props;
const description = getDescription(unread.unreads[0].body);
return (
<Box p="2">
<Box p={2}>
<Header
authors={[]}
description={description}

View File

@ -22,7 +22,7 @@ import { SwipeMenu } from '~/views/components/SwipeMenu';
import { GraphNotification } from './graph';
import { GroupNotification } from './group';
interface NotificationProps {
export interface NotificationProps {
notification: IndexedNotification;
time: BigInteger;
api: GlobalApi;
@ -40,7 +40,7 @@ function getMuted(
if (!('graph' in notification.contents)) {
throw new Error();
}
const parent = getParentIndex(index.graph, notification.contents.graph);
const parent = getParentIndex(idxNotif.index.graph, notification.contents.graph);
return (
_.findIndex(
@ -102,7 +102,7 @@ export function NotificationWrapper(props: {
menuWidth={100}
disabled={!isMobile}
menu={
<Button onClick={onArchive} ml="2" height="100%" width="92px" primary destructive>
<Button onClick={onArchive} ml={2} height="100%" width="92px" primary destructive>
Remove
</Button>
}
@ -125,7 +125,7 @@ export function NotificationWrapper(props: {
{children}
<Row
alignItems="flex-start"
gapX="2"
gapX={2}
gridArea="actions"
justifyContent="flex-end"
opacity={[0, hovering ? 1 : 0]}
@ -147,7 +147,7 @@ export function NotificationWrapper(props: {
}
export function Notification(props: NotificationProps) {
const { notification, associations, archived } = props;
const { notification, archived } = props;
const { read, contents, time } = notification.notification;
const wrapperProps = {

View File

@ -23,7 +23,7 @@ const HeaderLink = React.forwardRef((
return (
<Link to={to}>
<Text ref={ref} px="2" {...textProps} gray={!active} />
<Text ref={ref} px={2} {...textProps} gray={!active} />
</Link>
);
});
@ -67,21 +67,21 @@ export default function NotificationsScreen(props: any): ReactElement {
<Body>
<Col overflowY="hidden" height="100%">
<Row
p="3"
p={3}
alignItems="center"
height="48px"
justifyContent="space-between"
width="100%"
borderBottom="1"
borderBottom={1}
borderBottomColor="lightGray"
>
<Text fontWeight="bold" fontSize="2" lineHeight="1" ref={anchorRef}>
<Text fontWeight="bold" fontSize={2} lineHeight={1} ref={anchorRef}>
Notifications
</Text>
<Row
justifyContent="space-between"
gapX="3"
gapX={3}
>
<StatelessAsyncAction
overflow="hidden"
@ -93,7 +93,7 @@ export default function NotificationsScreen(props: any): ReactElement {
</StatelessAsyncAction>
<Link to="/~settings#notifications">
<Box>
<Icon lineHeight="1" icon="Adjust" />
<Icon lineHeight={1} icon="Adjust" />
</Box>
</Link>
</Row>

View File

@ -1,5 +1,5 @@
import { Anchor, Box, Col, Icon, Row, Text } from '@tlon/indigo-react';
import { Association, GraphNode, Group, Post } from '@urbit/api';
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';
@ -38,7 +38,7 @@ function TranscludedLinkNode(props: {
switch (idx.length) {
case 1:
const [{ text }, link] = node.post.contents;
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} />;
@ -76,6 +76,7 @@ function TranscludedLinkNode(props: {
</Box>
</Box>
);
case 2:
return (
<TranscludedComment
@ -170,7 +171,7 @@ function TranscludedPublishNode(props: {
?.get(bigInt.one)
?.children?.peekLargest()?.[1]!;
return (
<Col color="black" gapY="2">
<Col color="black" gapY={2}>
<Author
pl='12px'
pt='12px'
@ -182,7 +183,7 @@ function TranscludedPublishNode(props: {
group={group}
/>
<Text pl='44px' fontSize="2" fontWeight="medium">
{post.post.contents[0]?.text}
{(post.post.contents[0] as TextContent)?.text}
</Text>
<Box pl="44px" pr='3'>
<NotePreviewContent
@ -267,7 +268,7 @@ export function TranscludedNode(props: {
api: GlobalApi;
showOurContact?: boolean;
}) {
const { node, showOurContact, assoc, transcluded } = props;
const { node, showOurContact, assoc, transcluded, api } = props;
const group = useGroupForAssoc(assoc)!;
if (
@ -287,23 +288,19 @@ export function TranscludedNode(props: {
);
}
switch (assoc.metadata.config.graph) {
switch ((assoc.metadata.config as GraphConfig).graph) {
case 'chat':
return (
<Row width="100%" flexShrink={0} flexGrow={1} flexWrap="wrap">
<ChatMessage
width="100%"
renderSigil
transcluded={transcluded + 1}
containerClass="items-top cf hide-child"
className="items-top cf hide-child"
association={assoc}
group={group}
groups={{}}
msg={node.post}
fontSize="0"
ml="0"
mr="0"
fontSize={0}
showOurContact={showOurContact}
api={api}
mt='0'
/>
</Row>

View File

@ -1,4 +1,4 @@
import { Association } from '@urbit/api';
import { Association, GraphConfig } from '@urbit/api';
import React, { useCallback } from 'react';
import {
Redirect, Route, Switch
@ -82,7 +82,7 @@ function GroupRoutes(props: { group: string; url: string }) {
return <Redirect
to={toQuery(
{ auto: 'y', redir: location.pathname },
`${groupUrl}/join/${association.metadata.config.graph}${path}`
`${groupUrl}/join/${(association.metadata.config as GraphConfig).graph}${path}`
)}
/>;
}

View File

@ -1,27 +1,25 @@
import React, { useCallback, useEffect, useState } from "react";
import { useHistory, useLocation, Link } from 'react-router-dom';
import {
BaseAnchor, Box,
Center, Col, Icon, Row, Text
} from "@tlon/indigo-react";
import { Association, GraphNode, resourceFromPath } from '@urbit/api';
import React, { useCallback, useEffect, useState } from "react";
import { useHistory, useLocation } from 'react-router-dom';
import GlobalApi from '~/logic/api/global';
import {
getPermalinkForGraph, GraphPermalink as IGraphPermalink, parsePermalink
} from '~/logic/lib/permalinks';
import {
Action,
Box,
Text,
BaseAnchor,
Row,
Icon,
Col,
Center
} from "@tlon/indigo-react";
import { GroupLink } from "~/views/components/GroupLink";
import { getModuleIcon } from "~/logic/lib/util";
import useMetadataState from "~/logic/state/metadata";
import { useVirtualResizeProp } from "~/logic/lib/virtualContext";
import useGraphState from "~/logic/state/graph";
import { GraphNodeContent } from "../notifications/graph";
import useMetadataState from "~/logic/state/metadata";
import { GroupLink } from "~/views/components/GroupLink";
import { TranscludedNode } from "./TranscludedNode";
import {useVirtualResizeProp} from "~/logic/lib/virtualContext";
function GroupPermalink(props: { group: string; api: GlobalApi }) {
const { group, api } = props;
@ -29,9 +27,9 @@ function GroupPermalink(props: { group: string; api: GlobalApi }) {
<GroupLink
resource={group}
api={api}
pl="2"
border="1"
borderRadius="2"
pl={2}
border={1}
borderRadius={2}
borderColor="lightGray"
/>
);
@ -112,7 +110,7 @@ function GraphPermalink(
maxWidth={full ? null : "500px"}
border={full ? null : "1"}
borderColor="lightGray"
borderRadius="2"
borderRadius={2}
cursor="pointer"
onClick={(e) => {
navigate(e);
@ -140,7 +138,7 @@ function GraphPermalink(
<PermalinkDetails
known
showTransclusion={showTransclusion}
icon={getModuleIcon(association.metadata.config.graph)}
icon={getModuleIcon((association.metadata.config as GraphConfig).graph as GraphModule)}
title={association.metadata.title}
permalink={permalink}
/>

View File

@ -1,4 +1,4 @@
import { Association, Group } from '@urbit/api';
import { Association, GraphConfig, Group } from '@urbit/api';
import _ from 'lodash';
import React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
@ -8,7 +8,7 @@ export function getGraphPermalink(
group: Group,
index: string
) {
const mod = assoc.metadata.config.graph;
const mod = (assoc.metadata.config as GraphConfig).graph;
const groupPath = group.hidden
? '/~landscape/home'
: `/~landscape${assoc.group}`;

View File

@ -1,8 +1,8 @@
import {
Button, Col, ManagedCheckboxField as Checkbox, ManagedForm as Form,
ManagedTextInputField as Input,
Button, Col, ManagedCheckboxField as Checkbox, ManagedForm as Form,
ManagedTextInputField as Input,
Row, Text
Row, Text
} from '@tlon/indigo-react';
import { Formik } from 'formik';
import _ from 'lodash';
@ -18,9 +18,9 @@ import { ColorInput } from '~/views/components/ColorInput';
import GroupSearch from '~/views/components/GroupSearch';
import { ImageInput } from '~/views/components/ImageInput';
import {
ProfileControls, ProfileHeader,
ProfileControls, ProfileHeader,
ProfileImages, ProfileStatus
ProfileImages, ProfileStatus
} from './Profile';
const formSchema = Yup.object({
@ -61,7 +61,7 @@ export function ProfileHeaderImageEdit(props: any): ReactElement {
<ImageInput id='cover' marginTop='-8px' width='288px' />
) : (
<Row>
<Button mr='2' onClick={() => setEditCover(true)}>
<Button mr={2} onClick={() => setEditCover(true)}>
Replace Header
</Button>
<Button onClick={e => handleClear(e)}>
@ -90,11 +90,12 @@ export function EditProfile(props: any): ReactElement {
const onSubmit = async (values: any, actions: any) => {
try {
await Object.keys(values).reduce((acc, key) => {
Object.keys(values).forEach((key) => {
const newValue = key !== 'color' ? values[key] : uxToHex(values[key]);
if (newValue !== contact[key]) {
if (key === 'isPublic') {
return acc.then(() => api.contacts.setPublic(newValue));
api.contacts.setPublic(newValue)
return;
} else if (key === 'groups') {
const toRemove: string[] = _.difference(
contact?.groups || [],
@ -104,24 +105,18 @@ export function EditProfile(props: any): ReactElement {
newValue,
contact?.groups || []
);
const promises: Promise<any>[] = [];
promises.concat(
toRemove.map(e =>
toRemove.forEach(e =>
api.contacts.edit(ship, { 'remove-group': resourceFromPath(e) })
)
);
promises.concat(
toAdd.map(e =>
)
toAdd.forEach(e =>
api.contacts.edit(ship, { 'add-group': resourceFromPath(e) })
)
);
return acc.then(() => Promise.all(promises));
)
} else if (key !== 'last-updated' && key !== 'isPublic') {
return acc.then(() => api.contacts.edit(ship, { [key]: newValue }));
api.contacts.edit(ship, { [key]: newValue });
return;
}
}
return acc;
}, Promise.resolve());
});
// actions.setStatus({ success: null });
history.push(`/~profile/${ship}`);
} catch (e) {
@ -148,16 +143,16 @@ export function EditProfile(props: any): ReactElement {
cursor='pointer'
fontWeight='500'
color='blue'
pl='0'
pr='0'
border='0'
pl={0}
pr={0}
border={0}
style={{ appearance: 'none', background: 'transparent' }}
>
Save Edits
</Button>
<Text
py='2'
ml='3'
py={2}
ml={3}
fontWeight='500'
cursor='pointer'
onClick={() => {

View File

@ -16,7 +16,7 @@ export function ProfileHeader(props: any): ReactElement {
<Box
border='1px solid'
borderColor='lightGray'
borderRadius='3'
borderRadius={3}
overflow='hidden'
marginBottom='calc(64px + 2rem)'
>
@ -27,7 +27,7 @@ export function ProfileHeader(props: any): ReactElement {
export function ProfileImages(props: any): ReactElement {
const { hideAvatars } = useSettingsState(selectCalmState);
const { contact, hideCover, ship } = { ...props };
const { contact, hideCover, ship } = props;
const hexColor = contact?.color ? `#${uxToHex(contact.color)}` : '#000000';
const anchorRef = useRef<HTMLDivElement>(null);
@ -76,7 +76,7 @@ export function ProfileImages(props: any): ReactElement {
<Box
height='128px'
width='128px'
borderRadius='3'
borderRadius={3}
overflow='hidden'
position='absolute'
left='50%'
@ -91,18 +91,18 @@ export function ProfileImages(props: any): ReactElement {
export function ProfileControls(props: any): ReactElement {
return (
<Row alignItems='center' justifyContent='space-between' px='3'>
<Row alignItems='center' justifyContent='space-between' px={3}>
{props.children}
</Row>
);
}
export function ProfileStatus(props: any): ReactElement {
const { contact } = { ...props };
const { contact } = props;
return (
<RichText
mb='0'
py='2'
mb={0}
py={2}
disableRemoteContent
maxWidth='18rem'
overflowX='hidden'
@ -120,14 +120,14 @@ export function ProfileStatus(props: any): ReactElement {
}
export function ProfileActions(props: any): ReactElement {
const { ship, isPublic, contact, api } = { ...props };
const { ship, isPublic, contact, api } = props;
const history = useHistory();
return (
<Row>
{ship === `~${window.ship}` ? (
<>
<Text
py='2'
py={2}
cursor='pointer'
fontWeight='500'
onClick={() => {
@ -145,8 +145,8 @@ export function ProfileActions(props: any): ReactElement {
</Text>
<SetStatusBarModal
isControl
py='2'
ml='3'
py={2}
ml={3}
api={api}
ship={`~${window.ship}`}
contact={contact}
@ -155,7 +155,7 @@ export function ProfileActions(props: any): ReactElement {
) : (
<>
<Text
py='2'
py={2}
cursor='pointer'
fontWeight='500'
onClick={() => history.push(`/~landscape/dm/${ship.substring(1)}`)}

View File

@ -45,15 +45,15 @@ export function ViewProfile(props: any): ReactElement {
</Text>
</Center>
</Row>
<Col pb={2} mt='3' alignItems='center' justifyContent='center' width='100%'>
<Col pb={2} mt={3} alignItems='center' justifyContent='center' width='100%'>
<Center flexDirection='column' maxWidth='32rem'>
<RichText width='100%' disableRemoteContent>
<RichText api={props.api} width='100%' disableRemoteContent>
{contact?.bio ? contact.bio : ''}
</RichText>
</Center>
</Col>
{(contact?.groups || []).length > 0 && (
<Col gapY='3' mb='3' mt='6' alignItems='flex-start'>
<Col gapY={3} mb={3} mt={6} alignItems='flex-start'>
<Text gray>Pinned Groups</Text>
<Col>
{contact?.groups.slice().sort(lengthOrder).map(g => (

View File

@ -36,7 +36,7 @@ export default function ProfileScreen(props: any) {
border={1}
borderColor='lightGray'
overflowY='auto'
flexGrow
flexGrow={1}
>
<Box>
<Profile

View File

@ -1,17 +1,18 @@
import { GraphNode } from '@urbit/api';
import bigInt from 'big-integer';
import { FormikHelpers } from 'formik';
import _ from 'lodash';
import React, { ReactElement } from 'react';
import { RouteComponentProps, useLocation } from 'react-router-dom';
import GlobalApi from '~/logic/api/global';
import { referenceToPermalink } from '~/logic/lib/permalinks';
import { editPost, getLatestRevision } from '~/logic/lib/publish';
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
import { referenceToPermalink } from '~/logic/lib/permalinks';
import { PostForm, PostFormSchema } from './NoteForm';
interface EditPostProps {
ship: string;
noteId: number;
noteId: bigInt.BigInteger;
note: GraphNode;
api: GlobalApi;
book: string;

View File

@ -93,7 +93,6 @@ export function MarkdownEditor(
return (
<Box
height="100%"
position="relative"
className="publish"
p={1}
@ -111,10 +110,10 @@ export function MarkdownEditor(
value={value}
options={options}
onChange={handleChange}
onDragLeave={(editor, e) => bind.onDragLeave(e)}
onDragOver={(editor, e) => bind.onDragOver(e)}
onDrop={(editor, e) => bind.onDrop(e)}
onDragEnter={(editor, e) => bind.onDragEnter(e)}
onDragLeave={(editor, e: DragEvent) => bind.onDragLeave(e)}
onDragOver={(editor, e: DragEvent) => bind.onDragOver(e)}
onDrop={(editor, e: DragEvent) => bind.onDrop(e)}
onDragEnter={(editor, e: DragEvent) => bind.onDragEnter(e)}
/>
{dragging && <SubmitDragger />}
</Box>

View File

@ -36,7 +36,7 @@ export const MarkdownField = ({
value={value}
onChange={setValue}
/>
<ErrorLabel mt="2" hasError={Boolean(error && touched)}>
<ErrorLabel mt={2} hasError={Boolean(error && touched)}>
{error}
</ErrorLabel>
</Box>

View File

@ -1,18 +1,18 @@
import React, { useState, useEffect } from 'react';
import { Box, Text, Col, Anchor, Row, Action } from '@tlon/indigo-react';
import { Action, Anchor, Box, Col, Row, Text } from '@tlon/indigo-react';
import { Association, Graph, GraphNode, Group } from '@urbit/api';
import bigInt from 'big-integer';
import React, { useEffect, useState } from 'react';
import { Link, RouteComponentProps } from 'react-router-dom';
import GlobalApi from '~/logic/api/global';
import { roleForShip } from '~/logic/lib/group';
import { Contacts, GraphNode, Graph, Association, Unreads, Group, Post } from '@urbit/api';
import { getPermalinkForGraph } from '~/logic/lib/permalinks';
import { GraphContent } from '~/views/landscape/components/Graph/GraphContent';
import { getComments, getLatestRevision } from '~/logic/lib/publish';
import { useCopy } from '~/logic/lib/useCopy';
import { useQuery } from '~/logic/lib/useQuery';
import Author from '~/views/components/Author';
import { Comments } from '~/views/components/Comments';
import { Spinner } from '~/views/components/Spinner';
import { GraphContent } from '~/views/landscape/components/Graph/GraphContent';
import { NoteNavigation } from './NoteNavigation';
import { Redirect } from 'react-router-dom';
@ -117,7 +117,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
date={post?.['time-sent']}
group={group}
>
<Row px="2" gapX="2" alignItems="flex-end" height="14px">
<Row px={2} gapX={2} alignItems="flex-end" height="14px">
<Action bg="white" onClick={doCopy}>{copyDisplay}</Action>
{adminLinks}
</Row>
@ -128,8 +128,7 @@ export function Note(props: NoteProps & RouteComponentProps) {
<NoteNavigation
notebook={notebook}
noteId={noteId}
ship={props.ship}
book={props.book}
baseUrl={baseUrl}
/>
<Comments
ship={ship}

View File

@ -29,7 +29,7 @@ function NavigationItem(props: {
<Timestamp
stamp={moment(props.date)}
time={false}
fontSize="1"
fontSize={1}
justifyContent={props.prev ? 'flex-start' : 'flex-end'}
/>
</Link>
@ -48,12 +48,12 @@ function getAdjacentId(
return target?.[0] || null;
}
function makeNoteUrl(noteId: number) {
function makeNoteUrl(noteId: BigInteger) {
return noteId.toString();
}
interface NoteNavigationProps {
noteId: number;
noteId: BigInteger;
notebook: Graph;
baseUrl: string;
}

View File

@ -6,9 +6,9 @@ import ReactMarkdown from 'react-markdown';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import {
getComments,
getLatestRevision,
getSnippet
getComments,
getLatestRevision,
getSnippet
} from '~/logic/lib/publish';
import useHarkState from '~/logic/state/hark';
import Author from '~/views/components/Author';
@ -37,7 +37,7 @@ export function NotePreviewContent({ snippet }) {
style={{ backgroundSize: 'cover',
backgroundPosition: 'center' }}
>
<Image src={props.src} opacity="0" maxHeight="300px" />
<Image src={props.src} opacity={0} maxHeight="300px" />
</Box>
),
paragraph: props => (
@ -91,7 +91,7 @@ export function NotePreview(props: NotePreviewProps) {
borderRadius={2}
alignItems='flex-start'
overflow='hidden'
p='2'
p={2}
>
<WrappedBox mb={2}><Text bold>{title}</Text></WrappedBox>
<WrappedBox>
@ -101,7 +101,7 @@ export function NotePreview(props: NotePreviewProps) {
</WrappedBox>
</Col>
</Link>
<Row minWidth='0' flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white">
<Row minWidth={0} flexShrink={0} width="100%" justifyContent="space-between" py={3} bg="white">
<Author
showImage
ship={post?.author}

View File

@ -1,4 +1,5 @@
import { Association, Graph, GraphNode, Group } from '@urbit/api';
import bigInt from 'big-integer';
import React from 'react';
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
import GlobalApi from '~/logic/api/global';
@ -9,7 +10,7 @@ interface NoteRoutesProps {
ship: string;
book: string;
note: GraphNode;
noteId: number;
noteId: bigInt.BigInteger;
notebook: Graph;
api: GlobalApi;
association: Association;

View File

@ -40,7 +40,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps): ReactEleme
}
return (
<Col gapY="4" pt={4} mx="auto" px={3} maxWidth="768px">
<Col gapY={4} pt={4} mx="auto" px={3} maxWidth="768px">
<Row justifyContent="space-between">
<Box>
<Text display='block'>{association.metadata?.title}</Text>
@ -50,7 +50,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps): ReactEleme
</Text>
</Box>
</Row>
<Box borderBottom="1" borderBottomColor="lightGray" />
<Box borderBottom={1} borderBottomColor="lightGray" />
<NotebookPosts
graph={graph}
host={ship}

View File

@ -38,7 +38,7 @@ export const Writers = (props: WritersProps): ReactElement => {
return (
<Box maxWidth='512px'>
<Text display='block'>Writers</Text>
<Text display='block' mt='2' gray>Add additional writers to this notebook</Text>
<Text display='block' mt={2} gray>Add additional writers to this notebook</Text>
<Formik
initialValues={{ ships: [] }}
onSubmit={onSubmit}
@ -49,16 +49,16 @@ export const Writers = (props: WritersProps): ReactElement => {
label=""
maxLength={undefined}
/>
<AsyncButton width='100%' mt='3' primary>
<AsyncButton width='100%' mt={3} primary>
Submit
</AsyncButton>
</Form>
</Formik>
{writers.length > 0 ? <>
<Text display='block' mt='2'>Current writers:</Text>
<Text mt='2' display='block' mono>{writers}</Text>
<Text display='block' mt={2}>Current writers:</Text>
<Text mt={2} display='block' mono>{writers}</Text>
</> :
<Text display='block' mt='2'>
<Text display='block' mt={2}>
All group members can write to this channel
</Text>
}

View File

@ -7,7 +7,7 @@ export function BackButton(props: {}) {
<Link to='/~settings'>
<Text
display={['block', 'none']}
fontSize='2'
fontSize={2}
fontWeight='medium'
p={4}
pb={0}

View File

@ -3,6 +3,7 @@ import {
ManagedRadioButtonField as Radio, Row, Text
} from '@tlon/indigo-react';
import {useField} from 'formik';
import React, { ReactElement } from 'react';
import GlobalApi from '~/logic/api/global';
import { ColorInput } from '~/views/components/ColorInput';
@ -10,11 +11,7 @@ import { ImageInput } from '~/views/components/ImageInput';
export type BgType = 'none' | 'url' | 'color';
export function BackgroundPicker({
bgType,
bgUrl,
api
}: {
export function BackgroundPicker({ api }: {
bgType: BgType;
bgUrl?: string;
api: GlobalApi;
@ -32,27 +29,26 @@ export function BackgroundPicker({
<Label>Landscape Background</Label>
<Row flexWrap="wrap" {...rowSpace}>
<Col {...colProps}>
<Radio mb="1" name="bgType" label="Image" id="url" />
<Text ml="5" gray>Set an image background</Text>
<Radio mb={1} name="bgType" label="Image" id="url" />
<Text ml={5} gray>Set an image background</Text>
<ImageInput
ml="5"
ml={5}
api={api}
id="bgUrl"
placeholder="Drop or upload a file, or paste a link here"
name="bgUrl"
url={bgUrl || ''}
/>
</Col>
</Row>
<Row {...rowSpace}>
<Col {...colProps}>
<Radio mb="1" label="Color" id="color" name="bgType" />
<Text ml="5" gray>Set a hex-based background</Text>
<ColorInput placeholder="FFFFFF" ml="5" id="bgColor" />
<Radio mb={1} label="Color" id="color" name="bgType" />
<Text ml={5} gray>Set a hex-based background</Text>
<ColorInput placeholder="FFFFFF" ml={5} id="bgColor" />
</Col>
</Row>
<Radio
my="3"
my={3}
caption="Your home screen will simply render as its respective day/night mode color"
name="bgType"
label="None"

View File

@ -1,13 +1,13 @@
import {
Box,
Button, ManagedForm as Form, ManagedTextInputField as Input,
Box,
Button, ManagedForm as Form, ManagedTextInputField as Input,
Menu,
MenuButton,
Menu,
MenuButton,
MenuItem, MenuList,
MenuItem, MenuList,
Row, Text
Row, Text
} from '@tlon/indigo-react';
import { Formik, FormikHelpers } from 'formik';
import React, { ReactElement, useCallback, useState } from 'react';
@ -95,12 +95,12 @@ export function BucketList({
{adding && (
<Input
placeholder="Enter your new bucket"
mt="2"
mt={2}
label="New Bucket"
id="newBucket"
/>
)}
<Row gapX="3" mt="3">
<Row gapX={3} mt={3}>
<Button type="button" onClick={() => setAdding(false)}>
Cancel
</Button>

View File

@ -1,14 +1,16 @@
import {
Col, ManagedToggleSwitchField as Toggle,
Col, ManagedToggleSwitchField as Toggle,
Text
Text
} from '@tlon/indigo-react';
import { Form, Formik, FormikHelpers } from 'formik';
import React, { useCallback } from 'react';
import GlobalApi from '~/logic/api/global';
import useSettingsState, { selectSettingsState } from '~/logic/state/settings';
import useSettingsState, { selectSettingsState, SettingsState } from '~/logic/state/settings';
import { AsyncButton } from '~/views/components/AsyncButton';
import { BackButton } from './BackButton';
import _ from 'lodash';
import {FormikOnBlur} from '~/views/components/FormikOnBlur';
interface FormSchema {
hideAvatars: boolean;
@ -22,61 +24,43 @@ interface FormSchema {
videoShown: boolean;
}
const settingsSel = selectSettingsState(['calm', 'remoteContentPolicy']);
const settingsSel = (s: SettingsState): FormSchema => ({
hideAvatars: s.calm.hideAvatars,
hideNicknames: s.calm.hideAvatars,
hideUnreads: s.calm.hideUnreads,
hideGroups: s.calm.hideGroups,
hideUtilities: s.calm.hideUtilities,
imageShown: !s.remoteContentPolicy.imageShown,
videoShown: !s.remoteContentPolicy.videoShown,
oembedShown: !s.remoteContentPolicy.oembedShown,
audioShown: !s.remoteContentPolicy.audioShown
});
export function CalmPrefs(props: {
api: GlobalApi;
}) {
const { api } = props;
const {
calm: {
hideAvatars,
hideNicknames,
hideUnreads,
hideGroups,
hideUtilities
},
remoteContentPolicy: {
imageShown,
videoShown,
oembedShown,
audioShown
}
} = useSettingsState(settingsSel);
const initialValues: FormSchema = {
hideAvatars,
hideNicknames,
hideUnreads,
hideGroups,
hideUtilities,
imageShown: !imageShown,
videoShown: !videoShown,
oembedShown: !oembedShown,
audioShown: !audioShown
};
const initialValues = useSettingsState(settingsSel);
const onSubmit = useCallback(async (v: FormSchema, actions: FormikHelpers<FormSchema>) => {
await Promise.all([
api.settings.putEntry('calm', 'hideAvatars', v.hideAvatars),
api.settings.putEntry('calm', 'hideNicknames', v.hideNicknames),
api.settings.putEntry('calm', 'hideUnreads', v.hideUnreads),
api.settings.putEntry('calm', 'hideGroups', v.hideGroups),
api.settings.putEntry('calm', 'hideUtilities', v.hideUtilities),
api.settings.putEntry('remoteContentPolicy', 'imageShown', !v.imageShown),
api.settings.putEntry('remoteContentPolicy', 'videoShown', !v.videoShown),
api.settings.putEntry('remoteContentPolicy', 'audioShown', !v.audioShown),
api.settings.putEntry('remoteContentPolicy', 'oembedShown', !v.oembedShown)
]);
let promises: Promise<any>[] = [];
_.forEach(v, (bool, key) => {
const bucket = ['imageShown', 'videoShown', 'audioShown', 'oembedShown'].includes(key) ? 'remoteContentPolicy' : 'calm';
if(initialValues[key] !== bool) {
promises.push(api.settings.putEntry(bucket, key, bool));
}
})
await Promise.all(promises);
actions.setStatus({ success: null });
}, [api]);
return (
<Formik initialValues={initialValues} onSubmit={onSubmit}>
<FormikOnBlur initialValues={initialValues} onSubmit={onSubmit}>
<Form>
<BackButton />
<Col borderBottom="1" borderBottomColor="washedGray" p="5" pt="4" gapY="5">
<Col gapY="1" mt="0">
<Col borderBottom={1} borderBottomColor="washedGray" p={5} pt={4} gapY={5}>
<Col gapY={1} mt={0}>
<Text color="black" fontSize={2} fontWeight="medium">
CalmEngine
</Text>
@ -132,12 +116,8 @@ export function CalmPrefs(props: {
id="oembedShown"
caption="Embedded content may contain scripts that can track you"
/>
<AsyncButton primary width="fit-content" type="submit">
Save
</AsyncButton>
</Col>
</Form>
</Formik>
</FormikOnBlur>
);
}

View File

@ -52,7 +52,7 @@ const StoreDebugger = (props: StoreDebuggerProps) => {
backgroundColor='white'
color='black'
border='1px solid transparent'
borderRadius='2'
borderRadius={2}
fontSize={1}
placeholder="Drill Down"
width="100%"
@ -65,7 +65,7 @@ const StoreDebugger = (props: StoreDebuggerProps) => {
}
}}
/>
<Text mono p='1' borderRadius='1' display='block' overflow='auto' backgroundColor='washedGray' style={{ whiteSpace: 'pre', wordWrap: 'break-word' }}>{text}</Text>
<Text mono p={1} borderRadius={1} display='block' overflow='auto' backgroundColor='washedGray' style={{ whiteSpace: 'pre', wordWrap: 'break-word' }}>{text}</Text>
</Box>}
</Box>
);
@ -75,8 +75,8 @@ const DebugPane = () => {
return (
<>
<BackButton />
<Col borderBottom="1" borderBottomColor="washedGray" p="5" pt="4" gapY="5">
<Col gapY="1" mt="0">
<Col borderBottom={1} borderBottomColor="washedGray" p={5} pt={4} gapY={5}>
<Col gapY={1} mt={0}>
<Text color="black" fontSize={2} fontWeight="medium">
Debug Menu
</Text>

View File

@ -1,8 +1,8 @@
import {
Col,
Col,
Label,
ManagedRadioButtonField as Radio, Text
Label,
ManagedRadioButtonField as Radio, Text
} from '@tlon/indigo-react';
import { Form, Formik } from 'formik';
import React from 'react';
@ -11,6 +11,7 @@ import GlobalApi from '~/logic/api/global';
import { uxToHex } from '~/logic/lib/util';
import useSettingsState, { selectSettingsState } from '~/logic/state/settings';
import { AsyncButton } from '~/views/components/AsyncButton';
import {FormikOnBlur} from '~/views/components/FormikOnBlur';
import { BackButton } from './BackButton';
import { BackgroundPicker, BgType } from './BackgroundPicker';
@ -58,7 +59,7 @@ export default function DisplayForm(props: DisplayFormProps) {
const bgType = backgroundType || 'none';
return (
<Formik
<FormikOnBlur
validationSchema={formSchema}
initialValues={
{
@ -86,11 +87,10 @@ export default function DisplayForm(props: DisplayFormProps) {
actions.setStatus({ success: null });
}}
>
{props => (
<Form>
<BackButton />
<Col p="5" pt="4" gapY="5">
<Col gapY="1" mt="0">
<Col p={5} pt={4} gapY={5}>
<Col gapY={1} mt={0}>
<Text color="black" fontSize={2} fontWeight="medium">
Display Preferences
</Text>
@ -98,10 +98,7 @@ export default function DisplayForm(props: DisplayFormProps) {
Customize visual interfaces across your Landscape
</Text>
</Col>
<BackgroundPicker
bgType={props.values.bgType}
bgUrl={props.values.bgUrl}
api={api}
<BackgroundPicker api={api}
/>
<Label>Theme</Label>
<Radio name="theme" id="light" label="Light" />
@ -112,7 +109,6 @@ export default function DisplayForm(props: DisplayFormProps) {
</AsyncButton>
</Col>
</Form>
)}
</Formik>
</FormikOnBlur>
);
}

View File

@ -3,14 +3,15 @@ import {
Center, Col, Icon,
StatelessToggleSwitchField, Text
ToggleSwitch, Text,
StatelessToggleSwitchField
} from '@tlon/indigo-react';
import { Association, resourceFromPath } from '@urbit/api';
import { Association, GraphConfig, resourceFromPath } from '@urbit/api';
import { useField } from 'formik';
import _ from 'lodash';
import React, { useEffect, useState } from 'react';
import { isWatching } from '~/logic/lib/hark';
import { getModuleIcon } from '~/logic/lib/util';
import { getModuleIcon, GraphModule } from '~/logic/lib/util';
import useGraphState from '~/logic/state/graph';
import useHarkState from '~/logic/state/hark';
import useMetadataState, { useGraphsForGroup } from '~/logic/state/metadata';
@ -20,7 +21,7 @@ export function GroupChannelPicker(props: {}) {
const associations = useMetadataState(s => s.associations);
return (
<Col gapY="3">
<Col gapY={3}>
{_.map(associations.groups, (assoc: Association, group: string) => (
<GroupWithChannels key={group} association={assoc} />
))}
@ -62,7 +63,7 @@ function GroupWithChannels(props: { association: Association }) {
display="grid"
gridTemplateColumns="24px 24px 1fr 24px 24px"
gridTemplateRows="auto"
gridGap="2"
gridGap={2}
gridTemplateAreas="'arrow icon title graphToggle groupToggle'"
>
{Object.keys(joinedGroupGraphs).length > 0 && (
@ -100,7 +101,7 @@ function Channel(props: { association: Association }) {
return isWatching(config, association.resource);
});
const [{ value }, meta, { setValue }] = useField(
const [{ value }, meta, { setValue, setTouched }] = useField(
`graph["${association.resource}"]`
);
@ -108,22 +109,24 @@ function Channel(props: { association: Association }) {
setValue(watching);
}, [watching]);
const onChange = () => {
const onClick = () => {
setValue(!value);
};
setTouched(true);
}
const icon = getModuleIcon(metadata.config?.graph);
const icon = getModuleIcon((metadata.config as GraphConfig)?.graph as GraphModule);
return (
<>
<Center gridColumn="2">
<Center gridColumn={2}>
<Icon icon={icon} />
</Center>
<Box gridColumn="3">
<Box gridColumn={3}>
<Text> {metadata.title}</Text>
</Box>
<Box gridColumn="4">
<StatelessToggleSwitchField selected={value} onChange={onChange} />
<Box gridColumn={4}>
<StatelessToggleSwitchField selected={value} onClick={onClick} />
</Box>
</>
);

View File

@ -1,7 +1,7 @@
import {
Col,
Col,
ManagedCheckboxField, Text
ManagedCheckboxField, Text
} from '@tlon/indigo-react';
import { Form, useField, useFormikContext } from 'formik';
import _ from 'lodash';
@ -9,8 +9,8 @@ import React from 'react';
import GlobalApi from '~/logic/api/global';
import useSettingsState, { selectSettingsState } from '~/logic/state/settings';
import {
LeapCategories,
leapCategories
LeapCategories,
leapCategories
} from '~/types';
import { FormikOnBlur } from '~/views/components/FormikOnBlur';
import { ShuffleFields } from '~/views/components/ShuffleFields';
@ -50,7 +50,6 @@ export function LeapSettings(props: { api: GlobalApi; }) {
const { leap, set: setSettingsState } = useSettingsState(settingsSel);
const categories = leap.categories as LeapCategories[];
const missing = _.difference(leapCategories, categories);
console.log(categories);
const initialValues = {
categories: [
@ -73,9 +72,9 @@ export function LeapSettings(props: { api: GlobalApi; }) {
return (
<>
<BackButton />
<Col p="5" pt="4" gapY="5">
<Col gapY="1" mt="0">
<Text fontSize="2" fontWeight="medium">
<Col p={5} pt={4} gapY={5}>
<Col gapY={1} mt={0}>
<Text fontSize={2} fontWeight="medium">
Leap
</Text>
<Text gray>
@ -84,7 +83,7 @@ export function LeapSettings(props: { api: GlobalApi; }) {
</Col>
<FormikOnBlur initialValues={initialValues} onSubmit={onSubmit}>
<Form>
<Col gapY="4">
<Col gapY={4}>
<Text fontWeight="medium">
Customize default Leap sections
</Text>

View File

@ -1,15 +1,20 @@
import {
Button,
Col,
ManagedToggleSwitchField as Toggle, Text
} from '@tlon/indigo-react';
import { Form, Formik, FormikHelpers } from 'formik';
import _ from 'lodash';
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import GlobalApi from '~/logic/api/global';
import { isWatching } from '~/logic/lib/hark';
import useHarkState from '~/logic/state/hark';
import { AsyncButton } from '~/views/components/AsyncButton';
import {FormikOnBlur} from '~/views/components/FormikOnBlur';
import { BackButton } from './BackButton';
import { GroupChannelPicker } from './GroupChannelPicker';
@ -69,12 +74,14 @@ export function NotificationPreferences(props: {
}
}, [api, graphConfig, dnd]);
const [notificationsAllowed, setNotificationsAllowed] = useState(Notification.permission !== 'default');
return (
<>
<BackButton />
<Col p="5" pt="4" gapY="5">
<Col gapY="1" mt="0">
<Text fontSize="2" fontWeight="medium">
<Col p={5} pt={4} gapY={5}>
<Col gapY={1} mt={0}>
<Text fontSize={2} fontWeight="medium">
Notification Preferences
</Text>
<Text gray>
@ -82,9 +89,17 @@ export function NotificationPreferences(props: {
messaging
</Text>
</Col>
<Formik initialValues={initialValues} onSubmit={onSubmit}>
<FormikOnBlur initialValues={initialValues} onSubmit={onSubmit}>
<Form>
<Col gapY="4">
{notificationsAllowed
? null
: <Button alignSelf='flex-start' onClick={() => {
Notification.requestPermission().then(() => {
setNotificationsAllowed(Notification.permission !== 'default');
});
}}>Allow Browser Notifications</Button>
}
<Toggle
label="Do not disturb"
id="dnd"
@ -100,7 +115,7 @@ export function NotificationPreferences(props: {
id="mentions"
caption="Notify me if someone mentions my @p in a channel I've joined"
/>
<Col gapY="3">
<Col gapY={3}>
<Text lineHeight="tall">
Activity
</Text>
@ -109,12 +124,9 @@ export function NotificationPreferences(props: {
</Text>
<GroupChannelPicker />
</Col>
<AsyncButton primary width="fit-content">
Save
</AsyncButton>
</Col>
</Form>
</Formik>
</FormikOnBlur>
</Col>
</>
);

View File

@ -1,7 +1,7 @@
import {
Anchor, Col, ManagedForm as Form, ManagedTextInputField as Input,
Anchor, Col, ManagedForm as Form, ManagedTextInputField as Input,
Text
Text
} from '@tlon/indigo-react';
import { Formik, FormikHelpers } from 'formik';
import React, { ReactElement, useCallback } from 'react';
@ -47,7 +47,7 @@ export default function S3Form(props: S3FormProps): ReactElement {
return (
<>
<BackButton />
<Col p='5' pt='4' borderBottom='1' borderBottomColor='washedGray'>
<Col p={5} pt={4} borderBottom={1} borderBottomColor='washedGray'>
<Formik
initialValues={
{
@ -61,8 +61,8 @@ export default function S3Form(props: S3FormProps): ReactElement {
onSubmit={onSubmit}
>
<Form>
<Col maxWidth='600px' gapY='5'>
<Col gapY='1' mt='0'>
<Col maxWidth='600px' gapY={5}>
<Col gapY={1} mt={0}>
<Text color='black' fontSize={2} fontWeight='medium'>
S3 Storage Setup
</Text>
@ -72,8 +72,8 @@ export default function S3Form(props: S3FormProps): ReactElement {
<Anchor
target='_blank'
style={{ textDecoration: 'none' }}
borderBottom='1'
ml='1'
borderBottom={1}
ml={1}
href='https://urbit.org/using/os/s3/'
>
Learn more
@ -94,8 +94,8 @@ export default function S3Form(props: S3FormProps): ReactElement {
</Form>
</Formik>
</Col>
<Col maxWidth='600px' p='5' gapY='4'>
<Col gapY='1'>
<Col maxWidth='600px' p={5} gapY={4}>
<Col gapY={1}>
<Text color='black' mb={4} fontSize={2} fontWeight='medium'>
S3 Buckets
</Text>

View File

@ -1,7 +1,7 @@
import {
Button,
Col,
StatelessCheckboxField, Text
Button,
Col,
StatelessCheckboxField, Text
} from '@tlon/indigo-react';
import React, { useState } from 'react';
import GlobalApi from '~/logic/api/global';
@ -16,8 +16,8 @@ export default function SecuritySettings({ api }: SecuritySettingsProps) {
return (
<>
<BackButton />
<Col gapY="5" p="5" pt="4">
<Col gapY="1" mt="0">
<Col gapY={5} p={5} pt={4}>
<Col gapY={1} mt={0}>
<Text fontSize={2} fontWeight="medium">
Security Preferences
</Text>
@ -25,17 +25,17 @@ export default function SecuritySettings({ api }: SecuritySettingsProps) {
Manage sessions, login credentials and Landscape access
</Text>
</Col>
<Col gapY="1">
<Col gapY={1}>
<Text color="black">
Log out of this session
</Text>
<Text mb="3" gray>
<Text mb={3} gray>
{allSessions
? 'You will be logged out of all browsers that have currently logged into your Urbit.'
: 'You will be logged out of your Urbit on this browser.'}
</Text>
<StatelessCheckboxField
mb="3"
mb={3}
selected={allSessions}
onChange={() => setAllSessions(s => !s)}
>

View File

@ -10,14 +10,14 @@ export function SettingsItem(props: {
const { to, title, description } = props;
return (
<Link to={`/~settings/${to}`}>
<Row alignItems="center" gapX="3">
<Row alignItems="center" gapX={3}>
<Box
borderRadius="2"
borderRadius={2}
backgroundColor="blue"
width="64px"
height="64px"
/>
<Col gapY="2">
<Col gapY={2}>
<Text>{title}</Text>
<Text gray>{description}</Text>
</Col>
@ -28,9 +28,9 @@ export function SettingsItem(props: {
export default function Settings(props: {}) {
return (
<Col gapY="5" p="5">
<Col gapY="1">
<Text fontSize="2">System Preferences</Text>
<Col gapY={5} p={5}>
<Col gapY={1}>
<Text fontSize={2}>System Preferences</Text>
<Text gray>Configure and customize Landscape</Text>
</Col>
<Box
@ -38,7 +38,7 @@ export default function Settings(props: {}) {
width="100%"
height="100%"
gridTemplateColumns={['100%', '1fr 1fr']}
gridGap="3"
gridGap={3}
>
<SettingsItem
to="notifications"

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