mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-11-28 11:40:11 +03:00
Merge pull request #5206 from urbit/lf/nu-hark-store
hark-store: revise for third party distro
This commit is contained in:
commit
5e5e0cb681
8839
package-lock.json
generated
8839
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -17,6 +17,6 @@
|
||||
"build:prod": "lerna run build:prod"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts,tsx}": "eslint --cache --fix"
|
||||
"*.{js,ts,tsx}": "eslint --cache --fix --quiet"
|
||||
}
|
||||
}
|
||||
|
@ -119,6 +119,16 @@
|
||||
^- card
|
||||
[%give %fact paths cage]
|
||||
::
|
||||
++ fact-all
|
||||
|= =cage
|
||||
^- (unit card)
|
||||
=/ paths=(set path)
|
||||
%- ~(gas in *(set path))
|
||||
%+ turn ~(tap by sup.bowl)
|
||||
|=([duct ship =path] path)
|
||||
?: =(~ paths) ~
|
||||
`(fact cage ~(tap in paths))
|
||||
::
|
||||
++ kick
|
||||
|= paths=(list path)
|
||||
[%give %kick paths ~]
|
||||
|
13693
pkg/btc-wallet/package-lock.json
generated
13693
pkg/btc-wallet/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
159
pkg/garden-dev/sur/hark-store.hoon
Normal file
159
pkg/garden-dev/sur/hark-store.hoon
Normal file
@ -0,0 +1,159 @@
|
||||
^?
|
||||
::
|
||||
:: %hark-store: Notification, unreads store
|
||||
::
|
||||
:: Timeboxing & binning:
|
||||
::
|
||||
:: Unread notifications accumulate in $unreads. They are grouped by
|
||||
:: their $bin. A notification may become read by either:
|
||||
:: a) being read by a %read-count or %read-each or %read-note
|
||||
:: b) being read by a %seen
|
||||
::
|
||||
:: If a) then we insert the corresponding bin into $reads at the
|
||||
:: current timestamp
|
||||
:: If b) then we empty $unreads and move all bins to $reads at the
|
||||
:: current timestamp
|
||||
::
|
||||
:: Unread tracking:
|
||||
:: Unread tracking has two 'modes' which may be used concurrently,
|
||||
:: if necessary.
|
||||
::
|
||||
:: count:
|
||||
:: This stores the unreads as a simple atom, describing the number
|
||||
:: of unread items. May be increased with %unread-count and
|
||||
:: set to zero with %read-count. Ideal for high-frequency linear
|
||||
:: datastructures, e.g. chat
|
||||
:: each:
|
||||
:: This stores the unreads as a set of paths, describing the set of
|
||||
:: unread items. Unreads may be added to the set with %unread-each
|
||||
:: and removed with %read-each. Ideal for non-linear, low-frequency
|
||||
:: datastructures, e.g. blogs
|
||||
::
|
||||
|%
|
||||
:: $place: A location, under which landscape stores stats
|
||||
::
|
||||
:: .desk must match q.byk.bowl
|
||||
:: Examples:
|
||||
:: A chat:
|
||||
:: [%landscape /~dopzod/urbit-help]
|
||||
:: A note in a notebook:
|
||||
:: [%landscape /~darrux-landes/feature-requests/12374893234232]
|
||||
:: A group:
|
||||
:: [%hark-group-hook /~bitbet-bolbel/urbit-community]
|
||||
:: Comments on a link
|
||||
:: [%landscape /~dabben-larbet/urbit-in-the-news/17014118450499614194868/2]
|
||||
::
|
||||
+$ place [=desk =path]
|
||||
::
|
||||
:: $bin: Identifier for grouping notifications
|
||||
::
|
||||
:: Examples
|
||||
:: A mention in a chat:
|
||||
:: [/mention %landscape /~dopzod/urbit-help]
|
||||
:: New messages in a chat
|
||||
:: [/message %landscape /~dopzod/urbit-help]
|
||||
:: A new comment in a notebook:
|
||||
:: [/comment %landscape /~darrux-landes/feature-requests/12374893234232/2]
|
||||
::
|
||||
+$ bin [=path =place]
|
||||
::
|
||||
:: $lid: Reference to a timebox
|
||||
::
|
||||
+$ lid
|
||||
$% [%archive =time]
|
||||
[%seen ~]
|
||||
[%unseen ~]
|
||||
==
|
||||
:: $content: Notification content
|
||||
+$ content
|
||||
$% [%ship =ship]
|
||||
[%text =cord]
|
||||
==
|
||||
::
|
||||
:: $body: A notification body
|
||||
::
|
||||
+$ body
|
||||
$: title=(list content)
|
||||
content=(list content)
|
||||
=time
|
||||
binned=path
|
||||
link=path
|
||||
==
|
||||
::
|
||||
+$ notification
|
||||
[date=@da =bin body=(list body)]
|
||||
:: $timebox: Group of notificatons
|
||||
+$ timebox
|
||||
(map bin notification)
|
||||
:: $archive: Archived notifications, ordered by time
|
||||
+$ archive
|
||||
((mop @da timebox) gth)
|
||||
::
|
||||
+$ action
|
||||
$% :: hook actions
|
||||
::
|
||||
:: %add-note: add a notification
|
||||
[%add-note =bin =body]
|
||||
::
|
||||
:: %del-place: Underlying resource disappeared, remove all
|
||||
:: associated notifications
|
||||
[%del-place =place]
|
||||
:: %unread-count: Change unread count by .count
|
||||
[%unread-count =place inc=? count=@ud]
|
||||
:: %unread-each: Add .path to list of unreads for .place
|
||||
[%unread-each =place =path]
|
||||
:: %saw-place: Update last-updated for .place to now.bowl
|
||||
[%saw-place =place time=(unit time)]
|
||||
:: store actions
|
||||
::
|
||||
:: %archive: archive single notification
|
||||
:: if .time is ~, then archiving unread notification
|
||||
:: else, archiving read notification
|
||||
[%archive =lid =bin]
|
||||
:: %read-count: set unread count to zero
|
||||
[%read-count =place]
|
||||
:: %read-each: remove path from unreads for .place
|
||||
[%read-each =place =path]
|
||||
:: %read-note: Read note at .bin
|
||||
[%read-note =bin]
|
||||
:: %archive-all: Archive all notifications
|
||||
[%archive-all ~]
|
||||
:: %opened: User opened notifications, reset timeboxing logic.
|
||||
::
|
||||
[%opened ~]
|
||||
::
|
||||
:: XX: previously in hark-store, now deprecated
|
||||
:: the hooks responsible for creating notifications may offer pokes
|
||||
:: similar to this
|
||||
:: [%read-graph =resource]
|
||||
:: [%read-group =resource]
|
||||
:: [%remove-graph =resource]
|
||||
::
|
||||
==
|
||||
:: .stats: Statistics for a .place
|
||||
::
|
||||
+$ stats
|
||||
$: count=@ud
|
||||
each=(set path)
|
||||
last=@da
|
||||
timebox=(unit @da)
|
||||
==
|
||||
::
|
||||
+$ update
|
||||
$% action
|
||||
:: %more: more updates
|
||||
[%archived =time =lid =notification]
|
||||
[%more more=(list update)]
|
||||
:: %note-read: note has been read with timestamp
|
||||
[%note-read =time =bin]
|
||||
[%added =notification]
|
||||
:: %timebox: description of timebox.
|
||||
::
|
||||
[%timebox =lid =(list notification)]
|
||||
:: %place-stats: description of .stats for a .place
|
||||
[%place-stats =place =stats]
|
||||
:: %place-stats: stats for all .places
|
||||
[%all-stats places=(map place stats)]
|
||||
==
|
||||
--
|
||||
|
@ -25,7 +25,7 @@
|
||||
--
|
||||
^- agent:gall
|
||||
%- agent:dbug
|
||||
%+ verb &
|
||||
%+ verb |
|
||||
=| inflated-state
|
||||
=* state -
|
||||
=<
|
||||
|
518
pkg/garden/app/hark-store.hoon
Normal file
518
pkg/garden/app/hark-store.hoon
Normal file
@ -0,0 +1,518 @@
|
||||
:: hark-store: notifications and unread counts [landscape]
|
||||
::
|
||||
:: hark-store can store unread counts differently, depending on the
|
||||
:: resource.
|
||||
:: - last seen. This way, hark-store simply stores an index into
|
||||
:: graph-store, which represents the last "seen" item, useful for
|
||||
:: high-volume applications which are intrinsically time-ordered. i.e.
|
||||
:: chats, comments
|
||||
:: - each. Hark-store will store an index for each item that is unread.
|
||||
:: Usefull for non-linear, low-volume applications, i.e. blogs,
|
||||
:: collections
|
||||
::
|
||||
/- store=hark-store
|
||||
/+ verb, dbug, default-agent, re=hark-unreads, agentio
|
||||
::
|
||||
::
|
||||
~% %hark-store-top ..part ~
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
+$ versioned-state
|
||||
$% state-2
|
||||
state-3
|
||||
state-4
|
||||
state-5
|
||||
state-6
|
||||
state-7
|
||||
state-8
|
||||
==
|
||||
::
|
||||
+$ base-state
|
||||
$: places=(map place:store stats:store)
|
||||
seen=timebox:store
|
||||
unseen=timebox:store
|
||||
=archive:store
|
||||
half-open=(map bin:store @da)
|
||||
==
|
||||
::
|
||||
+$ state-2
|
||||
[%2 *]
|
||||
::
|
||||
+$ state-3
|
||||
[%3 *]
|
||||
::
|
||||
+$ state-4
|
||||
[%4 *]
|
||||
::
|
||||
+$ state-5
|
||||
[%5 *]
|
||||
::
|
||||
+$ state-6
|
||||
[%6 *]
|
||||
::
|
||||
+$ state-7
|
||||
[%7 *]
|
||||
::
|
||||
+$ state-8
|
||||
[%8 base-state]
|
||||
::
|
||||
::
|
||||
+$ cached-state
|
||||
$: by-place=(jug place:store [=lid:store =path])
|
||||
~
|
||||
==
|
||||
+$ inflated-state
|
||||
[state-8 cached-state]
|
||||
::
|
||||
++ orm ((ordered-map @da timebox:store) gth)
|
||||
--
|
||||
::
|
||||
=| inflated-state
|
||||
=* state -
|
||||
::
|
||||
=<
|
||||
%+ verb &
|
||||
%- agent:dbug
|
||||
^- agent:gall
|
||||
~% %hark-store-agent ..card ~
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
ha ~(. +> bowl)
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
io ~(. agentio bowl)
|
||||
pass pass:io
|
||||
::
|
||||
++ on-init
|
||||
=. current-timebox now.bowl
|
||||
`this
|
||||
::
|
||||
++ on-save !>(-.state)
|
||||
++ on-load
|
||||
|= =old=vase
|
||||
=/ old
|
||||
!<(versioned-state old-vase)
|
||||
=| cards=(list card)
|
||||
|^ ^- (quip card _this)
|
||||
?: ?=(%8 -.old)
|
||||
=. -.state old
|
||||
=. +.state inflate
|
||||
:_(this (flop cards))
|
||||
::
|
||||
:_ this
|
||||
(poke-our:pass %hark-graph-hook hark-graph-migrate+old-vase)^~
|
||||
::
|
||||
++ index-timebox
|
||||
|= [=lid:store =timebox:store out=_by-place]
|
||||
^+ by-place
|
||||
%+ roll ~(tap by timebox)
|
||||
|= [[=bin:store =notification:store] out=_out]
|
||||
(~(put ju out) place.bin [lid path.bin])
|
||||
::
|
||||
++ inflate
|
||||
=. by-place (index-timebox seen+~ seen by-place)
|
||||
=. by-place (index-timebox unseen+~ unseen by-place)
|
||||
=. by-place
|
||||
%+ roll (tap:orm archive)
|
||||
|= [[=time =timebox:store] out=_by-place]
|
||||
(index-timebox archive/time timebox out)
|
||||
+.state
|
||||
--
|
||||
::
|
||||
++ on-watch
|
||||
|= =path
|
||||
^- (quip card _this)
|
||||
?> (team:title [src our]:bowl)
|
||||
|^
|
||||
?+ path (on-watch:def path)
|
||||
[%notes ~] `this
|
||||
::
|
||||
[%updates ~]
|
||||
:_ this
|
||||
[%give %fact ~ hark-update+!>(initial-updates)]~
|
||||
::
|
||||
==
|
||||
::
|
||||
++ initial-updates
|
||||
^- update:store
|
||||
:- %more
|
||||
^- (list update:store)
|
||||
:~ [%timebox unseen+~ ~(val by unseen)]
|
||||
[%timebox seen+~ ~(val by seen)]
|
||||
[%all-stats places]
|
||||
==
|
||||
--
|
||||
::
|
||||
++ on-peek
|
||||
|= =path
|
||||
^- (unit (unit cage))
|
||||
?+ path (on-peek:def path)
|
||||
::
|
||||
[%x %recent %inbox @ @ ~]
|
||||
=/ date=@da
|
||||
(slav %ud i.t.t.t.path)
|
||||
=/ length=@ud
|
||||
(slav %ud i.t.t.t.t.path)
|
||||
:^ ~ ~ %hark-update
|
||||
!> ^- update:store
|
||||
:- %more
|
||||
%+ turn (tab:orm archive `date length)
|
||||
|= [time=@da =timebox:store]
|
||||
^- update:store
|
||||
[%timebox archive+time ~(val by timebox)]
|
||||
==
|
||||
::
|
||||
++ on-poke
|
||||
~/ %hark-store-poke
|
||||
|= [=mark =vase]
|
||||
^- (quip card _this)
|
||||
|^
|
||||
?> (team:title our.bowl src.bowl)
|
||||
=^ cards state
|
||||
?+ mark (on-poke:def mark vase)
|
||||
%hark-action (hark-action !<(action:store vase))
|
||||
%noun (poke-noun !<(* vase))
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
++ poke-noun
|
||||
|= val=*
|
||||
?+ val ~|(%bad-noun-poke !!)
|
||||
%print ~&(+.state [~ state])
|
||||
%clear [~ state(. *inflated-state)]
|
||||
==
|
||||
::
|
||||
++ poke-us
|
||||
|= =action:store
|
||||
^- card
|
||||
[%pass / %agent [our dap]:bowl %poke hark-action+!>(action)]
|
||||
::
|
||||
++ hark-action
|
||||
|= =action:store
|
||||
^- (quip card _state)
|
||||
abet:(abed:poke-engine:ha action)
|
||||
--
|
||||
::
|
||||
++ on-agent on-agent:def
|
||||
::
|
||||
++ on-leave on-leave:def
|
||||
++ on-arvo
|
||||
|= [=wire =sign-arvo]
|
||||
^- (quip card _this)
|
||||
?. ?=([%autoseen ~] wire)
|
||||
(on-arvo:def wire sign-arvo)
|
||||
`this
|
||||
::
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
|_ =bowl:gall
|
||||
+* met ~(. metadata bowl)
|
||||
io ~(. agentio bowl)
|
||||
pass pass:io
|
||||
++ poke-engine
|
||||
|_ [out=(list update:store) cards=(list card)]
|
||||
++ poke-core .
|
||||
::
|
||||
++ abed
|
||||
|= in=action:store
|
||||
^+ poke-core
|
||||
?- -.in
|
||||
::
|
||||
%add-note (add-note +.in)
|
||||
%del-place (del-place +.in)
|
||||
%archive (do-archive +.in)
|
||||
::
|
||||
%unread-count (unread-count +.in)
|
||||
%read-count (read-count +.in)
|
||||
::
|
||||
%read-each (read-each +.in)
|
||||
%unread-each (unread-each +.in)
|
||||
::
|
||||
%read-note (read-note +.in)
|
||||
::
|
||||
%saw-place (saw-place +.in)
|
||||
::
|
||||
%opened opened
|
||||
%archive-all archive-all
|
||||
::
|
||||
==
|
||||
::
|
||||
++ abet
|
||||
^- (quip card _state)
|
||||
:_ state
|
||||
%+ snoc (flop cards)
|
||||
[%give %fact ~[/updates] %hark-update !>([%more (flop out)])]
|
||||
::
|
||||
++ give |=(=update:store poke-core(out [update out]))
|
||||
++ emit |=(=card poke-core(cards [card cards]))
|
||||
::
|
||||
::
|
||||
:: +| %note
|
||||
::
|
||||
:: notification tracking
|
||||
++ put-notifs
|
||||
|= [time=@da =timebox:store]
|
||||
poke-core(archive (put:orm archive time timebox))
|
||||
::
|
||||
++ put-lid
|
||||
|= [=lid:store =bin:store =notification:store]
|
||||
^+ poke-core
|
||||
=. by-place (~(put ju by-place) place.bin [lid path.bin])
|
||||
?- -.lid
|
||||
%seen
|
||||
poke-core(seen (~(put by seen) bin notification))
|
||||
::
|
||||
%unseen
|
||||
poke-core(unseen (~(put by unseen) bin notification))
|
||||
::
|
||||
%archive
|
||||
poke-core(archive (~(put re archive) time.lid bin notification))
|
||||
==
|
||||
::
|
||||
++ del-lid
|
||||
|= [=lid:store =bin:store]
|
||||
=. by-place (~(del ju by-place) place.bin [lid path.bin])
|
||||
?- -.lid
|
||||
%seen poke-core(seen (~(del by seen) bin))
|
||||
%unseen poke-core(unseen (~(del by unseen) bin))
|
||||
%archive poke-core(archive (~(del re archive) time.lid bin))
|
||||
==
|
||||
::
|
||||
++ add-note
|
||||
|= [=bin:store =body:store]
|
||||
^+ poke-core
|
||||
=. poke-core
|
||||
(emit (fact:io hark-update+!>([%add-note bin body]) /notes ~))
|
||||
=. by-place
|
||||
(~(put ju by-place) place.bin unseen+~ path.bin)
|
||||
=/ existing-notif
|
||||
(~(gut by unseen) bin *notification:store)
|
||||
=/ new=notification:store
|
||||
[now.bowl bin [body body.existing-notif]]
|
||||
=. unseen
|
||||
(~(put by unseen) bin new)
|
||||
(give %added new)
|
||||
::
|
||||
++ del-place
|
||||
|= =place:store
|
||||
=. poke-core (give %del-place place)
|
||||
=/ notes=(list [=lid:store =path])
|
||||
~(tap in (~(get ju by-place) place))
|
||||
|- ^+ poke-core
|
||||
?~ notes poke-core
|
||||
=, i.notes
|
||||
=. poke-core
|
||||
(del-lid lid path place)
|
||||
$(notes t.notes)
|
||||
::
|
||||
++ do-archive
|
||||
|= [=lid:store =bin:store]
|
||||
^+ poke-core
|
||||
~| %already-archived
|
||||
?< ?=(%time -.lid)
|
||||
~| %non-existent
|
||||
=/ =notification:store (need (get-lid lid bin))
|
||||
=. poke-core (del-lid lid bin)
|
||||
=. poke-core (put-lid archive+now.bowl bin notification)
|
||||
=? poke-core ?=(%unseen -.lid)
|
||||
?~ n=(get-lid seen+~ bin) poke-core
|
||||
=. archive
|
||||
%^ ~(job re archive) now.bowl bin
|
||||
|= og=(unit notification:store)
|
||||
(merge-notification og u.n)
|
||||
poke-core
|
||||
(give %archived now.bowl lid notification)
|
||||
::
|
||||
++ read-note
|
||||
|= =bin:store
|
||||
=/ =notification:store
|
||||
(~(got by unseen) bin)
|
||||
=. unseen
|
||||
(~(del by unseen) bin)
|
||||
=/ =time
|
||||
(fall timebox:(gut-place place.bin) now.bowl)
|
||||
=. date.notification time
|
||||
=. archive (~(put re archive) time bin notification)
|
||||
(give %note-read time bin)
|
||||
::
|
||||
::
|
||||
:: +| %each
|
||||
::
|
||||
:: each unread tracking
|
||||
::
|
||||
++ unread-each
|
||||
|= [=place:store =path]
|
||||
=. poke-core (saw-place place ~)
|
||||
=. poke-core (give %unread-each place path)
|
||||
%+ jub-place place
|
||||
|=(=stats:store stats(each (~(put in each.stats) path)))
|
||||
::
|
||||
++ read-index-each
|
||||
|= [=place:store =path]
|
||||
%- read-bins
|
||||
%+ skim
|
||||
~(tap in ~(key by unseen))
|
||||
|= =bin:store
|
||||
?. =(place place.bin) %.n
|
||||
=/ not=notification:store
|
||||
(~(got by unseen) bin)
|
||||
(lien body.not |=(=body:store =(binned.body path)))
|
||||
::
|
||||
++ read-each
|
||||
|= [=place:store =path]
|
||||
=. poke-core (read-index-each place path)
|
||||
=. poke-core (give %read-each place path)
|
||||
%+ jub-place place
|
||||
|= =stats:store
|
||||
%_ stats
|
||||
timebox `now.bowl
|
||||
each (~(del in each.stats) path)
|
||||
==
|
||||
::
|
||||
++ gut-place
|
||||
|= =place:store
|
||||
?: (~(has by places) place) (~(got by places) place)
|
||||
=| def=stats:store
|
||||
def(timebox ~, last now.bowl)
|
||||
::
|
||||
++ jub-place
|
||||
|= $: =place:store
|
||||
f=$-(stats:store stats:store)
|
||||
==
|
||||
^+ poke-core
|
||||
=/ =stats:store
|
||||
(gut-place place)
|
||||
poke-core(places (~(put by places) place (f stats)))
|
||||
::
|
||||
++ unread-count
|
||||
|= [=place:store inc=? count=@ud]
|
||||
=. poke-core
|
||||
(give %unread-count place inc count)
|
||||
=. poke-core (saw-place place ~)
|
||||
=/ f
|
||||
?: inc (cury add count)
|
||||
(curr sub count)
|
||||
%+ jub-place place
|
||||
|= =stats:store
|
||||
stats(count (f count.stats))
|
||||
::
|
||||
++ half-archive
|
||||
|= =place:store
|
||||
=/ bins=(list [=lid:store =path])
|
||||
~(tap in (~(get ju by-place) place))
|
||||
|-
|
||||
?~ bins poke-core
|
||||
=/ =bin:store
|
||||
[path.i.bins place]
|
||||
=* lid lid.i.bins
|
||||
?: ?=(%archive -.lid)
|
||||
$(bins t.bins)
|
||||
=/ seen-place (~(get by seen) bin)
|
||||
=/ n=(unit notification:store) (get-lid lid bin)
|
||||
?~ n $(bins t.bins)
|
||||
=* note u.n
|
||||
=/ =time (~(gut by half-open) bin now.bowl)
|
||||
=? half-open !(~(has by half-open) bin)
|
||||
(~(put by half-open) bin now.bowl)
|
||||
=. archive
|
||||
%^ ~(job re archive) time bin
|
||||
|=(n=(unit notification:store) (merge-notification n note))
|
||||
=. by-place (~(put ju by-place) place [archive/now.bowl path.bin])
|
||||
=. poke-core (give %archived now.bowl unseen+~ (~(got re archive) time bin))
|
||||
=. poke-core (give %archived now.bowl seen+~ (~(got re archive) time bin))
|
||||
$(bins t.bins)
|
||||
::
|
||||
++ read-count
|
||||
|= =place:store
|
||||
=. poke-core (give %read-count place)
|
||||
=. poke-core (half-archive place)
|
||||
%+ jub-place place
|
||||
|= =stats:store
|
||||
stats(count 0, timebox `now.bowl)
|
||||
::
|
||||
++ read-bins
|
||||
|= bins=(list bin:store)
|
||||
|-
|
||||
?~ bins poke-core
|
||||
=/ core
|
||||
(read-note i.bins)
|
||||
$(poke-core core, bins t.bins)
|
||||
::
|
||||
++ saw-place
|
||||
|= [=place:store time=(unit time)]
|
||||
=. poke-core (give %saw-place place time)
|
||||
%+ jub-place place
|
||||
|=(=stats:store stats(last (fall time now.bowl)))
|
||||
::
|
||||
++ archive-seen
|
||||
=/ seen=(list [=bin:store =notification:store]) ~(tap by seen)
|
||||
poke-core
|
||||
::
|
||||
++ opened
|
||||
=. seen
|
||||
%- ~(gas by *timebox:store)
|
||||
%+ murn ~(tap in (~(uni in ~(key by seen)) ~(key by unseen)))
|
||||
|= =bin:store
|
||||
=/ se (~(get by seen) bin)
|
||||
=/ un (~(get by unseen) bin)
|
||||
?~ un
|
||||
?~(se ~ `[bin u.se])
|
||||
`[bin (merge-notification se u.un)]
|
||||
=. unseen ~
|
||||
=. poke-core (turn-places |=(=stats:store stats(timebox ~)))
|
||||
(give %opened ~)
|
||||
|
||||
::
|
||||
++ archive-all
|
||||
(give:opened %archive-all ~)
|
||||
::
|
||||
++ turn-places
|
||||
|= f=$-(stats:store stats:store)
|
||||
=/ places ~(tap in ~(key by places))
|
||||
|- ^+ poke-core
|
||||
?~ places poke-core
|
||||
=/ core=_poke-core (jub-place i.places f)
|
||||
$(poke-core core, places t.places)
|
||||
--
|
||||
::
|
||||
++ get-lid
|
||||
|= [=lid:store =bin:store]
|
||||
=/ =timebox:store ?:(?=(%unseen -.lid) unseen seen)
|
||||
(~(get by timebox) bin)
|
||||
::
|
||||
++ merge-notification
|
||||
|= [existing=(unit notification:store) new=notification:store]
|
||||
^- notification:store
|
||||
?~ existing new
|
||||
[(max date.u.existing date.new) bin.new (welp body.new body.u.existing)]
|
||||
::
|
||||
:: +key-orm: +key:by for ordered maps
|
||||
++ key-orm
|
||||
|= =archive:store
|
||||
^- (list @da)
|
||||
(turn (tap:orm archive) |=([@da *] +<-))
|
||||
::
|
||||
:: +gut-orm: +gut:by for ordered maps
|
||||
:: TODO: move to zuse.hoon
|
||||
++ gut-orm
|
||||
|= [=archive:store time=@da]
|
||||
^- timebox:store
|
||||
(fall (get:orm archive time) ~)
|
||||
::
|
||||
::
|
||||
++ scry
|
||||
|* [=mold p=path]
|
||||
?> ?=(^ p)
|
||||
?> ?=(^ t.p)
|
||||
.^(mold i.p (scot %p our.bowl) i.t.p (scot %da now.bowl) t.t.p)
|
||||
::
|
||||
++ give
|
||||
|= [paths=(list path) update=update:store]
|
||||
^- (list card)
|
||||
[%give %fact paths [%hark-update !>(update)]]~
|
||||
::
|
||||
++ tap-nonempty
|
||||
|= =archive:store
|
||||
^- (list [@da timebox:store])
|
||||
%+ skim (tap:orm archive)
|
||||
|=([@da =timebox:store] !=(~(wyt by timebox) 0))
|
||||
--
|
171
pkg/garden/app/hark-system-hook.hoon
Normal file
171
pkg/garden/app/hark-system-hook.hoon
Normal file
@ -0,0 +1,171 @@
|
||||
/- hark=hark-store, hood, docket
|
||||
/+ verb, dbug, default-agent, agentio
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
+$ state-0 [%0 lagging=_|]
|
||||
::
|
||||
++ lag-interval ~m10
|
||||
--
|
||||
%+ verb &
|
||||
%- agent:dbug
|
||||
^- agent:gall
|
||||
=| state-0
|
||||
=* state -
|
||||
=<
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
io ~(. agentio bowl)
|
||||
pass pass:io
|
||||
cc ~(. +> bowl)
|
||||
++ on-init
|
||||
^- (quip card _this)
|
||||
:_ this
|
||||
[onboard watch:kiln check:lag ~]:cc
|
||||
::
|
||||
++ on-load
|
||||
|= =vase
|
||||
=+ !<(old=state-0 vase)
|
||||
`this(state old)
|
||||
::
|
||||
++ on-save !>(state)
|
||||
++ on-poke on-poke:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-watch on-watch:def
|
||||
++ on-agent
|
||||
|= [=wire =sign:agent:gall]
|
||||
|^
|
||||
?+ wire (on-agent:def wire sign)
|
||||
[%kiln %vats ~] take-kiln-vats
|
||||
==
|
||||
++ take-kiln-vats
|
||||
?- -.sign
|
||||
?(%poke-ack %watch-ack) (on-agent:def wire sign)
|
||||
%kick :_(this (drop safe-watch:kiln:cc))
|
||||
::
|
||||
%fact
|
||||
?. ?=(%kiln-vats-diff p.cage.sign) `this
|
||||
=+ !<(=diff:hood q.cage.sign)
|
||||
?+ -.diff `this
|
||||
::
|
||||
%merge
|
||||
=/ =action:hark ~(updated de:cc desk.diff)
|
||||
:_ this
|
||||
~[(poke:ha:cc action)]
|
||||
::
|
||||
%block
|
||||
=/ =action:hark (~(blocked de:cc desk.diff) blockers.diff)
|
||||
:_ this
|
||||
~[(poke:ha:cc action)]
|
||||
==
|
||||
==
|
||||
--
|
||||
::
|
||||
++ on-arvo
|
||||
|= [=wire sign=sign-arvo]
|
||||
^- (quip card _this)
|
||||
?. ?=([%check-lag ~] wire) (on-arvo:def wire sign)
|
||||
?> ?=([%behn %wake *] sign)
|
||||
=+ .^(lag=? %$ (scry:io %$ /zen/lag))
|
||||
?: =(lagging lag) :_(this ~[check:lag:cc])
|
||||
:_ this(lagging lag)
|
||||
:_ ~[check:lag:cc]
|
||||
?:(lagging start:lag:cc stop:lag:cc)
|
||||
::
|
||||
++ on-fail on-fail:def
|
||||
++ on-leave on-leave:def
|
||||
--
|
||||
|_ =bowl:gall
|
||||
+* io ~(. agentio bowl)
|
||||
pass pass:io
|
||||
::
|
||||
++ onboard
|
||||
^- card
|
||||
%- poke:ha
|
||||
:+ %add-note [/ [q.byk.bowl /onboard]]
|
||||
:: We special case this in the grid UI, but should include something
|
||||
:: for third parties
|
||||
[~[text+'Welcome to urbit'] ~ now.bowl / /]
|
||||
::
|
||||
++ lag
|
||||
|%
|
||||
++ check (~(wait pass /check-lag) (add now.bowl lag-interval))
|
||||
++ place [q.byk.bowl /lag]
|
||||
++ body `body:hark`[~[text/'Runtime lagging'] ~ now.bowl / /]
|
||||
++ start (poke:ha %add-note [/ place] body)
|
||||
++ stop (poke:ha %del-place place)
|
||||
--
|
||||
++ ha
|
||||
|%
|
||||
++ pass ~(. ^pass /hark)
|
||||
++ poke
|
||||
|=(=action:hark (poke-our:pass %hark-store hark-action+!>(action)))
|
||||
--
|
||||
++ kiln
|
||||
|%
|
||||
++ path /kiln/vats
|
||||
++ pass ~(. ^pass path)
|
||||
++ watch (watch-our:pass %hood path)
|
||||
++ watching (~(has by wex.bowl) [path our.bowl %hood])
|
||||
++ safe-watch `(unit card)`?:(watching ~ `watch)
|
||||
--
|
||||
::
|
||||
++ de
|
||||
|_ =desk
|
||||
++ scry-path (scry:io desk /desk/docket)
|
||||
++ has-docket .^(? %cu scry-path)
|
||||
++ docket .^(docket:^docket %cx scry-path)
|
||||
++ hash .^(@uv %cz (scry:io desk ~))
|
||||
++ place `place:hark`[q.byk.bowl /desk/[desk]]
|
||||
++ body
|
||||
|= [=path title=cord content=cord]
|
||||
^- body:hark
|
||||
[~[text+title] ~[text+content] now.bowl ~ path]
|
||||
::
|
||||
::
|
||||
++ title-prefix
|
||||
|= =cord
|
||||
?: =(desk %base)
|
||||
'System software'
|
||||
?: has-docket
|
||||
(rap 3 'App: ' title:docket cord ~)
|
||||
(rap 3 'Desk: ' desk cord ~)
|
||||
::
|
||||
++ updated
|
||||
^- action:hark
|
||||
:+ %add-note [/update place]
|
||||
%^ body /desk/[desk] (title-prefix ' has been updated')
|
||||
?: has-docket
|
||||
(rap 3 'Version: ' (ver version:docket) ~)
|
||||
(rap 3 'Hash: ' (scot %uv hash) ~)
|
||||
::
|
||||
++ blocked
|
||||
|= blockers=(set ^desk)
|
||||
^- action:hark
|
||||
:+ %add-note [/blocked place]
|
||||
%^ body /blocked (title-prefix ' is blocked from upgrading')
|
||||
(rap 3 'Blocking desks: ' (join ', ' ~(tap in blockers)))
|
||||
::
|
||||
++ ver
|
||||
|= =version:^docket
|
||||
=, version
|
||||
`@t`(rap 3 (num major) '.' (num minor) '.' (num patch) ~)
|
||||
::
|
||||
++ num
|
||||
|= a=@ud
|
||||
`@t`(rsh 4 (scot %ui a))
|
||||
--
|
||||
++ note
|
||||
|%
|
||||
++ merge
|
||||
|= [=desk hash=@uv]
|
||||
^- (list body:hark)
|
||||
:_ ~
|
||||
:* ~[text+'Desk Updated']
|
||||
~[text+(crip "Desk {(trip desk)} has been updated to hash {(scow %uv hash)}")]
|
||||
now.bowl
|
||||
/update/[desk]
|
||||
/
|
||||
==
|
||||
--
|
||||
--
|
@ -1,6 +1,8 @@
|
||||
:~ :- %apes
|
||||
:~ %docket
|
||||
%treaty
|
||||
%hark-store
|
||||
%hark-system-hook
|
||||
%settings-store
|
||||
==
|
||||
:- %fish ~
|
||||
|
234
pkg/garden/lib/hark/store.hoon
Normal file
234
pkg/garden/lib/hark/store.hoon
Normal file
@ -0,0 +1,234 @@
|
||||
/- sur=hark-store
|
||||
^?
|
||||
=, sur
|
||||
=< [. sur]
|
||||
|%
|
||||
|
||||
++ enjs
|
||||
=, enjs:format
|
||||
|%
|
||||
++ update
|
||||
|= upd=^update
|
||||
^- json
|
||||
|^
|
||||
%+ frond -.upd
|
||||
?+ -.upd a+~
|
||||
%added (notification +.upd)
|
||||
%add-note (add-note +.upd)
|
||||
%timebox (timebox +.upd)
|
||||
%more (more +.upd)
|
||||
%read-each (read-each +.upd)
|
||||
%read-count (place +.upd)
|
||||
%unread-each (read-each +.upd)
|
||||
%unread-count (unread-count +.upd)
|
||||
%saw-place (saw-place +.upd)
|
||||
%all-stats (all-stats +.upd)
|
||||
::%read-note (index +.upd)
|
||||
::%note-read (note-read +.upd)
|
||||
%archived (archived +.upd)
|
||||
==
|
||||
::
|
||||
++ add-note
|
||||
|= [bi=^bin bo=^body]
|
||||
%- pairs
|
||||
:~ bin+(bin bi)
|
||||
body+(body bo)
|
||||
==
|
||||
::
|
||||
++ saw-place
|
||||
|= [p=^place t=(unit ^time)]
|
||||
%- pairs
|
||||
:~ place+(place p)
|
||||
time+?~(t ~ (time u.t))
|
||||
==
|
||||
::
|
||||
++ archived
|
||||
|= [t=^time l=^lid n=^notification]
|
||||
%- pairs
|
||||
:~ lid+(lid l)
|
||||
time+s+(scot %ud t)
|
||||
notification+(notification n)
|
||||
==
|
||||
::
|
||||
++ note-read
|
||||
|= *
|
||||
(pairs ~)
|
||||
::
|
||||
++ all-stats
|
||||
|= places=(map ^place ^stats)
|
||||
^- json
|
||||
:- %a
|
||||
^- (list json)
|
||||
%+ turn ~(tap by places)
|
||||
|= [p=^place s=^stats]
|
||||
%- pairs
|
||||
:~ stats+(stats s)
|
||||
place+(place p)
|
||||
|
||||
==
|
||||
::
|
||||
++ stats
|
||||
|= s=^stats
|
||||
^- json
|
||||
%- pairs
|
||||
:~ each+a+(turn ~(tap in each.s) (cork spat (lead %s)))
|
||||
last+(time last.s)
|
||||
count+(numb count.s)
|
||||
==
|
||||
++ more
|
||||
|= upds=(list ^update)
|
||||
^- json
|
||||
a+(turn upds update)
|
||||
::
|
||||
++ place
|
||||
|= =^place
|
||||
%- pairs
|
||||
:~ desk+s+desk.place
|
||||
path+s+(spat path.place)
|
||||
==
|
||||
::
|
||||
++ bin
|
||||
|= =^bin
|
||||
%- pairs
|
||||
:~ place+(place place.bin)
|
||||
path+s+(spat path.bin)
|
||||
==
|
||||
++ notification
|
||||
|= ^notification
|
||||
^- json
|
||||
%- pairs
|
||||
:~ time+(time date)
|
||||
bin+(^bin bin)
|
||||
body+(bodies body)
|
||||
==
|
||||
++ bodies
|
||||
|= bs=(list ^body)
|
||||
^- json
|
||||
a+(turn bs body)
|
||||
::
|
||||
++ contents
|
||||
|= cs=(list ^content)
|
||||
^- json
|
||||
a+(turn cs content)
|
||||
::
|
||||
++ content
|
||||
|= c=^content
|
||||
^- json
|
||||
%+ frond -.c
|
||||
?- -.c
|
||||
%ship s+(scot %p ship.c)
|
||||
%text s+cord.c
|
||||
==
|
||||
::
|
||||
++ body
|
||||
|= ^body
|
||||
^- json
|
||||
%- pairs
|
||||
:~ title+(contents title)
|
||||
content+(contents content)
|
||||
time+(^time time)
|
||||
link+s+(spat link)
|
||||
==
|
||||
::
|
||||
++ binned-notification
|
||||
|= [=^bin =^notification]
|
||||
%- pairs
|
||||
:~ bin+(^bin bin)
|
||||
notification+(^notification notification)
|
||||
==
|
||||
++ lid
|
||||
|= l=^lid
|
||||
^- json
|
||||
%+ frond -.l
|
||||
?- -.l
|
||||
?(%seen %unseen) ~
|
||||
%archive s+(scot %ud time.l)
|
||||
==
|
||||
::
|
||||
++ timebox
|
||||
|= [li=^lid l=(list ^notification)]
|
||||
^- json
|
||||
%- pairs
|
||||
:~ lid+(lid li)
|
||||
notifications+a+(turn l notification)
|
||||
==
|
||||
::
|
||||
++ read-each
|
||||
|= [p=^place pax=^path]
|
||||
%- pairs
|
||||
:~ place+(place p)
|
||||
path+(path pax)
|
||||
==
|
||||
::
|
||||
++ unread-count
|
||||
|= [p=^place inc=? count=@ud]
|
||||
%- pairs
|
||||
:~ place+(place p)
|
||||
inc+b+inc
|
||||
count+(numb count)
|
||||
==
|
||||
--
|
||||
--
|
||||
++ dejs
|
||||
=, dejs:format
|
||||
|%
|
||||
:: TODO: fix +stab
|
||||
::
|
||||
++ pa
|
||||
|= j=json
|
||||
^- path
|
||||
?> ?=(%s -.j)
|
||||
?: =('/' p.j) /
|
||||
(stab p.j)
|
||||
::
|
||||
++ place
|
||||
%- ot
|
||||
:~ desk+so
|
||||
path+pa
|
||||
==
|
||||
::
|
||||
++ bin
|
||||
%- ot
|
||||
:~ path+pa
|
||||
place+place
|
||||
==
|
||||
::
|
||||
++ read-each
|
||||
%- ot
|
||||
:~ place+place
|
||||
path+pa
|
||||
==
|
||||
::
|
||||
:: parse date as @ud
|
||||
:: TODO: move to zuse
|
||||
++ sd
|
||||
|= jon=json
|
||||
^- @da
|
||||
?> ?=(%s -.jon)
|
||||
`@da`(rash p.jon dem:ag)
|
||||
::
|
||||
++ lid
|
||||
%- of
|
||||
:~ archive+sd
|
||||
unseen+ul
|
||||
seen+ul
|
||||
==
|
||||
::
|
||||
++ archive
|
||||
%- ot
|
||||
:~ lid+lid
|
||||
bin+bin
|
||||
==
|
||||
::
|
||||
++ action
|
||||
^- $-(json ^action)
|
||||
%- of
|
||||
:~ archive-all+ul
|
||||
archive+archive
|
||||
opened+ul
|
||||
read-count+place
|
||||
read-each+read-each
|
||||
read-note+bin
|
||||
==
|
||||
--
|
||||
--
|
35
pkg/garden/lib/hark/unreads.hoon
Normal file
35
pkg/garden/lib/hark/unreads.hoon
Normal file
@ -0,0 +1,35 @@
|
||||
/+ store=hark-store
|
||||
|_ =archive:store
|
||||
++ orm ((on @da timebox:store) gth)
|
||||
++ del
|
||||
|= [=time =bin:store]
|
||||
?~ box=(get:orm archive time) archive
|
||||
(put:orm archive time (~(del by u.box) bin))
|
||||
++ put
|
||||
|= [=time =bin:store =notification:store]
|
||||
=/ box=timebox:store (fall (get:orm archive time) ~)
|
||||
=. box (~(put by box) bin notification)
|
||||
(put:orm archive time box)
|
||||
::
|
||||
++ get
|
||||
|= [=time =bin:store]
|
||||
^- (unit notification:store)
|
||||
?~ box=(get:orm archive time) ~
|
||||
(~(get by u.box) bin)
|
||||
::
|
||||
++ got
|
||||
|= [=time =bin:store]
|
||||
(need (get time bin))
|
||||
::
|
||||
++ has
|
||||
|= [=time =bin:store]
|
||||
?~((get time bin) %.n %.y)
|
||||
::
|
||||
++ jab
|
||||
|= [=time =bin:store f=$-(notification:store notification:store)]
|
||||
(put time bin (f (got time bin)))
|
||||
::
|
||||
++ job
|
||||
|= [=time =bin:store f=$-((unit notification:store) notification:store)]
|
||||
(put time bin (f (get time bin)))
|
||||
--
|
13
pkg/garden/mar/hark/action.hoon
Normal file
13
pkg/garden/mar/hark/action.hoon
Normal file
@ -0,0 +1,13 @@
|
||||
/+ *hark-store
|
||||
|_ act=action
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun act
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun action
|
||||
++ json action:dejs
|
||||
--
|
||||
--
|
13
pkg/garden/mar/hark/update.hoon
Normal file
13
pkg/garden/mar/hark/update.hoon
Normal file
@ -0,0 +1,13 @@
|
||||
/+ *hark-store
|
||||
|_ upd=update
|
||||
++ grad %noun
|
||||
++ grow
|
||||
|%
|
||||
++ noun upd
|
||||
++ json (update:enjs upd)
|
||||
--
|
||||
++ grab
|
||||
|%
|
||||
++ noun update
|
||||
--
|
||||
--
|
1
pkg/garden/sur/hark-store.hoon
Symbolic link
1
pkg/garden/sur/hark-store.hoon
Symbolic link
@ -0,0 +1 @@
|
||||
../../garden-dev/sur/hark-store.hoon
|
@ -47,6 +47,7 @@ module.exports = {
|
||||
'@typescript-eslint/no-shadow': ['error'],
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'react/no-array-index-key': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react/jsx-props-no-spreading': 'off',
|
||||
|
5481
pkg/grid/package-lock.json
generated
5481
pkg/grid/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -20,8 +20,10 @@
|
||||
"@radix-ui/react-portal": "^0.0.15",
|
||||
"@radix-ui/react-toggle": "^0.0.10",
|
||||
"@tlon/sigil-js": "^1.4.4",
|
||||
"@types/lodash": "^4.14.172",
|
||||
"@urbit/api": "^1.4.0",
|
||||
"@urbit/http-api": "^1.3.1",
|
||||
"big-integer": "^1.6.48",
|
||||
"classnames": "^2.3.1",
|
||||
"clipboard-copy": "^4.0.1",
|
||||
"color2k": "^1.2.4",
|
||||
@ -32,8 +34,8 @@
|
||||
"mousetrap": "^1.6.5",
|
||||
"postcss-import": "^14.0.2",
|
||||
"query-string": "^7.0.1",
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"slugify": "^1.6.0",
|
||||
"zustand": "^3.5.7"
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import { BrowserRouter, Switch, Route, useHistory } from 'react-router-dom';
|
||||
import { BrowserRouter, Switch, Route, useHistory, useLocation } from 'react-router-dom';
|
||||
import { Grid } from './pages/Grid';
|
||||
import useDocketState from './state/docket';
|
||||
import { PermalinkRoutes } from './pages/PermalinkRoutes';
|
||||
@ -12,8 +12,25 @@ import { useHarkStore } from './state/hark';
|
||||
import { useTheme } from './state/settings';
|
||||
import { useLocalState } from './state/local';
|
||||
|
||||
const getNoteRedirect = (path: string) => {
|
||||
if (path.startsWith('/desk/')) {
|
||||
const [, , desk] = path.split('/');
|
||||
return `/app/${desk}`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const AppRoutes = () => {
|
||||
const { push } = useHistory();
|
||||
const { search } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const query = new URLSearchParams(search);
|
||||
if (query.has('grid-note')) {
|
||||
const redir = getNoteRedirect(query.get('grid-note')!);
|
||||
push(redir);
|
||||
}
|
||||
}, [search]);
|
||||
const theme = useTheme();
|
||||
const isDarkMode = useMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
@ -27,6 +44,8 @@ const AppRoutes = () => {
|
||||
}
|
||||
}, [isDarkMode, theme]);
|
||||
|
||||
useEffect(() => {}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.name = 'grid';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { chadIsRunning, Treaty } from '@urbit/api/docket';
|
||||
import { chadIsRunning, Treaty } from '@urbit/api';
|
||||
import clipboardCopy from 'clipboard-copy';
|
||||
import React, { FC } from 'react';
|
||||
import cn from 'classnames';
|
||||
|
32
pkg/grid/src/components/DeskLink.tsx
Normal file
32
pkg/grid/src/components/DeskLink.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useCharge } from '../state/docket';
|
||||
import { getAppHref } from '../state/util';
|
||||
|
||||
interface DeskLinkProps extends React.AnchorHTMLAttributes<any> {
|
||||
desk: string;
|
||||
to?: string;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function DeskLink({ children, className, desk, to = '', ...rest }: DeskLinkProps) {
|
||||
const charge = useCharge(desk);
|
||||
|
||||
if (!charge) {
|
||||
return null;
|
||||
}
|
||||
if (desk === window.desk) {
|
||||
return (
|
||||
<Link to={to} className={className} {...rest}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
const href = `${getAppHref(charge.href)}${to}`;
|
||||
return (
|
||||
<a href={href} target={desk} className={className} {...rest}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
@ -1,22 +1,32 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Link, NavLink, Route, Switch } from 'react-router-dom';
|
||||
import { Notification, HarkLid } from '@urbit/api';
|
||||
import { useLeapStore } from './Nav';
|
||||
import { Button } from '../components/Button';
|
||||
import { Notification } from '../state/hark-types';
|
||||
import { BasicNotification } from './notifications/BasicNotification';
|
||||
import {
|
||||
BaseBlockedNotification,
|
||||
RuntimeLagNotification
|
||||
} from './notifications/SystemNotification';
|
||||
import { useNotifications } from '../state/notifications';
|
||||
import { useHarkStore } from '../state/hark';
|
||||
import { OnboardingNotification } from './notifications/OnboardingNotification';
|
||||
import { Inbox } from './notifications/Inbox';
|
||||
|
||||
function renderNotification(notification: Notification, key: string) {
|
||||
if (notification.type === 'system-updates-blocked') {
|
||||
return <BaseBlockedNotification key={key} notification={notification} />;
|
||||
function renderNotification(notification: Notification, key: string, lid: HarkLid) {
|
||||
// Special casing
|
||||
if (notification.bin.place.desk === window.desk) {
|
||||
if (notification.bin.place.path === '/lag') {
|
||||
return <RuntimeLagNotification key={key} />;
|
||||
}
|
||||
if (notification.bin.place.path === '/blocked') {
|
||||
return <BaseBlockedNotification key={key} />;
|
||||
}
|
||||
if (notification.bin.place.path === '/onboard') {
|
||||
return <OnboardingNotification key={key} unread />
|
||||
}
|
||||
}
|
||||
if (notification.type === 'runtime-lag') {
|
||||
return <RuntimeLagNotification key={key} />;
|
||||
}
|
||||
return <BasicNotification key={key} notification={notification} />;
|
||||
return <BasicNotification key={key} notification={notification} lid={lid} />;
|
||||
}
|
||||
|
||||
const Empty = () => (
|
||||
@ -26,17 +36,49 @@ const Empty = () => (
|
||||
);
|
||||
|
||||
export const Notifications = () => {
|
||||
// const select = useLeapStore((s) => s.select);
|
||||
const { notifications, systemNotifications, hasAnyNotifications } = useNotifications();
|
||||
const select = useLeapStore((s) => s.select);
|
||||
const { unseen, seen, hasAnyNotifications } = useNotifications();
|
||||
const markAllAsRead = () => {
|
||||
const { archiveAll } = useHarkStore.getState();
|
||||
archiveAll();
|
||||
};
|
||||
|
||||
// useEffect(() => {
|
||||
// select('Notifications');
|
||||
// }, []);
|
||||
useEffect(() => {
|
||||
select('Notifications');
|
||||
const { getMore } = useHarkStore.getState();
|
||||
getMore();
|
||||
|
||||
function visibilitychange() {
|
||||
useHarkStore.getState().opened();
|
||||
}
|
||||
document.addEventListener('visibilitychange', visibilitychange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', visibilitychange);
|
||||
useHarkStore.getState().opened();
|
||||
};
|
||||
}, []);
|
||||
// const select = useLeapStore((s) => s.select);
|
||||
|
||||
return (
|
||||
<div className="grid grid-rows-[auto,1fr] h-full p-4 md:p-8 overflow-hidden">
|
||||
<header className="space-x-2 mb-8">
|
||||
<Button variant="secondary" className="py-1.5 px-6 rounded-full">
|
||||
<NavLink
|
||||
exact
|
||||
activeClassName="text-black"
|
||||
className="text-base font-semibold px-4"
|
||||
to="/leap/notifications"
|
||||
>
|
||||
New
|
||||
</NavLink>
|
||||
<NavLink
|
||||
activeClassName="text-black"
|
||||
className="text-base font-semibold px-4"
|
||||
to="/leap/notifications/archive"
|
||||
>
|
||||
Archive
|
||||
</NavLink>
|
||||
<Button onClick={markAllAsRead} variant="secondary" className="py-1.5 px-6 rounded-full">
|
||||
Mark All as Read
|
||||
</Button>
|
||||
<Button
|
||||
@ -48,16 +90,14 @@ export const Notifications = () => {
|
||||
Notification Settings
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
{!hasAnyNotifications && <Empty />}
|
||||
{hasAnyNotifications && (
|
||||
<section className="text-gray-400 space-y-2 overflow-y-auto">
|
||||
{notifications.map((n, index) => renderNotification(n, index.toString()))}
|
||||
{systemNotifications.map((n, index) =>
|
||||
renderNotification(n, (notifications.length + index).toString())
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
<Switch>
|
||||
<Route path="/leap/notifications" exact>
|
||||
<Inbox />
|
||||
</Route>
|
||||
<Route path="/leap/notifications/archive" exact>
|
||||
<Inbox archived />
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,25 +1,25 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { Timebox } from '@urbit/api';
|
||||
import { Link, LinkProps } from 'react-router-dom';
|
||||
import { Bullet } from '../components/icons/Bullet';
|
||||
import { Cross } from '../components/icons/Cross';
|
||||
import { Notification } from '../state/hark-types';
|
||||
import { useNotifications } from '../state/notifications';
|
||||
import { useHarkStore } from '../state/hark';
|
||||
|
||||
type NotificationsState = 'empty' | 'unread' | 'attention-needed' | 'open';
|
||||
|
||||
function getNotificationsState(
|
||||
notificationsOpen: boolean,
|
||||
notifications: Notification[],
|
||||
systemNotifications: Notification[]
|
||||
): NotificationsState {
|
||||
if (notificationsOpen) {
|
||||
return 'open';
|
||||
}
|
||||
|
||||
if (systemNotifications.length > 0) {
|
||||
function getNotificationsState(isOpen: boolean, box: Timebox): NotificationsState {
|
||||
const notifications = Object.values(box);
|
||||
if (
|
||||
notifications.filter(
|
||||
({ bin }) => bin.place.desk === window.desk && ['/lag', 'blocked'].includes(bin.place.path)
|
||||
).length > 0
|
||||
) {
|
||||
return 'attention-needed';
|
||||
}
|
||||
if (isOpen) {
|
||||
return 'open';
|
||||
}
|
||||
|
||||
// TODO: when real structure, this should be actually be unread not just existence
|
||||
if (notifications.length > 0) {
|
||||
@ -40,8 +40,8 @@ export const NotificationsLink = ({
|
||||
notificationsOpen,
|
||||
shouldDim
|
||||
}: NotificationsLinkProps) => {
|
||||
const { notifications, systemNotifications } = useNotifications();
|
||||
const state = getNotificationsState(notificationsOpen, notifications, systemNotifications);
|
||||
const unseen = useHarkStore((s) => s.unseen);
|
||||
const state = getNotificationsState(notificationsOpen, unseen);
|
||||
|
||||
return (
|
||||
<Link
|
||||
@ -58,7 +58,7 @@ export const NotificationsLink = ({
|
||||
)}
|
||||
>
|
||||
{state === 'empty' && <Bullet className="w-6 h-6" />}
|
||||
{state === 'unread' && notifications.length}
|
||||
{state === 'unread' && Object.keys(unseen).length}
|
||||
{state === 'attention-needed' && (
|
||||
<span className="h2">
|
||||
! <span className="sr-only">Attention needed</span>
|
||||
|
@ -1,10 +1,87 @@
|
||||
import React from 'react';
|
||||
import { BasicNotification as BasicNotificationType } from '../../state/hark-types';
|
||||
import cn from 'classnames';
|
||||
import { Notification, harkBinToId, HarkContent, HarkLid } from '@urbit/api';
|
||||
import { map, take } from 'lodash';
|
||||
import { useCharge } from '../../state/docket';
|
||||
import { Elbow } from '../../components/icons/Elbow';
|
||||
import { ShipName } from '../../components/ShipName';
|
||||
import { getAppHref } from '../../state/util';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { DeskLink } from '../../components/DeskLink';
|
||||
import {useHarkStore} from '../../state/hark';
|
||||
|
||||
interface BasicNotificationProps {
|
||||
notification: BasicNotificationType;
|
||||
notification: Notification;
|
||||
lid: HarkLid;
|
||||
}
|
||||
|
||||
export const BasicNotification = ({ notification }: BasicNotificationProps) => (
|
||||
<div>{notification.message}</div>
|
||||
);
|
||||
const MAX_CONTENTS = 5;
|
||||
|
||||
const NotificationText = ({ contents }: { contents: HarkContent[] }) => {
|
||||
return (
|
||||
<>
|
||||
{contents.map((content, idx) => {
|
||||
if ('ship' in content) {
|
||||
return <ShipName className="color-blue" key={idx} name={content.ship} />;
|
||||
}
|
||||
return content.text;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const BasicNotification = ({ notification, lid }: BasicNotificationProps) => {
|
||||
const { desk } = notification.bin.place;
|
||||
const binId = harkBinToId(notification.bin);
|
||||
const id = `notif-${notification.time}-${binId}`;
|
||||
|
||||
const charge = useCharge(desk);
|
||||
const first = notification.body?.[0];
|
||||
if (!first || !charge) {
|
||||
return null;
|
||||
}
|
||||
const contents = map(notification.body, 'content').filter((c) => c.length > 0);
|
||||
const large = contents.length === 0;
|
||||
const archive = () => {
|
||||
const { bin } = notification;
|
||||
useHarkStore.getState().archiveNote(notification.bin, lid);
|
||||
};
|
||||
|
||||
return (
|
||||
<DeskLink
|
||||
onClick={archive}
|
||||
to={`?grid-note=${encodeURIComponent(first.link)}`}
|
||||
desk={desk}
|
||||
className={cn(
|
||||
'text-black rounded',
|
||||
'unseen' in lid ? 'bg-blue-100' : 'bg-gray-100',
|
||||
large ? 'note-grid-no-content' : 'note-grid-content'
|
||||
)}
|
||||
aria-labelledby={id}
|
||||
>
|
||||
<header id={id} className="contents">
|
||||
<div
|
||||
className="note-grid-icon rounded w-full h-full"
|
||||
style={{ backgroundColor: charge?.color ?? '#ee5432' }}
|
||||
/>
|
||||
<div className="note-grid-title font-semibold">{charge?.title || desk}</div>
|
||||
{!large ? <Elbow className="note-grid-arrow w-6 h-6 text-gray-300" /> : null}
|
||||
<h2 id={`${id}-title`} className="note-grid-head font-semibold text-gray-600">
|
||||
<NotificationText contents={first.title} />
|
||||
</h2>
|
||||
</header>
|
||||
{contents.length > 0 ? (
|
||||
<div className="note-grid-body space-y-2">
|
||||
{take(contents, MAX_CONTENTS).map((content) => (
|
||||
<p className="">
|
||||
<NotificationText contents={content} />
|
||||
</p>
|
||||
))}
|
||||
{contents.length > MAX_CONTENTS ? (
|
||||
<p className="text-gray-300">and {contents.length - MAX_CONTENTS} more</p>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</DeskLink>
|
||||
);
|
||||
};
|
||||
|
92
pkg/grid/src/nav/notifications/Inbox.tsx
Normal file
92
pkg/grid/src/nav/notifications/Inbox.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { HarkLid, Notification } from '@urbit/api';
|
||||
import { useLeapStore } from '../Nav';
|
||||
import { Button } from '../../components/Button';
|
||||
import { BasicNotification } from './BasicNotification';
|
||||
import { BaseBlockedNotification, RuntimeLagNotification } from './SystemNotification';
|
||||
import { useNotifications } from '../../state/notifications';
|
||||
import { useHarkStore } from '../../state/hark';
|
||||
import { OnboardingNotification } from './OnboardingNotification';
|
||||
|
||||
function renderNotification(notification: Notification, key: string, lid: HarkLid) {
|
||||
// Special casing
|
||||
if (notification.bin.place.desk === window.desk) {
|
||||
if (notification.bin.place.path === '/lag') {
|
||||
return <RuntimeLagNotification key={key} />;
|
||||
}
|
||||
if (notification.bin.place.path === '/blocked') {
|
||||
return <BaseBlockedNotification key={key} />;
|
||||
}
|
||||
if (notification.bin.place.path === '/onboard') {
|
||||
return <OnboardingNotification key={key} unread={false} />;
|
||||
}
|
||||
}
|
||||
return <BasicNotification key={key} notification={notification} lid={lid} />;
|
||||
}
|
||||
|
||||
const Empty = () => (
|
||||
<section className="flex justify-center items-center min-h-[480px] text-gray-400 space-y-2">
|
||||
<span className="h4">All clear!</span>
|
||||
</section>
|
||||
);
|
||||
|
||||
export const Inbox = ({ archived = false }) => {
|
||||
const select = useLeapStore((s) => s.select);
|
||||
const { unseen, seen, hasAnyNotifications } = useNotifications();
|
||||
const archive = useHarkStore((s) => s.archive);
|
||||
const markAllAsRead = () => {};
|
||||
|
||||
useEffect(() => {
|
||||
useHarkStore.getState().getMore();
|
||||
|
||||
}, [archived]);
|
||||
|
||||
useEffect(() => {
|
||||
select('Notifications');
|
||||
const { getMore } = useHarkStore.getState();
|
||||
getMore();
|
||||
|
||||
function visibilitychange() {
|
||||
setTimeout(() => useHarkStore.getState().opened(), 100);
|
||||
}
|
||||
document.addEventListener('visibilitychange', visibilitychange);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', visibilitychange);
|
||||
visibilitychange();
|
||||
};
|
||||
}, []);
|
||||
// const select = useLeapStore((s) => s.select);
|
||||
|
||||
if (false) {
|
||||
return <Empty />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-gray-400 space-y-2 overflow-y-auto">
|
||||
{archived ? (
|
||||
Array.from(archive).map(([key, box]) => {
|
||||
return Object.entries(box)
|
||||
.sort(([, a], [, b]) => b.time - a.time)
|
||||
.map(([binId, n], index) => renderNotification(n, `${key.toString}-${binId}`, { time: key.toString() }));
|
||||
})
|
||||
) : (
|
||||
<>
|
||||
<header>Unseen</header>
|
||||
<section className="space-y-2">
|
||||
{Object.entries(unseen)
|
||||
.sort(([, a], [, b]) => b.time - a.time)
|
||||
.map(([binId, n], index) => renderNotification(n, `unseen-${binId}`, { unseen: null }))}
|
||||
</section>
|
||||
<header>Seen</header>
|
||||
<section className="space-y-2">
|
||||
{Object.entries(seen)
|
||||
.sort(([, a], [, b]) => b.time - a.time)
|
||||
.map(([binId, n], index) => renderNotification(n, `seen-${binId}`, { seen: null }))}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
86
pkg/grid/src/nav/notifications/OnboardingNotification.tsx
Normal file
86
pkg/grid/src/nav/notifications/OnboardingNotification.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Button } from '../../components/Button';
|
||||
|
||||
const cards: OnboardingCardProps[] = [
|
||||
{
|
||||
title: 'Terminal',
|
||||
body: "Install a web terminal to access your Urbit's command line",
|
||||
button: 'Install',
|
||||
color: '#9CA4B1'
|
||||
},
|
||||
{
|
||||
title: 'Groups',
|
||||
body: 'Install Groups, a suite of social software to communicate with other urbit users',
|
||||
button: 'Install',
|
||||
color: '#D1DDD3'
|
||||
},
|
||||
{
|
||||
title: 'Bitcoin',
|
||||
body: 'Install a bitcoin wallet. You can send bitcoin to any btc address, or even another ship',
|
||||
button: 'Install',
|
||||
color: '#F6EBDB'
|
||||
},
|
||||
{
|
||||
title: 'Debug',
|
||||
body: "Install a debugger. You can inspect your ship's internals using this interface",
|
||||
button: 'Install'
|
||||
},
|
||||
{
|
||||
title: 'Build an app',
|
||||
body: 'You can instantly get started building new things on Urbit. Just right click your Landscape and select “New App”',
|
||||
button: 'Learn more',
|
||||
color: '#82A6CA'
|
||||
}
|
||||
];
|
||||
|
||||
interface OnboardingCardProps {
|
||||
title: string;
|
||||
button: string;
|
||||
body: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const OnboardingCard = ({ title, button, body, color }: OnboardingCardProps) => (
|
||||
<div
|
||||
className="p-4 flex flex-col space-y-2 text-black bg-gray-100 justify-between rounded-xl"
|
||||
style={color ? { backgroundColor: color } : {}}
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<h4 className="font-semibold text-black">{title}</h4>
|
||||
<p>{body}</p>
|
||||
</div>
|
||||
<Button variant="primary" className="bg-gray-500">
|
||||
{button}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface OnboardingNotificationProps {
|
||||
unread: boolean;
|
||||
}
|
||||
|
||||
export const OnboardingNotification = ({ unread }: OnboardingNotificationProps) => (
|
||||
<section
|
||||
className={cn('notification space-y-2 text-black', unread ? 'bg-blue-100' : 'bg-gray-100')}
|
||||
aria-labelledby=""
|
||||
>
|
||||
<header id="system-updates-blocked" className="relative space-y-2">
|
||||
<div className="flex space-x-2">
|
||||
<span className="inline-block w-6 h-6 bg-orange-500 rounded-full" />
|
||||
<span className="font-medium">Grid</span>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<h2 id="runtime-lag">Hello there, welcome to Grid!</h2>
|
||||
</div>
|
||||
</header>
|
||||
<div className="grid grid-cols-3 grid-rows-2 gap-4">
|
||||
{
|
||||
/* eslint-disable-next-line react/no-array-index-key */
|
||||
cards.map((card, i) => (
|
||||
<OnboardingCard key={i} {...card} />
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
);
|
@ -7,15 +7,10 @@ import { Dialog, DialogClose, DialogContent, DialogTrigger } from '../../compone
|
||||
import { Elbow } from '../../components/icons/Elbow';
|
||||
import api from '../../state/api';
|
||||
import { useCharges } from '../../state/docket';
|
||||
import { BaseBlockedNotification as BaseBlockedNotificationType } from '../../state/hark-types';
|
||||
|
||||
import { NotificationButton } from './NotificationButton';
|
||||
import { disableDefault } from '../../state/util';
|
||||
|
||||
interface BaseBlockedNotificationProps {
|
||||
notification: BaseBlockedNotificationType;
|
||||
}
|
||||
|
||||
export const RuntimeLagNotification = () => (
|
||||
<section
|
||||
className="notification pl-12 space-y-2 text-black bg-orange-50"
|
||||
@ -40,8 +35,8 @@ export const RuntimeLagNotification = () => (
|
||||
</section>
|
||||
);
|
||||
|
||||
export const BaseBlockedNotification = ({ notification }: BaseBlockedNotificationProps) => {
|
||||
const { desks } = notification;
|
||||
export const BaseBlockedNotification = () => {
|
||||
const desks: string[] = [];
|
||||
const charges = useCharges();
|
||||
const blockedCharges = Object.values(pick(charges, desks));
|
||||
const count = blockedCharges.length;
|
||||
|
@ -8,7 +8,11 @@ import { useSettingsState, SettingsState } from '../../state/settings';
|
||||
const selDnd = (s: SettingsState) => s.display.doNotDisturb;
|
||||
async function toggleDnd() {
|
||||
const state = useSettingsState.getState();
|
||||
await state.putEntry('display', 'doNotDisturb', !selDnd(state));
|
||||
const curr = selDnd(state);
|
||||
if(curr) {
|
||||
Notification.requestPermission();
|
||||
}
|
||||
await state.putEntry('display', 'doNotDisturb', !curr);
|
||||
}
|
||||
|
||||
const selMentions = (s: HarkState) => s.notificationsGraphConfig.mentions;
|
||||
|
@ -1,30 +1,25 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { AppInfo } from '../../components/AppInfo';
|
||||
import { ShipName } from '../../components/ShipName';
|
||||
import useDocketState, { useCharge, useTreaty } from '../../state/docket';
|
||||
import { useVat } from '../../state/kiln';
|
||||
import { useLeapStore } from '../Nav';
|
||||
|
||||
export const TreatyInfo = () => {
|
||||
const select = useLeapStore((state) => state.select);
|
||||
const { ship, host, desk } = useParams<{ ship: string; host: string; desk: string }>();
|
||||
const { host, desk } = useParams<{ host: string; desk: string }>();
|
||||
const treaty = useTreaty(host, desk);
|
||||
const vat = useVat(desk);
|
||||
const charge = useCharge(desk);
|
||||
|
||||
useEffect(() => {
|
||||
if(!charge) {
|
||||
if (!charge) {
|
||||
useDocketState.getState().requestTreaty(host, desk);
|
||||
}
|
||||
}, [host, desk]);
|
||||
|
||||
useEffect(() => {
|
||||
select(
|
||||
<>
|
||||
{treaty?.title}
|
||||
</>
|
||||
);
|
||||
select(<>{treaty?.title}</>);
|
||||
}, [treaty?.title]);
|
||||
|
||||
if (!treaty) {
|
||||
|
@ -3,41 +3,39 @@ import { Switch, Route, Redirect, RouteComponentProps } from 'react-router-dom';
|
||||
import { Spinner } from '../components/Spinner';
|
||||
import { useQuery } from '../logic/useQuery';
|
||||
import { useCharge } from '../state/docket';
|
||||
import useKilnState, { useKilnLoaded, useVat } from '../state/kiln';
|
||||
import useKilnState, { useKilnLoaded } from '../state/kiln';
|
||||
import { getAppHref } from '../state/util';
|
||||
|
||||
function getDeskByForeignRef(ship: string, desk: string): string | undefined {
|
||||
const { vats } = useKilnState.getState();
|
||||
console.log(ship, desk);
|
||||
const found = Object.entries(vats).find(
|
||||
([d, vat]) => vat.arak.ship === ship && vat.arak.desk === desk
|
||||
([, vat]) => vat.arak.ship === ship && vat.arak.desk === desk
|
||||
);
|
||||
return !!found ? found[0] : undefined;
|
||||
return found ? found[0] : undefined;
|
||||
}
|
||||
|
||||
interface AppLinkProps
|
||||
extends RouteComponentProps<{
|
||||
ship: string;
|
||||
desk: string;
|
||||
link: string;
|
||||
}> {}
|
||||
type AppLinkProps = RouteComponentProps<{
|
||||
ship: string;
|
||||
desk: string;
|
||||
link: string;
|
||||
}>;
|
||||
|
||||
function AppLink(props: AppLinkProps) {
|
||||
const { ship, desk, link = '' } = props.match.params;
|
||||
function AppLink({ match, history, location }: AppLinkProps) {
|
||||
const { ship, desk, link = '' } = match.params;
|
||||
const ourDesk = getDeskByForeignRef(ship, desk);
|
||||
|
||||
if (ourDesk) {
|
||||
return <AppLinkRedirect desk={ourDesk} link={link} />;
|
||||
}
|
||||
return <AppLinkNotFound {...props} />;
|
||||
return <AppLinkNotFound match={match} history={history} location={location} />;
|
||||
}
|
||||
|
||||
function AppLinkNotFound(props: AppLinkProps) {
|
||||
const { ship, desk } = props.match.params;
|
||||
return (<Redirect to={`/leap/search/direct/apps/${ship}/${desk}`} />);
|
||||
function AppLinkNotFound({ match }: AppLinkProps) {
|
||||
const { ship, desk } = match.params;
|
||||
return <Redirect to={`/leap/search/direct/apps/${ship}/${desk}`} />;
|
||||
}
|
||||
|
||||
function AppLinkInvalid(props: AppLinkProps) {
|
||||
function AppLinkInvalid() {
|
||||
return (
|
||||
<div>
|
||||
<h4>Link was malformed</h4>
|
||||
@ -46,7 +44,6 @@ function AppLinkInvalid(props: AppLinkProps) {
|
||||
);
|
||||
}
|
||||
function AppLinkRedirect({ desk, link }: { desk: string; link: string }) {
|
||||
const vat = useVat(desk);
|
||||
const charge = useCharge(desk);
|
||||
useEffect(() => {
|
||||
const query = new URLSearchParams({
|
||||
@ -61,8 +58,8 @@ function AppLinkRedirect({ desk, link }: { desk: string; link: string }) {
|
||||
const LANDSCAPE_SHIP = '~zod';
|
||||
const LANDSCAPE_DESK = 'groups';
|
||||
|
||||
function LandscapeLink(props: RouteComponentProps<{ link: string }>) {
|
||||
const { link } = props.match.params;
|
||||
function LandscapeLink({ match }: RouteComponentProps<{ link: string }>) {
|
||||
const { link } = match.params;
|
||||
|
||||
return <Redirect to={`/perma/${LANDSCAPE_SHIP}/${LANDSCAPE_DESK}/${link}`} />;
|
||||
}
|
||||
|
@ -16,9 +16,10 @@ import {
|
||||
AllyUpdateIni,
|
||||
TreatyUpdateIni,
|
||||
docketInstall,
|
||||
ChargeUpdate
|
||||
} from '@urbit/api/docket';
|
||||
import { kilnRevive, kilnSuspend } from '@urbit/api/hood';
|
||||
ChargeUpdate,
|
||||
kilnRevive,
|
||||
kilnSuspend
|
||||
} from '@urbit/api';
|
||||
import api from './api';
|
||||
import { mockAllies, mockCharges, mockTreaties } from './mock-data';
|
||||
import { fakeRequest, useMockData } from './util';
|
||||
|
@ -1,17 +1,50 @@
|
||||
import _ from 'lodash';
|
||||
import { NotificationGraphConfig } from '@urbit/api';
|
||||
import { Notification } from './hark-types';
|
||||
import { mockNotification } from './mock-data';
|
||||
/* eslint-disable no-param-reassign */
|
||||
import create from 'zustand';
|
||||
import {
|
||||
Notification as HarkNotification,
|
||||
harkBinEq,
|
||||
makePatDa,
|
||||
readAll,
|
||||
decToUd,
|
||||
unixToDa,
|
||||
Timebox,
|
||||
harkBinToId,
|
||||
opened,
|
||||
HarkBin,
|
||||
HarkLid,
|
||||
archive,
|
||||
HarkContent,
|
||||
NotificationGraphConfig
|
||||
} from '@urbit/api';
|
||||
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
|
||||
/* eslint-disable-next-line camelcase */
|
||||
import { unstable_batchedUpdates } from 'react-dom';
|
||||
import produce from 'immer';
|
||||
import _, { map } from 'lodash';
|
||||
import api from './api';
|
||||
import { useMockData } from './util';
|
||||
import { BaseState, createState, createSubscription, reduceStateN } from './base';
|
||||
import { mockNotifications } from './mock-data';
|
||||
import useDocketState from './docket';
|
||||
import { useSettingsState } from './settings';
|
||||
import { BaseState, createState, createSubscription, reduceStateN} from './base';
|
||||
|
||||
export interface HarkState {
|
||||
notifications: Notification[];
|
||||
seen: Timebox;
|
||||
unseen: Timebox;
|
||||
archive: BigIntOrderedMap<Timebox>;
|
||||
set: (f: (s: HarkState) => void) => void;
|
||||
opened: () => Promise<void>;
|
||||
notificationsGraphConfig: NotificationGraphConfig;
|
||||
archiveAll: () => Promise<void>;
|
||||
archiveNote: (bin: HarkBin, lid: HarkLid) => Promise<void>;
|
||||
getMore: () => Promise<void>;
|
||||
webNotes: {
|
||||
[binId: string]: Notification[];
|
||||
};
|
||||
[ref: string]: unknown;
|
||||
}
|
||||
|
||||
type BaseHarkState = HarkState & BaseState<HarkState>;
|
||||
type BaseHarkState = BaseState<HarkState> & HarkState;
|
||||
|
||||
function updateState(
|
||||
key: string,
|
||||
@ -34,16 +67,41 @@ export const reduceGraph = [
|
||||
})
|
||||
];
|
||||
|
||||
|
||||
export const useHarkStore = createState<HarkState>(
|
||||
'Hark',
|
||||
() => ({
|
||||
notifications: useMockData ? [mockNotification] : [],
|
||||
notificationsGraphConfig: {
|
||||
watchOnSelf: false,
|
||||
mentions: false,
|
||||
watching: []
|
||||
}
|
||||
}),
|
||||
(set, get) => ({
|
||||
seen: {},
|
||||
unseen: {},
|
||||
archive: new BigIntOrderedMap<Timebox>(),
|
||||
webNotes: {},
|
||||
notificationsGraphConfig: {
|
||||
watchOnSelf: false,
|
||||
mentions: false,
|
||||
watching: []
|
||||
},
|
||||
|
||||
set: (f) => {
|
||||
const newState = produce(get(), f);
|
||||
set(newState);
|
||||
},
|
||||
archiveAll: async () => {},
|
||||
archiveNote: async (bin, lid) => {
|
||||
await api.poke(archive(bin, lid));
|
||||
},
|
||||
opened: async () => {
|
||||
await api.poke(opened);
|
||||
},
|
||||
getMore: async () => {
|
||||
const { archive } = get();
|
||||
const idx = decToUd((archive.peekSmallest()?.[0] || unixToDa(Date.now() * 1000)).toString());
|
||||
const update = await api.scry({
|
||||
app: 'hark-store',
|
||||
path: `/recent/inbox/${idx}/5`
|
||||
});
|
||||
reduceHark(update);
|
||||
}
|
||||
}),
|
||||
[],
|
||||
[
|
||||
(set, get) =>
|
||||
@ -52,6 +110,128 @@ export const useHarkStore = createState<HarkState>(
|
||||
if (graphHookData) {
|
||||
reduceStateN(get(), graphHookData, reduceGraph);
|
||||
}
|
||||
}),
|
||||
(set, get) =>
|
||||
createSubscription('hark-store', '/updates', u => {
|
||||
/* eslint-ignore-next-line camelcase */
|
||||
unstable_batchedUpdates(() => {
|
||||
reduceHark(u);
|
||||
});
|
||||
})
|
||||
]
|
||||
);
|
||||
|
||||
function reduceHark(u: any) {
|
||||
const { set } = useHarkStore.getState();
|
||||
if (!u) {
|
||||
return;
|
||||
}
|
||||
if ('more' in u) {
|
||||
u.more.forEach((upd: any) => {
|
||||
reduceHark(upd);
|
||||
});
|
||||
} else if ('all-stats' in u) {
|
||||
// TODO: probably ignore?
|
||||
} else if ('added' in u) {
|
||||
set((draft) => {
|
||||
const { bin } = u.added;
|
||||
const binId = harkBinToId(bin);
|
||||
draft.unseen[binId] = u.added;
|
||||
});
|
||||
} else if ('timebox' in u) {
|
||||
const { timebox } = u;
|
||||
console.log(timebox);
|
||||
const { lid, notifications } = timebox;
|
||||
if ('archive' in lid) {
|
||||
set((draft) => {
|
||||
const time = makePatDa(lid.archive);
|
||||
const timebox = draft.archive.get(time) || {};
|
||||
console.log(timebox);
|
||||
notifications.forEach((note: any) => {
|
||||
console.log(note);
|
||||
const binId = harkBinToId(note.bin);
|
||||
timebox[binId] = note;
|
||||
});
|
||||
console.log(notifications);
|
||||
draft.archive = draft.archive.set(time, timebox);
|
||||
});
|
||||
} else {
|
||||
set((draft) => {
|
||||
const seen = 'seen' in lid ? 'seen' : 'unseen';
|
||||
notifications.forEach((note: any) => {
|
||||
const binId = harkBinToId(note.bin);
|
||||
draft[seen][binId] = note;
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if ('archived' in u) {
|
||||
const { lid, notification } = u.archived;
|
||||
console.log(u.archived);
|
||||
set((draft) => {
|
||||
const seen = 'seen' in lid ? 'seen' : 'unseen';
|
||||
const binId = harkBinToId(notification.bin);
|
||||
delete draft[seen][binId];
|
||||
const time = makePatDa(u.archived.time);
|
||||
const timebox = draft.archive.get(time) || {};
|
||||
timebox[binId] = notification;
|
||||
draft.archive = draft.archive.set(time, timebox);
|
||||
});
|
||||
} else if ('opened' in u) {
|
||||
set((draft) => {
|
||||
const bins = Object.keys(draft.unseen);
|
||||
bins.forEach((bin) => {
|
||||
const old = draft.seen[bin];
|
||||
const curr = draft.unseen[bin];
|
||||
curr.body = [...curr.body, ...(old?.body || [])];
|
||||
draft.seen[bin] = curr;
|
||||
delete draft.unseen[bin];
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
api.subscribe({
|
||||
app: 'hark-store',
|
||||
path: '/updates',
|
||||
event: (u: any) => {
|
||||
/* eslint-ignore-next-line camelcase */
|
||||
unstable_batchedUpdates(() => {
|
||||
reduceHark(u);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function harkContentsToPlainText(contents: HarkContent[]) {
|
||||
return contents
|
||||
.map((c) => {
|
||||
if ('ship' in c) {
|
||||
return c.ship;
|
||||
}
|
||||
return c.text;
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
api.subscribe({
|
||||
app: 'hark-store',
|
||||
path: '/notes',
|
||||
event: (u: any) => {
|
||||
if ('add-note' in u) {
|
||||
if (useSettingsState.getState().display.doNotDisturb) {
|
||||
//return;
|
||||
}
|
||||
const { bin, body } = u['add-note'];
|
||||
const binId = harkBinToId(bin);
|
||||
const { title, content } = body;
|
||||
const image = useDocketState.getState().charges[bin.desk]?.image;
|
||||
|
||||
const notification = new Notification(harkContentsToPlainText(title), {
|
||||
body: harkContentsToPlainText(content),
|
||||
tag: binId,
|
||||
renotify: true
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
@ -1,9 +1,20 @@
|
||||
import {
|
||||
Vat,
|
||||
Vats,
|
||||
Allies,
|
||||
Charges,
|
||||
DocketHrefGlob,
|
||||
Treaties,
|
||||
Treaty,
|
||||
Notification,
|
||||
HarkContent,
|
||||
HarkBody,
|
||||
unixToDa,
|
||||
Contact,
|
||||
Contacts
|
||||
} from '@urbit/api';
|
||||
import _ from 'lodash';
|
||||
import { Contact, Contacts } from '@urbit/api';
|
||||
import { Allies, Charges, DocketHrefGlob, Treaties, Treaty } from '@urbit/api/docket';
|
||||
import { Vat, Vats } from '@urbit/api/hood';
|
||||
import systemUrl from '../assets/system.png';
|
||||
import { BasicNotification } from './hark-types';
|
||||
|
||||
export const appMetaData: Pick<Treaty, 'cass' | 'hash' | 'website' | 'license' | 'version'> = {
|
||||
cass: {
|
||||
@ -165,6 +176,90 @@ export const mockAllies: Allies = [
|
||||
'~nalrys'
|
||||
].reduce((acc, val) => ({ ...acc, [val]: charter }), {});
|
||||
|
||||
function ship(s: string) {
|
||||
return { ship: s };
|
||||
}
|
||||
|
||||
function text(t: string) {
|
||||
return { text: t };
|
||||
}
|
||||
|
||||
function createDmNotification(...content: HarkContent[]): HarkBody {
|
||||
return {
|
||||
title: [ship('~hastuc-dibtux'), text(' messaged you')],
|
||||
time: unixToDa(Date.now() - 3_600).toString(),
|
||||
content,
|
||||
binned: '/',
|
||||
link: '/'
|
||||
};
|
||||
}
|
||||
|
||||
function createBitcoinNotif(amount: string) {
|
||||
return {
|
||||
title: [ship('~silnem'), text(` sent you ${amount}`)],
|
||||
time: unixToDa(Date.now() - 3_600).toString(),
|
||||
content: [],
|
||||
binned: '/',
|
||||
link: '/'
|
||||
};
|
||||
}
|
||||
|
||||
function createGroupNotif(to: string): HarkBody {
|
||||
return {
|
||||
title: [ship('~ridlur-figbud'), text(` invited you to ${to}`)],
|
||||
content: [],
|
||||
time: unixToDa(Date.now() - 3_600).toString(),
|
||||
binned: '/',
|
||||
link: '/'
|
||||
};
|
||||
}
|
||||
|
||||
window.desk = window.desk || 'garden';
|
||||
|
||||
function createMockSysNotification(path: string) {
|
||||
return {
|
||||
bin: {
|
||||
place: {
|
||||
desk: window.desk,
|
||||
path
|
||||
},
|
||||
path: '/'
|
||||
},
|
||||
time: Date.now() - 3_600,
|
||||
body: []
|
||||
};
|
||||
}
|
||||
|
||||
const lag = createMockSysNotification('/lag');
|
||||
const blocked = createMockSysNotification('/blocked');
|
||||
const onboard = createMockSysNotification('/onboard');
|
||||
|
||||
export function createMockNotification(desk: string, body: HarkBody[]): Notification {
|
||||
return {
|
||||
bin: {
|
||||
place: {
|
||||
desk,
|
||||
path: '/'
|
||||
},
|
||||
path: '/'
|
||||
},
|
||||
time: Date.now() - 3_600,
|
||||
body
|
||||
};
|
||||
}
|
||||
|
||||
export const mockNotifications: Notification[] = [
|
||||
lag,
|
||||
blocked,
|
||||
onboard,
|
||||
createMockNotification('groups', [
|
||||
createDmNotification(text('ie the hook agent responsible for marking the notifications')),
|
||||
createDmNotification(ship('~hastuc-dibtux'), text(' sent a link'))
|
||||
]),
|
||||
createMockNotification('bitcoin-wallet', [createBitcoinNotif('0.025 BTC')]),
|
||||
createMockNotification('groups', [createGroupNotif('a Group: Tlon Corporation')])
|
||||
];
|
||||
|
||||
const contact: Contact = {
|
||||
nickname: '',
|
||||
bio: '',
|
||||
@ -212,18 +307,13 @@ export const mockContacts: Contacts = {
|
||||
}
|
||||
};
|
||||
|
||||
export const mockNotification: BasicNotification = {
|
||||
type: 'basic',
|
||||
time: '',
|
||||
message: 'test'
|
||||
};
|
||||
|
||||
export const mockVat = (desk: string, blockers?: boolean): Vat => ({
|
||||
cass: {
|
||||
da: '~2021.9.13..05.41.04..ae65',
|
||||
ud: 1
|
||||
},
|
||||
desk,
|
||||
paused: false,
|
||||
arak: {
|
||||
rein: {
|
||||
sub: [],
|
||||
|
@ -1,28 +1,13 @@
|
||||
import shallow from 'zustand/shallow';
|
||||
import { useHarkStore } from './hark';
|
||||
import { Notification } from './hark-types';
|
||||
import { useBlockers, useLag } from './kiln';
|
||||
|
||||
function getSystemNotifications(lag: boolean, blockers: string[]) {
|
||||
const nots = [] as Notification[];
|
||||
if (lag) {
|
||||
nots.push({ type: 'runtime-lag' });
|
||||
}
|
||||
if (blockers.length > 0) {
|
||||
nots.push({ type: 'system-updates-blocked', desks: blockers });
|
||||
}
|
||||
return nots;
|
||||
}
|
||||
|
||||
export const useNotifications = () => {
|
||||
const notifications = useHarkStore((s) => s.notifications);
|
||||
const blockers = useBlockers();
|
||||
const lag = useLag();
|
||||
const systemNotifications = getSystemNotifications(lag, blockers);
|
||||
const hasAnyNotifications = notifications.length > 0 || systemNotifications.length > 0;
|
||||
const [unseen, seen] = useHarkStore((s) => [s.unseen, s.seen], shallow);
|
||||
const hasAnyNotifications = Object.keys(seen).length > 0 || Object.keys(unseen).length > 0;
|
||||
|
||||
return {
|
||||
notifications,
|
||||
systemNotifications,
|
||||
unseen,
|
||||
seen,
|
||||
hasAnyNotifications
|
||||
};
|
||||
};
|
||||
|
45
pkg/grid/src/styles/grids.css
Normal file
45
pkg/grid/src/styles/grids.css
Normal file
@ -0,0 +1,45 @@
|
||||
.note-grid-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5rem 1fr;
|
||||
grid-template-rows: 1.5rem 1.5rem;
|
||||
grid-gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
grid-template-areas:
|
||||
'icon title'
|
||||
'arrow head'
|
||||
'. body';
|
||||
}
|
||||
|
||||
.note-grid-no-content {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
grid-template-rows: 1.75rem 1.75rem;
|
||||
grid-template-columns: 3.5rem 1fr;
|
||||
grid-column-gap: 0.75rem;
|
||||
align-items: center;
|
||||
grid-template-areas:
|
||||
'icon title'
|
||||
'icon head';
|
||||
}
|
||||
.note-grid-title {
|
||||
grid-area: title;
|
||||
}
|
||||
|
||||
.note-grid-icon {
|
||||
grid-area: icon;
|
||||
}
|
||||
|
||||
.note-grid-body {
|
||||
grid-area: body;
|
||||
}
|
||||
|
||||
.note-grid-arrow {
|
||||
grid-area: arrow;
|
||||
}
|
||||
|
||||
.note-grid-head {
|
||||
grid-area: head;
|
||||
}
|
||||
|
||||
|
@ -5,4 +5,6 @@
|
||||
@import "./components.css";
|
||||
|
||||
@import "tailwindcss/utilities";
|
||||
@import "./utilities.css";
|
||||
@import "./utilities.css";
|
||||
|
||||
@import "./grids.css";
|
||||
|
@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { chadIsRunning } from '@urbit/api/docket';
|
||||
import { chadIsRunning } from '@urbit/api';
|
||||
import { TileMenu } from './TileMenu';
|
||||
import { Spinner } from '../components/Spinner';
|
||||
import { getAppHref } from '../state/util';
|
||||
|
@ -20,6 +20,7 @@ export default ({ mode }) => {
|
||||
mode === 'mock'
|
||||
? undefined
|
||||
: {
|
||||
https: true,
|
||||
proxy: {
|
||||
'^/apps/grid/desk.js': {
|
||||
target: SHIP_URL
|
||||
|
@ -9,12 +9,14 @@ const { execSync } = require('child_process');
|
||||
const GIT_DESC = execSync('git describe --always', { encoding: 'utf8' }).trim();
|
||||
|
||||
let devServer = {
|
||||
contentBase: path.join(__dirname, '../dist'),
|
||||
contentBase: path.join(__dirname, '../public'),
|
||||
hot: true,
|
||||
port: 9000,
|
||||
host: '0.0.0.0',
|
||||
disableHostCheck: true,
|
||||
historyApiFallback: true,
|
||||
historyApiFallback: {
|
||||
disableDotRule: true
|
||||
},
|
||||
publicPath: '/apps/landscape/'
|
||||
};
|
||||
|
||||
@ -24,17 +26,20 @@ if(urbitrc.URL) {
|
||||
devServer = {
|
||||
...devServer,
|
||||
index: 'index.html',
|
||||
proxy: [{
|
||||
changeOrigin: true,
|
||||
target: urbitrc.URL,
|
||||
router,
|
||||
context: (path) => {
|
||||
if(path === '/apps/landscape/desk.js') {
|
||||
return true;
|
||||
}
|
||||
return !path.startsWith('/apps/landscape');
|
||||
}
|
||||
}]
|
||||
proxy: [
|
||||
{
|
||||
context: (path) => {
|
||||
console.log(path);
|
||||
if(path === '/apps/landscape/desk.js') {
|
||||
return true;
|
||||
}
|
||||
return !path.startsWith('/apps/landscape');
|
||||
},
|
||||
changeOrigin: true,
|
||||
target: urbitrc.URL,
|
||||
router
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
|
29398
pkg/interface/package-lock.json
generated
29398
pkg/interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,6 +6,7 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@radix-ui/react-dialog": "^0.1.0",
|
||||
"@reach/disclosure": "^0.10.5",
|
||||
"@reach/menu-button": "^0.10.5",
|
||||
"@reach/tabs": "^0.10.5",
|
||||
@ -33,9 +34,9 @@
|
||||
"oembed-parser": "^1.4.5",
|
||||
"prop-types": "^15.7.2",
|
||||
"querystring": "^0.2.0",
|
||||
"react": "^16.14.0",
|
||||
"react": "^17.0.2",
|
||||
"react-codemirror2": "^6.0.1",
|
||||
"react-dom": "^16.14.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-markdown": "^4.3.1",
|
||||
"react-oembed-container": "^1.0.0",
|
||||
|
@ -554,3 +554,14 @@ export async function jsonFetch<T>(info: RequestInfo, init?: RequestInit): Promi
|
||||
export function clone<T>(a: T) {
|
||||
return JSON.parse(JSON.stringify(a)) as T;
|
||||
}
|
||||
|
||||
export function toHarkPath(path: string, index = '') {
|
||||
return `/graph/${path.slice(6)}${index}`;
|
||||
}
|
||||
|
||||
export function toHarkPlace(graph: string, index = '') {
|
||||
return {
|
||||
desk: (window as any).desk,
|
||||
path: toHarkPath(graph, index)
|
||||
};
|
||||
}
|
||||
|
@ -1,13 +1,11 @@
|
||||
import {
|
||||
NotificationContents,
|
||||
NotifIndex,
|
||||
Timebox
|
||||
HarkPlace,
|
||||
Timebox,
|
||||
HarkStats
|
||||
} from '@urbit/api';
|
||||
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
|
||||
import _ from 'lodash';
|
||||
import { compose } from 'lodash/fp';
|
||||
import { makePatDa } from '~/logic/lib/util';
|
||||
import { describeNotification, getReferent } from '../lib/hark';
|
||||
import { BaseState } from '../state/base';
|
||||
import { HarkState as State } from '../state/hark';
|
||||
|
||||
@ -97,18 +95,21 @@ function readAll(json: any, state: HarkState): HarkState {
|
||||
return state;
|
||||
}
|
||||
|
||||
function removeGraph(json: any, state: HarkState): HarkState {
|
||||
const data = _.get(json, 'remove-graph');
|
||||
if(data) {
|
||||
delete state.unreads.graph[data];
|
||||
}
|
||||
return state;
|
||||
const emptyStats = () => ({
|
||||
each: [],
|
||||
count: 0,
|
||||
last: 0
|
||||
});
|
||||
|
||||
function updateNotificationStats(state: HarkState, place: HarkPlace, f: (s: HarkStats) => Partial<HarkStats>) {
|
||||
const old = state.unreads?.[place.path] || emptyStats();
|
||||
state.unreads[place.path] = { ...old, ...f(old) };
|
||||
}
|
||||
|
||||
function seenIndex(json: any, state: HarkState): HarkState {
|
||||
const data = _.get(json, 'seen-index');
|
||||
if(data) {
|
||||
updateNotificationStats(state, data.index, 'last', () => data.time);
|
||||
updateNotificationStats(state, data, s => ({ last: Date.now() }));
|
||||
}
|
||||
return state;
|
||||
}
|
||||
@ -116,7 +117,8 @@ function seenIndex(json: any, state: HarkState): HarkState {
|
||||
function readEach(json: any, state: HarkState): HarkState {
|
||||
const data = _.get(json, 'read-each');
|
||||
if (data) {
|
||||
updateUnreads(state, data.index, u => u.delete(data.target));
|
||||
const { place, path } = data;
|
||||
updateNotificationStats(state, place, s => ({ each: s.each.filter(e => e !== path) }));
|
||||
}
|
||||
return state;
|
||||
}
|
||||
@ -124,7 +126,8 @@ function readEach(json: any, state: HarkState): HarkState {
|
||||
function readSince(json: any, state: HarkState): HarkState {
|
||||
const data = _.get(json, 'read-count');
|
||||
if(data) {
|
||||
updateUnreadCount(state, data, () => 0);
|
||||
console.log(data);
|
||||
updateNotificationStats(state, data, s => ({ count: 0 }));
|
||||
}
|
||||
return state;
|
||||
}
|
||||
@ -132,7 +135,9 @@ function readSince(json: any, state: HarkState): HarkState {
|
||||
function unreadSince(json: any, state: HarkState): HarkState {
|
||||
const data = _.get(json, 'unread-count');
|
||||
if (data) {
|
||||
updateUnreadCount(state, data.index, u => u + 1);
|
||||
console.log(data);
|
||||
const { inc, count, place } = data;
|
||||
updateNotificationStats(state, place, s => ({ count: inc ? s.count + count : s.count - count }));
|
||||
}
|
||||
return state;
|
||||
}
|
||||
@ -140,34 +145,17 @@ function unreadSince(json: any, state: HarkState): HarkState {
|
||||
function unreadEach(json: any, state: HarkState): HarkState {
|
||||
const data = _.get(json, 'unread-each');
|
||||
if(data) {
|
||||
updateUnreads(state, data.index, us => us.add(data.target));
|
||||
const { place, path } = data;
|
||||
updateNotificationStats(state, place, s => ({ each: [...s.each, path] }));
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function unreads(json: any, state: HarkState): HarkState {
|
||||
const data = _.get(json, 'unreads');
|
||||
if(data) {
|
||||
clearState(state);
|
||||
data.forEach(({ index, stats }) => {
|
||||
const { unreads, notifications, last } = stats;
|
||||
updateNotificationStats(state, index, 'last', () => last);
|
||||
if(index.graph.graph === '/ship/~hastuc-dibtux/test-book-7531') {
|
||||
console.log(index, stats);
|
||||
}
|
||||
_.each(notifications, ({ time, index }) => {
|
||||
if(!time) {
|
||||
addNotificationToUnread(state, index);
|
||||
}
|
||||
});
|
||||
if('count' in unreads) {
|
||||
updateUnreadCount(state, index, (u = 0) => u + unreads.count);
|
||||
} else {
|
||||
updateUnreads(state, index, s => new Set());
|
||||
unreads.each.forEach((u: string) => {
|
||||
updateUnreads(state, index, s => s.add(u));
|
||||
});
|
||||
}
|
||||
function allStats(json: any, state: HarkState): HarkState {
|
||||
if('all-stats' in json) {
|
||||
const data = json['all-stats'];
|
||||
data.forEach(({ place, stats }) => {
|
||||
state.unreads[place.path] = stats;
|
||||
});
|
||||
}
|
||||
return state;
|
||||
@ -183,10 +171,7 @@ function clearState(state: HarkState): HarkState {
|
||||
mentions: false,
|
||||
watching: []
|
||||
},
|
||||
unreads: {
|
||||
graph: {},
|
||||
group: {}
|
||||
},
|
||||
unreads: {},
|
||||
notificationsCount: 0,
|
||||
unreadNotes: {}
|
||||
};
|
||||
@ -195,98 +180,6 @@ function clearState(state: HarkState): HarkState {
|
||||
return state;
|
||||
}
|
||||
|
||||
function updateUnreadCount(state: HarkState, index: NotifIndex, count: (c: number) => number): HarkState {
|
||||
if(!('graph' in index)) {
|
||||
return state;
|
||||
}
|
||||
const property = [index.graph.graph, index.graph.index, 'unreads'];
|
||||
const curr = _.get(state.unreads.graph, property, 0);
|
||||
if(typeof curr !== 'number') {
|
||||
return state;
|
||||
}
|
||||
const newCount = count(curr);
|
||||
_.set(state.unreads.graph, property, newCount);
|
||||
return state;
|
||||
}
|
||||
|
||||
function updateUnreads(state: HarkState, index: NotifIndex, f: (us: Set<string>) => void): HarkState {
|
||||
if(!('graph' in index)) {
|
||||
return state;
|
||||
}
|
||||
const unreads: any = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set<string>());
|
||||
f(unreads);
|
||||
|
||||
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], unreads);
|
||||
return state;
|
||||
}
|
||||
|
||||
function addNotificationToUnread(state: HarkState, index: NotifIndex) {
|
||||
if('graph' in index) {
|
||||
const path = [index.graph.graph, index.graph.index, 'notifications'];
|
||||
const curr = _.get(state.unreads.graph, path, []);
|
||||
_.set(state.unreads.graph, path,
|
||||
[
|
||||
...curr.filter((c) => {
|
||||
return !(notifIdxEqual(c.index, index));
|
||||
}),
|
||||
{ index }
|
||||
]
|
||||
);
|
||||
} else if ('group' in index) {
|
||||
const path = [index.group.group, 'notifications'];
|
||||
const curr = _.get(state.unreads.group, path, []);
|
||||
_.set(state.unreads.group, path,
|
||||
[
|
||||
...curr.filter(c => !notifIdxEqual(c.index, index)),
|
||||
{ index }
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
function removeNotificationFromUnread(state: HarkState, index: NotifIndex) {
|
||||
if('graph' in index) {
|
||||
const path = [index.graph.graph, index.graph.index, 'notifications'];
|
||||
const curr = _.get(state.unreads.graph, path, []);
|
||||
_.set(state.unreads.graph, path, curr.filter(c => !notifIdxEqual(c.index, index)));
|
||||
} else if ('group' in index) {
|
||||
const path = [index.group.group, 'notifications'];
|
||||
const curr = _.get(state.unreads.group, path, []);
|
||||
_.set(state.unreads.group, path, curr.filter(c => !notifIdxEqual(c.index, index)));
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
} else if('group' in index) {
|
||||
const curr: any = _.get(state.unreads.group, [index.group.group, statField], 0);
|
||||
_.set(state.unreads.group, [index.group.group, statField], f(curr));
|
||||
}
|
||||
}
|
||||
|
||||
function added(json: any, state: HarkState): HarkState {
|
||||
const data = _.get(json, 'added', false);
|
||||
if (data) {
|
||||
const { index, notification } = data;
|
||||
const [fresh] = _.partition(state.unreadNotes, ({ index: idx }) => !notifIdxEqual(index, idx));
|
||||
state.unreadNotes = [...fresh, { index, notification }];
|
||||
|
||||
if ('Notification' in window && !state.doNotDisturb) {
|
||||
const description = describeNotification(data);
|
||||
const referent = getReferent(data);
|
||||
new Notification(`${description} ${referent}`, {
|
||||
tag: 'landscape',
|
||||
image: '/img/favicon.png',
|
||||
icon: '/img/favicon.png',
|
||||
badge: '/img/favicon.png',
|
||||
renotify: true
|
||||
});
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
const dnd = (json: any, state: HarkState): HarkState => {
|
||||
const data = _.get(json, 'set-dnd', undefined);
|
||||
if (!_.isUndefined(data)) {
|
||||
@ -295,22 +188,6 @@ const dnd = (json: any, state: HarkState): HarkState => {
|
||||
return state;
|
||||
};
|
||||
|
||||
const timebox = (json: any, state: HarkState): HarkState => {
|
||||
const data = _.get(json, 'timebox', false);
|
||||
if (data) {
|
||||
if (data.time) {
|
||||
const time = makePatDa(data.time);
|
||||
state.notifications = state.notifications.set(time, data.notifications);
|
||||
} else {
|
||||
state.unreadNotes = data.notifications;
|
||||
_.each(data.notifications, ({ index }) => {
|
||||
addNotificationToUnread(state, index);
|
||||
});
|
||||
}
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
function more(json: any, state: HarkState): HarkState {
|
||||
const data = _.get(json, 'more', false);
|
||||
if (data) {
|
||||
@ -321,98 +198,17 @@ function more(json: any, state: HarkState): HarkState {
|
||||
return state;
|
||||
}
|
||||
|
||||
function notifIdxEqual(a: NotifIndex, b: NotifIndex) {
|
||||
if ('graph' in a && 'graph' in b) {
|
||||
return (
|
||||
a.graph.graph === b.graph.graph &&
|
||||
a.graph.group === b.graph.group &&
|
||||
a.graph.mark === b.graph.mark &&
|
||||
a.graph.description === b.graph.description
|
||||
);
|
||||
} else if ('group' in a && 'group' in b) {
|
||||
return (
|
||||
a.group.group === b.group.group &&
|
||||
a.group.description === b.group.description
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function mergeNotifs(a: NotificationContents, b: NotificationContents) {
|
||||
if ('graph' in a && 'graph' in b) {
|
||||
return {
|
||||
graph: [...a.graph, ...b.graph]
|
||||
};
|
||||
} else if ('group' in a && 'group' in b) {
|
||||
return {
|
||||
group: [...a.group, ...b.group]
|
||||
};
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
function read(json: any, state: HarkState): HarkState {
|
||||
const data = _.get(json, 'note-read', false);
|
||||
if (data) {
|
||||
const { index } = data;
|
||||
const time = makePatDa(data.time);
|
||||
const [read, unread] = _.partition(state.unreadNotes,({ index: idx }) => notifIdxEqual(index, idx));
|
||||
state.unreadNotes = unread;
|
||||
const oldTimebox = state.notifications.get(time) ?? [];
|
||||
const [toMerge, rest] = _.partition(oldTimebox, i => notifIdxEqual(index, i.index));
|
||||
if(toMerge.length > 0 && read.length > 0) {
|
||||
read[0].notification.contents = mergeNotifs(read[0].notification.contents, toMerge[0].notification.contents);
|
||||
}
|
||||
state.notifications = state.notifications.set(time, [...read, ...rest]);
|
||||
removeNotificationFromUnread(state, index);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function archive(json: any, state: HarkState): HarkState {
|
||||
const data = _.get(json, 'archive', false);
|
||||
if (data) {
|
||||
const { index } = data;
|
||||
if(data.time) {
|
||||
const time = makePatDa(data.time);
|
||||
const timebox = state.notifications.get(time);
|
||||
if (!timebox) {
|
||||
console.warn('Modifying nonexistent timebox');
|
||||
return state;
|
||||
}
|
||||
const unarchived = _.filter(timebox, idxNotif =>
|
||||
!notifIdxEqual(index, idxNotif.index)
|
||||
);
|
||||
if(unarchived.length === 0) {
|
||||
console.log('deleting entire timebox');
|
||||
state.notifications = state.notifications.delete(time);
|
||||
} else {
|
||||
state.notifications = state.notifications.set(time, unarchived);
|
||||
}
|
||||
} else {
|
||||
state.unreadNotes = state.unreadNotes.filter(({ index: idx }) => !notifIdxEqual(idx, index));
|
||||
removeNotificationFromUnread(state, index);
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
export function reduce(data, state) {
|
||||
const reducers = [
|
||||
calculateCount,
|
||||
read,
|
||||
archive,
|
||||
timebox,
|
||||
allStats,
|
||||
more,
|
||||
dnd,
|
||||
added,
|
||||
unreads,
|
||||
readEach,
|
||||
readSince,
|
||||
unreadSince,
|
||||
unreadEach,
|
||||
seenIndex,
|
||||
removeGraph,
|
||||
readAll
|
||||
];
|
||||
const reducer = compose(reducers.map(r => (s) => {
|
||||
|
@ -1,10 +1,10 @@
|
||||
import {
|
||||
archive,
|
||||
HarkBin,
|
||||
markCountAsRead,
|
||||
Notification,
|
||||
NotificationGraphConfig,
|
||||
NotifIndex,
|
||||
readNote,
|
||||
Timebox,
|
||||
Unreads
|
||||
} from '@urbit/api';
|
||||
import { Poke } from '@urbit/http-api';
|
||||
@ -12,58 +12,68 @@ import { patp2dec } from 'urbit-ob';
|
||||
import _ from 'lodash';
|
||||
import BigIntOrderedMap from '@urbit/api/lib/BigIntOrderedMap';
|
||||
import api from '~/logic/api';
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { createState, createSubscription, pokeOptimisticallyN, reduceState, reduceStateN } from './base';
|
||||
import {
|
||||
createState,
|
||||
createSubscription,
|
||||
pokeOptimisticallyN,
|
||||
reduceState,
|
||||
reduceStateN
|
||||
} from './base';
|
||||
import { reduce, reduceGraph, reduceGroup } from '../reducers/hark-update';
|
||||
import { BigInteger } from 'big-integer';
|
||||
|
||||
export const HARK_FETCH_MORE_COUNT = 3;
|
||||
|
||||
export interface HarkState {
|
||||
archivedNotifications: BigIntOrderedMap<Timebox>;
|
||||
archivedNotifications: BigIntOrderedMap<Notification[]>;
|
||||
doNotDisturb: boolean;
|
||||
poke: (poke: Poke<any>) => Promise<void>;
|
||||
getMore: () => Promise<boolean>;
|
||||
getSubset: (offset: number, count: number, isArchive: boolean) => Promise<void>;
|
||||
getSubset: (
|
||||
offset: number,
|
||||
count: number,
|
||||
isArchive: boolean
|
||||
) => Promise<void>;
|
||||
// getTimeSubset: (start?: Date, end?: Date) => Promise<void>;
|
||||
notifications: BigIntOrderedMap<Timebox>;
|
||||
unreadNotes: Timebox;
|
||||
notifications: BigIntOrderedMap<Notification[]>;
|
||||
unreadNotes: Notification[];
|
||||
notificationsCount: number;
|
||||
notificationsGraphConfig: NotificationGraphConfig; // TODO unthread this everywhere
|
||||
notificationsGroupConfig: string[];
|
||||
unreads: Unreads;
|
||||
archive: (index: NotifIndex, time?: BigInteger) => Promise<void>;
|
||||
readNote: (index: NotifIndex) => Promise<void>;
|
||||
readCount: (resource: string, index?: string) => Promise<void>;
|
||||
archive: (bin: HarkBin, time?: BigInteger) => Promise<void>;
|
||||
readNote: (bin: HarkBin) => Promise<void>;
|
||||
readCount: (path: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const useHarkState = createState<HarkState>(
|
||||
'Hark',
|
||||
(set, get) => ({
|
||||
archivedNotifications: new BigIntOrderedMap<Timebox>(),
|
||||
archivedNotifications: new BigIntOrderedMap<Notification[]>(),
|
||||
doNotDisturb: false,
|
||||
unreadNotes: [],
|
||||
poke: async (poke: Poke<any>) => {
|
||||
await pokeOptimisticallyN(useHarkState, poke, [reduce]);
|
||||
},
|
||||
readCount: async (resource: string, index?: string) => {
|
||||
const poke = markCountAsRead(resource, index);
|
||||
readCount: async (path) => {
|
||||
const poke = markCountAsRead({ desk: (window as any).desk, path });
|
||||
await pokeOptimisticallyN(useHarkState, poke, [reduce]);
|
||||
},
|
||||
archive: async (index: NotifIndex, time?: BigInteger) => {
|
||||
const poke = archive(index, time);
|
||||
archive: async (bin: HarkBin, time?: BigInteger) => {
|
||||
const poke = archive(bin, time);
|
||||
await pokeOptimisticallyN(useHarkState, poke, [reduce]);
|
||||
},
|
||||
readNote: async (index) => {
|
||||
await pokeOptimisticallyN(useHarkState, readNote(index), [reduce]);
|
||||
readNote: async (bin) => {
|
||||
await pokeOptimisticallyN(useHarkState, readNote(bin), [reduce]);
|
||||
},
|
||||
getMore: async (): Promise<boolean> => {
|
||||
const state = get();
|
||||
const offset = state.notifications.size || 0;
|
||||
await state.getSubset(offset, HARK_FETCH_MORE_COUNT, false);
|
||||
const newState = get();
|
||||
return offset === (newState?.notifications?.size || 0);
|
||||
const state = get();
|
||||
const offset = state.notifications.size || 0;
|
||||
await state.getSubset(offset, HARK_FETCH_MORE_COUNT, false);
|
||||
const newState = get();
|
||||
return offset === (newState?.notifications?.size || 0);
|
||||
},
|
||||
getSubset: async (offset, count, isArchive): Promise<void> => {
|
||||
const where = isArchive ? 'archive' : 'inbox';
|
||||
@ -73,7 +83,7 @@ const useHarkState = createState<HarkState>(
|
||||
});
|
||||
reduceState(useHarkState, harkUpdate, [reduce]);
|
||||
},
|
||||
notifications: new BigIntOrderedMap<Timebox>(),
|
||||
notifications: new BigIntOrderedMap<Notification[]>(),
|
||||
notificationsCount: 0,
|
||||
notificationsGraphConfig: {
|
||||
watchOnSelf: false,
|
||||
@ -81,10 +91,7 @@ const useHarkState = createState<HarkState>(
|
||||
watching: []
|
||||
},
|
||||
notificationsGroupConfig: [],
|
||||
unreads: {
|
||||
graph: {},
|
||||
group: {}
|
||||
}
|
||||
unreads: {}
|
||||
}),
|
||||
[
|
||||
'unreadNotes',
|
||||
@ -94,38 +101,69 @@ const useHarkState = createState<HarkState>(
|
||||
'notificationsCount'
|
||||
],
|
||||
[
|
||||
(set, get) => createSubscription('hark-store', '/updates', (j) => {
|
||||
const d = _.get(j, 'harkUpdate', false);
|
||||
if (d) {
|
||||
(set, get) =>
|
||||
createSubscription('hark-store', '/updates', (d) => {
|
||||
reduceStateN(get(), d, [reduce]);
|
||||
}
|
||||
}),
|
||||
(set, get) => createSubscription('hark-graph-hook', '/updates', (j) => {
|
||||
const graphHookData = _.get(j, 'hark-graph-hook-update', false);
|
||||
if (graphHookData) {
|
||||
reduceStateN(get(), graphHookData, reduceGraph);
|
||||
}
|
||||
}),
|
||||
(set, get) => createSubscription('hark-group-hook', '/updates', (j) => {
|
||||
const data = _.get(j, 'hark-group-hook-update', false);
|
||||
if (data) {
|
||||
reduceStateN(get(), data, reduceGroup);
|
||||
}
|
||||
})
|
||||
}),
|
||||
(set, get) =>
|
||||
createSubscription('hark-graph-hook', '/updates', (j) => {
|
||||
const graphHookData = _.get(j, 'hark-graph-hook-update', false);
|
||||
if (graphHookData) {
|
||||
reduceStateN(get(), graphHookData, reduceGraph);
|
||||
}
|
||||
}),
|
||||
(set, get) =>
|
||||
createSubscription('hark-group-hook', '/updates', (j) => {
|
||||
const data = _.get(j, 'hark-group-hook-update', false);
|
||||
if (data) {
|
||||
reduceStateN(get(), data, reduceGroup);
|
||||
}
|
||||
})
|
||||
]
|
||||
);
|
||||
|
||||
const emptyStats = () => ({
|
||||
last: 0,
|
||||
count: 0,
|
||||
each: []
|
||||
});
|
||||
|
||||
export function useHarkDm(ship: string) {
|
||||
return useHarkState(
|
||||
useCallback(
|
||||
(s) => {
|
||||
return s.unreads.graph[`/ship/~${window.ship}/dm-inbox`]?.[
|
||||
return s.unreads[`/graph/~${window.ship}/dm-inbox`]?.[
|
||||
`/${patp2dec(ship)}`
|
||||
];
|
||||
] || emptyStats();
|
||||
},
|
||||
[ship]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function useHarkStat(path: string) {
|
||||
return useHarkState(
|
||||
useCallback(s => s.unreads[path] || emptyStats(), [path])
|
||||
);
|
||||
}
|
||||
|
||||
export function useHarkGraph(graph: string) {
|
||||
const [, ship, name] = useMemo(() => graph.split('/'), [graph]);
|
||||
return useHarkState(
|
||||
useCallback(s => s.unreads[`/graph/${ship}/${name}`], [ship, name])
|
||||
);
|
||||
}
|
||||
|
||||
export function useHarkGraphIndex(graph: string, index: string) {
|
||||
const [, ship, name] = useMemo(() => graph.split('/'), [graph]);
|
||||
return useHarkState(
|
||||
useCallback(s => s.unreads[`/graph/${ship}/${name}/index`], [
|
||||
ship,
|
||||
name,
|
||||
index
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
window.hark = useHarkState.getState;
|
||||
export default useHarkState;
|
||||
|
@ -11,12 +11,13 @@ import { isWriter, resourceFromPath } from '~/logic/lib/group';
|
||||
import { getPermalinkForGraph } from '~/logic/lib/permalinks';
|
||||
import useGraphState, { useGraphForAssoc } from '~/logic/state/graph';
|
||||
import { useGroupForAssoc } from '~/logic/state/group';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
import useHarkState, { useHarkStat } from '~/logic/state/hark';
|
||||
import { Loading } from '~/views/components/Loading';
|
||||
import { ChatPane } from './components/ChatPane';
|
||||
import airlock from '~/logic/api';
|
||||
import { disallowedShipsForOurContact } from '~/logic/lib/contact';
|
||||
import shallow from 'zustand/shallow';
|
||||
import { toHarkPath } from '~/logic/lib/util';
|
||||
|
||||
const getCurrGraphSize = (ship: string, name: string) => {
|
||||
const { graphs } = useGraphState.getState();
|
||||
@ -35,9 +36,8 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
|
||||
const [toShare, setToShare] = useState<string[] | string | undefined>();
|
||||
const group = useGroupForAssoc(association)!;
|
||||
const graph = useGraphForAssoc(association);
|
||||
const unreads = useHarkState(state => state.unreads);
|
||||
const unreadCount =
|
||||
(unreads.graph?.[resource]?.['/']?.unreads as number) || 0;
|
||||
const stats = useHarkStat(toHarkPath(association.resource));
|
||||
const unreadCount = stats.count;
|
||||
const canWrite = group ? isWriter(group, resource) : false;
|
||||
const [
|
||||
getNewest,
|
||||
@ -94,8 +94,8 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
|
||||
[group]
|
||||
);
|
||||
|
||||
const fetchMessages = useCallback(async (newer: boolean) => {
|
||||
const pageSize = 100;
|
||||
const fetchMessages = useCallback(async (newer: boolean) => {
|
||||
const pageSize = 100;
|
||||
|
||||
const [, , ship, name] = resource.split('/');
|
||||
const graphSize = graph?.size ?? 0;
|
||||
@ -140,7 +140,7 @@ const ChatResource = (props: ChatResourceProps): ReactElement => {
|
||||
}, [resource]);
|
||||
|
||||
const dismissUnread = useCallback(() => {
|
||||
useHarkState.getState().readCount(association.resource);
|
||||
useHarkState.getState().readCount(toHarkPath(association.resource));
|
||||
}, [association.resource]);
|
||||
|
||||
const getPermalink = useCallback(
|
||||
|
@ -52,7 +52,7 @@ export function DmResource(props: DmResourceProps) {
|
||||
const { ship } = props;
|
||||
const dm = useDM(ship);
|
||||
const hark = useHarkDm(ship);
|
||||
const unreadCount = (hark?.unreads as number) ?? 0;
|
||||
const unreadCount = hark.count;
|
||||
const contact = useContact(ship);
|
||||
const { hideNicknames } = useSettingsState(selectCalmState);
|
||||
const showNickname = !hideNicknames && Boolean(contact);
|
||||
@ -111,8 +111,8 @@ export function DmResource(props: DmResourceProps) {
|
||||
);
|
||||
|
||||
const dismissUnread = useCallback(() => {
|
||||
const resource = `/ship/~${window.ship}/dm-inbox`;
|
||||
useHarkState.getState().readCount(resource, `/${patp2dec(ship)}`);
|
||||
const harkPath = `/graph/~${window.ship}/dm-inbox/${patp2dec(ship)}`;
|
||||
useHarkState.getState().readCount(harkPath);
|
||||
}, [ship]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
|
@ -3,6 +3,7 @@ import { Box, Button, Col, Icon, Row, Text } from '@tlon/indigo-react';
|
||||
import f from 'lodash/fp';
|
||||
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Route, useHistory } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
hasTutorialGroup,
|
||||
@ -30,6 +31,7 @@ import Groups from './components/Groups';
|
||||
import ModalButton from './components/ModalButton';
|
||||
import Tiles from './components/tiles';
|
||||
import Tile from './components/tiles/tile';
|
||||
import { Invite } from './components/Invite';
|
||||
import './css/custom.css';
|
||||
import { join } from '@urbit/api/groups';
|
||||
import { joinGraph } from '@urbit/api/graph';
|
||||
@ -185,6 +187,9 @@ export const LaunchApp = (props: LaunchAppProps): ReactElement | null => {
|
||||
<Helmet defer={false}>
|
||||
<title>{ notificationsCount ? `(${String(notificationsCount) }) `: '' }Landscape</title>
|
||||
</Helmet>
|
||||
<Route path="/invites/:app/:uid">
|
||||
<Invite />
|
||||
</Route>
|
||||
<ScrollbarLessBox height='100%' overflowY='scroll' display="flex" flexDirection="column">
|
||||
{modal}
|
||||
<Box
|
||||
|
242
pkg/interface/src/views/apps/launch/components/Invite.tsx
Normal file
242
pkg/interface/src/views/apps/launch/components/Invite.tsx
Normal file
@ -0,0 +1,242 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { useHistory, useParams } from "react-router-dom";
|
||||
import {
|
||||
Box,
|
||||
Col,
|
||||
Row,
|
||||
Text,
|
||||
Button,
|
||||
Action,
|
||||
LoadingSpinner,
|
||||
} from "@tlon/indigo-react";
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { PropFunc } from "~/types";
|
||||
import { useRunIO } from "~/logic/lib/useRunIO";
|
||||
import useMetadataState from "~/logic/state/metadata";
|
||||
import { GroupSummary } from "~/views/landscape/components/GroupSummary";
|
||||
import {
|
||||
accept,
|
||||
decline,
|
||||
GraphConfig,
|
||||
Invite as IInvite,
|
||||
join,
|
||||
Metadata,
|
||||
MetadataUpdatePreview,
|
||||
resourceFromPath,
|
||||
} from "@urbit/api";
|
||||
import useInviteState from "~/logic/state/invite";
|
||||
import useGroupState from "~/logic/state/group";
|
||||
import useGraphState from "~/logic/state/graph";
|
||||
import { useWaitForProps } from "~/logic/lib/useWaitForProps";
|
||||
import airlock from "~/logic/api";
|
||||
|
||||
function InviteDialog({ children, ...rest }: PropFunc<typeof Col>) {
|
||||
return (
|
||||
<Dialog.Root open>
|
||||
<Dialog.Overlay asChild>
|
||||
<Box
|
||||
backgroundColor="scales.black20"
|
||||
left="0px"
|
||||
top="0px"
|
||||
width="100%"
|
||||
height="100%"
|
||||
position="fixed"
|
||||
display="flex"
|
||||
zIndex={10}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
p="4"
|
||||
></Box>
|
||||
</Dialog.Overlay>
|
||||
<Dialog.DialogContent asChild>
|
||||
<Col
|
||||
left="0px"
|
||||
top="0px"
|
||||
width="100%"
|
||||
height="100%"
|
||||
position="fixed"
|
||||
zIndex={10}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<Col
|
||||
p="4"
|
||||
gapY="4"
|
||||
border="1"
|
||||
borderColor="lightGray"
|
||||
backgroundColor="white"
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Col>
|
||||
</Col>
|
||||
</Dialog.DialogContent>
|
||||
</Dialog.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function inviteUrl(
|
||||
hidden: boolean,
|
||||
resource: string,
|
||||
metadata?: Metadata<GraphConfig>
|
||||
) {
|
||||
if (!hidden) {
|
||||
return `/~landscape${resource}`;
|
||||
}
|
||||
|
||||
if (metadata.config?.graph === "chat") {
|
||||
return `/~landscape/messages/resource/${metadata.config.graph}${resource}`;
|
||||
} else {
|
||||
return `/~landscape/home/resource/${metadata.config?.graph}${resource}`;
|
||||
}
|
||||
}
|
||||
|
||||
function useInviteAccept(resource: string, app?: string, uid?: string) {
|
||||
const { ship, name } = resourceFromPath(resource);
|
||||
const history = useHistory();
|
||||
const associations = useMetadataState((s) => s.associations);
|
||||
const groups = useGroupState((s) => s.groups);
|
||||
const graphKeys = useGraphState((s) => s.graphKeys);
|
||||
|
||||
const waiter = useWaitForProps({ associations, graphKeys, groups });
|
||||
return useRunIO<void, boolean>(
|
||||
async () => {
|
||||
if (!(app && uid)) {
|
||||
return false;
|
||||
}
|
||||
if (resource in groups) {
|
||||
await airlock.poke(decline(app, uid));
|
||||
return false;
|
||||
}
|
||||
|
||||
await airlock.poke(join(ship, name));
|
||||
await airlock.poke(accept(app, uid));
|
||||
await waiter((p) => {
|
||||
return (
|
||||
(resource in p.groups &&
|
||||
resource in (p.associations?.graph ?? {}) &&
|
||||
p.graphKeys.has(resource.slice(7))) ||
|
||||
resource in (p.associations?.groups ?? {})
|
||||
);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
(success: boolean) => {
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
const redir = inviteUrl(
|
||||
groups?.[resource]?.hidden,
|
||||
resource,
|
||||
associations?.graph?.[resource]?.metadata
|
||||
);
|
||||
if (redir) {
|
||||
// weird race condition
|
||||
setTimeout(() => {
|
||||
history.push(redir);
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
resource
|
||||
);
|
||||
}
|
||||
|
||||
export function Invite() {
|
||||
const { uid, app } = useParams<{
|
||||
app: string;
|
||||
uid: string;
|
||||
}>();
|
||||
|
||||
const invite = useInviteState((s) => s.invites?.[app]?.[uid]);
|
||||
|
||||
return (
|
||||
<InviteDialog>
|
||||
{!!invite ? (
|
||||
<>
|
||||
{renderInviteContent(app, uid, invite)}
|
||||
<InviteActions app={app} uid={uid} invite={invite} />
|
||||
</>
|
||||
) : (
|
||||
<LoadingSpinner />
|
||||
)}
|
||||
</InviteDialog>
|
||||
);
|
||||
}
|
||||
|
||||
function InviteActions({
|
||||
app,
|
||||
uid,
|
||||
invite,
|
||||
}: {
|
||||
app: string;
|
||||
uid: string;
|
||||
invite: IInvite;
|
||||
}) {
|
||||
const { push } = useHistory();
|
||||
const { ship, name } = invite.resource;
|
||||
const resource = `/ship/~${ship}/${name}`;
|
||||
|
||||
const inviteAccept = useInviteAccept(resource, app, uid);
|
||||
const acceptInvite = () => inviteAccept();
|
||||
|
||||
const declineInvite = useCallback(async () => {
|
||||
if (!(app && uid)) {
|
||||
return;
|
||||
}
|
||||
await airlock.poke(decline(app, uid));
|
||||
push('/');
|
||||
}, [app, uid]);
|
||||
|
||||
return (
|
||||
<Row gapX="4">
|
||||
<Action onClick={acceptInvite} backgroundColor="white" color="blue">
|
||||
Accept
|
||||
</Action>
|
||||
<Action onClick={declineInvite} backgroundColor="white" destructive>
|
||||
Decline
|
||||
</Action>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
function GroupInvite({ uid, invite }: { uid: string; invite: IInvite }) {
|
||||
const {
|
||||
resource: { ship, name },
|
||||
} = invite;
|
||||
const { associations, getPreview } = useMetadataState();
|
||||
const [preview, setPreview] = useState<MetadataUpdatePreview | null>(null);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setPreview(await getPreview(`/ship/~${ship}/${name}`));
|
||||
})();
|
||||
|
||||
return () => {
|
||||
setPreview(null);
|
||||
};
|
||||
}, [ship, name]);
|
||||
|
||||
return preview ? (
|
||||
<GroupSummary
|
||||
metadata={preview.metadata}
|
||||
channelCount={preview["channel-count"]}
|
||||
memberCount={preview["members"]}
|
||||
/>
|
||||
) : (
|
||||
<LoadingSpinner />
|
||||
);
|
||||
}
|
||||
|
||||
function GraphInvite({ uid, invite }: { uid: string; invite: IInvite }) {
|
||||
return <Text>You have been invited to a group chat by {invite.ship}</Text>;
|
||||
}
|
||||
|
||||
function renderInviteContent(app: string, uid: string, invite: IInvite) {
|
||||
switch (app) {
|
||||
case "groups":
|
||||
return <GroupInvite uid={uid} invite={invite} />;
|
||||
case "graph":
|
||||
return <GraphInvite uid={uid} invite={invite} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
import { Col, Row, RowProps } from '@tlon/indigo-react';
|
||||
import { Association, GraphNode, TextContent, UrlContent } from '@urbit/api';
|
||||
import React from 'react';
|
||||
import { Association, GraphNode, markEachAsRead, TextContent, UrlContent } from '@urbit/api';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useGroup } from '~/logic/state/group';
|
||||
import Author from '~/views/components/Author';
|
||||
import Comments from '~/views/components/Comments';
|
||||
import { TruncatedText } from '~/views/components/TruncatedText';
|
||||
import { LinkBlockItem } from './LinkBlockItem';
|
||||
import airlock from '~/logic/api';
|
||||
import { toHarkPlace } from '~/logic/lib/util';
|
||||
|
||||
export interface LinkDetailProps extends RowProps {
|
||||
node: GraphNode;
|
||||
@ -17,6 +19,10 @@ export function LinkDetail(props: LinkDetailProps) {
|
||||
const { node, association, baseUrl, ...rest } = props;
|
||||
const group = useGroup(association.group);
|
||||
const { post } = node;
|
||||
|
||||
useEffect(() => {
|
||||
airlock.poke(markEachAsRead(toHarkPlace(association.resource), node.post.index));
|
||||
}, [association, node]);
|
||||
const [{ text: title }] = post.contents as [TextContent, UrlContent];
|
||||
return (
|
||||
/* @ts-ignore indio props?? */
|
||||
|
@ -5,7 +5,7 @@ import { Link, Redirect } from 'react-router-dom';
|
||||
import { roleForShip } from '~/logic/lib/group';
|
||||
import { getPermalinkForGraph, referenceToPermalink } from '~/logic/lib/permalinks';
|
||||
import { useCopy } from '~/logic/lib/useCopy';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
import { useHarkStat } from '~/logic/state/hark';
|
||||
import Author from '~/views/components/Author';
|
||||
import { Dropdown } from '~/views/components/Dropdown';
|
||||
import RemoteContent from '~/views/components/RemoteContent';
|
||||
@ -40,8 +40,12 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<H
|
||||
}, []);
|
||||
const index = node.post.index.split('/')[1];
|
||||
|
||||
const [ship, name] = resource.split('/');
|
||||
const harkPath = `/graph/~${ship}/${name}`;
|
||||
const markRead = useCallback(() => {
|
||||
airlock.poke(markEachAsRead(resource, '/', `/${index}`));
|
||||
airlock.poke(
|
||||
markEachAsRead({ desk: (window as any).desk, path: harkPath }, `/${index}`)
|
||||
);
|
||||
}, [resource, index]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -74,7 +78,6 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<H
|
||||
const baseUrl = props.baseUrl || `/~404/${resource}`;
|
||||
|
||||
const ourRole = group ? roleForShip(group, window.ship) : undefined;
|
||||
const [ship, name] = resource.split('/');
|
||||
|
||||
const permalink = getPermalinkForGraph(
|
||||
association.group,
|
||||
@ -98,11 +101,10 @@ export const LinkItem = React.forwardRef((props: LinkItemProps, ref: RefObject<H
|
||||
}
|
||||
};
|
||||
|
||||
const appPath = `/ship/~${resource}`;
|
||||
const unreads = useHarkState(state => state.unreads?.[appPath]);
|
||||
const commColor = (unreads?.[`/${index}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray';
|
||||
// @ts-ignore hark will have to choose between sets and numbers
|
||||
const isUnread = unreads?.['/']?.unreads?.has?.(node.post.index);
|
||||
const linkStats = useHarkStat(harkPath);
|
||||
const commStats = useHarkStat(`${harkPath}/${index}`);
|
||||
const commColor = commStats.count > 0 ? 'blue' : 'gray';
|
||||
const isUnread = linkStats.each.includes(`/${index}`);
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
@ -95,19 +95,10 @@ export default function Inbox(props: {
|
||||
})
|
||||
);
|
||||
|
||||
const scrollRef = useRef(null);
|
||||
|
||||
const { isDone, isLoading } = useLazyScroll(
|
||||
scrollRef,
|
||||
ready,
|
||||
0.2,
|
||||
_.flatten(notifications).length,
|
||||
getMore
|
||||
);
|
||||
const date = unixToDa(Date.now());
|
||||
|
||||
return (
|
||||
<Col p={1} ref={scrollRef} position="relative" height="100%" overflowY="auto" overflowX="hidden">
|
||||
<Col p={1} 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" />
|
||||
@ -117,28 +108,6 @@ export default function Inbox(props: {
|
||||
</Box>
|
||||
)}
|
||||
<Invites pendingJoin={props.pendingJoin} />
|
||||
<DaySection unread key="unread" timeboxes={[[date,unreadNotes]]} />
|
||||
{[...notificationsByDayMap.keys()].sort().reverse().map((day, index) => {
|
||||
const timeboxes = notificationsByDayMap.get(day)!;
|
||||
return timeboxes.length > 0 && (
|
||||
<DaySection
|
||||
key={day}
|
||||
timeboxes={timeboxes}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{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>
|
||||
) : isLoading ? (
|
||||
<Center mt={2} borderTop={notifications.length !== 0 ? 1 : 0} borderTopColor="lightGray" width="100%" height="96px">
|
||||
<LoadingSpinner />
|
||||
</Center>
|
||||
) : (
|
||||
<Box mt={2} height="96px" />
|
||||
)}
|
||||
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import { Spinner } from '~/views/components/Spinner';
|
||||
import { GraphContent } from '~/views/landscape/components/Graph/GraphContent';
|
||||
import { NoteNavigation } from './NoteNavigation';
|
||||
import airlock from '~/logic/api';
|
||||
import { toHarkPlace } from '~/logic/lib/util';
|
||||
|
||||
interface NoteProps {
|
||||
ship: string;
|
||||
@ -59,8 +60,8 @@ export function Note(props: NoteProps & RouteComponentProps) {
|
||||
|
||||
const noteId = bigInt(index[1]);
|
||||
useEffect(() => {
|
||||
airlock.poke(markEachAsRead(props.association.resource, '/',`/${index[1]}/1/1`));
|
||||
}, [props.association, props.note]);
|
||||
airlock.poke(markEachAsRead(toHarkPlace(association.resource), `/${index[1]}`));
|
||||
}, [association, props.note]);
|
||||
|
||||
const adminLinks: JSX.Element[] = [];
|
||||
const ourRole = roleForShip(group, window.ship);
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
getLatestRevision,
|
||||
getSnippet
|
||||
} from '~/logic/lib/publish';
|
||||
import useHarkState from '~/logic/state/hark';
|
||||
import { useHarkStat } from '~/logic/state/hark';
|
||||
import Author from '~/views/components/Author';
|
||||
|
||||
interface NotePreviewProps {
|
||||
@ -67,14 +67,15 @@ export function NotePreview(props: NotePreviewProps) {
|
||||
const url = `${props.baseUrl}/note/${noteId}`;
|
||||
|
||||
const [, title, body] = getLatestRevision(node);
|
||||
const appPath = `/ship/${props.host}/${props.book}`;
|
||||
const unreads = useHarkState(state => state.unreads.graph?.[appPath]);
|
||||
const harkPath = `/graph/${props.host}/${props.book}`;
|
||||
const bookStats = useHarkStat(harkPath);
|
||||
const noteStats = useHarkStat(`${harkPath}/${noteId}`);
|
||||
// @ts-ignore hark will have to choose between sets and numbers
|
||||
const isUnread = (unreads?.['/'].unreads ?? new Set()).has(`/${noteId}/1/1`);
|
||||
const isUnread = bookStats.each.includes(`/${noteId}`);
|
||||
|
||||
const snippet = getSnippet(body);
|
||||
|
||||
const commColor = (unreads?.[`/${noteId}`]?.unreads ?? 0) > 0 ? 'blue' : 'gray';
|
||||
const commColor = noteStats.count > 0 ? 'blue' : 'gray';
|
||||
|
||||
const cursorStyle = post.pending ? 'default' : 'pointer';
|
||||
|
||||
|
@ -25,6 +25,7 @@ import { CommentItem } from './CommentItem';
|
||||
import airlock from '~/logic/api';
|
||||
import useGraphState from '~/logic/state/graph';
|
||||
import { useHistory } from 'react-router';
|
||||
import { toHarkPlace } from '~/logic/lib/util';
|
||||
|
||||
interface CommentsProps {
|
||||
comments: GraphNode;
|
||||
@ -126,10 +127,11 @@ export function Comments(props: CommentsProps & PropFunc<typeof Col>) {
|
||||
const children = Array.from(comments.children);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(parentIndex);
|
||||
return () => {
|
||||
airlock.poke(markCountAsRead(association.resource));
|
||||
airlock.poke(markCountAsRead(toHarkPlace(association.resource, parentIndex)));
|
||||
};
|
||||
}, [comments.post?.index]);
|
||||
}, [comments.post?.index, association.resource]);
|
||||
|
||||
const unreads = useHarkState(state => state.unreads);
|
||||
const readCount = children.length - getUnreadCount(unreads, association.resource, parentIndex);
|
||||
|
@ -22,6 +22,7 @@ import { ProfileStatus } from './ProfileStatus';
|
||||
import ReconnectButton from './ReconnectButton';
|
||||
import { StatusBarItem } from './StatusBarItem';
|
||||
import { useTutorialModal } from './useTutorialModal';
|
||||
import {StatusBarJoins} from './StatusBarJoins';
|
||||
|
||||
const localSel = selectLocalState(['toggleOmnibox']);
|
||||
|
||||
@ -104,6 +105,7 @@ const StatusBar = (props) => {
|
||||
{metaKey}/
|
||||
</Text>
|
||||
</StatusBarItem>
|
||||
<StatusBarJoins />
|
||||
<ReconnectButton />
|
||||
</Row>
|
||||
<Row justifyContent='flex-end'>
|
||||
|
116
pkg/interface/src/views/components/StatusBarJoins.tsx
Normal file
116
pkg/interface/src/views/components/StatusBarJoins.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
import { LoadingSpinner } from "@tlon/indigo-react";
|
||||
import React, { useState } from "react";
|
||||
import { Box, Row, Col, Text } from "@tlon/indigo-react";
|
||||
import { PropFunc } from "~/types";
|
||||
import _ from "lodash";
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { StatusBarItem } from "./StatusBarItem";
|
||||
import useGroupState from "~/logic/state/group";
|
||||
import { JoinRequest, joinProgress } from "@urbit/api";
|
||||
import { usePreview } from "~/logic/state/metadata";
|
||||
import { Dropdown } from "./Dropdown";
|
||||
import { MetadataIcon } from "../landscape/components/MetadataIcon";
|
||||
|
||||
function Elbow(
|
||||
props: { size?: number; color?: string } & PropFunc<typeof Box>
|
||||
) {
|
||||
const { size = 12, color = "lightGray", ...rest } = props;
|
||||
|
||||
return (
|
||||
<Box
|
||||
{...rest}
|
||||
overflow="hidden"
|
||||
width={size}
|
||||
height={size}
|
||||
position="relative"
|
||||
>
|
||||
<Box
|
||||
border="2px solid"
|
||||
borderRadius={3}
|
||||
borderColor={color}
|
||||
position="absolute"
|
||||
left="0px"
|
||||
bottom="0px"
|
||||
width={size * 2}
|
||||
height={size * 2}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function StatusBarJoins() {
|
||||
const pendingJoin = useGroupState((s) => s.pendingJoin);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
if (
|
||||
Object.keys(_.omitBy(pendingJoin, (j) => j.progress === "done")).length ===
|
||||
0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
dropWidth="256px"
|
||||
options={
|
||||
<Col
|
||||
left="0px"
|
||||
top="120%"
|
||||
position="absolute"
|
||||
zIndex={10}
|
||||
alignItems="center"
|
||||
p="2"
|
||||
gapY="4"
|
||||
border="1"
|
||||
borderColor="lightGray"
|
||||
backgroundColor="white"
|
||||
>
|
||||
<Col>
|
||||
{Object.keys(pendingJoin).map((g) => (
|
||||
<JoinStatus key={g} group={g} join={pendingJoin[g]} />
|
||||
))}
|
||||
</Col>
|
||||
</Col>
|
||||
}
|
||||
alignX="left"
|
||||
alignY="bottom"
|
||||
>
|
||||
<StatusBarItem mr="2" width="32px" flexShrink={0} border={0}>
|
||||
<LoadingSpinner foreground="black" />
|
||||
</StatusBarItem>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
const description: string[] = [
|
||||
"Contacting host...",
|
||||
"Retrieving data...",
|
||||
"Finished join",
|
||||
"Unable to join, you do not have the correct permissions",
|
||||
"Internal error, please file an issue",
|
||||
];
|
||||
|
||||
export function JoinStatus({
|
||||
group,
|
||||
join,
|
||||
}: {
|
||||
group: string;
|
||||
join: JoinRequest;
|
||||
}) {
|
||||
const { preview, error } = usePreview(group);
|
||||
const current = join && joinProgress.indexOf(join.progress);
|
||||
const desc = _.isNumber(current) && description[current];
|
||||
return (
|
||||
<Col gapY="2">
|
||||
<Row alignItems="center" gapX="2">
|
||||
{preview ? (
|
||||
<MetadataIcon height={4} width={4} metadata={preview.metadata} />
|
||||
) : null}
|
||||
<Text>{preview?.metadata.title || group.slice(6)}</Text>
|
||||
</Row>
|
||||
<Row ml="2" alignItems="center" gapX="2">
|
||||
<Elbow />
|
||||
<Text>{desc}</Text>
|
||||
</Row>
|
||||
</Col>
|
||||
);
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
import { Box } from '@tlon/indigo-react';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { Route, Switch, useHistory } from 'react-router-dom';
|
||||
import { Route, Switch, useHistory, useLocation } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import ob from 'urbit-ob';
|
||||
import { useLocalStorageState } from '~/logic/lib/useLocalStorageState';
|
||||
import useMetadataState from '~/logic/state/metadata';
|
||||
import LaunchApp from '~/views/apps/launch/App';
|
||||
import Notifications from '~/views/apps/notifications/notifications';
|
||||
import { PermalinkRoutes } from '~/views/apps/permalinks/app';
|
||||
@ -22,8 +24,93 @@ export const Container = styled(Box)`
|
||||
height: calc(100% - 62px);
|
||||
`;
|
||||
|
||||
function getGroupResourceRedirect(key: string) {
|
||||
const association = useMetadataState.getState().associations.graph[`/ship/${key}`];
|
||||
const { metadata } = association;
|
||||
if(!association || !('graph' in metadata.config)) {
|
||||
return '';
|
||||
}
|
||||
return `/~landscape${association.group}/resource/${metadata.config.graph}${association.resource}`;
|
||||
}
|
||||
|
||||
function getPostRedirect(key: string, segs: string[]) {
|
||||
const association = useMetadataState.getState().associations.graph[`/ship/${key}`];
|
||||
const { metadata } = association;
|
||||
if(!association || !('graph' in metadata.config)) {
|
||||
return '';
|
||||
}
|
||||
return `/~landscape${association.group}/feed/thread/${segs.slice(0, -1).join('/')}`;
|
||||
}
|
||||
|
||||
function getChatRedirect(chat: string, segs: string[]) {
|
||||
const qs = segs.length > 0 ? `?msg=${segs[0]}` : '';
|
||||
return `${getGroupResourceRedirect(chat)}${qs}`;
|
||||
}
|
||||
|
||||
function getPublishRedirect(graphKey: string, segs: string[]) {
|
||||
const base = getGroupResourceRedirect(graphKey);
|
||||
if(segs.length === 3) {
|
||||
return `${base}/note/${segs[0]}`;
|
||||
} else if (segs.length === 4) {
|
||||
return `${base}/note/${segs[0]}?selected=${segs[2]}`;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function getLinkRedirect(graphKey: string, segs: string[]) {
|
||||
const base = getGroupResourceRedirect(graphKey);
|
||||
if(segs.length === 1) {
|
||||
return `${base}/index/${segs[0]}`;
|
||||
} else if (segs.length === 3) {
|
||||
return `${base}/index/${segs[0]}?selected=${segs[1]}`;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function getGraphRedirect(link: string) {
|
||||
const [,mark, ship, name, ...rest] = link.split('/');
|
||||
const graphKey = `${ship}/${name}`;
|
||||
switch(mark) {
|
||||
case 'graph-validator-dm':
|
||||
return `/~landscape/messages/dm/${ob.patp(rest[0])}`;
|
||||
case 'graph-validator-chat':
|
||||
return getChatRedirect(graphKey, rest);
|
||||
case 'graph-validator-publish':
|
||||
return getPublishRedirect(graphKey, rest);
|
||||
case 'graph-validator-link':
|
||||
return getLinkRedirect(graphKey, rest);
|
||||
case 'graph-validator-post':
|
||||
return getPostRedirect(graphKey, rest);
|
||||
default:
|
||||
return'';
|
||||
}
|
||||
}
|
||||
|
||||
function getInviteRedirect(link: string) {
|
||||
const [,,app,uid] = link.split('/');
|
||||
return `/invites/${app}/${uid}`;
|
||||
}
|
||||
|
||||
function getNotificationRedirect(link: string) {
|
||||
if(link.startsWith('/graph-validator')) {
|
||||
return getGraphRedirect(link);
|
||||
} else if (link.startsWith('/invite')) {
|
||||
return getInviteRedirect(link);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const Content = (props) => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const associations = useMetadataState(s => s.associations.graph);
|
||||
|
||||
useEffect(() => {
|
||||
const query = new URLSearchParams(location.search);
|
||||
if(Object.keys(associations).length > 0 && query.has('grid-note')) {
|
||||
history.push(getNotificationRedirect(query.get('grid-note')));
|
||||
}
|
||||
}, [location.search]);
|
||||
|
||||
useShortcut('navForward', useCallback((e) => {
|
||||
e.preventDefault();
|
||||
@ -58,7 +145,7 @@ export const Content = (props) => {
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path='/'
|
||||
path={['/', '/invites/:app/:uid']}
|
||||
render={p => (
|
||||
<LaunchApp
|
||||
location={p.location}
|
||||
@ -102,6 +189,7 @@ export const Content = (props) => {
|
||||
/>
|
||||
<GraphApp path="/~graph" {...props} />
|
||||
<PermalinkRoutes {...props} />
|
||||
|
||||
<Route
|
||||
render={p => (
|
||||
<ErrorComponent
|
||||
|
@ -8,7 +8,7 @@ import { addTag, createManagedGraph, createUnmanagedGraph } from '@urbit/api';
|
||||
import { Form, Formik } from 'formik';
|
||||
import _ from 'lodash';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useHistory, useRouteMatch } from 'react-router-dom';
|
||||
import * as Yup from 'yup';
|
||||
import { resourceFromPath } from '~/logic/lib/group';
|
||||
import { useWaitForProps } from '~/logic/lib/useWaitForProps';
|
||||
@ -49,6 +49,7 @@ type NewChannelProps = {
|
||||
|
||||
export function NewChannel(props: NewChannelProps): ReactElement {
|
||||
const history = useHistory();
|
||||
const match = useRouteMatch();
|
||||
const { group, workspace, existingMembers, ...rest } = props;
|
||||
const groups = useGroupState(state => state.groups);
|
||||
const waiter = useWaitForProps({ groups }, 5000);
|
||||
@ -121,9 +122,9 @@ export function NewChannel(props: NewChannelProps): ReactElement {
|
||||
);
|
||||
}
|
||||
actions.setStatus({ success: null });
|
||||
const resourceUrl = location.pathname.includes('/messages')
|
||||
const resourceUrl = match.url.includes('/messages')
|
||||
? '/~landscape/messages'
|
||||
: parentPath(location.pathname);
|
||||
: parentPath(match.path);
|
||||
history.push(
|
||||
`${resourceUrl}/resource/${moduleType}/ship/~${window.ship}/${resId}`
|
||||
);
|
||||
|
@ -12,7 +12,7 @@ import useContactState, { useContact } from '~/logic/state/contact';
|
||||
import { getItemTitle, getModuleIcon, uxToHex } from '~/logic/lib/util';
|
||||
import useGroupState from '~/logic/state/group';
|
||||
import Dot from '~/views/components/Dot';
|
||||
import useHarkState, { useHarkDm } from '~/logic/state/hark';
|
||||
import { useHarkDm, useHarkStat } from '~/logic/state/hark';
|
||||
import useSettingsState from '~/logic/state/settings';
|
||||
import useGraphState from '~/logic/state/graph';
|
||||
|
||||
@ -20,14 +20,10 @@ function useAssociationStatus(resource: string) {
|
||||
const [, , ship, name] = resource.split('/');
|
||||
const graphKey = `${ship.slice(1)}/${name}`;
|
||||
const isSubscribed = useGraphState(s => s.graphKeys.has(graphKey));
|
||||
const { unreads, notifications } = useHarkState(
|
||||
s => s.unreads.graph?.[resource]?.['/'] || { unreads: 0, notifications: 0, last: 0 }
|
||||
);
|
||||
const hasNotifications =
|
||||
(typeof notifications === 'number' && notifications > 0) ||
|
||||
(typeof notifications === 'object' && notifications.length);
|
||||
const hasUnread =
|
||||
typeof unreads === 'number' ? unreads > 0 : unreads?.size ?? 0 > 0;
|
||||
const stats = useHarkStat(`/graph/~${graphKey}`);
|
||||
const { count, each } = stats;
|
||||
const hasNotifications = false;
|
||||
const hasUnread = count > 0 || each.length > 0;
|
||||
return hasNotifications
|
||||
? 'notification'
|
||||
: hasUnread
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { ReactElement, useCallback } from 'react';
|
||||
import { AppAssociations, Associations, Graph, UnreadStats } from '@urbit/api';
|
||||
import { Associations, Graph } from '@urbit/api';
|
||||
import { patp, patp2dec } from 'urbit-ob';
|
||||
|
||||
import { SidebarAssociationItem, SidebarDmItem } from './SidebarItem';
|
||||
@ -12,10 +12,9 @@ import useMetadataState from '~/logic/state/metadata';
|
||||
import { useHistory } from 'react-router';
|
||||
import { useShortcut } from '~/logic/state/settings';
|
||||
|
||||
function sidebarSort(
|
||||
associations: AppAssociations,
|
||||
unreads: Record<string, Record<string, UnreadStats>>
|
||||
): Record<SidebarSort, (a: string, b: string) => number> {
|
||||
function sidebarSort(): Record<SidebarSort, (a: string, b: string) => number> {
|
||||
const { associations } = useMetadataState.getState();
|
||||
const { unreads } = useHarkState.getState();
|
||||
const alphabetical = (a: string, b: string) => {
|
||||
const aAssoc = associations[a];
|
||||
const bAssoc = associations[b];
|
||||
@ -26,18 +25,13 @@ function sidebarSort(
|
||||
};
|
||||
|
||||
const lastUpdated = (a: string, b: string) => {
|
||||
const aAssoc = associations[a];
|
||||
const bAssoc = associations[b];
|
||||
const aResource = aAssoc?.resource;
|
||||
const bResource = bAssoc?.resource;
|
||||
|
||||
const aUpdated = a.startsWith('~')
|
||||
? (unreads?.[`/ship/~${window.ship}/dm-inbox`]?.[`/${patp2dec(a)}`]?.last || 0)
|
||||
: ((unreads?.[aResource]?.['/']?.last) || 0);
|
||||
? (unreads?.[`/graph/~${window.ship}/dm-inbox/${patp2dec(a)}`]?.last || 0)
|
||||
: (unreads?.[`/graph/${a.slice(6)}`]?.last || 0);
|
||||
|
||||
const bUpdated = b.startsWith('~')
|
||||
? (unreads?.[`/ship/~${window.ship}/dm-inbox`]?.[`/${patp2dec(b)}`]?.last || 0)
|
||||
: ((unreads?.[bResource]?.['/']?.last) || 0);
|
||||
? (unreads?.[`/graph/~${window.ship}/dm-inbox/${patp2dec(b)}`]?.last || 0)
|
||||
: (unreads?.[`/graph/${b.slice(6)}`]?.last || 0);
|
||||
|
||||
return bUpdated - aUpdated || alphabetical(a, b);
|
||||
};
|
||||
@ -94,11 +88,10 @@ export function SidebarList(props: {
|
||||
const { selected, config, workspace } = props;
|
||||
const associations = useMetadataState(state => state.associations);
|
||||
const inbox = useInbox();
|
||||
const unreads = useHarkState(s => s.unreads.graph);
|
||||
const graphKeys = useGraphState(s => s.graphKeys);
|
||||
|
||||
const ordered = getItems(associations, workspace, inbox)
|
||||
.sort(sidebarSort(associations.graph, unreads)[config.sortBy]);
|
||||
.sort(sidebarSort()[config.sortBy]);
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
:: hark-graph-hook: notifications for graph-store [landscape]
|
||||
::
|
||||
/- post, group-store, metadata=metadata-store, hook=hark-graph-hook, store=hark-store
|
||||
/+ resource, mdl=metadata, default-agent, dbug, graph-store, graph, grouplib=group, store=hark-store
|
||||
/- hist=hark-store-historical
|
||||
/+ resource, mdl=metadata, default-agent, dbug, graph-store, graph, grouplib=group
|
||||
/+ agentio
|
||||
::
|
||||
::
|
||||
~% %hark-graph-hook-top ..part ~
|
||||
@ -18,23 +20,34 @@
|
||||
+$ state-1
|
||||
[%1 base-state-0]
|
||||
::
|
||||
+$ state-2
|
||||
[%2 base-state-1]
|
||||
::
|
||||
+$ base-state-0
|
||||
$: watching=(set [resource index:post])
|
||||
mentions=_&
|
||||
watch-on-self=_&
|
||||
==
|
||||
::
|
||||
+$ base-state-1
|
||||
$: watching=(set [resource index:post])
|
||||
mentions=_&
|
||||
watch-on-self=_&
|
||||
places=(map resource place:store)
|
||||
==
|
||||
::
|
||||
++ scry
|
||||
|* [[our=@p now=@da] =mold p=path]
|
||||
?> ?=(^ p)
|
||||
~! p
|
||||
?> ?=(^ t.p)
|
||||
.^(mold i.p (scot %p our) i.t.p (scot %da now) t.t.p)
|
||||
::
|
||||
++ scry-notif-conversion
|
||||
|= [[our=@p now=@da] desk=term =mark]
|
||||
^- $-(indexed-post:graph-store (unit notif-kind:hook))
|
||||
^- $-(indexed-post:graph-store $-(cord (unit notif-kind:hook)))
|
||||
%^ scry [our now]
|
||||
$-(indexed-post:graph-store (unit notif-kind:hook))
|
||||
$-(indexed-post:graph-store $-(cord (unit notif-kind:hook)))
|
||||
/cf/[desk]/[mark]/notification-kind
|
||||
--
|
||||
::
|
||||
@ -52,6 +65,8 @@
|
||||
met ~(. mdl bowl)
|
||||
grp ~(. grouplib bowl)
|
||||
gra ~(. graph bowl)
|
||||
io ~(. agentio bowl)
|
||||
pass pass:io
|
||||
::
|
||||
++ on-init
|
||||
:_ this
|
||||
@ -63,6 +78,7 @@
|
||||
^- (quip card _this)
|
||||
=+ !<(old=versioned-state vase)
|
||||
=| cards=(list card)
|
||||
=. cards [watch-graph:ha cards]
|
||||
|-
|
||||
?: ?=(%0 -.old)
|
||||
%_ $
|
||||
@ -103,13 +119,57 @@
|
||||
?> (team:title our.bowl src.bowl)
|
||||
=^ cards state
|
||||
?+ mark (on-poke:def mark vase)
|
||||
%hark-graph-migrate
|
||||
=+ !<(old=versioned-state:hist vase)
|
||||
?. ?=(%7 -.old) ~|(%old-hark-dropping !!)
|
||||
(hark-graph-migrate old)
|
||||
::
|
||||
%hark-graph-hook-action
|
||||
(hark-graph-hook-action !<(action:hook vase))
|
||||
::
|
||||
%noun
|
||||
(poke-noun !<(* vase))
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
++ hark-graph-migrate
|
||||
|= old=state-7:hist
|
||||
=| cards=(list card)
|
||||
|^
|
||||
[(flop get-places) state]
|
||||
::
|
||||
++ hark
|
||||
|= =action:store
|
||||
[(poke-our:pass %hark-store hark-action+!>(action)) cards]
|
||||
::
|
||||
++ get-places
|
||||
^- (list card)
|
||||
=/ stats-indices=(set stats-index:hist)
|
||||
(~(uni in ~(key by last-seen.old)) ~(key by unreads-count.old))
|
||||
%- zing
|
||||
(turn ~(tap in stats-indices) get-stats)
|
||||
::
|
||||
++ get-stats
|
||||
|= =stats-index:hist
|
||||
^- (list card)
|
||||
=/ place=(unit place:store)
|
||||
(stats-index-to-place stats-index)
|
||||
?~ place ~
|
||||
=/ count (~(get by unreads-count.old) stats-index)
|
||||
=? cards ?=(^ count)
|
||||
(hark %unread-count u.place & u.count)
|
||||
=/ last (~(get by last-seen.old) stats-index)
|
||||
=? cards ?=(^ last)
|
||||
(hark %saw-place u.place `u.last)
|
||||
cards
|
||||
::
|
||||
++ stats-index-to-place
|
||||
|= =stats-index:hist
|
||||
^- (unit place:store)
|
||||
?. ?=(%graph -.stats-index) ~
|
||||
`(get-place [graph index]:stats-index)
|
||||
--
|
||||
::
|
||||
++ poke-noun
|
||||
|= non=*
|
||||
[~ state]
|
||||
@ -202,7 +262,7 @@
|
||||
%- ~(gas by *(set [resource index:graph-store]))
|
||||
(turn ~(tap in indices) (lead rid))
|
||||
:_ state(watching (~(dif in watching) to-remove))
|
||||
=/ convert (get-conversion:ha rid)
|
||||
=/ convert (get-conversion:ha rid '')
|
||||
%+ roll
|
||||
~(tap in indices)
|
||||
|= [=index:graph-store out=(list card)]
|
||||
@ -211,11 +271,11 @@
|
||||
=/ notif-kind=(unit notif-kind:hook)
|
||||
(convert indexed-post)
|
||||
?~ notif-kind out
|
||||
=/ =stats-index:store
|
||||
[%graph rid (scag parent.index-len.u.notif-kind index)]
|
||||
=/ =place:store
|
||||
(get-place rid index)
|
||||
?. ?=(%each mode.u.notif-kind) out
|
||||
:_ out
|
||||
(poke-hark %read-each stats-index index)
|
||||
(poke-hark %read-each place index)
|
||||
::
|
||||
++ poke-hark
|
||||
|= =action:store
|
||||
@ -231,11 +291,8 @@
|
||||
=(r rid)
|
||||
:_ state(watching (~(dif in watching) unwatched))
|
||||
^- (list card)
|
||||
:- (poke-hark:ha %remove-graph rid)
|
||||
%- zing
|
||||
%+ turn ~(tap in unwatched)
|
||||
|= [r=resource =index:graph-store]
|
||||
(give:ha ~[/updates] %ignore r index)
|
||||
~
|
||||
:: XX: fix
|
||||
::
|
||||
++ add-graph
|
||||
|= rid=resource
|
||||
@ -280,15 +337,47 @@
|
||||
grp ~(. grouplib bowl)
|
||||
gra ~(. graph bowl)
|
||||
::
|
||||
++ graph-index-to-path
|
||||
|= =index:graph-store
|
||||
^- path
|
||||
(turn index (cork (cury scot %ui) (cury rsh 4)))
|
||||
::
|
||||
++ summarize
|
||||
|= contents=(list content:post)
|
||||
%+ rap 3
|
||||
%+ join ' '
|
||||
%+ turn contents
|
||||
|= =content:post
|
||||
?- -.content
|
||||
%text text.content
|
||||
%url url.content
|
||||
%code '<Code fragment>'
|
||||
%reference '<A reference>'
|
||||
%mention (scot %p ship.content)
|
||||
==
|
||||
::
|
||||
++ get-place
|
||||
|= [rid=resource =index:graph-store]
|
||||
:- q.byk.bowl
|
||||
(welp /graph/(scot %p entity.rid)/[name.rid] (graph-index-to-path index))
|
||||
::
|
||||
++ get-bin
|
||||
|= [rid=resource parent=index:graph-store is-mention=?]
|
||||
^- bin:store
|
||||
[?:(is-mention /mention /) (get-place rid parent)]
|
||||
::
|
||||
++ get-conversion
|
||||
|= rid=resource
|
||||
|= [rid=resource title=cord]
|
||||
^- $-(indexed-post:graph-store (unit notif-kind:hook))
|
||||
=+ %^ scry [our now]:bowl
|
||||
,mark=(unit mark)
|
||||
/gx/graph-store/graph/(scot %p entity.rid)/[name.rid]/mark/noun
|
||||
?~ mark
|
||||
|=(=indexed-post:graph-store ~)
|
||||
(scry-notif-conversion [our now]:bowl q.byk.bowl u.mark)
|
||||
=/ f=$-(indexed-post:graph-store $-(cord (unit notif-kind:hook)))
|
||||
(scry-notif-conversion [our now]:bowl q.byk.bowl u.mark)
|
||||
|= =indexed-post:graph-store
|
||||
((f indexed-post) title)
|
||||
::
|
||||
++ give
|
||||
|= [paths=(list path) =update:hook]
|
||||
@ -338,8 +427,11 @@
|
||||
(get-mark:gra r)
|
||||
update-core(rid r, updates upds, mark m)
|
||||
::
|
||||
++ title
|
||||
~+ title:(fall (peek-metadatum:met %graph rid) *metadatum:metadata)
|
||||
::
|
||||
++ get-conversion
|
||||
~+ (^get-conversion rid)
|
||||
~+ (^get-conversion rid title)
|
||||
::
|
||||
++ abet
|
||||
^- (quip card _state)
|
||||
@ -391,63 +483,65 @@
|
||||
=. update-core (check-node-children node)
|
||||
?: ?=(%| -.post.node)
|
||||
update-core
|
||||
::?~ mark update-core
|
||||
=* pos p.post.node
|
||||
=/ notif-kind=(unit notif-kind:hook)
|
||||
(get-conversion [0 pos])
|
||||
(get-conversion [0 pos])
|
||||
?~ notif-kind
|
||||
update-core
|
||||
=/ desc=@t
|
||||
?: (is-mention contents.pos)
|
||||
%mention
|
||||
name.u.notif-kind
|
||||
=* not-kind u.notif-kind
|
||||
=/ parent=index:post
|
||||
(scag parent.index-len.not-kind index.pos)
|
||||
=/ notif-index=index:store
|
||||
[%graph rid mark desc parent]
|
||||
=/ is-mention (is-mention contents.pos)
|
||||
=/ =bin:store
|
||||
(get-bin rid parent is-mention)
|
||||
?: =(our.bowl author.pos)
|
||||
(self-post node notif-index not-kind)
|
||||
(self-post node bin u.notif-kind)
|
||||
=. update-core
|
||||
(update-unread-count not-kind notif-index [time-sent index]:pos)
|
||||
%^ update-unread-count u.notif-kind bin
|
||||
(scag self.index-len.not-kind index.pos)
|
||||
=? update-core
|
||||
?| =(desc %mention)
|
||||
?| is-mention
|
||||
(~(has in watching) [rid parent])
|
||||
=(mark `%graph-validator-dm)
|
||||
==
|
||||
=/ =contents:store
|
||||
[%graph (limo pos ~)]
|
||||
(add-unread notif-index [time-sent.pos %.n contents])
|
||||
=/ link=path
|
||||
(welp /(fall mark '')/(scot %p entity.rid)/[name.rid] (graph-index-to-path index.pos))
|
||||
=/ title=(list content:store)
|
||||
?. is-mention title.not-kind
|
||||
~[text/(rap 3 'You were mentioned in ' title ~)]
|
||||
=/ =body:store
|
||||
[title body.not-kind now.bowl path.bin link]
|
||||
(add-unread bin body)
|
||||
update-core
|
||||
::
|
||||
::
|
||||
++ update-unread-count
|
||||
|= [=notif-kind:hook =index:store time=@da ref=index:graph-store]
|
||||
=/ =stats-index:store
|
||||
(to-stats-index:store index)
|
||||
|= [=notif-kind:hook =bin:store =index:graph-store]
|
||||
?- mode.notif-kind
|
||||
%count (hark %unread-count stats-index time)
|
||||
%each (hark %unread-each stats-index ref time)
|
||||
%count (hark %unread-count place.bin %.y 1)
|
||||
%each (hark %unread-each place.bin /(rsh 4 (scot %ui (rear index))))
|
||||
%none update-core
|
||||
==
|
||||
::
|
||||
::
|
||||
++ self-post
|
||||
|= $: =node:graph-store
|
||||
=index:store
|
||||
=bin:store
|
||||
=notif-kind:hook
|
||||
==
|
||||
^+ update-core
|
||||
?> ?=(%& -.post.node)
|
||||
=/ =stats-index:store
|
||||
(to-stats-index:store index)
|
||||
=. update-core
|
||||
(hark %seen-index time-sent.p.post.node stats-index)
|
||||
(hark %saw-place place.bin `now.bowl)
|
||||
=? update-core ?=(%count mode.notif-kind)
|
||||
(hark %read-count stats-index)
|
||||
(hark %read-count place.bin)
|
||||
=? update-core watch-on-self
|
||||
(new-watch index.p.post.node [watch-for index-len]:notif-kind)
|
||||
update-core
|
||||
::
|
||||
++ add-unread
|
||||
|= [=index:store =notification:store]
|
||||
(hark %add-note index notification)
|
||||
|= [=bin:store =body:store]
|
||||
(hark %add-note bin body)
|
||||
--
|
||||
--
|
||||
|
@ -14,6 +14,8 @@
|
||||
$: %0
|
||||
watching=(set resource)
|
||||
==
|
||||
+$ update
|
||||
$>(?(%add-members %remove-members) update:group-store)
|
||||
::
|
||||
--
|
||||
::
|
||||
@ -127,13 +129,13 @@
|
||||
[~ state]
|
||||
?. (~(has in watching) resource.update)
|
||||
[~ state]
|
||||
=/ =contents:store
|
||||
[%group ~[update]]
|
||||
=/ =notification:store [now.bowl %.n contents]
|
||||
=/ =index:store
|
||||
[%group resource.update -.update]
|
||||
=/ body=(unit body:store)
|
||||
(get-content:ha update)
|
||||
?~ body `state
|
||||
=/ =bin:store
|
||||
(get-bin:ha resource.update -.update)
|
||||
:_ state
|
||||
~[(add-unread index notification)]
|
||||
~[(add-unread bin u.body)]
|
||||
:: +metadata-update is stubbed for now, for the following reasons
|
||||
:: - There's no semantic difference in metadata-store between
|
||||
:: adding and editing a channel
|
||||
@ -145,12 +147,12 @@
|
||||
[~ state]
|
||||
::
|
||||
++ add-unread
|
||||
|= [=index:store =notification:store]
|
||||
|= [=bin:store =body:store]
|
||||
^- card
|
||||
=- [%pass / %agent [our.bowl %hark-store] %poke -]
|
||||
:- %hark-action
|
||||
!> ^- action:store
|
||||
[%add-note index notification]
|
||||
[%add-note bin body]
|
||||
--
|
||||
::
|
||||
++ on-peek on-peek:def
|
||||
@ -159,7 +161,48 @@
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
|_ =bowl:gall
|
||||
+* met ~(. metadata bowl)
|
||||
+* met ~(. mdl bowl)
|
||||
++ get-content
|
||||
|= =update:group-store
|
||||
^- (unit body:store)
|
||||
?. ?=(?(%add-members %remove-members) -.update) ~
|
||||
?~ meta=(peek-metadatum:met %groups resource.update)
|
||||
~
|
||||
=/ ships=(list content:store)
|
||||
%+ turn ~(tap in ships.update)
|
||||
|= =ship
|
||||
^- content:store
|
||||
ship+ship
|
||||
=/ sep=content:store text+', '
|
||||
=. ships
|
||||
(join sep ships)
|
||||
?- -.update
|
||||
%add-members
|
||||
:- ~
|
||||
:* (snoc ships text+(rap 3 ' joined ' title.u.meta ~))
|
||||
~
|
||||
now.bowl
|
||||
/
|
||||
/
|
||||
==
|
||||
::
|
||||
%remove-members
|
||||
:- ~
|
||||
:* (snoc ships text+(rap 3 ' joined ' title.u.meta ~))
|
||||
~
|
||||
now.bowl
|
||||
/
|
||||
/
|
||||
==
|
||||
==
|
||||
++ get-bin
|
||||
|= [rid=resource reason=@t]
|
||||
^- bin:store
|
||||
[/[reason] (get-place rid)]
|
||||
++ get-place
|
||||
|= rid=resource
|
||||
^- place:store
|
||||
[q.byk.bowl /(scot %p entity.rid)/[name.rid]]
|
||||
::
|
||||
++ watch-groups
|
||||
^- card
|
||||
|
174
pkg/landscape/app/hark-invite-hook.hoon
Normal file
174
pkg/landscape/app/hark-invite-hook.hoon
Normal file
@ -0,0 +1,174 @@
|
||||
/- hark=hark-store, invite=invite-store
|
||||
/+ verb, dbug, default-agent, agentio, resource, metadata=metadata-store
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
+$ versioned-state
|
||||
$% state-0
|
||||
==
|
||||
::
|
||||
+$ state-0
|
||||
$: blocked=(jug resource [uid=serial:invite =invite:invite])
|
||||
~
|
||||
==
|
||||
|
||||
|
||||
|
||||
--
|
||||
%+ verb &
|
||||
%- agent:dbug
|
||||
^- agent:gall
|
||||
=| state-0
|
||||
=* state -
|
||||
=<
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
io ~(. agentio bowl)
|
||||
pass pass:io
|
||||
cc ~(. +> bowl)
|
||||
++ on-init
|
||||
:_ this
|
||||
~[watch:invite-store:cc]
|
||||
::
|
||||
++ on-load
|
||||
|= =vase
|
||||
:_ this
|
||||
(drop safe-watch:invite-store:cc)
|
||||
++ on-save !>(state)
|
||||
::
|
||||
++ on-poke on-poke:def
|
||||
++ on-peek on-peek:def
|
||||
++ on-watch on-watch:def
|
||||
++ on-agent
|
||||
|= [=wire =sign:agent:gall]
|
||||
|^
|
||||
?+ wire (on-agent:def wire sign)
|
||||
[%invites ~] take-invites
|
||||
::
|
||||
[%groups @ @ @ ~]
|
||||
=/ rid=resource (de-path:resource t.wire)
|
||||
(take-groups rid)
|
||||
==
|
||||
++ take-groups
|
||||
|= rid=resource
|
||||
=/ blocking (~(get ju blocked) rid)
|
||||
|^
|
||||
?+ -.sign (on-agent:def wire sign)
|
||||
::
|
||||
%kick
|
||||
?: =(~ blocking) `this
|
||||
(flush-blocking (rap 3 (scot %p entity.rid) '/' name.rid ~))
|
||||
::
|
||||
%fact
|
||||
?. ?=(%metadata-hook-update p.cage.sign) `this
|
||||
=+ !<(=hook-update:metadata q.cage.sign)
|
||||
?. ?=(%preview -.hook-update) `this
|
||||
(flush-blocking title.metadatum.hook-update)
|
||||
==
|
||||
::
|
||||
++ flush-blocking
|
||||
|= to=cord
|
||||
:_ this(blocked (~(del by blocked) rid))
|
||||
%+ turn ~(tap in blocking)
|
||||
|= [uid=serial:invite =invite:invite]
|
||||
(~(created inv:cc %groups uid) invite to)
|
||||
--
|
||||
::
|
||||
++ take-invites
|
||||
?- -.sign
|
||||
?(%poke-ack %watch-ack) (on-agent:def wire sign)
|
||||
%kick :_(this (drop safe-watch:invite-store:cc))
|
||||
::
|
||||
%fact
|
||||
?. ?=(%invite-update p.cage.sign) `this
|
||||
=+ !<(=update:invite q.cage.sign)
|
||||
?+ -.update `this
|
||||
::
|
||||
%accepted
|
||||
:_ this
|
||||
~(dismissed inv:cc [term uid]:update)^~
|
||||
::
|
||||
%decline
|
||||
~& decline/uid.update
|
||||
:_ this
|
||||
~(dismissed inv:cc [term uid]:update)^~
|
||||
::
|
||||
%invite
|
||||
=* rid resource.invite.update
|
||||
?+ term.update
|
||||
:: default
|
||||
=/ fallback (rap 3 (scot %p entity.rid) '/' name.rid ~)
|
||||
:_ this
|
||||
(~(created inv:cc [term uid]:update) invite.update fallback)^~
|
||||
::
|
||||
%groups
|
||||
=. blocked (~(put ju blocked) rid [uid invite]:update)
|
||||
:_ this
|
||||
(drop ~(safe-watch preview:cc rid))
|
||||
::
|
||||
%graph
|
||||
:_ this
|
||||
(~(created inv:cc [term uid]:update) invite.update 'a group chat')^~
|
||||
|
||||
==
|
||||
==
|
||||
==
|
||||
--
|
||||
::
|
||||
++ on-arvo on-arvo:def
|
||||
++ on-fail on-fail:def
|
||||
++ on-leave on-leave:def
|
||||
--
|
||||
|_ =bowl:gall
|
||||
+* io ~(. agentio bowl)
|
||||
pass pass:io
|
||||
++ ha
|
||||
|%
|
||||
++ pass ~(. ^pass /hark)
|
||||
++ poke
|
||||
|=(=action:hark (poke-our:pass %hark-store hark-action+!>(action)))
|
||||
--
|
||||
++ invite-store
|
||||
|%
|
||||
++ path /updates
|
||||
++ wire /invites
|
||||
++ pass ~(. ^pass wire)
|
||||
++ watch (watch-our:pass %invite-store path)
|
||||
++ watching (~(has by wex.bowl) [wire our.bowl %invite-store])
|
||||
++ safe-watch `(unit card)`?:(watching ~ `watch)
|
||||
--
|
||||
::
|
||||
++ preview
|
||||
|_ rid=resource
|
||||
++ path preview+(en-path:resource rid)
|
||||
++ wire groups+(en-path:resource rid)
|
||||
++ pass ~(. ^pass wire)
|
||||
++ watching (~(has by wex.bowl) [wire our.bowl %metadata-pull-hook])
|
||||
++ watch (watch-our:pass %metadata-pull-hook path)
|
||||
++ safe-watch `(unit card)`?:(watching ~ `watch)
|
||||
--
|
||||
|
||||
|
||||
::
|
||||
++ inv
|
||||
|_ [=term uid=serial:invite]
|
||||
++ bin [/ place]
|
||||
++ path /[term]/(scot %uv uid)
|
||||
++ place `place:hark`[q.byk.bowl path]
|
||||
++ dismissed
|
||||
(poke:ha %del-place place)
|
||||
++ created
|
||||
|= [=invite:invite to=cord]
|
||||
=; =body:hark
|
||||
(poke:ha %add-note bin body)
|
||||
=, invite
|
||||
=/ title=@t
|
||||
(rap 3 'You have been invited to ' to ' by ' ~)
|
||||
:* ~[text+title ship+ship]
|
||||
~[text+text]
|
||||
now.bowl
|
||||
/
|
||||
invite+path
|
||||
==
|
||||
--
|
||||
--
|
@ -1,745 +0,0 @@
|
||||
:: hark-store: notifications and unread counts [landscape]
|
||||
::
|
||||
:: hark-store can store unread counts differently, depending on the
|
||||
:: resource.
|
||||
:: - last seen. This way, hark-store simply stores an index into
|
||||
:: graph-store, which represents the last "seen" item, useful for
|
||||
:: high-volume applications which are intrinsically time-ordered. i.e.
|
||||
:: chats, comments
|
||||
:: - each. Hark-store will store an index for each item that is unread.
|
||||
:: Usefull for non-linear, low-volume applications, i.e. blogs,
|
||||
:: collections
|
||||
::
|
||||
/- post, group-store, metadata-store, store=hark-store
|
||||
/+ resource, metadata, default-agent, dbug, graph-store, graphl=graph, verb, store=hark-store
|
||||
::
|
||||
::
|
||||
~% %hark-store-top ..part ~
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
+$ versioned-state
|
||||
$% state:state-zero:store
|
||||
state:state-one:store
|
||||
state-2
|
||||
state-3
|
||||
state-4
|
||||
state-5
|
||||
state-6
|
||||
state-7
|
||||
==
|
||||
+$ unread-stats
|
||||
[indices=(set index:graph-store) last=@da]
|
||||
::
|
||||
+$ base-state
|
||||
$: unreads-each=(jug stats-index:store index:graph-store)
|
||||
unreads-count=(map stats-index:store @ud)
|
||||
timeboxes=(map stats-index:store @da)
|
||||
unread-notes=timebox:store
|
||||
last-seen=(map stats-index:store @da)
|
||||
=notifications:store
|
||||
archive=notifications:store
|
||||
current-timebox=@da
|
||||
dnd=_|
|
||||
==
|
||||
::
|
||||
+$ state-2
|
||||
[%2 state-two:store]
|
||||
::
|
||||
+$ state-3
|
||||
[%3 state-two:store]
|
||||
::
|
||||
+$ state-4
|
||||
[%4 state-three:store]
|
||||
::
|
||||
+$ state-5
|
||||
[%5 state-three:store]
|
||||
::
|
||||
+$ state-6
|
||||
[%6 state-four:store]
|
||||
::
|
||||
+$ state-7
|
||||
[%7 base-state]
|
||||
::
|
||||
::
|
||||
++ orm ((ordered-map @da timebox:store) gth)
|
||||
--
|
||||
::
|
||||
=| state-7
|
||||
=* state -
|
||||
::
|
||||
=<
|
||||
%+ verb |
|
||||
%- agent:dbug
|
||||
^- agent:gall
|
||||
~% %hark-store-agent ..card ~
|
||||
|_ =bowl:gall
|
||||
+* this .
|
||||
ha ~(. +> bowl)
|
||||
def ~(. (default-agent this %|) bowl)
|
||||
met ~(. metadata bowl)
|
||||
gra ~(. graphl bowl)
|
||||
::
|
||||
++ on-init
|
||||
:_ this
|
||||
~[autoseen-timer]
|
||||
::
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= =old=vase
|
||||
^- (quip card _this)
|
||||
=/ old
|
||||
!<(versioned-state old-vase)
|
||||
=| cards=(list card)
|
||||
|^
|
||||
^- (quip card _this)
|
||||
?- -.old
|
||||
%7
|
||||
:- (flop cards)
|
||||
this(state old)
|
||||
::
|
||||
%6
|
||||
%_ $
|
||||
-.old %7
|
||||
::
|
||||
+.old
|
||||
%* . *base-state
|
||||
notifications (notifications:to-five:upgrade:store notifications.old)
|
||||
archive ~
|
||||
unreads-each unreads-each.old
|
||||
unreads-count unreads-count.old
|
||||
last-seen last-seen.old
|
||||
current-timebox current-timebox
|
||||
dnd dnd.old
|
||||
==
|
||||
==
|
||||
::
|
||||
%5
|
||||
%_ $
|
||||
-.old %6
|
||||
notifications.old (notifications:to-four:upgrade:store notifications.old)
|
||||
archive.old *notifications:state-four:store
|
||||
==
|
||||
::
|
||||
%4
|
||||
%_ $
|
||||
-.old %5
|
||||
::
|
||||
last-seen.old
|
||||
%- ~(run by last-seen.old)
|
||||
|=(old=@da (min old now.bowl))
|
||||
==
|
||||
::
|
||||
%3
|
||||
%_ $
|
||||
-.old %4
|
||||
notifications.old (notifications:to-three:upgrade:store notifications.old)
|
||||
archive.old *notifications:state-three:store
|
||||
==
|
||||
::
|
||||
%2
|
||||
%_ $
|
||||
-.old %3
|
||||
::
|
||||
cards
|
||||
:_ cards
|
||||
[%pass / %agent [our dap]:bowl %poke noun+!>(%fix-dangling)]
|
||||
==
|
||||
::
|
||||
%1
|
||||
%_ $
|
||||
::
|
||||
old
|
||||
%* . *state-2
|
||||
unreads-each ((convert-unread ,(set index:graph-store)) uni-by unreads-each.old)
|
||||
unreads-count ((convert-unread ,@ud) add unreads-count.old)
|
||||
last-seen ((convert-unread ,@da) max last-seen.old)
|
||||
notifications notifications.old
|
||||
archive archive.old
|
||||
current-timebox current-timebox.old
|
||||
dnd dnd.old
|
||||
==
|
||||
==
|
||||
::
|
||||
%0
|
||||
%_ $
|
||||
::
|
||||
old
|
||||
%* . *state:state-one:store
|
||||
notifications (convert-notifications-1 notifications.old)
|
||||
archive (convert-notifications-1 archive.old)
|
||||
current-timebox current-timebox.old
|
||||
dnd dnd.old
|
||||
==
|
||||
==
|
||||
==
|
||||
::
|
||||
++ uni-by
|
||||
|= [a=(set index:graph-store) b=(set index:graph-store)]
|
||||
=/ merged
|
||||
(~(uni in a) b)
|
||||
%- ~(gas in *(set index:graph-store))
|
||||
%+ skip ~(tap in merged)
|
||||
|=(=index:graph-store &(=((lent index) 3) !=(-:(flop index) 1)))
|
||||
::
|
||||
++ convert-unread
|
||||
|* value=mold
|
||||
|= [combine=$-([value value] value) unreads=(map index:store value)]
|
||||
^- (map stats-index:store value)
|
||||
%+ roll
|
||||
~(tap in unreads)
|
||||
|= [[=index:store val=value] out=(map stats-index:store value)]
|
||||
=/ old=value
|
||||
(~(gut by unreads) index (combine))
|
||||
=/ =stats-index:store
|
||||
(to-stats-index:store index)
|
||||
(~(put by out) stats-index (combine old val))
|
||||
::
|
||||
++ convert-notifications-1
|
||||
|= old=notifications:state-zero:store
|
||||
%+ gas:orm:state-two:store *notifications:state-two:store
|
||||
^- (list [@da timebox:state-two:store])
|
||||
%+ murn
|
||||
(tap:orm:state-zero:store old)
|
||||
|= [time=@da =timebox:state-zero:store]
|
||||
^- (unit [@da timebox:state-two:store])
|
||||
=/ new-timebox=timebox:state-two:store
|
||||
(convert-timebox-1 timebox)
|
||||
?: =(0 ~(wyt by new-timebox))
|
||||
~
|
||||
`[time new-timebox]
|
||||
::
|
||||
++ convert-timebox-1
|
||||
|= =timebox:state-zero:store
|
||||
^- timebox:state-two:store
|
||||
%- ~(gas by *timebox:state-two:store)
|
||||
^- (list [index:state-two:store notification:state-two:store])
|
||||
%+ murn
|
||||
~(tap by timebox)
|
||||
|= [=index:state-zero:store =notification:state-zero:store]
|
||||
^- (unit [index:state-two:store notification:state-two:store])
|
||||
=/ new-index=(unit index:state-two:store)
|
||||
(convert-index-1 index)
|
||||
=/ new-notification=(unit notification:state-two:store)
|
||||
(convert-notification-1 notification)
|
||||
?~ new-index ~
|
||||
?~ new-notification ~
|
||||
`[u.new-index u.new-notification]
|
||||
::
|
||||
++ convert-index-1
|
||||
|= =index:state-zero:store
|
||||
^- (unit index:state-two:store)
|
||||
?+ -.index `index
|
||||
%chat ~
|
||||
::
|
||||
%graph
|
||||
=, index
|
||||
`[%graph graph *resource module description ~]
|
||||
==
|
||||
::
|
||||
++ convert-notification-1
|
||||
|= =notification:state-zero:store
|
||||
^- (unit notification:state-two:store)
|
||||
?: ?=(%chat -.contents.notification)
|
||||
~
|
||||
`notification
|
||||
--
|
||||
::
|
||||
++ on-watch
|
||||
|= =path
|
||||
^- (quip card _this)
|
||||
?> (team:title [src our]:bowl)
|
||||
|^
|
||||
?+ path (on-watch:def path)
|
||||
::
|
||||
[%updates ~]
|
||||
:_ this
|
||||
[%give %fact ~ hark-update+!>(initial-updates)]~
|
||||
==
|
||||
::
|
||||
++ initial-updates
|
||||
^- update:store
|
||||
:- %more
|
||||
^- (list update:store)
|
||||
:~ give-unreads
|
||||
[%set-dnd dnd]
|
||||
give-notifications
|
||||
==
|
||||
::
|
||||
++ give-notifications
|
||||
^- update:store
|
||||
[%timebox ~ ~(tap by unread-notes)]
|
||||
::
|
||||
++ give-since-unreads
|
||||
^- (list [stats-index:store stats:store])
|
||||
%+ turn
|
||||
~(tap by unreads-count)
|
||||
|= [=stats-index:store count=@ud]
|
||||
:* stats-index
|
||||
[%count count]
|
||||
(~(gut by last-seen) stats-index *time)
|
||||
==
|
||||
::
|
||||
++ give-each-unreads
|
||||
^- (list [stats-index:store stats:store])
|
||||
%+ turn
|
||||
~(tap by unreads-each)
|
||||
|= [=stats-index:store indices=(set index:graph-store)]
|
||||
:* stats-index
|
||||
[%each indices]
|
||||
(~(gut by last-seen) stats-index *time)
|
||||
==
|
||||
::
|
||||
++ give-unreads
|
||||
^- update:store
|
||||
:- %unreads
|
||||
;: weld
|
||||
give-each-unreads
|
||||
give-since-unreads
|
||||
==
|
||||
--
|
||||
::
|
||||
++ on-peek
|
||||
|= =path
|
||||
^- (unit (unit cage))
|
||||
?+ path (on-peek:def path)
|
||||
::
|
||||
[%x %recent ?(%archive %inbox) @ @ ~]
|
||||
=/ is-archive
|
||||
=(%archive i.t.t.path)
|
||||
=/ offset=@ud
|
||||
(slav %ud i.t.t.t.path)
|
||||
=/ length=@ud
|
||||
(slav %ud i.t.t.t.t.path)
|
||||
:^ ~ ~ %hark-update
|
||||
!> ^- update:store
|
||||
:- %more
|
||||
%+ turn
|
||||
%+ scag length
|
||||
%+ slag offset
|
||||
%- tap-nonempty:ha
|
||||
?:(is-archive archive notifications)
|
||||
|= [time=@da =timebox:store]
|
||||
^- update:store
|
||||
[%timebox `time ~(tap by timebox)]
|
||||
==
|
||||
::
|
||||
++ on-poke
|
||||
~/ %hark-store-poke
|
||||
|= [=mark =vase]
|
||||
^- (quip card _this)
|
||||
|^
|
||||
?> (team:title our.bowl src.bowl)
|
||||
=^ cards state
|
||||
?+ mark (on-poke:def mark vase)
|
||||
%hark-action (hark-action !<(action:store vase))
|
||||
%noun (poke-noun !<(* vase))
|
||||
==
|
||||
[cards this]
|
||||
::
|
||||
++ poke-noun
|
||||
|= val=*
|
||||
?+ val ~|(%bad-noun-poke !!)
|
||||
%fix-dangling fix-dangling
|
||||
%print ~&(+.state [~ state])
|
||||
==
|
||||
::
|
||||
++ fix-dangling
|
||||
=/ graphs get-keys:gra
|
||||
:_ state
|
||||
%+ roll
|
||||
~(tap by unreads-each)
|
||||
|= $: [=stats-index:store indices=(set index:graph-store)]
|
||||
out=(list card)
|
||||
==
|
||||
?. ?=(%graph -.stats-index) out
|
||||
?. (~(has in graphs) graph.stats-index)
|
||||
:_(out (poke-us %remove-graph graph.stats-index))
|
||||
%+ welp out
|
||||
%+ turn
|
||||
%+ skip
|
||||
~(tap in indices)
|
||||
|= =index:graph-store
|
||||
(check-node-existence:gra graph.stats-index index)
|
||||
|=(=index:graph-store (poke-us %read-each stats-index index))
|
||||
::
|
||||
++ poke-us
|
||||
|= =action:store
|
||||
^- card
|
||||
[%pass / %agent [our dap]:bowl %poke hark-action+!>(action)]
|
||||
::
|
||||
++ hark-action
|
||||
|= =action:store
|
||||
^- (quip card _state)
|
||||
abet:translate:(abed:poke-engine:ha action)
|
||||
--
|
||||
::
|
||||
++ on-agent on-agent:def
|
||||
::
|
||||
++ on-leave on-leave:def
|
||||
++ on-arvo
|
||||
|= [=wire =sign-arvo]
|
||||
^- (quip card _this)
|
||||
?. ?=([%autoseen ~] wire)
|
||||
(on-arvo:def wire sign-arvo)
|
||||
`this
|
||||
::
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
|_ =bowl:gall
|
||||
+* met ~(. metadata bowl)
|
||||
++ poke-engine
|
||||
|_ [in=action:store out=(list update:store) cards=(list card)]
|
||||
++ poke-core .
|
||||
::
|
||||
++ abed
|
||||
|= =action:store poke-core(in action)
|
||||
::
|
||||
++ abet
|
||||
^- (quip card _state)
|
||||
:_ state
|
||||
%+ snoc (flop cards)
|
||||
[%give %fact ~[/updates] %hark-update !>([%more (flop out)])]
|
||||
::
|
||||
++ give
|
||||
|= =update:store poke-core(out [update out])
|
||||
::
|
||||
++ emit
|
||||
|= =card poke-core(cards [card cards])
|
||||
::
|
||||
++ translate
|
||||
^+ poke-core
|
||||
?- -.in
|
||||
::
|
||||
%add-note (add-note +.in)
|
||||
%archive (do-archive +.in)
|
||||
::
|
||||
%unread-count (unread-count +.in)
|
||||
%read-count (read-count +.in)
|
||||
::
|
||||
%read-each (read-each +.in)
|
||||
%unread-each (unread-each +.in)
|
||||
::
|
||||
%read-note (read-note +.in)
|
||||
::
|
||||
%seen-index (seen-index +.in)
|
||||
::
|
||||
%remove-graph (remove-graph +.in)
|
||||
%read-graph (read-graph +.in)
|
||||
%read-group (read-group +.in)
|
||||
::
|
||||
%set-dnd (set-dnd +.in)
|
||||
%seen seen
|
||||
%read-all read-all
|
||||
::
|
||||
==
|
||||
::
|
||||
:: +| %note
|
||||
::
|
||||
:: notification tracking
|
||||
++ put-notifs
|
||||
|= [time=@da =timebox:store]
|
||||
poke-core(notifications (put:orm notifications time timebox))
|
||||
::
|
||||
++ add-note
|
||||
|= [=index:store =notification:store]
|
||||
^+ poke-core
|
||||
=/ existing-notif
|
||||
(~(get by unread-notes) index)
|
||||
=/ new=notification:store
|
||||
(merge-notification existing-notif notification)
|
||||
=. unread-notes
|
||||
(~(put by unread-notes) index new)
|
||||
=/ timebox=@da
|
||||
(~(gut by timeboxes) (to-stats-index:store index) current-timebox)
|
||||
(give %added index new)
|
||||
::
|
||||
++ do-archive
|
||||
|= [time=(unit @da) =index:store]
|
||||
^+ poke-core
|
||||
|^
|
||||
?~(time archive-unread (archive-read u.time))
|
||||
::
|
||||
++ archive-unread
|
||||
=. unread-notes
|
||||
(~(del by unread-notes) index)
|
||||
(give %archive ~ index)
|
||||
::
|
||||
++ archive-read
|
||||
|= time=@da
|
||||
=/ =timebox:store
|
||||
(gut-orm notifications time)
|
||||
=/ =notification:store
|
||||
(~(got by timebox) index)
|
||||
=/ new-timebox=timebox:store
|
||||
(~(del by timebox) index)
|
||||
=. poke-core
|
||||
(put-notifs time new-timebox)
|
||||
(give %archive `time index)
|
||||
--
|
||||
::
|
||||
++ read-note
|
||||
|= =index:store
|
||||
=/ =notification:store
|
||||
(~(got by unread-notes) index)
|
||||
=. unread-notes
|
||||
(~(del by unread-notes) index)
|
||||
=/ =time
|
||||
(~(gut by timeboxes) (to-stats-index:store index) current-timebox)
|
||||
=/ =timebox:store
|
||||
(gut-orm notifications time)
|
||||
=/ existing-notif
|
||||
(~(get by timebox) index)
|
||||
=/ new=notification:store
|
||||
(merge-notification existing-notif notification)
|
||||
=. timebox
|
||||
(~(put by timebox) index new)
|
||||
=. notifications
|
||||
(put:orm notifications time timebox)
|
||||
(give %note-read time index)
|
||||
::
|
||||
::
|
||||
:: +| %each
|
||||
::
|
||||
:: each unread tracking
|
||||
::
|
||||
++ unread-each
|
||||
|= [=stats-index:store unread=index:graph-store time=@da]
|
||||
=. poke-core (seen-index time stats-index)
|
||||
%+ jub-unreads-each:(give %unread-each stats-index unread time)
|
||||
stats-index
|
||||
|= indices=(set index:graph-store)
|
||||
(~(put ^in indices) unread)
|
||||
::
|
||||
++ read-index-each
|
||||
|= [=stats-index:store ref=index:graph-store]
|
||||
%- read-indices
|
||||
%+ skim
|
||||
~(tap ^in ~(key by unread-notes))
|
||||
|= =index:store
|
||||
?. (stats-index-is-index:store stats-index index) %.n
|
||||
=/ not=notification:store
|
||||
(~(got by unread-notes) index)
|
||||
?. ?=(%graph -.index) %.n
|
||||
?. ?=(%graph -.contents.not) %.n
|
||||
(lien list.contents.not |=(p=post:post =(index.p ref)))
|
||||
::
|
||||
++ read-each
|
||||
|= [=stats-index:store ref=index:graph-store]
|
||||
=. timeboxes (~(put by timeboxes) stats-index now.bowl)
|
||||
=. poke-core (read-index-each stats-index ref)
|
||||
%+ jub-unreads-each:(give %read-each stats-index ref)
|
||||
stats-index
|
||||
|= indices=(set index:graph-store)
|
||||
(~(del ^in indices) ref)
|
||||
::
|
||||
++ jub-unreads-each
|
||||
|= $: =stats-index:store
|
||||
f=$-((set index:graph-store) (set index:graph-store))
|
||||
==
|
||||
poke-core(unreads-each (jub stats-index f))
|
||||
::
|
||||
++ unread-count
|
||||
|= [=stats-index:store time=@da]
|
||||
=/ new-count
|
||||
+((~(gut by unreads-count) stats-index 0))
|
||||
=. unreads-count
|
||||
(~(put by unreads-count) stats-index new-count)
|
||||
(seen-index:(give %unread-count stats-index time) time stats-index)
|
||||
::
|
||||
++ read-count
|
||||
|= =stats-index:store
|
||||
=. unreads-count (~(put by unreads-count) stats-index 0)
|
||||
=/ times=(list index:store)
|
||||
(unread-for-stats-index stats-index)
|
||||
=? timeboxes !(~(has by timeboxes) stats-index) (~(put by timeboxes) stats-index now.bowl)
|
||||
(give:(read-indices times) %read-count stats-index)
|
||||
::
|
||||
++ read-indices
|
||||
|= times=(list =index:store)
|
||||
|-
|
||||
?~ times poke-core
|
||||
=/ core
|
||||
(read-note i.times)
|
||||
$(poke-core core, times t.times)
|
||||
::
|
||||
++ seen-index
|
||||
|= [time=@da =stats-index:store]
|
||||
=/ new-time=@da
|
||||
(max time (~(gut by last-seen) stats-index 0))
|
||||
=. last-seen
|
||||
(~(put by last-seen) stats-index new-time)
|
||||
(give %seen-index new-time stats-index)
|
||||
::
|
||||
++ get-stats-indices
|
||||
|= rid=resource
|
||||
%- ~(gas ^in *(set stats-index:store))
|
||||
%+ skim
|
||||
;: weld
|
||||
~(tap ^in ~(key by unreads-count))
|
||||
~(tap ^in ~(key by last-seen))
|
||||
~(tap ^in ~(key by unreads-each))
|
||||
==
|
||||
|= =stats-index:store
|
||||
?. ?=(%graph -.stats-index) %.n
|
||||
=(graph.stats-index rid)
|
||||
::
|
||||
++ read-all-each
|
||||
|= =stats-index:store
|
||||
=/ refs=(list index:graph-store)
|
||||
~(tap ^in (~(get ju unreads-each) stats-index))
|
||||
|-
|
||||
?~ refs poke-core
|
||||
$(refs t.refs, poke-core (read-each stats-index i.refs))
|
||||
::
|
||||
++ read-graph
|
||||
|= rid=resource
|
||||
=/ indices=(list stats-index:store)
|
||||
~(tap ^in (get-stats-indices rid))
|
||||
|-
|
||||
?~ indices poke-core
|
||||
=* index i.indices
|
||||
=? poke-core (~(has by unreads-count) index)
|
||||
(read-count i.indices)
|
||||
=? poke-core (~(has by unreads-each) index)
|
||||
(read-all-each i.indices)
|
||||
$(indices t.indices)
|
||||
::
|
||||
++ read-group
|
||||
|= rid=resource
|
||||
=/ graphs=(list resource)
|
||||
(graphs-of-group:met rid)
|
||||
|-
|
||||
?~ graphs poke-core
|
||||
=/ core=_poke-core (read-graph i.graphs)
|
||||
$(graphs t.graphs, poke-core core)
|
||||
::
|
||||
++ remove-graph
|
||||
|= rid=resource
|
||||
|^
|
||||
=/ indices (get-stats-indices rid)
|
||||
=. poke-core
|
||||
(give %remove-graph rid)
|
||||
=. poke-core
|
||||
(remove-notifications indices)
|
||||
=. unreads-count
|
||||
((dif-map-by-key ,@ud) unreads-count indices)
|
||||
=. unreads-each
|
||||
%+ (dif-map-by-key ,(set index:graph-store))
|
||||
unreads-each indices
|
||||
=. last-seen
|
||||
((dif-map-by-key ,@da) last-seen indices)
|
||||
poke-core
|
||||
::
|
||||
++ dif-map-by-key
|
||||
|* value=mold
|
||||
|= [=(map stats-index:store value) =(set stats-index:store)]
|
||||
=/ to-remove ~(tap ^in set)
|
||||
|-
|
||||
?~ to-remove map
|
||||
=. map
|
||||
(~(del by map) i.to-remove)
|
||||
$(to-remove t.to-remove)
|
||||
::
|
||||
++ remove-notifications
|
||||
|= =(set stats-index:store)
|
||||
^+ poke-core
|
||||
=/ indices
|
||||
~(tap ^in set)
|
||||
|-
|
||||
?~ indices poke-core
|
||||
=/ times=(list =index:store)
|
||||
(unread-for-stats-index i.indices)
|
||||
=. poke-core
|
||||
(read-indices times)
|
||||
$(indices t.indices)
|
||||
--
|
||||
::
|
||||
++ seen
|
||||
=. poke-core
|
||||
(read-indices ~(tap ^in ~(key by unread-notes)))
|
||||
poke-core(current-timebox now.bowl, timeboxes ~)
|
||||
::
|
||||
++ read-all
|
||||
=: unreads-count (~(run by unreads-count) _0)
|
||||
unreads-each (~(run by unreads-each) _~)
|
||||
notifications (~(run by notifications) _~)
|
||||
==
|
||||
(give:seen %read-all ~)
|
||||
::
|
||||
++ set-dnd
|
||||
|= d=?
|
||||
(give:poke-core(dnd d) %set-dnd d)
|
||||
--
|
||||
::
|
||||
++ unread-for-stats-index
|
||||
|= =stats-index:store
|
||||
%+ skim ~(tap in ~(key by unread-notes))
|
||||
(cury stats-index-is-index:store stats-index)
|
||||
::
|
||||
++ merge-notification
|
||||
|= [existing=(unit notification:store) new=notification:store]
|
||||
^- notification:store
|
||||
?~ existing new
|
||||
?- -.contents.u.existing
|
||||
::
|
||||
%graph
|
||||
?> ?=(%graph -.contents.new)
|
||||
u.existing(list.contents (weld list.contents.u.existing list.contents.new))
|
||||
::
|
||||
%group
|
||||
?> ?=(%group -.contents.new)
|
||||
u.existing(list.contents (weld list.contents.u.existing list.contents.new))
|
||||
==
|
||||
::
|
||||
:: +key-orm: +key:by for ordered maps
|
||||
++ key-orm
|
||||
|= =notifications:store
|
||||
^- (list @da)
|
||||
(turn (tap:orm notifications) |=([@da *] +<-))
|
||||
:: +jub-orm: combo +jab/+gut for ordered maps
|
||||
:: TODO: move to zuse.hoon
|
||||
++ jub-orm
|
||||
|= [=notifications:store time=@da fun=$-(timebox:store timebox:store)]
|
||||
^- notifications:store
|
||||
=/ =timebox:store
|
||||
(fun (gut-orm notifications time))
|
||||
(put:orm notifications time timebox)
|
||||
++ jub
|
||||
|= [=stats-index:store f=$-((set index:graph-store) (set index:graph-store))]
|
||||
^- (jug stats-index:store index:graph-store)
|
||||
=/ val=(set index:graph-store)
|
||||
(~(gut by unreads-each) stats-index ~)
|
||||
(~(put by unreads-each) stats-index (f val))
|
||||
:: +gut-orm: +gut:by for ordered maps
|
||||
:: TODO: move to zuse.hoon
|
||||
++ gut-orm
|
||||
|= [=notifications:store time=@da]
|
||||
^- timebox:store
|
||||
(fall (get:orm notifications time) ~)
|
||||
::
|
||||
++ autoseen-interval ~h3
|
||||
++ cancel-autoseen
|
||||
^- card
|
||||
[%pass /autoseen %arvo %b %rest (add current-timebox autoseen-interval)]
|
||||
::
|
||||
++ autoseen-timer
|
||||
^- card
|
||||
[%pass /autoseen %arvo %b %wait (add now.bowl autoseen-interval)]
|
||||
::
|
||||
++ scry
|
||||
|* [=mold p=path]
|
||||
?> ?=(^ p)
|
||||
?> ?=(^ t.p)
|
||||
.^(mold i.p (scot %p our.bowl) i.t.p (scot %da now.bowl) t.t.p)
|
||||
::
|
||||
++ give
|
||||
|= [paths=(list path) update=update:store]
|
||||
^- (list card)
|
||||
[%give %fact paths [%hark-update !>(update)]]~
|
||||
::
|
||||
++ tap-nonempty
|
||||
|= =notifications:store
|
||||
^- (list [@da timebox:store])
|
||||
%+ skim (tap:orm notifications)
|
||||
|=([@da =timebox:store] !=(~(wyt by timebox) 0))
|
||||
--
|
@ -1,6 +1,6 @@
|
||||
::
|
||||
/- *notify, resource, hark-store, post
|
||||
/+ default-agent, verb, dbug, group, agentio
|
||||
/+ default-agent, verb, dbug, group, agentio, graphlib=graph
|
||||
::
|
||||
|%
|
||||
+$ card card:agent:gall
|
||||
@ -18,19 +18,25 @@
|
||||
$: providers=(jug @p term)
|
||||
==
|
||||
::
|
||||
+$ state-0
|
||||
$: %0
|
||||
=provider-state
|
||||
+$ base-state-0
|
||||
$: =provider-state
|
||||
=client-state
|
||||
==
|
||||
::
|
||||
+$ state-0
|
||||
[%0 base-state-0]
|
||||
::
|
||||
+$ state-1
|
||||
[%1 base-state-0]
|
||||
::
|
||||
+$ versioned-state
|
||||
$% state-0
|
||||
state-1
|
||||
==
|
||||
::
|
||||
--
|
||||
::
|
||||
=| state-0
|
||||
=| state-1
|
||||
=* state -
|
||||
::
|
||||
%- agent:dbug
|
||||
@ -47,19 +53,27 @@
|
||||
::
|
||||
++ on-init
|
||||
:_ this
|
||||
[(~(watch-our pass:io /hark) %hark-store /updates)]~
|
||||
[(~(watch-our pass:io /hark) %hark-store /notes)]~
|
||||
::
|
||||
++ on-save !>(state)
|
||||
++ on-load
|
||||
|= =old=vase
|
||||
^- (quip card _this)
|
||||
=/ old !<(versioned-state old-vase)
|
||||
=| cards=(list card)
|
||||
|-
|
||||
?- -.old
|
||||
%1 [(flop cards) this]
|
||||
::
|
||||
%0
|
||||
:_ this(state old)
|
||||
?. (~(has by wex.bowl) [/hark our.bowl %hark-store])
|
||||
~
|
||||
[(~(watch-our pass:io /hark) %hark-store /updates)]~
|
||||
%_ $
|
||||
::
|
||||
cards
|
||||
%+ welp cards
|
||||
:~ (~(leave-our pass:io /hark) %hark-store)
|
||||
(~(watch-our pass:io /hark) %hark-store /notes)
|
||||
==
|
||||
==
|
||||
==
|
||||
::
|
||||
++ on-poke
|
||||
@ -199,12 +213,10 @@
|
||||
?. ?=(%hark-update p.cage.sign)
|
||||
~
|
||||
=+ !<(hark-update=update:hark-store q.cage.sign)
|
||||
=/ notes=(list notification) (filter-notifications:do hark-update)
|
||||
?~ notes
|
||||
~
|
||||
?~ not=(filter-notifications:do hark-update) ~
|
||||
:: only send the last one, since hark accumulates notifcations
|
||||
=/ =update [%notification `notification`(snag 0 (flop notes))]
|
||||
=/ card (fact-all:io %notify-update !>(update))
|
||||
=/ =update [%notification u.not]
|
||||
=/ card=(unit card) ~ ::(fact-all:io %notify-update !>(update))
|
||||
(drop card)
|
||||
::
|
||||
%kick
|
||||
@ -281,39 +293,23 @@
|
||||
++ on-fail on-fail:def
|
||||
--
|
||||
|_ bowl=bowl:gall
|
||||
+* gra ~(. graphlib bowl)
|
||||
::
|
||||
++ filter-notifications
|
||||
|= =update:hark-store
|
||||
^- (list notification)
|
||||
?+ -.update ~
|
||||
%more
|
||||
(zing (turn more.update filter-notifications))
|
||||
::
|
||||
%added
|
||||
?- -.index.update
|
||||
%graph
|
||||
?: =(`%graph-validator-dm mark.index.update)
|
||||
?. ?=(%graph -.contents.notification.update)
|
||||
~
|
||||
%+ turn list.contents.notification.update
|
||||
|= =post:post
|
||||
^- notification
|
||||
[graph.index.update index.post]
|
||||
?: =(`%graph-validator-chat mark.index.update)
|
||||
=/ hid (group-is-hidden graph.index.update)
|
||||
?~ hid ~
|
||||
?. u.hid ~
|
||||
?. ?=(%graph -.contents.notification.update)
|
||||
~
|
||||
%+ turn list.contents.notification.update
|
||||
|= =post:post
|
||||
^- notification
|
||||
[graph.index.update index.post]
|
||||
~
|
||||
::
|
||||
%group ~
|
||||
==
|
||||
==
|
||||
^- (unit notification)
|
||||
?. ?=(%add-note -.update) ~
|
||||
=* place place.bin.update
|
||||
?. ?=(%landscape desk.place) ~
|
||||
?. ?=([%graph *] path.place) ~
|
||||
=/ link=path link.body.update
|
||||
?. ?=([@ @ @ *] link) ~
|
||||
?~ ship=(slaw %p i.t.link) ~
|
||||
=* name i.t.t.link
|
||||
=/ =resource:resource [u.ship name]
|
||||
=/ =index:graph-store
|
||||
(turn t.t.t.link (curr rash dim:ag))
|
||||
`[resource index]
|
||||
::
|
||||
++ group-is-hidden
|
||||
|= =resource:resource
|
@ -1,39 +1,38 @@
|
||||
:~ :- %apes
|
||||
:~ %chat-cli
|
||||
%chat-hook
|
||||
:~ %metadata-store :: start stores first
|
||||
%contact-store
|
||||
%chat-store
|
||||
%graph-store
|
||||
%group-store
|
||||
%invite-store
|
||||
%s3-store
|
||||
%settings-store
|
||||
%chat-cli
|
||||
%chat-hook
|
||||
%chat-view
|
||||
%clock
|
||||
%contact-hook
|
||||
%contact-pull-hook
|
||||
%contact-push-hook
|
||||
%contact-store
|
||||
%contact-view
|
||||
%demo-pull-hook
|
||||
%demo-push-hook
|
||||
%demo-store
|
||||
%dm-hook
|
||||
%graph-pull-hook
|
||||
%graph-push-hook
|
||||
%graph-store
|
||||
%group-pull-hook
|
||||
%group-push-hook
|
||||
%group-store
|
||||
%group-view
|
||||
%hark-chat-hook
|
||||
%hark-graph-hook
|
||||
%hark-group-hook
|
||||
%hark-store
|
||||
%hark-invite-hook
|
||||
%invite-hook
|
||||
%invite-store
|
||||
%invite-view
|
||||
%launch
|
||||
%metadata-hook
|
||||
%metadata-pull-hook
|
||||
%metadata-push-hook
|
||||
%metadata-store
|
||||
%notify
|
||||
%observe-hook
|
||||
%s3-store
|
||||
%sane
|
||||
%weather
|
||||
==
|
||||
|
@ -1,9 +1,9 @@
|
||||
:~ title+'Landscape'
|
||||
info+'A suite of applications to communicate on Urbit'
|
||||
color+0xee.5432
|
||||
glob-http+'https://bootstrap.urbit.org/glob-0v2dl97.2n2r9.4glcs.e533a.uakd1.glob'
|
||||
glob-http+'https://bootstrap.urbit.org/glob-0v7.bcnlj.eocmq.cmbeu.b8s54.8se40.glob'
|
||||
base+'landscape'
|
||||
version+[0 0 1]
|
||||
version+[1 3 5]
|
||||
website+'https://tlon.io'
|
||||
license+'MIT'
|
||||
==
|
||||
|
@ -1,10 +1,24 @@
|
||||
/- sur=graph-store, pos=post, pull-hook
|
||||
/- sur=graph-store, pos=post, pull-hook, hark=hark-store
|
||||
/+ res=resource, migrate
|
||||
=< [sur .]
|
||||
=< [pos .]
|
||||
=, sur
|
||||
=, pos
|
||||
|%
|
||||
++ hark-content
|
||||
|= =content
|
||||
^- content:hark
|
||||
?- -.content
|
||||
%text content
|
||||
%mention ship+ship.content
|
||||
%url text+url.content
|
||||
%code text+'A code excerpt'
|
||||
%reference text+'A reference'
|
||||
==
|
||||
::
|
||||
++ hark-contents
|
||||
|= cs=(list content)
|
||||
(turn cs hark-content)
|
||||
:: NOTE: move these functions to zuse
|
||||
++ nu :: parse number as hex
|
||||
|= jon=json
|
||||
|
@ -1,4 +1,5 @@
|
||||
/- *post, met=metadata-store, graph=graph-store, hark=hark-graph-hook
|
||||
/- *post, met=metadata-store, hark=hark-graph-hook
|
||||
/+ graph=graph-store
|
||||
|_ i=indexed-post
|
||||
++ grow
|
||||
|%
|
||||
@ -24,9 +25,15 @@
|
||||
==
|
||||
::
|
||||
++ notification-kind
|
||||
|= title=cord
|
||||
^- (unit notif-kind:hark)
|
||||
?+ index.p.i ~
|
||||
[@ ~] `[%message [0 1] %count %none]
|
||||
[@ ~]
|
||||
:- ~
|
||||
:* ~[text+(rap 3 'New messages in ' title ~)]
|
||||
[ship+author.p.i text+': ' (hark-contents:graph contents.p.i)]
|
||||
[0 1] %count %none
|
||||
==
|
||||
==
|
||||
::
|
||||
++ transform-add-nodes
|
||||
|
@ -1,6 +1,8 @@
|
||||
/- *post, met=metadata-store, graph=graph-store, hark=hark-graph-hook
|
||||
/- *post, met=metadata-store, hark=hark-graph-hook
|
||||
/+ graph=graph-store
|
||||
|_ i=indexed-post
|
||||
++ grow
|
||||
::
|
||||
++ grow
|
||||
|%
|
||||
++ noun i
|
||||
::
|
||||
@ -11,9 +13,15 @@
|
||||
i
|
||||
::
|
||||
++ notification-kind
|
||||
|= title=cord
|
||||
^- (unit notif-kind:hark)
|
||||
?+ index.p.i ~
|
||||
[@ @ ~] `[%message [1 2] %count %none]
|
||||
[@ @ ~]
|
||||
:- ~
|
||||
:* ~[text+'New messages from ' ship+author.p.i]
|
||||
(hark-contents:graph contents.p.i)
|
||||
[1 2] %count %none
|
||||
==
|
||||
==
|
||||
::
|
||||
--
|
||||
|
@ -1,4 +1,5 @@
|
||||
/- *post, met=metadata-store, graph=graph-store, hark=hark-graph-hook
|
||||
/- *post, met=metadata-store, hark=hark-graph-hook
|
||||
/+ graph=graph-store
|
||||
|_ i=indexed-post
|
||||
++ grow
|
||||
|%
|
||||
@ -49,10 +50,24 @@
|
||||
==
|
||||
::
|
||||
++ notification-kind
|
||||
|= title=cord
|
||||
^- (unit notif-kind:hark)
|
||||
?+ index.p.i ~
|
||||
[@ ~] `[%link [0 1] %each %children]
|
||||
[@ @ %1 ~] `[%comment [1 2] %count %siblings]
|
||||
[@ ~]
|
||||
:- ~
|
||||
:* [text+(rap 3 'New links in ' title ~)]~
|
||||
[ship+author.p.i text+': ' (hark-contents:graph contents.p.i)]
|
||||
[0 1] %each %children
|
||||
==
|
||||
|
||||
|
||||
[@ @ %1 ~]
|
||||
:- ~
|
||||
:* [text+(rap 3 'New comments on a post in ' title ~)]~
|
||||
[ship+author.p.i text+': ' (hark-contents:graph contents.p.i)]
|
||||
[1 2] %count %siblings
|
||||
==
|
||||
|
||||
==
|
||||
::
|
||||
++ transform-add-nodes
|
||||
|
@ -1,4 +1,5 @@
|
||||
/- *post, met=metadata-store, graph=graph-store, hark=hark-graph-hook
|
||||
/- *post, met=metadata-store, hark=hark-graph-hook
|
||||
/+ graph=graph-store
|
||||
|_ i=indexed-post
|
||||
++ grow
|
||||
|%
|
||||
@ -26,11 +27,16 @@
|
||||
:: +notification-kind: don't track unreads, notify on replies
|
||||
::
|
||||
++ notification-kind
|
||||
|= title=cord
|
||||
^- (unit notif-kind:hark)
|
||||
=/ len (lent index.p.i)
|
||||
=/ =mode:hark
|
||||
?:(=(1 len) %count %none)
|
||||
`[%post [(dec len) len] mode %children]
|
||||
:- ~
|
||||
:* ~[text+(rap 3 'Your post in ' title ' received replies ' ~)]
|
||||
[ship+author.p.i text+': ' (hark-contents:graph contents.p.i)]
|
||||
[(dec len) len] mode %children
|
||||
==
|
||||
::
|
||||
++ transform-add-nodes
|
||||
|= [=index =post =atom was-parent-modified=?]
|
||||
|
@ -1,4 +1,5 @@
|
||||
/- *post, met=metadata-store, graph=graph-store, hark=hark-graph-hook
|
||||
/+ graph=graph-store
|
||||
|_ i=indexed-post
|
||||
++ grow
|
||||
|%
|
||||
@ -64,10 +65,22 @@
|
||||
:: ignore all containers, only notify on content
|
||||
::
|
||||
++ notification-kind
|
||||
|= title=cord
|
||||
^- (unit notif-kind:hark)
|
||||
?+ index.p.i ~
|
||||
[@ %1 %1 ~] `[%note [0 1] %each %children]
|
||||
[@ %2 @ %1 ~] `[%comment [1 3] %count %siblings]
|
||||
[@ %1 %1 ~]
|
||||
:- ~
|
||||
:* [%text (rap 3 'New notes in ' title ~)]~
|
||||
~[(hark-content:graph (snag 0 contents.p.i)) text+' by ' ship+author.p.i]
|
||||
[0 1] %each %children
|
||||
==
|
||||
::
|
||||
[@ %2 @ %1 ~]
|
||||
:- ~
|
||||
:* [%text (rap 3 'New comments in ' title ~)]~
|
||||
[ship+author.p.i text+': ' (hark-contents:graph contents.p.i)]
|
||||
[1 3] %count %siblings
|
||||
==
|
||||
==
|
||||
::
|
||||
++ transform-add-nodes
|
||||
|
@ -1,4 +1,4 @@
|
||||
/- *resource, graph-store, post
|
||||
/- *resource, graph-store, post, store=hark-store
|
||||
^?
|
||||
|%
|
||||
::
|
||||
@ -10,7 +10,12 @@
|
||||
[parent=@ud self=@ud]
|
||||
::
|
||||
+$ notif-kind
|
||||
[name=@t =index-len =mode =watch-for]
|
||||
$: title=(list content:store)
|
||||
body=(list content:store)
|
||||
=index-len
|
||||
=mode
|
||||
=watch-for
|
||||
==
|
||||
::
|
||||
+$ action
|
||||
$%
|
||||
|
347
pkg/landscape/sur/hark-store-historical.hoon
Normal file
347
pkg/landscape/sur/hark-store-historical.hoon
Normal file
@ -0,0 +1,347 @@
|
||||
/- chat-store, graph-store, post, *resource, group-store, metadata-store
|
||||
^?
|
||||
|%
|
||||
::
|
||||
+$ old-state
|
||||
[?(%0 %1 %2 %3 %4 %5 %6) *]
|
||||
::
|
||||
+$ state-7
|
||||
[%7 base-state]
|
||||
::
|
||||
+$ versioned-state
|
||||
$% old-state
|
||||
state-7
|
||||
==
|
||||
+$ unread-stats
|
||||
[indices=(set index:graph-store) last=@da]
|
||||
::
|
||||
+$ base-state
|
||||
$: unreads-each=(jug stats-index index:graph-store)
|
||||
unreads-count=(map stats-index @ud)
|
||||
timeboxes=(map stats-index @da)
|
||||
unread-notes=timebox
|
||||
last-seen=(map stats-index @da)
|
||||
=notifications
|
||||
archive=notifications
|
||||
current-timebox=@da
|
||||
dnd=_|
|
||||
==
|
||||
::
|
||||
+$ index
|
||||
$% $: %graph
|
||||
graph=resource
|
||||
mark=(unit mark)
|
||||
description=@t
|
||||
=index:graph-store
|
||||
==
|
||||
[%group group=resource description=@t]
|
||||
==
|
||||
::
|
||||
+$ group-contents
|
||||
$~ [%add-members *resource ~]
|
||||
$>(?(%add-members %remove-members) update:group-store)
|
||||
::
|
||||
+$ notification
|
||||
[date=@da read=? =contents]
|
||||
::
|
||||
+$ contents
|
||||
$% [%graph =(list post:post)]
|
||||
[%group =(list group-contents)]
|
||||
==
|
||||
::
|
||||
+$ timebox
|
||||
(map index notification)
|
||||
::
|
||||
+$ notifications
|
||||
((mop @da timebox) gth)
|
||||
::
|
||||
+$ action
|
||||
$% [%add-note =index =notification]
|
||||
:: if .time is ~, then archiving unread notification
|
||||
:: else, archiving read notification
|
||||
[%archive time=(unit @da) =index]
|
||||
::
|
||||
[%unread-count =stats-index =time]
|
||||
[%read-count =stats-index]
|
||||
::
|
||||
[%unread-each =stats-index ref=index:graph-store time=@da]
|
||||
[%read-each =stats-index ref=index:graph-store]
|
||||
::
|
||||
[%read-note =index]
|
||||
::
|
||||
[%seen-index time=@da =stats-index]
|
||||
::
|
||||
[%read-graph =resource]
|
||||
[%read-group =resource]
|
||||
[%remove-graph =resource]
|
||||
::
|
||||
[%read-all ~]
|
||||
[%set-dnd dnd=?]
|
||||
[%seen ~]
|
||||
==
|
||||
::
|
||||
++ stats-index
|
||||
$% [%graph graph=resource =index:graph-store]
|
||||
[%group group=resource]
|
||||
==
|
||||
::
|
||||
+$ indexed-notification
|
||||
[index notification]
|
||||
::
|
||||
+$ stats
|
||||
[=unreads last-seen=@da]
|
||||
::
|
||||
+$ unreads
|
||||
$% [%count num=@ud]
|
||||
[%each indices=(set index:graph-store)]
|
||||
==
|
||||
::
|
||||
+$ update
|
||||
$% action
|
||||
[%more more=(list update)]
|
||||
[%added =index =notification]
|
||||
[%note-read =time =index]
|
||||
[%timebox time=(unit @da) =(list [index notification])]
|
||||
[%count count=@ud]
|
||||
[%clear =stats-index]
|
||||
[%unreads unreads=(list [stats-index stats])]
|
||||
==
|
||||
:: historical
|
||||
++ state-zero
|
||||
|%
|
||||
+$ state
|
||||
$: %0
|
||||
notifications=notifications
|
||||
archive=notifications
|
||||
current-timebox=@da
|
||||
dnd=_|
|
||||
==
|
||||
++ orm
|
||||
((ordered-map @da timebox) gth)
|
||||
::
|
||||
+$ notifications
|
||||
((mop @da timebox) gth)
|
||||
::
|
||||
+$ timebox
|
||||
(map index notification)
|
||||
::
|
||||
+$ index
|
||||
$% [%graph graph=resource module=@t description=@t]
|
||||
[%group group=resource description=@t]
|
||||
[%chat chat=path mention=?]
|
||||
==
|
||||
::
|
||||
+$ group-contents
|
||||
$~ [%add-members *resource ~]
|
||||
$% [%add *]
|
||||
[%remove *] :: old metadata actions
|
||||
$>(?(%add-members %remove-members) update:group-store)
|
||||
==
|
||||
::
|
||||
+$ contents
|
||||
$% [%graph =(list post:post-zero:post)]
|
||||
[%group =(list group-contents)]
|
||||
[%chat =(list envelope:chat-store)]
|
||||
==
|
||||
::
|
||||
+$ notification
|
||||
[date=@da read=? =contents]
|
||||
--
|
||||
::
|
||||
++ state-one
|
||||
|%
|
||||
+$ state
|
||||
$: %1
|
||||
unreads-each=(jug index index:graph-store)
|
||||
unreads-count=(map index @ud)
|
||||
last-seen=(map index @da)
|
||||
=notifications:state-two
|
||||
archive=notifications:state-two
|
||||
current-timebox=@da
|
||||
dnd=_|
|
||||
==
|
||||
--
|
||||
++ state-two
|
||||
=< state
|
||||
|%
|
||||
+$ state
|
||||
$: unreads-each=(jug stats-index index:graph-store)
|
||||
unreads-count=(map stats-index @ud)
|
||||
last-seen=(map stats-index @da)
|
||||
=notifications
|
||||
archive=notifications
|
||||
current-timebox=@da
|
||||
dnd=_|
|
||||
==
|
||||
::
|
||||
+$ index
|
||||
$% $: %graph
|
||||
group=resource
|
||||
graph=resource
|
||||
module=@t
|
||||
description=@t
|
||||
=index:graph-store
|
||||
==
|
||||
[%group group=resource description=@t]
|
||||
==
|
||||
::
|
||||
++ orm
|
||||
((ordered-map @da timebox) gth)
|
||||
::
|
||||
+$ notification
|
||||
[date=@da read=? =contents]
|
||||
::
|
||||
+$ contents
|
||||
$% [%graph =(list post:post-zero:post)]
|
||||
[%group =(list group-contents)]
|
||||
==
|
||||
::
|
||||
+$ group-contents
|
||||
group-contents:state-zero
|
||||
::
|
||||
+$ timebox
|
||||
(map index notification)
|
||||
::
|
||||
+$ notifications
|
||||
((mop @da timebox) gth)
|
||||
::
|
||||
--
|
||||
::
|
||||
++ state-three
|
||||
=< state
|
||||
|%
|
||||
+$ state
|
||||
$: unreads-each=(jug stats-index index:graph-store)
|
||||
unreads-count=(map stats-index @ud)
|
||||
last-seen=(map stats-index @da)
|
||||
=notifications
|
||||
archive=notifications
|
||||
current-timebox=@da
|
||||
dnd=_|
|
||||
==
|
||||
::
|
||||
++ orm
|
||||
((ordered-map @da timebox) gth)
|
||||
::
|
||||
+$ index
|
||||
$% $: %graph
|
||||
group=resource
|
||||
graph=resource
|
||||
module=@t
|
||||
description=@t
|
||||
=index:graph-store
|
||||
==
|
||||
[%group group=resource description=@t]
|
||||
==
|
||||
::
|
||||
+$ notification
|
||||
[date=@da read=? =contents]
|
||||
::
|
||||
+$ contents
|
||||
$% [%graph =(list post:post-zero:post)]
|
||||
[%group =(list group-contents)]
|
||||
==
|
||||
::
|
||||
+$ timebox
|
||||
(map index notification)
|
||||
::
|
||||
+$ notifications
|
||||
((mop @da timebox) gth)
|
||||
::
|
||||
--
|
||||
::
|
||||
++ state-four
|
||||
=< base-state
|
||||
|%
|
||||
++ orm
|
||||
((ordered-map @da timebox) gth)
|
||||
::
|
||||
+$ base-state
|
||||
$: unreads-each=(jug stats-index index:graph-store)
|
||||
unreads-count=(map stats-index @ud)
|
||||
last-seen=(map stats-index @da)
|
||||
=notifications
|
||||
archive=notifications
|
||||
current-timebox=@da
|
||||
dnd=_|
|
||||
==
|
||||
::
|
||||
+$ index
|
||||
$% $: %graph
|
||||
group=resource
|
||||
graph=resource
|
||||
module=@t
|
||||
description=@t
|
||||
=index:graph-store
|
||||
==
|
||||
[%group group=resource description=@t]
|
||||
==
|
||||
::
|
||||
+$ group-contents
|
||||
$~ [%add-members *resource ~]
|
||||
$>(?(%add-members %remove-members) update:group-store)
|
||||
::
|
||||
+$ notification
|
||||
[date=@da read=? =contents]
|
||||
::
|
||||
+$ contents
|
||||
$% [%graph =(list post:post)]
|
||||
[%group =(list group-contents)]
|
||||
==
|
||||
::
|
||||
+$ timebox
|
||||
(map index notification)
|
||||
::
|
||||
+$ notifications
|
||||
((mop @da timebox) gth)
|
||||
::
|
||||
+$ action
|
||||
$% [%add-note =index =notification]
|
||||
[%archive time=@da index]
|
||||
::
|
||||
[%unread-count =stats-index =time]
|
||||
[%read-count =stats-index]
|
||||
::
|
||||
::
|
||||
[%unread-each =stats-index ref=index:graph-store time=@da]
|
||||
[%read-each =stats-index ref=index:graph-store]
|
||||
::
|
||||
[%read-note time=@da index]
|
||||
[%unread-note time=@da index]
|
||||
::
|
||||
[%seen-index time=@da =stats-index]
|
||||
::
|
||||
[%remove-graph =resource]
|
||||
::
|
||||
[%read-all ~]
|
||||
[%set-dnd dnd=?]
|
||||
[%seen ~]
|
||||
==
|
||||
::
|
||||
++ stats-index
|
||||
$% [%graph graph=resource =index:graph-store]
|
||||
[%group group=resource]
|
||||
==
|
||||
::
|
||||
+$ indexed-notification
|
||||
[index notification]
|
||||
::
|
||||
+$ stats
|
||||
[notifications=(set [time index]) =unreads last-seen=@da]
|
||||
::
|
||||
+$ unreads
|
||||
$% [%count num=@ud]
|
||||
[%each indices=(set index:graph-store)]
|
||||
==
|
||||
::
|
||||
+$ update
|
||||
$% action
|
||||
[%more more=(list update)]
|
||||
[%added time=@da =index =notification]
|
||||
[%timebox time=@da archived=? =(list [index notification])]
|
||||
[%count count=@ud]
|
||||
[%clear =stats-index]
|
||||
[%unreads unreads=(list [stats-index stats])]
|
||||
==
|
||||
--
|
||||
--
|
@ -1,321 +0,0 @@
|
||||
/- chat-store, graph-store, post, *resource, group-store, metadata-store
|
||||
^?
|
||||
|%
|
||||
+$ index
|
||||
$% $: %graph
|
||||
graph=resource
|
||||
mark=(unit mark)
|
||||
description=@t
|
||||
=index:graph-store
|
||||
==
|
||||
[%group group=resource description=@t]
|
||||
==
|
||||
::
|
||||
+$ group-contents
|
||||
$~ [%add-members *resource ~]
|
||||
$>(?(%add-members %remove-members) update:group-store)
|
||||
::
|
||||
+$ notification
|
||||
[date=@da read=? =contents]
|
||||
::
|
||||
+$ contents
|
||||
$% [%graph =(list post:post)]
|
||||
[%group =(list group-contents)]
|
||||
==
|
||||
::
|
||||
+$ timebox
|
||||
(map index notification)
|
||||
::
|
||||
+$ notifications
|
||||
((mop @da timebox) gth)
|
||||
::
|
||||
+$ action
|
||||
$% [%add-note =index =notification]
|
||||
:: if .time is ~, then archiving unread notification
|
||||
:: else, archiving read notification
|
||||
[%archive time=(unit @da) =index]
|
||||
::
|
||||
[%unread-count =stats-index =time]
|
||||
[%read-count =stats-index]
|
||||
::
|
||||
[%unread-each =stats-index ref=index:graph-store time=@da]
|
||||
[%read-each =stats-index ref=index:graph-store]
|
||||
::
|
||||
[%read-note =index]
|
||||
::
|
||||
[%seen-index time=@da =stats-index]
|
||||
::
|
||||
[%read-graph =resource]
|
||||
[%read-group =resource]
|
||||
[%remove-graph =resource]
|
||||
::
|
||||
[%read-all ~]
|
||||
[%set-dnd dnd=?]
|
||||
[%seen ~]
|
||||
==
|
||||
::
|
||||
++ stats-index
|
||||
$% [%graph graph=resource =index:graph-store]
|
||||
[%group group=resource]
|
||||
==
|
||||
::
|
||||
+$ indexed-notification
|
||||
[index notification]
|
||||
::
|
||||
+$ stats
|
||||
[=unreads last-seen=@da]
|
||||
::
|
||||
+$ unreads
|
||||
$% [%count num=@ud]
|
||||
[%each indices=(set index:graph-store)]
|
||||
==
|
||||
::
|
||||
+$ update
|
||||
$% action
|
||||
[%more more=(list update)]
|
||||
[%added =index =notification]
|
||||
[%note-read =time =index]
|
||||
[%timebox time=(unit @da) =(list [index notification])]
|
||||
[%count count=@ud]
|
||||
[%clear =stats-index]
|
||||
[%unreads unreads=(list [stats-index stats])]
|
||||
==
|
||||
:: historical
|
||||
++ state-zero
|
||||
|%
|
||||
+$ state
|
||||
$: %0
|
||||
notifications=notifications
|
||||
archive=notifications
|
||||
current-timebox=@da
|
||||
dnd=_|
|
||||
==
|
||||
++ orm
|
||||
((ordered-map @da timebox) gth)
|
||||
::
|
||||
+$ notifications
|
||||
((mop @da timebox) gth)
|
||||
::
|
||||
+$ timebox
|
||||
(map index notification)
|
||||
::
|
||||
+$ index
|
||||
$% [%graph graph=resource module=@t description=@t]
|
||||
[%group group=resource description=@t]
|
||||
[%chat chat=path mention=?]
|
||||
==
|
||||
::
|
||||
+$ group-contents
|
||||
$~ [%add-members *resource ~]
|
||||
$% [%add *]
|
||||
[%remove *] :: old metadata actions
|
||||
$>(?(%add-members %remove-members) update:group-store)
|
||||
==
|
||||
::
|
||||
+$ contents
|
||||
$% [%graph =(list post:post-zero:post)]
|
||||
[%group =(list group-contents)]
|
||||
[%chat =(list envelope:chat-store)]
|
||||
==
|
||||
::
|
||||
+$ notification
|
||||
[date=@da read=? =contents]
|
||||
--
|
||||
::
|
||||
++ state-one
|
||||
|%
|
||||
+$ state
|
||||
$: %1
|
||||
unreads-each=(jug index index:graph-store)
|
||||
unreads-count=(map index @ud)
|
||||
last-seen=(map index @da)
|
||||
=notifications:state-two
|
||||
archive=notifications:state-two
|
||||
current-timebox=@da
|
||||
dnd=_|
|
||||
==
|
||||
--
|
||||
++ state-two
|
||||
=< state
|
||||
|%
|
||||
+$ state
|
||||
$: unreads-each=(jug stats-index index:graph-store)
|
||||
unreads-count=(map stats-index @ud)
|
||||
last-seen=(map stats-index @da)
|
||||
=notifications
|
||||
archive=notifications
|
||||
current-timebox=@da
|
||||
dnd=_|
|
||||
==
|
||||
::
|
||||
+$ index
|
||||
$% $: %graph
|
||||
group=resource
|
||||
graph=resource
|
||||
module=@t
|
||||
description=@t
|
||||
=index:graph-store
|
||||
==
|
||||
[%group group=resource description=@t]
|
||||
==
|
||||
::
|
||||
++ orm
|
||||
((ordered-map @da timebox) gth)
|
||||
::
|
||||
+$ notification
|
||||
[date=@da read=? =contents]
|
||||
::
|
||||
+$ contents
|
||||
$% [%graph =(list post:post-zero:post)]
|
||||
[%group =(list group-contents)]
|
||||
==
|
||||
::
|
||||
+$ group-contents
|
||||
group-contents:state-zero
|
||||
::
|
||||
+$ timebox
|
||||
(map index notification)
|
||||
::
|
||||
+$ notifications
|
||||
((mop @da timebox) gth)
|
||||
::
|
||||
--
|
||||
::
|
||||
++ state-three
|
||||
=< state
|
||||
|%
|
||||
+$ state
|
||||
$: unreads-each=(jug stats-index index:graph-store)
|
||||
unreads-count=(map stats-index @ud)
|
||||
last-seen=(map stats-index @da)
|
||||
=notifications
|
||||
archive=notifications
|
||||
current-timebox=@da
|
||||
dnd=_|
|
||||
==
|
||||
::
|
||||
++ orm
|
||||
((ordered-map @da timebox) gth)
|
||||
::
|
||||
+$ index
|
||||
$% $: %graph
|
||||
group=resource
|
||||
graph=resource
|
||||
module=@t
|
||||
description=@t
|
||||
=index:graph-store
|
||||
==
|
||||
[%group group=resource description=@t]
|
||||
==
|
||||
::
|
||||
+$ notification
|
||||
[date=@da read=? =contents]
|
||||
::
|
||||
+$ contents
|
||||
$% [%graph =(list post:post-zero:post)]
|
||||
[%group =(list group-contents)]
|
||||
==
|
||||
::
|
||||
+$ timebox
|
||||
(map index notification)
|
||||
::
|
||||
+$ notifications
|
||||
((mop @da timebox) gth)
|
||||
::
|
||||
--
|
||||
::
|
||||
++ state-four
|
||||
=< base-state
|
||||
|%
|
||||
++ orm
|
||||
((ordered-map @da timebox) gth)
|
||||
::
|
||||
+$ base-state
|
||||
$: unreads-each=(jug stats-index index:graph-store)
|
||||
unreads-count=(map stats-index @ud)
|
||||
last-seen=(map stats-index @da)
|
||||
=notifications
|
||||
archive=notifications
|
||||
current-timebox=@da
|
||||
dnd=_|
|
||||
==
|
||||
::
|
||||
+$ index
|
||||
$% $: %graph
|
||||
group=resource
|
||||
graph=resource
|
||||
module=@t
|
||||
description=@t
|
||||
=index:graph-store
|
||||
==
|
||||
[%group group=resource description=@t]
|
||||
==
|
||||
::
|
||||
+$ group-contents
|
||||
$~ [%add-members *resource ~]
|
||||
$>(?(%add-members %remove-members) update:group-store)
|
||||
::
|
||||
+$ notification
|
||||
[date=@da read=? =contents]
|
||||
::
|
||||
+$ contents
|
||||
$% [%graph =(list post:post)]
|
||||
[%group =(list group-contents)]
|
||||
==
|
||||
::
|
||||
+$ timebox
|
||||
(map index notification)
|
||||
::
|
||||
+$ notifications
|
||||
((mop @da timebox) gth)
|
||||
::
|
||||
+$ action
|
||||
$% [%add-note =index =notification]
|
||||
[%archive time=@da index]
|
||||
::
|
||||
[%unread-count =stats-index =time]
|
||||
[%read-count =stats-index]
|
||||
::
|
||||
::
|
||||
[%unread-each =stats-index ref=index:graph-store time=@da]
|
||||
[%read-each =stats-index ref=index:graph-store]
|
||||
::
|
||||
[%read-note time=@da index]
|
||||
[%unread-note time=@da index]
|
||||
::
|
||||
[%seen-index time=@da =stats-index]
|
||||
::
|
||||
[%remove-graph =resource]
|
||||
::
|
||||
[%read-all ~]
|
||||
[%set-dnd dnd=?]
|
||||
[%seen ~]
|
||||
==
|
||||
::
|
||||
++ stats-index
|
||||
$% [%graph graph=resource =index:graph-store]
|
||||
[%group group=resource]
|
||||
==
|
||||
::
|
||||
+$ indexed-notification
|
||||
[index notification]
|
||||
::
|
||||
+$ stats
|
||||
[notifications=(set [time index]) =unreads last-seen=@da]
|
||||
::
|
||||
+$ unreads
|
||||
$% [%count num=@ud]
|
||||
[%each indices=(set index:graph-store)]
|
||||
==
|
||||
::
|
||||
+$ update
|
||||
$% action
|
||||
[%more more=(list update)]
|
||||
[%added time=@da =index =notification]
|
||||
[%timebox time=@da archived=? =(list [index notification])]
|
||||
[%count count=@ud]
|
||||
[%clear =stats-index]
|
||||
[%unreads unreads=(list [stats-index stats])]
|
||||
==
|
||||
--
|
||||
--
|
1
pkg/landscape/sur/hark-store.hoon
Symbolic link
1
pkg/landscape/sur/hark-store.hoon
Symbolic link
@ -0,0 +1 @@
|
||||
../../garden-dev/sur/hark-store.hoon
|
@ -1,10 +1,13 @@
|
||||
import f from 'lodash/fp';
|
||||
import bigInt, { BigInteger } from 'big-integer';
|
||||
import { BigInteger } from 'big-integer';
|
||||
|
||||
import { Poke } from '../lib/types';
|
||||
import { GraphNotifDescription, GraphNotificationContents, GraphNotifIndex, IndexedNotification, NotifIndex, Unreads } from './types';
|
||||
import {
|
||||
HarkBin,
|
||||
HarkBinId,
|
||||
HarkLid,
|
||||
HarkPlace
|
||||
} from './types';
|
||||
import { decToUd } from '../lib';
|
||||
import { Association } from '../metadata/types';
|
||||
|
||||
export const harkAction = <T>(data: T): Poke<T> => ({
|
||||
app: 'hark-store',
|
||||
@ -31,232 +34,116 @@ export { groupHookAction as harkGroupHookAction };
|
||||
export const actOnNotification = (
|
||||
frond: string,
|
||||
intTime: BigInteger,
|
||||
index: NotifIndex
|
||||
): Poke<unknown> => harkAction({
|
||||
[frond]: {
|
||||
time: decToUd(intTime.toString()),
|
||||
index
|
||||
}
|
||||
});
|
||||
|
||||
export const getParentIndex = (
|
||||
idx: GraphNotifIndex,
|
||||
contents: GraphNotificationContents
|
||||
): string | undefined => {
|
||||
const origIndex = contents[0].index.slice(1).split('/');
|
||||
const ret = (i: string[]) => `/${i.join('/')}`;
|
||||
switch (idx.description) {
|
||||
case 'link':
|
||||
return '/';
|
||||
case 'comment':
|
||||
return ret(origIndex.slice(0, 1));
|
||||
case 'note':
|
||||
return '/';
|
||||
case 'mention':
|
||||
return undefined;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export const setMentions = (
|
||||
mentions: boolean
|
||||
): Poke<unknown> => graphHookAction({
|
||||
'set-mentions': mentions
|
||||
});
|
||||
|
||||
export const setWatchOnSelf = (
|
||||
watchSelf: boolean
|
||||
): Poke<unknown> => graphHookAction({
|
||||
'set-watch-on-self': watchSelf
|
||||
});
|
||||
|
||||
export const setDoNotDisturb = (
|
||||
dnd: boolean
|
||||
): Poke<unknown> => harkAction({
|
||||
'set-dnd': dnd
|
||||
});
|
||||
|
||||
export const archive = (
|
||||
index: NotifIndex,
|
||||
time?: BigInteger,
|
||||
): Poke<unknown> => harkAction({
|
||||
'archive': {
|
||||
time: time ? decToUd(time.toString()) : null,
|
||||
index
|
||||
}
|
||||
});
|
||||
|
||||
export const readNote = (
|
||||
index: NotifIndex
|
||||
): Poke<unknown> => harkAction({ 'read-note': index });
|
||||
|
||||
export const readIndex = (
|
||||
index: NotifIndex
|
||||
): Poke<unknown> => harkAction({
|
||||
'read-index': index
|
||||
});
|
||||
|
||||
export const unread = (
|
||||
time: BigInteger,
|
||||
index: NotifIndex
|
||||
): Poke<unknown> => actOnNotification('unread-note', time, index);
|
||||
|
||||
export const markCountAsRead = (
|
||||
graph: string,
|
||||
index = '/'
|
||||
): Poke<unknown> => harkAction({
|
||||
'read-count': {
|
||||
graph: {
|
||||
graph,
|
||||
index
|
||||
bin: HarkBin
|
||||
): Poke<unknown> =>
|
||||
harkAction({
|
||||
[frond]: {
|
||||
time: decToUd(intTime.toString()),
|
||||
bin
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const setMentions = (mentions: boolean): Poke<unknown> =>
|
||||
graphHookAction({
|
||||
'set-mentions': mentions
|
||||
});
|
||||
|
||||
export const setWatchOnSelf = (watchSelf: boolean): Poke<unknown> =>
|
||||
graphHookAction({
|
||||
'set-watch-on-self': watchSelf
|
||||
});
|
||||
|
||||
export const setDoNotDisturb = (dnd: boolean): Poke<unknown> =>
|
||||
harkAction({
|
||||
'set-dnd': dnd
|
||||
});
|
||||
|
||||
export const archive = (bin: HarkBin, lid: HarkLid): Poke<unknown> =>
|
||||
harkAction({
|
||||
archive: {
|
||||
lid,
|
||||
bin
|
||||
}
|
||||
});
|
||||
|
||||
export const opened = harkAction({
|
||||
opened: null
|
||||
});
|
||||
|
||||
|
||||
|
||||
export const markCountAsRead = (place: HarkPlace): Poke<unknown> =>
|
||||
harkAction({
|
||||
'read-count': place
|
||||
});
|
||||
|
||||
export const markEachAsRead = (
|
||||
graph: string,
|
||||
index: string,
|
||||
target: string
|
||||
): Poke<unknown> => harkAction({
|
||||
'read-each': {
|
||||
index: {
|
||||
graph: {
|
||||
graph,
|
||||
index
|
||||
}
|
||||
},
|
||||
target
|
||||
}
|
||||
});
|
||||
|
||||
export const dec = (
|
||||
index: NotifIndex,
|
||||
ref: string
|
||||
): Poke<unknown> => harkAction({
|
||||
dec: {
|
||||
index,
|
||||
ref
|
||||
}
|
||||
});
|
||||
place: HarkPlace,
|
||||
path: string
|
||||
): Poke<unknown> =>
|
||||
harkAction({
|
||||
'read-each': {
|
||||
place,
|
||||
path
|
||||
}
|
||||
});
|
||||
|
||||
export const seen = () => harkAction({ seen: null });
|
||||
|
||||
export const readAll = () => harkAction({ 'read-all': null });
|
||||
export const readAll = harkAction({ 'read-all': null });
|
||||
export const archiveAll = harkAction({ 'archive-all': null });
|
||||
|
||||
export const ignoreGroup = (
|
||||
group: string
|
||||
): Poke<unknown> => groupHookAction({
|
||||
ignore: group
|
||||
});
|
||||
export const ignoreGroup = (group: string): Poke<unknown> =>
|
||||
groupHookAction({
|
||||
ignore: group
|
||||
});
|
||||
|
||||
export const ignoreGraph = (
|
||||
graph: string,
|
||||
index: string
|
||||
): Poke<unknown> => graphHookAction({
|
||||
ignore: {
|
||||
graph,
|
||||
index
|
||||
}
|
||||
});
|
||||
|
||||
export const listenGroup = (
|
||||
group: string
|
||||
): Poke<unknown> => groupHookAction({
|
||||
listen: group
|
||||
});
|
||||
|
||||
export const listenGraph = (
|
||||
graph: string,
|
||||
index: string
|
||||
): Poke<unknown> => graphHookAction({
|
||||
listen: {
|
||||
graph,
|
||||
index
|
||||
}
|
||||
});
|
||||
|
||||
export const mute = (
|
||||
notif: IndexedNotification
|
||||
): Poke<any> | {} => {
|
||||
if('graph' in notif.index && 'graph' in notif.notification.contents) {
|
||||
const { index } = notif;
|
||||
const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph);
|
||||
if(!parentIndex) {
|
||||
return {};
|
||||
export const ignoreGraph = (graph: string, index: string): Poke<unknown> =>
|
||||
graphHookAction({
|
||||
ignore: {
|
||||
graph,
|
||||
index
|
||||
}
|
||||
return ignoreGraph(index.graph.graph, parentIndex);
|
||||
}
|
||||
if('group' in notif.index) {
|
||||
const { group } = notif.index.group;
|
||||
return ignoreGroup(group);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
export const unmute = (
|
||||
notif: IndexedNotification
|
||||
): Poke<any> | {} => {
|
||||
if('graph' in notif.index && 'graph' in notif.notification.contents) {
|
||||
const { index } = notif;
|
||||
const parentIndex = getParentIndex(index.graph, notif.notification.contents.graph);
|
||||
if(!parentIndex) {
|
||||
return {};
|
||||
export const listenGroup = (group: string): Poke<unknown> =>
|
||||
groupHookAction({
|
||||
listen: group
|
||||
});
|
||||
|
||||
export const listenGraph = (graph: string, index: string): Poke<unknown> =>
|
||||
graphHookAction({
|
||||
listen: {
|
||||
graph,
|
||||
index
|
||||
}
|
||||
return listenGraph(index.graph.graph, parentIndex);
|
||||
}
|
||||
if('group' in notif.index) {
|
||||
return listenGroup(notif.index.group.group);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
export const getLastSeen = (
|
||||
unreads: Unreads,
|
||||
path: string,
|
||||
index: string
|
||||
): BigInteger | undefined => {
|
||||
const lastSeenIdx = unreads.graph?.[path]?.[index]?.unreads;
|
||||
if (!(typeof lastSeenIdx === 'string')) {
|
||||
return bigInt.zero;
|
||||
}
|
||||
return f.flow(f.split('/'), f.last, x => (x ? bigInt(x) : undefined))(
|
||||
lastSeenIdx
|
||||
);
|
||||
}
|
||||
|
||||
export const getUnreadCount = (
|
||||
unreads: Unreads,
|
||||
path: string,
|
||||
index: string
|
||||
): number => {
|
||||
const graphUnreads = unreads.graph?.[path]?.[index]?.unreads ?? 0;
|
||||
return typeof graphUnreads === 'number' ? graphUnreads : graphUnreads.size;
|
||||
}
|
||||
|
||||
export const getNotificationCount = (
|
||||
unreads: Unreads,
|
||||
path: string
|
||||
): number => {
|
||||
const unread = unreads.graph?.[path] || {};
|
||||
return Object.keys(unread)
|
||||
.map(index => unread[index]?.notifications as number || 0)
|
||||
.reduce(f.add, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Read all graphs belonging to a particular group
|
||||
*/
|
||||
export const readGroup = (group: string) =>
|
||||
export const readGroup = (group: string) =>
|
||||
harkAction({
|
||||
'read-group': group
|
||||
});
|
||||
|
||||
/**
|
||||
/**
|
||||
* Read all unreads in a graph
|
||||
*/
|
||||
export const readGraph = (graph: string) =>
|
||||
export const readGraph = (graph: string) =>
|
||||
harkAction({
|
||||
'read-graph': graph
|
||||
});
|
||||
|
||||
export function harkBinToId(bin: HarkBin): HarkBinId {
|
||||
const { place, path } = bin;
|
||||
return `${place.desk}${place.path}${path}`;
|
||||
}
|
||||
|
||||
export function harkBinEq(a: HarkBin, b: HarkBin): boolean {
|
||||
return (
|
||||
a.place.path === b.place.path &&
|
||||
a.place.desk === b.place.desk &&
|
||||
a.path === b.path
|
||||
);
|
||||
}
|
||||
|
@ -1,60 +1,46 @@
|
||||
import { BigInteger } from "big-integer";
|
||||
import { Post } from "../graph/types";
|
||||
import { GroupUpdate } from "../groups/types";
|
||||
import BigIntOrderedMap from "../lib/BigIntOrderedMap";
|
||||
|
||||
export type GraphNotifDescription = "link" | "comment" | "note" | "mention" | "message" | "post";
|
||||
|
||||
export interface UnreadStats {
|
||||
unreads: Set<string> | number;
|
||||
notifications: NotifRef[] | number;
|
||||
export interface HarkStats {
|
||||
count: number;
|
||||
each: string[];
|
||||
last: number;
|
||||
}
|
||||
|
||||
interface NotifRef {
|
||||
time: BigInteger | undefined;
|
||||
index: NotifIndex;
|
||||
export interface Timebox {
|
||||
[binId: string]: Notification;
|
||||
}
|
||||
|
||||
export interface GraphNotifIndex {
|
||||
graph: string;
|
||||
group: string;
|
||||
description: GraphNotifDescription;
|
||||
mark: string;
|
||||
index: string;
|
||||
export type HarkContent = { ship: string; } | { text: string; };
|
||||
|
||||
export interface HarkBody {
|
||||
title: HarkContent[];
|
||||
time: string;
|
||||
content: HarkContent[];
|
||||
link: string;
|
||||
binned: string;
|
||||
}
|
||||
|
||||
export interface GroupNotifIndex {
|
||||
group: string;
|
||||
description: string;
|
||||
export interface HarkPlace {
|
||||
desk: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export type NotifIndex =
|
||||
| { graph: GraphNotifIndex }
|
||||
| { group: GroupNotifIndex };
|
||||
export interface HarkBin {
|
||||
path: string;
|
||||
place: HarkPlace;
|
||||
}
|
||||
|
||||
export type GraphNotificationContents = Post[];
|
||||
export type HarkLid =
|
||||
{ unseen: null; }
|
||||
| { seen: null; }
|
||||
| { time: string; };
|
||||
|
||||
export type GroupNotificationContents = GroupUpdate[];
|
||||
|
||||
export type NotificationContents =
|
||||
| { graph: GraphNotificationContents }
|
||||
| { group: GroupNotificationContents };
|
||||
export type HarkBinId = string;
|
||||
export interface Notification {
|
||||
read: boolean;
|
||||
bin: HarkBin;
|
||||
time: number;
|
||||
contents: NotificationContents;
|
||||
body: HarkBody[];
|
||||
}
|
||||
|
||||
export interface IndexedNotification {
|
||||
index: NotifIndex;
|
||||
notification: Notification;
|
||||
}
|
||||
|
||||
export type Timebox = IndexedNotification[];
|
||||
|
||||
export type Notifications = BigIntOrderedMap<Timebox>;
|
||||
|
||||
export interface NotificationGraphConfig {
|
||||
watchOnSelf: boolean;
|
||||
mentions: boolean;
|
||||
@ -62,8 +48,7 @@ export interface NotificationGraphConfig {
|
||||
}
|
||||
|
||||
export interface Unreads {
|
||||
graph: Record<string, Record<string, UnreadStats>>;
|
||||
group: Record<string, UnreadStats>;
|
||||
[path: string]: HarkStats;
|
||||
}
|
||||
|
||||
interface WatchedIndex {
|
||||
|
1694
pkg/npm/api/package-lock.json
generated
1694
pkg/npm/api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
2312
pkg/npm/eslint-config/package-lock.json
generated
2312
pkg/npm/eslint-config/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14352
pkg/npm/http-api/package-lock.json
generated
14352
pkg/npm/http-api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user