hark: separate unread tracking from notifications

This commit is contained in:
Liam Fitzgerald 2020-11-24 12:20:44 +10:00
parent 5d7fa0463c
commit b7b4000986
No known key found for this signature in database
GPG Key ID: D390E12C61D1CFFB
31 changed files with 662 additions and 397 deletions

View File

@ -1,214 +1,21 @@
:: hark-chat-hook: notifications for chat-store [landscape] :: hark-chat-hook: notifications for chat-store [landscape]
:: ::
/- store=hark-store, post, group-store, metadata-store, hook=hark-chat-hook /+ default-agent
/+ resource, metadata, default-agent, dbug, chat-store, grpl=group
:: ::
~% %hark-chat-hook-top ..is ~
|%
+$ card card:agent:gall
+$ versioned-state
$% state-0
==
::
+$ state-0
$: %0
watching=(set path)
mentions=_&
==
::
--
::
=| state-0
=* state -
::
=>
|_ =bowl:gall
::
++ give
|= [paths=(list path) =update:hook]
^- (list card)
[%give %fact paths hark-chat-hook-update+!>(update)]~
::
++ watch-chat
^- card
[%pass /chat %agent [our.bowl %chat-store] %watch /all]
--
%- agent:dbug
^- agent:gall ^- agent:gall
~% %hark-chat-hook-agent ..card ~
|_ =bowl:gall |_ =bowl:gall
+* this . +* this .
ha ~(. +> bowl)
def ~(. (default-agent this %|) bowl) def ~(. (default-agent this %|) bowl)
met ~(. metadata bowl) ++ on-init [~ this]
grp ~(. grpl bowl) ++ on-save !>(~)
::
++ on-init
:_ this
~[watch-chat:ha]
::
++ on-save !>(state)
++ on-load ++ on-load
|= old=vase |= =vase
^- (quip card _this) `this
:_ this(state !<(state-0 old))
?: (~(has by wex.bowl) [/chat our.bowl %chat-store])
~
~[watch-chat:ha]
::
++ on-watch
|= =path
^- (quip card _this)
=^ cards state
?+ path (on-watch:def path)
::
[%updates ~]
:_ state
%+ give:ha ~
:* %initial
watching
==
==
[cards this]
::
++ on-poke
~/ %hark-chat-hook-poke
|= [=mark =vase]
^- (quip card _this)
|^
?> (team:title our.bowl src.bowl)
=^ cards state
?+ mark (on-poke:def mark vase)
%hark-chat-hook-action
(hark-chat-hook-action !<(action:hook vase))
==
[cards this]
::
++ hark-chat-hook-action
|= =action:hook
^- (quip card _state)
|^
:- (give:ha ~[/updates] action)
?- -.action
%listen (listen +.action)
%ignore (ignore +.action)
%set-mentions (set-mentions +.action)
==
++ listen
|= chat=path
^+ state
state(watching (~(put in watching) chat))
::
++ ignore
|= chat=path
^+ state
state(watching (~(del in watching) chat))
::
++ set-mentions
|= ment=?
^+ state
state(mentions ment)
--
--
::
++ on-agent
~/ %hark-chat-hook-agent
|= [=wire =sign:agent:gall]
^- (quip card _this)
|^
?+ -.sign (on-agent:def wire sign)
%kick
:_ this
?. ?=([%chat ~] wire)
~
~[watch-chat:ha]
::
%fact
?. ?=(%chat-update p.cage.sign)
(on-agent:def wire sign)
=^ cards state
(chat-update !<(update:chat-store q.cage.sign))
[cards this]
==
::
++ chat-update
|= =update:chat-store
^- (quip card _state)
?+ -.update `state
%initial (process-initial +.update)
%create (process-new +.update)
::
%message
:_ state
(process-envelope path.update envelope.update)
::
%messages
:_ state
%- zing
(turn envelopes.update (cury process-envelope path.update))
==
++ process-initial
|= =inbox:chat-store
^- (quip card _state)
=/ keys=(list path)
~(tap in ~(key by inbox))
=| cards=(list card)
|-
?~ keys
[cards state]
=* path i.keys
=^ cs state
(process-new path)
$(cards (weld cards cs), keys t.keys)
::
++ process-new
|= chat=path
^- (quip card _state)
=/ groups=(list path)
(groups-from-resource:met %chat chat)
?~ groups
`state
?: (is-managed-path:grp i.groups)
`state
`state(watching (~(put in watching) chat))
::
++ is-mention
|= =envelope:chat-store
?. ?=(%text -.letter.envelope) %.n
?& mentions
?= ^
(find (scow %p our.bowl) (trip text.letter.envelope))
==
::
++ is-notification
|= [=path =envelope:chat-store]
?& (~(has in watching) path)
!=(author.envelope our.bowl)
==
::
++ process-envelope
|= [=path =envelope:chat-store]
^- (list card)
=/ mention=?
(is-mention envelope)
?. ?|(mention (is-notification path envelope))
~
=/ =index:store
[%chat path mention]
=/ =contents:store
[%chat ~[envelope]]
~[(poke-store %add index when.envelope %.n contents)]
::
++ poke-store
|= =action:store
^- card
=- [%pass /store %agent [our.bowl %hark-store] %poke -]
hark-action+!>(action)
--
::
++ on-peek on-peek:def
::
++ on-leave on-leave:def
++ on-arvo on-arvo:def ++ on-arvo on-arvo:def
++ on-agent on-agent:def
++ on-poke on-poke:def
++ on-peek on-peek:def
++ on-watch on-watch:def
++ on-leave on-leave:def
++ on-fail on-fail:def ++ on-fail on-fail:def
-- --

View File

@ -3,6 +3,7 @@
/- store=hark-store, post, group-store, metadata-store, hook=hark-graph-hook /- store=hark-store, post, group-store, metadata-store, hook=hark-graph-hook
/+ resource, metadata, default-agent, dbug, graph-store /+ resource, metadata, default-agent, dbug, graph-store
:: ::
::
~% %hark-graph-hook-top ..is ~ ~% %hark-graph-hook-top ..is ~
|% |%
+$ card card:agent:gall +$ card card:agent:gall
@ -57,7 +58,10 @@
++ on-load ++ on-load
|= old=vase |= old=vase
^- (quip card _this) ^- (quip card _this)
`this(state !<(state-0 old)) :_ this(state !<(state-0 old))
?: (~(has by wex.bowl) [/graph our.bowl %graph-store])
~
~[watch-graph:ha]
:: ::
++ on-watch ++ on-watch
|= =path |= =path
@ -204,12 +208,7 @@
^- (quip card _state) ^- (quip card _state)
=^ child-cards state =^ child-cards state
(check-node-children node tube) (check-node-children node tube)
?: =(our.bowl author.post.node) =+ !< notif-kind=(unit [name=@t parent-lent=@ud mode=?(%each %since)])
=^ self-cards state
(self-post node)
:_ state
(weld child-cards self-cards)
=+ !< notif-kind=(unit [name=@t parent-lent=@ud])
(tube !>([0 post.node])) (tube !>([0 post.node]))
?~ notif-kind ?~ notif-kind
[child-cards state] [child-cards state]
@ -219,17 +218,25 @@
name.u.notif-kind name.u.notif-kind
=/ parent=index:post =/ parent=index:post
(scag parent-lent.u.notif-kind index.post.node) (scag parent-lent.u.notif-kind index.post.node)
=/ notif-index=index:store
[%graph group rid module.metadata desc parent]
?: =(our.bowl author.post.node)
=^ self-cards state
(self-post node notif-index mode.u.notif-kind)
:_ state
(weld child-cards self-cards)
:_ state
%+ weld child-cards
:- %^ update-unread-count
mode.u.notif-kind notif-index
[time-sent index]:post.node
?. ?| =(desc %mention) ?. ?| =(desc %mention)
(~(has in watching) [rid parent]) (~(has in watching) [rid parent])
== ==
[child-cards state] ~
=/ notif-index=index:store
[%graph group rid module.metadata desc]
=/ =contents:store =/ =contents:store
[%graph (limo post.node ~)] [%graph (limo post.node ~)]
:_ state ~[(add-unread notif-index [time-sent.post.node %.n contents])]
%+ snoc child-cards
(add-unread notif-index [time-sent.post.node %.n contents])
:: ::
++ is-mention ++ is-mention
|= contents=(list content:post) |= contents=(list content:post)
@ -243,17 +250,35 @@
$(contents t.contents) $(contents t.contents)
:: ::
++ self-post ++ self-post
|= =node:graph-store |= $: =node:graph-store
=index:store
mode=?(%since %each)
==
^- (quip card _state) ^- (quip card _state)
=| cards=(list card)
=? cards ?=(%since mode)
:_ cards
(poke-hark %read-since index index.post.node)
?. ?=(%.y watch-on-self) ?. ?=(%.y watch-on-self)
[~ state] [cards state]
`state(watching (~(put in watching) [rid index.post.node])) :- cards
state(watching (~(put in watching) [rid index.post.node]))
::
++ poke-hark
|= =action:store
^- card
=- [%pass / %agent [our.bowl %hark-store] %poke -]
hark-action+!>(action)
::
++ update-unread-count
|= [mode=?(%since %each) =index:store time=@da ref=index:graph-store]
?: ?=(%since mode)
(poke-hark %unread-since index time)
(poke-hark %unread-each index ref time)
:: ::
++ add-unread ++ add-unread
|= [=index:store =notification:store] |= [=index:store =notification:store]
^- card (poke-hark %add-note index notification)
=- [%pass / %agent [our.bowl %hark-store] %poke -]
hark-action+!>([%add index notification])
:: ::
-- --
-- --

View File

@ -1,31 +1,47 @@
:: hark-store: notifications [landscape] :: 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, post, group-store, metadata-store /- store=hark-store, post, group-store, metadata-store
/+ resource, metadata, default-agent, dbug, graph-store /+ resource, metadata, default-agent, dbug, graph-store
:: ::
::
~% %hark-store-top ..is ~ ~% %hark-store-top ..is ~
|% |%
+$ card card:agent:gall +$ card card:agent:gall
+$ versioned-state +$ versioned-state
$% state-0 $% state:state-zero:store
state-1
== ==
+$ unread-stats
[indices=(set index:graph-store) last=@da]
:: ::
+$ state-0 +$ state-1
$: %0 $: %1
unreads-each=(jug index:store index:graph-store)
unreads-since=(map index:store index:graph-store)
last-seen=(map index:store @da)
=notifications:store =notifications:store
archive=notifications:store archive=notifications:store
last-seen=@da current-timebox=@da
dnd=_| dnd=_|
== ==
+$ inflated-state +$ inflated-state
$: state-0 $: state-1
cache cache
== ==
:: $cache: useful to have precalculated, but can be derived from state :: $cache: useful to have precalculated, but can be derived from state
:: albeit expensively :: albeit expensively
+$ cache +$ cache
$: unread-count=@ud $: by-index=(jug index:store @da)
by-index=(jug index:store @da)
~ ~
== ==
:: ::
@ -54,16 +70,26 @@
|= =old=vase |= =old=vase
^- (quip card _this) ^- (quip card _this)
=/ old =/ old
!<(state-0 old-vase) !<(versioned-state old-vase)
=. notifications.old =| cards=(list card)
(gas:orm *notifications:store (tap:orm notifications.old)) |-
=. archive.old ?- -.old
(gas:orm *notifications:store (tap:orm archive.old)) %1
`this(-.state old, +.state (inflate-cache old)) [cards this(+.state (inflate-cache:ha old), -.state old)]
::
%0
%_ $
::
old
*state-1
==
==
:: ::
++ on-watch ++ on-watch
|= =path |= =path
^- (quip card _this) ^- (quip card _this)
?> (team:title [src our]:bowl)
|^ |^
?+ path (on-watch:def path) ?+ path (on-watch:def path)
:: ::
@ -76,26 +102,41 @@
^- update:store ^- update:store
:- %more :- %more
^- (list update:store) ^- (list update:store)
:- unreads :+ give-unreads
:+ [%set-dnd dnd] [%set-dnd dnd]
[%count unread-count]
%+ weld %+ weld
%+ turn %+ turn
%+ scag 3
(tap-nonempty:ha archive) (tap-nonempty:ha archive)
(timebox-update &) (timebox-update &)
%+ turn %+ turn
%+ scag 3
(tap-nonempty:ha notifications) (tap-nonempty:ha notifications)
(timebox-update |) (timebox-update |)
:: ::
++ unreads ++ give-since-unreads
^- (list [index:store index-stats:store])
%+ turn
~(tap by unreads-since)
|= [=index:store since=index:graph-store]
:* index
~(wyt in (~(gut by by-index) index ~))
[%since since]
(~(gut by last-seen) index *time)
==
++ give-each-unreads
^- (list [index:store index-stats:store])
%+ turn
~(tap by unreads-each)
|= [=index:store indices=(set index:graph-store)]
:* index
~(wyt in (~(gut by by-index) index ~))
[%each indices]
(~(gut by last-seen) index *time)
==
::
++ give-unreads
^- update:store ^- update:store
:- %unreads :- %unreads
^- (list [index:store @ud]) (weld give-each-unreads give-since-unreads)
%+ turn
~(tap by by-index)
|=([=index:store =(set @da)] [index ~(wyt in set)])
:: ::
++ timebox-update ++ timebox-update
|= archived=? |= archived=?
@ -139,6 +180,7 @@
=^ cards state =^ cards state
?+ mark (on-poke:def mark vase) ?+ mark (on-poke:def mark vase)
%hark-action (hark-action !<(action:store vase)) %hark-action (hark-action !<(action:store vase))
%noun ~& +.state [~ state]
== ==
[cards this] [cards this]
:: ::
@ -147,45 +189,43 @@
^- (quip card _state) ^- (quip card _state)
|^ |^
?- -.action ?- -.action
%add (add +.action) %add-note (add-note +.action)
%archive (do-archive +.action) %archive (do-archive +.action)
%seen seen ::
%read (read +.action) %read-each (read-each +.action)
%read-index (read-index +.action) %unread-each (unread-each +.action)
%unread (unread +.action) ::
%read-since (read-since +.action)
%unread-since (unread-since +.action)
::
%read-note (read-note +.action)
%unread-note (unread-note +.action)
::
%read-all read-all
::
%set-dnd (set-dnd +.action) %set-dnd (set-dnd +.action)
%seen seen
== ==
++ add ::
++ add-note
|= [=index:store =notification:store] |= [=index:store =notification:store]
^- (quip card _state) ^- (quip card _state)
=/ =timebox:store =/ =timebox:store
(gut-orm:ha notifications last-seen) (gut-orm:ha notifications current-timebox)
=/ existing-notif =/ existing-notif
(~(get by timebox) index) (~(get by timebox) index)
=/ new=notification:store =/ new=notification:store
?~ existing-notif ?~ existing-notif
notification notification
(merge-notification:ha u.existing-notif notification) (merge-notification:ha u.existing-notif notification)
=. read.new %.y
=/ new-timebox=timebox:store =/ new-timebox=timebox:store
(~(put by timebox) index new) (~(put by timebox) index new)
:- (give:ha [/updates]~ %added last-seen index new) :- (give:ha [/updates]~ %added current-timebox index new)
%_ state %_ state
+ ?~(existing-notif (upd-unreads:ha index last-seen %.n) +.state) + ?~(existing-notif (upd-unreads:ha index current-timebox %.n) +.state)
notifications (put:orm notifications last-seen new-timebox) notifications (put:orm notifications current-timebox new-timebox)
== ==
++ read-index
|= =index:store
^- (quip card _state)
=/ times=(list @da)
~(tap in (~(gut by by-index) index ~))
=| cards=(list card)
|-
?~ times
[cards state]
=* time i.times
=^ crds state
(read time index)
$(cards (weld cards crds), times t.times)
:: ::
++ do-archive ++ do-archive
|= [time=@da =index:store] |= [time=@da =index:store]
@ -210,29 +250,118 @@
(~(put by archive-box) index notification(read %.y)) (~(put by archive-box) index notification(read %.y))
== ==
:: ::
++ read ++ unread-each
|= [=index:store unread=index:graph-store time=@da]
:- (give:ha ~[/updates] %unread-each index unread time)
%_ state
unreads-each
%+ jub index
|= indices=(set index:graph-store)
(~(put in indices) unread)
::
last-seen
(~(put by last-seen) index time)
==
::
++ jub
|= [=index:store f=$-((set index:graph-store) (set index:graph-store))]
^- (jug index:store index:graph-store)
=/ val=(set index:graph-store)
(~(gut by unreads-each) index ~)
(~(put by unreads-each) index (f val))
::
++ read-each
|= [=index:store ref=index:graph-store]
=/ to-dismiss=(list @da)
%+ skim
~(tap in (~(get ju by-index) index))
|= time=@da
=/ =timebox:store
(gut-orm notifications time)
=/ not=(unit notification:store)
(~(get by timebox) index)
?~ not %.n
?> ?=(%graph -.contents.u.not)
(lien list.contents.u.not |=(p=post:post =(index.p ref)))
=| cards=(list card)
|-
?^ to-dismiss
=^ crds state
(read-note i.to-dismiss index)
$(cards (weld cards crds), to-dismiss t.to-dismiss)
:- (weld cards (give:ha ~[/updates] %read-each index ref))
%_ state
::
unreads-each
%+ jub index
|= indices=(set index:graph-store)
(~(del in indices) ref)
==
::
++ read-note
|= [time=@da =index:store] |= [time=@da =index:store]
^- (quip card _state) ^- (quip card _state)
:- (give:ha [/updates]~ %read time index) :- (give:ha [/updates]~ %read time index)
%_ state %_ state
+ (upd-unreads:ha index time %.y) + (upd-unreads:ha index time %.y)
unread-count (dec unread-count)
notifications (change-read-status:ha time index %.y) notifications (change-read-status:ha time index %.y)
== ==
:: ::
++ unread ++ unread-note
|= [time=@da =index:store] |= [time=@da =index:store]
^- (quip card _state) ^- (quip card _state)
:- (give:ha [/updates]~ %unread time index) :- (give:ha [/updates]~ %unread-note time index)
%_ state %_ state
+ (upd-unreads:ha index time %.n) + (upd-unreads:ha index time %.n)
unread-count +(unread-count)
notifications (change-read-status:ha time index %.n) notifications (change-read-status:ha time index %.n)
== ==
:: ::
++ read-since
|= [=index:store since=index:graph-store]
^- (quip card _state)
=^ cards state
(read-index index)
:- %+ weld cards
(give:ha [/updates]~ %read-since index since)
%_ state
unreads-since (~(put by unreads-since) index since)
==
::
++ read-boxes
|= [boxes=(set @da) =index:store]
^+ state
=/ boxes=(list @da)
~(tap in boxes)
|-
?~ boxes state
=* box i.boxes
=^ cards state
(read-note box index)
$(boxes t.boxes)
::
++ read-index
|= =index:store
^- (quip card _state)
=/ boxes=(set @da)
(~(get ju by-index) index)
:- (give:ha ~[/updates] %read-index index)
(read-boxes boxes index)
::
++ read-all
^- (quip card _state)
`state
::
++ unread-since
|= [=index:store time=@da]
^- (quip card _state)
:- (give:ha [/updates]~ %unread-since index time)
%_ state
last-seen (~(put by last-seen) index time)
==
::
++ seen ++ seen
^- (quip card _state) ^- (quip card _state)
:_ state(last-seen now.bowl) :_ state(current-timebox now.bowl)
:~ cancel-autoseen:ha :~ cancel-autoseen:ha
autoseen-timer:ha autoseen-timer:ha
== ==
@ -254,7 +383,7 @@
?. ?=([%autoseen ~] wire) ?. ?=([%autoseen ~] wire)
(on-arvo:def wire sign-arvo) (on-arvo:def wire sign-arvo)
?> ?=([%b %wake *] sign-arvo) ?> ?=([%b %wake *] sign-arvo)
:_ this(last-seen now.bowl) :_ this(current-timebox now.bowl)
~[autoseen-timer:ha] ~[autoseen-timer:ha]
:: ::
++ on-fail on-fail:def ++ on-fail on-fail:def
@ -262,12 +391,6 @@
|_ =bowl:gall |_ =bowl:gall
+* met ~(. metadata bowl) +* met ~(. metadata bowl)
:: ::
++ tap-nonempty
|= =notifications:store
^- (list [@da timebox:store])
%+ skip (tap:orm notifications)
|=([@da =timebox:store] =(0 ~(wyt by timebox)))
::
++ merge-notification ++ merge-notification
|= [existing=notification:store new=notification:store] |= [existing=notification:store new=notification:store]
^- notification:store ^- notification:store
@ -275,15 +398,15 @@
:: ::
%chat %chat
?> ?=(%chat -.contents.new) ?> ?=(%chat -.contents.new)
existing(list.contents (weld list.contents.existing list.contents.new)) existing(read %.n, list.contents (weld list.contents.existing list.contents.new))
:: ::
%graph %graph
?> ?=(%graph -.contents.new) ?> ?=(%graph -.contents.new)
existing(list.contents (weld list.contents.existing list.contents.new)) existing(read %.n, list.contents (weld list.contents.existing list.contents.new))
:: ::
%group %group
?> ?=(%group -.contents.new) ?> ?=(%group -.contents.new)
existing(list.contents (weld list.contents.existing list.contents.new)) existing(read %.n, list.contents (weld list.contents.existing list.contents.new))
== ==
:: ::
++ change-read-status ++ change-read-status
@ -318,12 +441,18 @@
++ autoseen-interval ~h3 ++ autoseen-interval ~h3
++ cancel-autoseen ++ cancel-autoseen
^- card ^- card
[%pass /autoseen %arvo %b %rest (add last-seen autoseen-interval)] [%pass /autoseen %arvo %b %rest (add current-timebox autoseen-interval)]
:: ::
++ autoseen-timer ++ autoseen-timer
^- card ^- card
[%pass /autoseen %arvo %b %wait (add now.bowl autoseen-interval)] [%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 ++ give
|= [paths=(list path) update=update:store] |= [paths=(list path) update=update:store]
^- (list card) ^- (list card)
@ -341,8 +470,29 @@
~(put ju by-index) ~(put ju by-index)
== ==
:: ::
++ group-for-index
|= =index:store
^- (unit resource)
?. ?=(%graph -.index)
~
`group.index
::
++ give-dirtied-unreads
|= [=index:store =update:store]
^- (list card)
=/ group
(group-for-index index)
?~ group ~
(give ~[group+(en-path:resource u.group)] update)
::
++ tap-nonempty
|= =notifications:store
^- (list [@da timebox:store])
%+ skim (tap:orm notifications)
|=([@da =timebox:store] !=(~(wyt by timebox) 0))
::
++ inflate-cache ++ inflate-cache
|= state-0 |= state-1
^+ +.state ^+ +.state
=/ nots=(list [p=@da =timebox:store]) =/ nots=(list [p=@da =timebox:store])
(tap:orm notifications) (tap:orm notifications)

View File

@ -32,6 +32,7 @@
graph+dejs-path:resource graph+dejs-path:resource
module+so module+so
description+so description+so
index+(su ;~(pfix net (more net dem)))
== ==
:: parse date as @ud :: parse date as @ud
:: TODO: move to zuse :: TODO: move to zuse
@ -53,16 +54,23 @@
|= jon=json |= jon=json
[*^index *notification] [*^index *notification]
:: ::
++ read-graph-index
%- ot
:~ index+index
target+(su ;~(pfix net (more net dem)))
==
::
++ action ++ action
^- $-(json ^action) ^- $-(json ^action)
%- of %- of
:~ seen+ul :~ seen+ul
archive+notif-ref archive+notif-ref
unread+notif-ref unread-note+notif-ref
read+notif-ref read-note+notif-ref
add+add add-note+add
set-dnd+bo set-dnd+bo
read-index+index read-since+read-graph-index
read-each+read-graph-index
== ==
-- --
:: ::
@ -79,25 +87,46 @@
%timebox (timebox +.upd) %timebox (timebox +.upd)
%set-dnd b+dnd.upd %set-dnd b+dnd.upd
%count (numb count.upd) %count (numb count.upd)
%unreads (unreads unreads.upd)
%more (more +.upd) %more (more +.upd)
%read-each (read-graph +.upd)
%read-since (read-graph +.upd)
%unread-each (unread-each +.upd)
%unread-since (unread-since +.upd)
%unreads (unreads +.upd)
:: ::
?(%archive %read %unread) ?(%archive %read-note %unread-note)
(notif-ref +.upd) (notif-ref +.upd)
== ==
:: ::
++ unreads ++ unreads
|= l=(list [^index @ud]) |= l=(list [^index ^index-stats])
^- json ^- json
:- %a :- %a
^- (list json) ^- (list json)
%+ turn l %+ turn l
|= [idx=^index unread=@ud] |= [idx=^index stats=^index-stats]
%- pairs %- pairs
:~ unread+(numb unread) :~ unread+(numb unread)
index+(index idx) index+(index idx)
== ==
:: ::
++ unread
|= =^unreads
%+ frond
-.unreads
?- -.unreads
%since (index:enjs:graph-store index.unreads)
%each a+(turn ~(tap by indices.unreads) index:enjs:graph-store)
==
::
++ index-stats
|= stats=^index-stats
^- json
%- pairs
:~ unreads+(unread unreads.stats)
notifications+(numb notifications.stats)
last+(time last-seen.stats)
==
++ added ++ added
|= [tim=@da idx=^index not=^notification] |= [tim=@da idx=^index not=^notification]
^- json ^- json
@ -139,13 +168,19 @@
== ==
:: ::
++ graph-index ++ graph-index
|= [group=resource graph=resource module=@t description=@t] |= $: group=resource
graph=resource
module=@t
description=@t
idx=index:graph-store
==
^- json ^- json
%- pairs %- pairs
:~ group+s+(enjs-path:resource group) :~ group+s+(enjs-path:resource group)
graph+s+(enjs-path:resource graph) graph+s+(enjs-path:resource graph)
module+s+module module+s+module
description+s+description description+s+description
index+(index:enjs:graph-store idx)
== ==
:: ::
++ group-index ++ group-index
@ -221,6 +256,28 @@
^- json ^- json
(indexed-notification index notification) (indexed-notification index notification)
== ==
::
++ read-graph
|= [=^index target=index:graph-store]
%- pairs
:~ index+(^index index)
target+(index:enjs:graph-store target)
==
::
++ unread-each
|= [=^index target=index:graph-store tim=@da]
%- pairs
:~ index+(^index index)
target+(index:enjs:graph-store target)
last+(time tim)
==
::
++ unread-since
|= [=^index tim=@da]
%- pairs
:~ index+(^index index)
last+(time tim)
==
-- --
-- --
-- --

View File

@ -5,8 +5,8 @@
++ noun i ++ noun i
++ notification-kind ++ notification-kind
?+ index.p.i ~ ?+ index.p.i ~
[@ ~] `[%link 0] [@ ~] `[%link 0 %each]
[@ @ @ ~] `[%comment 1] [@ @ @ ~] `[%comment 1 %since]
== ==
-- --
++ grab ++ grab

View File

@ -8,8 +8,8 @@
:: ::
++ notification-kind ++ notification-kind
?+ index.p.i ~ ?+ index.p.i ~
[@ %1 @ ~] `[%note 0] [@ %1 @ ~] `[%note 0 %each]
[@ %2 @ @ ~] `[%comment 1] [@ %2 @ @ ~] `[%comment 1 %since]
== ==
-- --
++ grab ++ grab

View File

@ -1,11 +1,40 @@
/- *resource, graph-store, post, group-store, metadata-store, chat-store /- chat-store, graph-store, post, *resource, group-store, metadata-store
^? ^?
|% |%
+$ index ++ state-zero
|%
+$ state
$: %0
notifications=notifications
archive=notifications
last-seen=@da
dnd=_|
==
::
+$ notifications
((mop @da timebox) gth)
::
+$ timebox
(map index notification)
::
+$ index
$% [%graph group=resource graph=resource module=@t description=@t] $% [%graph group=resource graph=resource module=@t description=@t]
[%group group=resource description=@t] [%group group=resource description=@t]
[%chat chat=path mention=?] [%chat chat=path mention=?]
== ==
--
::
+$ index
$% $: %graph
group=resource
graph=resource
module=@t
description=@t
=index:graph-store
==
[%group group=resource description=@t]
[%chat chat=path mention=?]
==
:: ::
+$ group-contents +$ group-contents
$~ [%add-members *resource ~] $~ [%add-members *resource ~]
@ -29,24 +58,42 @@
((mop @da timebox) gth) ((mop @da timebox) gth)
:: ::
+$ action +$ action
$% [%add =index =notification] $% [%add-note =index =notification]
[%archive time=@da index] [%archive time=@da index]
[%read time=@da index] ::
[%read-index index] [%unread-since =index time=@da]
[%unread time=@da index] [%read-since =index =index:graph-store]
::
[%unread-each =index ref=index:graph-store time=@da]
[%read-each index ref=index:graph-store]
::
[%read-note time=@da index]
[%unread-note time=@da index]
::
[%read-all ~]
[%set-dnd dnd=?] [%set-dnd dnd=?]
[%seen ~] [%seen ~]
== ==
:: ::
++ indexed-notification +$ indexed-notification
[index notification] [index notification]
:: ::
+$ index-stats
[notifications=@ud =unreads last-seen=@da]
::
+$ unreads
$% [%since =index:graph-store]
[%each indices=(set index:graph-store)]
==
::
+$ update +$ update
$% action $% action
[%more more=(list update)] [%more more=(list update)]
[%added time=@da =index =notification] [%added time=@da =index =notification]
[%read-index =index]
[%read time=@da =index]
[%timebox time=@da archived=? =(list [index notification])] [%timebox time=@da archived=? =(list [index notification])]
[%count count=@ud] [%count count=@ud]
[%unreads unreads=(list [index @ud])] [%unreads unreads=(list [index index-stats])]
== ==
-- --

View File

@ -1,7 +1,7 @@
import BaseApi from "./base"; import BaseApi from "./base";
import { StoreState } from "../store/type"; import { StoreState } from "../store/type";
import { dateToDa, decToUd } from "../lib/util"; import { dateToDa, decToUd } from "../lib/util";
import {NotifIndex, IndexedNotification} from "~/types"; import {NotifIndex, IndexedNotification, Association, GraphNotifDescription} from "~/types";
import { BigInteger } from 'big-integer'; import { BigInteger } from 'big-integer';
import {getParentIndex} from "../lib/notification"; import {getParentIndex} from "../lib/notification";
@ -71,6 +71,48 @@ export class HarkApi extends BaseApi<StoreState> {
return this.actOnNotification('unread', time, index); return this.actOnNotification('unread', time, index);
} }
markSinceAsRead(association: Association, parent: string, description: GraphNotifDescription, since: string) {
return this.harkAction(
{ 'read-since': {
index: { graph: {
graph: association['app-path'],
group: association['group-path'],
module: association.metadata.module,
description,
index: parent
} },
target: since
}});
}
markEachAsRead(association: Association, parent: string, child: string, description: GraphNotifDescription, mod: string) {
return this.harkAction({
'read-each': {
index:
{ graph:
{ graph: association['app-path'],
group: association['group-path'],
description,
module: mod,
index: parent
}
},
target: child
}
});
}
dec(index: NotifIndex, ref: string) {
return this.harkAction({
dec: {
index,
ref
}
});
}
seen() { seen() {
return this.harkAction({ seen: null }); return this.harkAction({ seen: null });
} }

View File

@ -0,0 +1,17 @@
import bigInt, { BigInteger } from "big-integer";
import f from "lodash/fp";
import { Unreads } from "~/types";
export function 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
);
}

View File

@ -3,19 +3,14 @@ import {
NotifIndex, NotifIndex,
NotificationGraphConfig, NotificationGraphConfig,
GroupNotificationsConfig, GroupNotificationsConfig,
UnreadStats,
} from "~/types"; } from "~/types";
import { makePatDa } from "~/logic/lib/util"; import { makePatDa } from "~/logic/lib/util";
import _ from "lodash"; import _ from "lodash";
import { StoreState } from "../store/type"; import {StoreState} from "../store/type";
type HarkState = Pick<StoreState, "notifications" | "notificationsGraphConfig" | "notificationsGroupConfig" | "unreads" | "notificationsChatConfig">;
type HarkState = Pick<StoreState,
"notificationsChatConfig"
| "notificationsGroupConfig"
| "notificationsGraphConfig"
| "notifications"
| "notificationsCount"
| "archivedNotifications"
| "unreads">;
export const HarkReducer = (json: any, state: HarkState) => { export const HarkReducer = (json: any, state: HarkState) => {
const data = _.get(json, "harkUpdate", false); const data = _.get(json, "harkUpdate", false);
@ -141,28 +136,89 @@ function reduce(data: any, state: HarkState) {
dnd(data, state); dnd(data, state);
added(data, state); added(data, state);
unreads(data, state); unreads(data, state);
readEach(data, state);
readSince(data, state);
unreadSince(data, state);
unreadEach(data, state);
}
function readEach(json: any, state: HarkState) {
const data = _.get(json, 'read-each');
if(data) {
updateUnreads(state, data.index, u => u.delete(data.target))
}
}
function readSince(json: any, state: HarkState) {
const data = _.get(json, 'read-since');
if(data) {
updateUnreadSince(state, data.index, data.target)
}
}
function unreadSince(json: any, state: HarkState) {
const data = _.get(json, 'unread-since');
if(data) {
updateNotificationStats(state, data.index, 'last', () => data.last)
}
}
function unreadEach(json: any, state: HarkState) {
const data = _.get(json, 'unread-each');
if(data) {
updateNotificationStats(state, data.index, 'last', () => data.last);
updateUnreads(state, data.index, us => us.add(data.target))
}
} }
function unreads(json: any, state: HarkState) { function unreads(json: any, state: HarkState) {
const data = _.get(json, 'unreads'); const data = _.get(json, 'unreads');
if(data) { if(data) {
data.forEach(({ index, unread }) => { data.forEach(({ index, stats }) => {
updateUnreads(state, index, x => x + unread); const { unreads, notifications, last } = stats;
updateNotificationStats(state, index, 'notifications', x => x + notifications);
updateNotificationStats(state, index, 'last', () => last);
if('since' in unreads) {
updateUnreadSince(state, index, unreads.since);
} else {
unreads.each.forEach((u: string) => {
updateUnreads(state, index, s => s.add(u));
});
}
}); });
} }
} }
function updateUnreads(state: HarkState, index: NotifIndex, f: (u: number) => number) { function updateUnreadSince(state: HarkState, index: NotifIndex, since: string) {
if(!('graph' in index)) {
return;
}
_.set(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], since);
}
function updateUnreads(state: HarkState, index: NotifIndex, f: (us: Set<string>) => void) {
if(!('graph' in index)) {
return;
}
const unreads = _.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);
}
function updateNotificationStats(state: HarkState, index: NotifIndex, statField: 'notifications' | 'unreads' | 'last', f: (x: number) => number) {
if(statField === 'notifications') {
state.notificationsCount = f(state.notificationsCount); state.notificationsCount = f(state.notificationsCount);
}
if('graph' in index) { if('graph' in index) {
const curr = state.unreads.graph[index.graph.graph] || 0; const curr = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, statField], 0);
state.unreads.graph[index.graph.graph] = f(curr); _.set(state.unreads.graph, [index.graph.graph, index.graph.index, statField], f(curr));
} else if('group' in index) { } else if('group' in index) {
const curr = state.unreads.group[index.group.group] || 0; const curr = _.get(state.unreads.group, [index.group.group, statField], 0);
state.unreads.group[index.group.group] = f(curr); _.set(state.unreads.group, [index.group.group, statField], f(curr));
} else if('chat' in index) { } else if('chat' in index) {
const curr = state.unreads.chat[index.chat.chat] || 0 const curr = _.get(state.unreads.chat, [index.chat.chat, statField], 0);
state.unreads.chat[index.chat.chat] = f(curr); _.set(state.unreads.chat, [index.chat.chat, statField], f(curr));
} }
} }
@ -178,12 +234,12 @@ function added(json: any, state: HarkState) {
); );
if (arrIdx !== -1) { if (arrIdx !== -1) {
if(timebox[arrIdx]?.notification?.read) { if(timebox[arrIdx]?.notification?.read) {
updateUnreads(state, index, x => x+1); updateNotificationStats(state, index, 'notifications', x => x+1);
} }
timebox[arrIdx] = { index, notification }; timebox[arrIdx] = { index, notification };
state.notifications.set(time, timebox); state.notifications.set(time, timebox);
} else { } else {
updateUnreads(state, index, x => x+1); updateNotificationStats(state, index, 'notifications', x => x+1);
state.notifications.set(time, [...timebox, { index, notification }]); state.notifications.set(time, [...timebox, { index, notification }]);
} }
} }
@ -200,9 +256,7 @@ const timebox = (json: any, state: HarkState) => {
const data = _.get(json, "timebox", false); const data = _.get(json, "timebox", false);
if (data) { if (data) {
const time = makePatDa(data.time); const time = makePatDa(data.time);
if (data.archive) { if (!data.archive) {
state.archivedNotifications.set(time, data.notifications);
} else {
state.notifications.set(time, data.notifications); state.notifications.set(time, data.notifications);
} }
} }
@ -262,7 +316,7 @@ function read(json: any, state: HarkState) {
const data = _.get(json, "read", false); const data = _.get(json, "read", false);
if (data) { if (data) {
const { time, index } = data; const { time, index } = data;
updateUnreads(state, index, x => x-1); updateNotificationStats(state, index, 'notifications', x => x-1);
setRead(time, index, true, state); setRead(time, index, true, state);
} }
} }
@ -271,7 +325,7 @@ function unread(json: any, state: HarkState) {
const data = _.get(json, "unread", false); const data = _.get(json, "unread", false);
if (data) { if (data) {
const { time, index } = data; const { time, index } = data;
updateUnreads(state, index, x => x+1); updateNotificationStats(state, index, 'notifications', x => x+1);
setRead(time, index, false, state); setRead(time, index, false, state);
} }
} }
@ -294,7 +348,7 @@ function archive(json: any, state: HarkState) {
const readCount = archived.filter( const readCount = archived.filter(
({ notification }) => !notification.read ({ notification }) => !notification.read
).length; ).length;
updateUnreads(state, index, x => x - readCount); updateNotificationStats(state, index, 'notifications', x => x - readCount);
state.archivedNotifications.set(time, [ state.archivedNotifications.set(time, [
...archiveBox, ...archiveBox,
...archived.map(({ notification, index }) => ({ ...archived.map(({ notification, index }) => ({

View File

@ -106,12 +106,12 @@ export default class GlobalStore extends BaseStore<StoreState> {
mentions: false, mentions: false,
watching: [], watching: [],
}, },
notificationsCount: 0,
unreads: { unreads: {
graph: {},
group: {},
chat: {}, chat: {},
} graph: {},
group: {}
},
notificationsCount: 0
}; };
} }

View File

@ -66,6 +66,7 @@ export interface StoreState {
notificationsGroupConfig: GroupNotificationsConfig; notificationsGroupConfig: GroupNotificationsConfig;
notificationsChatConfig: string[]; notificationsChatConfig: string[];
notificationsCount: number, notificationsCount: number,
unreads: Unreads;
doNotDisturb: boolean; doNotDisturb: boolean;
unreads: Unreads; unreads: Unreads;
} }

View File

@ -54,7 +54,7 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
this.subscribe('/updates', 'hark-store'); this.subscribe('/updates', 'hark-store');
this.subscribe('/updates', 'hark-graph-hook'); this.subscribe('/updates', 'hark-graph-hook');
this.subscribe('/updates', 'hark-group-hook'); this.subscribe('/updates', 'hark-group-hook');
this.subscribe('/updates', 'hark-chat-hook'); this.startApp('chat');
} }
restart() { restart() {

View File

@ -55,7 +55,7 @@ export interface Inbox {
[chatName: string]: Mailbox; [chatName: string]: Mailbox;
} }
interface Mailbox { export interface Mailbox {
config: MailboxConfig; config: MailboxConfig;
envelopes: Envelope[]; envelopes: Envelope[];
} }

View File

@ -4,13 +4,20 @@ import { GroupUpdate } from "./group-update";
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap"; import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
import { Envelope } from './chat-update'; import { Envelope } from './chat-update';
type GraphNotifDescription = "link" | "comment" | "note" | "mention"; export type GraphNotifDescription = "link" | "comment" | "note" | "mention";
export interface UnreadStats {
unreads: Set<string>;
notifications: number;
last: number;
}
export interface GraphNotifIndex { export interface GraphNotifIndex {
graph: string; graph: string;
group: string; group: string;
description: GraphNotifDescription; description: GraphNotifDescription;
module: string; module: string;
index: string;
} }
export interface GroupNotifIndex { export interface GroupNotifIndex {
@ -61,9 +68,9 @@ export interface NotificationGraphConfig {
} }
export interface Unreads { export interface Unreads {
chat: Record<string, number>; chat: Record<string, UnreadStats>;
group: Record<string, number>; graph: Record<string, Record<string, UnreadStats>>;
graph: Record<string, number>; group: Record<string, UnreadStats>;
} }
interface WatchedIndex { interface WatchedIndex {

View File

@ -3,7 +3,7 @@ import { Box, Text, Col } from "@tlon/indigo-react";
import f from "lodash/fp"; import f from "lodash/fp";
import _ from "lodash"; import _ from "lodash";
import { Associations, Association, Unreads } from "~/types"; import { Associations, Association, Unreads, UnreadStats } from "~/types";
import { alphabeticalOrder } from "~/logic/lib/util"; import { alphabeticalOrder } from "~/logic/lib/util";
import Tile from "../components/tiles/tile"; import Tile from "../components/tiles/tile";
@ -14,32 +14,29 @@ interface GroupsProps {
const sortGroupsAlph = (a: Association, b: Association) => const sortGroupsAlph = (a: Association, b: Association) =>
alphabeticalOrder(a.metadata.title, b.metadata.title); alphabeticalOrder(a.metadata.title, b.metadata.title);
const getKindUnreads = (associations: Associations) => (path: string) => (
kind: "chat" | "graph" const getGraphUnreads = (associations: Associations, unreads: Unreads) => (path: string): number =>
): ((unreads: Unreads) => number) =>
f.flow( f.flow(
(x) => x[kind], (x) => x['graph'],
f.pickBy((_v, key) => associations[kind]?.[key]?.["group-path"] === path), f.pickBy((_v, key) => associations.graph?.[key]["group-path"] === path),
f.values, f.map((x: Record<string, UnreadStats>) => 0), // x?.['/']?.unreads?.size),
f.reduce(f.add, 0) f.reduce(f.add, 0)
); )(unreads);
export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) { export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
const { associations, unreads, ...boxProps } = props; const { associations, unreads, inbox, ...boxProps } = props;
const groups = Object.values(associations?.contacts || {}) const groups = Object.values(associations?.contacts || {})
.filter((e) => e?.["group-path"] in props.groups) .filter((e) => e?.["group-path"] in props.groups)
.sort(sortGroupsAlph); .sort(sortGroupsAlph);
const getUnreads = getKindUnreads(associations || {}); const graphUnreads = getGraphUnreads(associations || {}, unreads);
return ( return (
<> <>
{groups.map((group) => { {groups.map((group) => {
const path = group?.["group-path"]; const path = group?.["group-path"];
const unreadCount = (["chat", "graph"] as const) const unreadCount = graphUnreads(path)
.map(getUnreads(path))
.map((f) => f(unreads))
.reduce(f.add, 0);
return ( return (
<Group <Group
unreads={unreadCount} unreads={unreadCount}
@ -55,6 +52,7 @@ export default function Groups(props: GroupsProps & Parameters<typeof Box>[0]) {
interface GroupProps { interface GroupProps {
path: string; path: string;
title: string; title: string;
updates?: number;
unreads: number; unreads: number;
} }
function Group(props: GroupProps) { function Group(props: GroupProps) {

View File

@ -32,6 +32,7 @@ export function LinkResource(props: LinkResourceProps) {
groups, groups,
associations, associations,
graphKeys, graphKeys,
unreads,
s3, s3,
hideAvatars, hideAvatars,
hideNicknames, hideNicknames,
@ -68,6 +69,7 @@ export function LinkResource(props: LinkResourceProps) {
exact exact
path={relativePath("")} path={relativePath("")}
render={(props) => { render={(props) => {
const graphUnreads = unreads.graph?.[appPath]?.['/']?.unreads || new Set();
return ( return (
<Col width="100%" p={4} alignItems="center" maxWidth="768px"> <Col width="100%" p={4} alignItems="center" maxWidth="768px">
<Col width="100%" flexShrink='0'> <Col width="100%" flexShrink='0'>
@ -80,6 +82,8 @@ export function LinkResource(props: LinkResourceProps) {
key={date.toString()} key={date.toString()}
resource={resourcePath} resource={resourcePath}
node={node} node={node}
contacts={contactDetails}
unread={graphUnreads.has(node.post.index)}
nickname={contact?.nickname} nickname={contact?.nickname}
hideAvatars={hideAvatars} hideAvatars={hideAvatars}
hideNicknames={hideNicknames} hideNicknames={hideNicknames}
@ -118,6 +122,8 @@ export function LinkResource(props: LinkResourceProps) {
<LinkPreview <LinkPreview
resourcePath={resourcePath} resourcePath={resourcePath}
post={node.post} post={node.post}
association={association}
api={api}
nickname={contact?.nickname} nickname={contact?.nickname}
hideNicknames={hideNicknames} hideNicknames={hideNicknames}
commentNumber={node.children.size} commentNumber={node.children.size}
@ -128,6 +134,8 @@ export function LinkResource(props: LinkResourceProps) {
name={name} name={name}
comments={node} comments={node}
resource={resourcePath} resource={resourcePath}
association={association}
unreads={unreads}
contacts={contactDetails} contacts={contactDetails}
api={api} api={api}
hideAvatars={hideAvatars} hideAvatars={hideAvatars}

View File

@ -4,6 +4,7 @@ import { Row, Col, Anchor, Box, Text, BaseImage } from '@tlon/indigo-react';
import { Sigil } from '~/logic/lib/sigil'; import { Sigil } from '~/logic/lib/sigil';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { cite } from '~/logic/lib/util'; import { cite } from '~/logic/lib/util';
import { Author } from "~/views/apps/publish/components/Author";
import { roleForShip } from '~/logic/lib/group'; import { roleForShip } from '~/logic/lib/group';
@ -12,6 +13,8 @@ export const LinkItem = (props) => {
node, node,
nickname, nickname,
avatar, avatar,
contacts,
unread,
resource, resource,
hideAvatars, hideAvatars,
hideNicknames, hideNicknames,
@ -26,6 +29,7 @@ export const LinkItem = (props) => {
const author = node.post.author; const author = node.post.author;
const index = node.post.index.split('/')[1]; const index = node.post.index.split('/')[1];
const size = node.children ? node.children.size : 0; const size = node.children ? node.children.size : 0;
const date = node.post['time-sent'];
const contents = node.post.contents; const contents = node.post.contents;
const hostname = URLparser.exec(contents[1].url) ? URLparser.exec(contents[1].url)[4] : null; const hostname = URLparser.exec(contents[1].url) ? URLparser.exec(contents[1].url)[4] : null;
@ -57,18 +61,22 @@ export const LinkItem = (props) => {
<Text display='inline-block' overflow='hidden' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}>{contents[0].text}</Text> <Text display='inline-block' overflow='hidden' style={{ textOverflow: 'ellipsis', whiteSpace: 'pre' }}>{contents[0].text}</Text>
<Text ml="2" color="gray" display='inline-block' flexShrink='0'>{hostname} </Text> <Text ml="2" color="gray" display='inline-block' flexShrink='0'>{hostname} </Text>
</Anchor> </Anchor>
<Box width="100%"> <Row alignItems="center" width="100%">
<Text <Author
fontFamily={showNickname ? 'sans' : 'mono'} pr={2} contacts={contacts}
ship={author}
hideAvatars={hideAvatars}
hideNicknames={hideNicknames}
unread={unread}
date={date}
> >
{showNickname ? nickname : cite(author) }
</Text>
<Link to={`${baseUrl}/${index}`}> <Link to={`${baseUrl}/${index}`}>
<Text color="gray">{size} comments</Text> <Text ml="2" color="gray">{size} comments</Text>
</Link> </Link>
{(ourRole === 'admin' || node.post.author === window.ship) {(ourRole === 'admin' || node.post.author === window.ship)
&& (<Text color='red' ml='2' cursor='pointer' onClick={() => api.graph.removeNodes(`~${ship}`, name, [node.post.index])}>Delete</Text>)} && (<Text color='red' ml='2' cursor='pointer' onClick={() => api.graph.removeNodes(`~${ship}`, name, [node.post.index])}>Delete</Text>)}
</Box> </Author>
</Row>
</Col> </Col>
</Row> </Row>
); );

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useEffect } from 'react';
import { cite } from '~/logic/lib/util'; import { cite } from '~/logic/lib/util';
import RemoteContent from '~/views/components/RemoteContent'; import RemoteContent from '~/views/components/RemoteContent';
@ -20,6 +20,10 @@ export const LinkPreview = (props) => {
const timeSent = const timeSent =
moment.unix(props.post['time-sent'] / 1000).format('hh:mm a'); moment.unix(props.post['time-sent'] / 1000).format('hh:mm a');
useEffect(() => {
return () => props.api.hark.markEachAsRead(props.association, '/', props.post.index, 'link', 'link');
}, [props.association, props.post.index]);
const embed = ( const embed = (
<RemoteContent <RemoteContent
unfold={true} unfold={true}

View File

@ -36,6 +36,7 @@ export function PublishResource(props: PublishResourceProps) {
match={props.match} match={props.match}
location={props.location} location={props.location}
hideAvatars={props.hideAvatars} hideAvatars={props.hideAvatars}
unreads={props.unreads}
hideNicknames={props.hideNicknames} hideNicknames={props.hideNicknames}
remoteContentPolicy={props.remoteContentPolicy} remoteContentPolicy={props.remoteContentPolicy}
graphs={props.graphs} graphs={props.graphs}

View File

@ -13,6 +13,7 @@ interface AuthorProps {
hideAvatars: boolean; hideAvatars: boolean;
hideNicknames: boolean; hideNicknames: boolean;
children?: ReactNode; children?: ReactNode;
unread?: boolean;
} }
export function Author(props: AuthorProps) { export function Author(props: AuthorProps) {
@ -54,7 +55,7 @@ export function Author(props: AuthorProps) {
> >
{name} {name}
</Box> </Box>
<Box ml={2} color="gray"> <Box ml={2} color={props.unread ? "blue" : "gray"}>
{dateFmt} {dateFmt}
</Box> </Box>
{props.children} {props.children}

View File

@ -10,12 +10,14 @@ import { NoteNavigation } from "./NoteNavigation";
import GlobalApi from "~/logic/api/global"; import GlobalApi from "~/logic/api/global";
import { getLatestRevision, getComments } from '~/logic/lib/publish'; import { getLatestRevision, getComments } from '~/logic/lib/publish';
import { Author } from "./Author"; import { Author } from "./Author";
import { Contacts, GraphNode, Graph, LocalUpdateRemoteContentPolicy } from "~/types"; import { Contacts, GraphNode, Graph, LocalUpdateRemoteContentPolicy, Association, Unreads } from "~/types";
interface NoteProps { interface NoteProps {
ship: string; ship: string;
book: string; book: string;
note: GraphNode; note: GraphNode;
unreads: Unreads;
association: Association;
notebook: Graph; notebook: Graph;
contacts: Contacts; contacts: Contacts;
api: GlobalApi; api: GlobalApi;
@ -39,8 +41,13 @@ export function Note(props: NoteProps & RouteComponentProps) {
props.history.push(rootUrl); props.history.push(rootUrl);
}; };
const comments = getComments(note); const comments = getComments(note);
const [revNum, title, body, post] = getLatestRevision(note); const [revNum, title, body, post] = getLatestRevision(note);
useEffect(() => {
api.hark.markEachAsRead(props.association, '/', post.index, 'note', 'publish');
}, [props.association]);
const noteId = bigInt(note.post.index.split('/')[1]); const noteId = bigInt(note.post.index.split('/')[1]);
@ -108,8 +115,10 @@ export function Note(props: NoteProps & RouteComponentProps) {
<Comments <Comments
ship={ship} ship={ship}
name={props.book} name={props.book}
unreads={props.unreads}
comments={comments} comments={comments}
contacts={props.contacts} contacts={props.contacts}
association={props.association}
api={props.api} api={props.api}
hideNicknames={props.hideNicknames} hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars} hideAvatars={props.hideAvatars}

View File

@ -13,6 +13,7 @@ import {
getLatestRevision, getLatestRevision,
getSnippet, getSnippet,
} from "~/logic/lib/publish"; } from "~/logic/lib/publish";
import {Unreads} from "~/types";
interface NotePreviewProps { interface NotePreviewProps {
host: string; host: string;
@ -21,6 +22,7 @@ interface NotePreviewProps {
contact?: Contact; contact?: Contact;
hideNicknames?: boolean; hideNicknames?: boolean;
baseUrl: string; baseUrl: string;
unreads: Unreads;
} }
const WrappedBox = styled(Box)` const WrappedBox = styled(Box)`
@ -52,10 +54,8 @@ export function NotePreview(props: NotePreviewProps) {
const date = moment(post["time-sent"]).fromNow(); const date = moment(post["time-sent"]).fromNow();
const url = `${props.baseUrl}/note/${post.index.split("/")[1]}`; const url = `${props.baseUrl}/note/${post.index.split("/")[1]}`;
// stubbing pending notification-store const [rev, title, body, content] = getLatestRevision(node);
const isRead = true; const isUnread = props.unreads.graph?.[`/ship/${props.host}/${props.book}`]?.['/']?.unreads?.has(content.index);
const [rev, title, body] = getLatestRevision(node);
const snippet = getSnippet(body); const snippet = getSnippet(body);
@ -79,7 +79,7 @@ export function NotePreview(props: NotePreviewProps) {
> >
{name} {name}
</Box> </Box>
<Box color={isRead ? "gray" : "green"} mr={3}> <Box color={isUnread ? "blue" : "gray"} mr={3}>
{date} {date}
</Box> </Box>
<Box mr={3}>{commentDesc}</Box> <Box mr={3}>{commentDesc}</Box>

View File

@ -6,7 +6,7 @@ import { RouteComponentProps } from "react-router-dom";
import Note from "./Note"; import Note from "./Note";
import { EditPost } from "./EditPost"; import { EditPost } from "./EditPost";
import { GraphNode, Graph, Contacts, LocalUpdateRemoteContentPolicy } from "~/types"; import { GraphNode, Graph, Contacts, LocalUpdateRemoteContentPolicy, Association } from "~/types";
interface NoteRoutesProps { interface NoteRoutesProps {
ship: string; ship: string;
@ -19,6 +19,7 @@ interface NoteRoutesProps {
remoteContentPolicy: LocalUpdateRemoteContentPolicy; remoteContentPolicy: LocalUpdateRemoteContentPolicy;
hideNicknames: boolean; hideNicknames: boolean;
hideAvatars: boolean; hideAvatars: boolean;
association: Association;
baseUrl?: string; baseUrl?: string;
rootUrl?: string; rootUrl?: string;
} }

View File

@ -7,7 +7,7 @@ import { Groups } from "~/types/group-update";
import { Contacts, Rolodex } from "~/types/contact-update"; import { Contacts, Rolodex } from "~/types/contact-update";
import GlobalApi from "~/logic/api/global"; import GlobalApi from "~/logic/api/global";
import styled from "styled-components"; import styled from "styled-components";
import { Associations, Graph, Association } from "~/types"; import { Associations, Graph, Association, Unreads } from "~/types";
import { deSig } from "~/logic/lib/util"; import { deSig } from "~/logic/lib/util";
import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton"; import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton";
@ -24,7 +24,7 @@ interface NotebookProps {
hideNicknames: boolean; hideNicknames: boolean;
baseUrl: string; baseUrl: string;
rootUrl: string; rootUrl: string;
associations: Associations; unreads: Unreads;
} }
interface NotebookState { interface NotebookState {
@ -84,6 +84,7 @@ export function Notebook(props: NotebookProps & RouteComponentProps) {
book={book} book={book}
contacts={!!notebookContacts ? notebookContacts : {}} contacts={!!notebookContacts ? notebookContacts : {}}
hideNicknames={hideNicknames} hideNicknames={hideNicknames}
unreads={props.unreads}
baseUrl={props.baseUrl} baseUrl={props.baseUrl}
/> />
</Col> </Col>

View File

@ -1,7 +1,7 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { Col } from "@tlon/indigo-react"; import { Col } from "@tlon/indigo-react";
import { NotePreview } from "./NotePreview"; import { NotePreview } from "./NotePreview";
import { Contacts, Graph } from "~/types"; import { Contacts, Graph, Unreads } from "~/types";
interface NotebookPostsProps { interface NotebookPostsProps {
contacts: Contacts; contacts: Contacts;
@ -10,6 +10,7 @@ interface NotebookPostsProps {
book: string; book: string;
hideNicknames?: boolean; hideNicknames?: boolean;
baseUrl: string; baseUrl: string;
unreads: Unreads;
} }
export function NotebookPosts(props: NotebookPostsProps) { export function NotebookPosts(props: NotebookPostsProps) {
@ -22,6 +23,7 @@ export function NotebookPosts(props: NotebookPostsProps) {
key={date.toString()} key={date.toString()}
host={props.host} host={props.host}
book={props.book} book={props.book}
unreads={props.unreads}
contact={props.contacts[node.post.author]} contact={props.contacts[node.post.author]}
node={node} node={node}
hideNicknames={props.hideNicknames} hideNicknames={props.hideNicknames}

View File

@ -8,7 +8,8 @@ import {
Groups, Groups,
Contacts, Contacts,
Rolodex, Rolodex,
LocalUpdateRemoteContentPolicy LocalUpdateRemoteContentPolicy,
Unreads
} from "~/types"; } from "~/types";
import { Center, LoadingSpinner } from "@tlon/indigo-react"; import { Center, LoadingSpinner } from "@tlon/indigo-react";
import { Notebook as INotebook } from "~/types/publish-update"; import { Notebook as INotebook } from "~/types/publish-update";
@ -26,6 +27,7 @@ interface NotebookRoutesProps {
book: string; book: string;
graphs: Graphs; graphs: Graphs;
notebookContacts: Contacts; notebookContacts: Contacts;
unreads: Unreads;
contacts: Rolodex; contacts: Rolodex;
groups: Groups; groups: Groups;
baseUrl: string; baseUrl: string;
@ -102,8 +104,10 @@ export function NotebookRoutes(
ship={ship} ship={ship}
note={note} note={note}
notebook={graph} notebook={graph}
unreads={props.unreads}
noteId={noteIdNum} noteId={noteIdNum}
contacts={notebookContacts} contacts={notebookContacts}
association={props.association}
hideAvatars={props.hideAvatars} hideAvatars={props.hideAvatars}
hideNicknames={props.hideNicknames} hideNicknames={props.hideNicknames}
remoteContentPolicy={props.remoteContentPolicy} remoteContentPolicy={props.remoteContentPolicy}

View File

@ -21,6 +21,7 @@ interface CommentItemProps {
comment: GraphNode; comment: GraphNode;
baseUrl: string; baseUrl: string;
contacts: Contacts; contacts: Contacts;
unread: boolean;
name: string; name: string;
ship: string; ship: string;
api: GlobalApi; api: GlobalApi;
@ -50,6 +51,7 @@ export function CommentItem(props: CommentItemProps) {
contacts={contacts} contacts={contacts}
ship={post?.author} ship={post?.author}
date={post?.['time-sent']} date={post?.['time-sent']}
unread={props.unread}
hideAvatars={props.hideAvatars} hideAvatars={props.hideAvatars}
hideNicknames={props.hideNicknames} hideNicknames={props.hideNicknames}
> >

View File

@ -6,14 +6,15 @@ import CommentInput from './CommentInput';
import { Contacts } from '~/types/contact-update'; import { Contacts } from '~/types/contact-update';
import GlobalApi from '~/logic/api/global'; import GlobalApi from '~/logic/api/global';
import { FormikHelpers } from 'formik'; import { FormikHelpers } from 'formik';
import { GraphNode } from '~/types/graph-update'; import { GraphNode, LocalUpdateRemoteContentPolicy, Unreads, Association } from '~/types';
import { createPost, createBlankNodeWithChildPost } from '~/logic/api/graph'; import { createPost, createBlankNodeWithChildPost } from '~/logic/api/graph';
import { getLatestCommentRevision } from '~/logic/lib/publish'; import { getLatestCommentRevision } from '~/logic/lib/publish';
import { LocalUpdateRemoteContentPolicy } from '~/types';
import { scanForMentions } from '~/logic/lib/graph'; import { scanForMentions } from '~/logic/lib/graph';
import { getLastSeen } from '~/logic/lib/hark';
interface CommentsProps { interface CommentsProps {
comments: GraphNode; comments: GraphNode;
association: Association;
name: string; name: string;
ship: string; ship: string;
editCommentId: string; editCommentId: string;
@ -26,7 +27,7 @@ interface CommentsProps {
} }
export function Comments(props: CommentsProps) { export function Comments(props: CommentsProps) {
const { comments, ship, name, api, baseUrl, history} = props; const { association, comments, ship, name, api, history } = props;
const onSubmit = async ( const onSubmit = async (
{ comment }, { comment },
@ -87,6 +88,20 @@ export function Comments(props: CommentsProps) {
return val; return val;
}, ''); }, '');
} }
const parentIndex = `/${comments?.post.index.slice(1).split('/')[0]}`;
const children = Array.from(comments.children);
const [latestIdx, latest] = children?.[0] || [];
useEffect(() => {
return latest
? () => api.hark.markSinceAsRead(association, parentIndex, 'comment', latest.post.index)
: () => {};
}, [association])
const lastSeen = getLastSeen(props.unreads, association['app-path'], parentIndex) || latestIdx || bigInt.zero;
return ( return (
<Col> <Col>
@ -99,7 +114,7 @@ export function Comments(props: CommentsProps) {
initial={commentContent} initial={commentContent}
/> />
) : null )} ) : null )}
{Array.from(comments.children).reverse() {children.reverse()
.map(([idx, comment]) => { .map(([idx, comment]) => {
return ( return (
<CommentItem <CommentItem
@ -109,6 +124,7 @@ export function Comments(props: CommentsProps) {
api={api} api={api}
name={name} name={name}
ship={ship} ship={ship}
unread={lastSeen.lt(idx)}
hideNicknames={props.hideNicknames} hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars} hideAvatars={props.hideAvatars}
remoteContentPolicy={props.remoteContentPolicy} remoteContentPolicy={props.remoteContentPolicy}

View File

@ -1,5 +1,5 @@
import { useEffect, useCallback } from "react"; import { useEffect, useCallback } from "react";
import { Inbox, ChatHookUpdate, Notebooks, Graphs } from "~/types"; import { Inbox, ChatHookUpdate, Notebooks, Graphs, UnreadStats } from "~/types";
import { SidebarItemStatus, SidebarAppConfig } from "./types"; import { SidebarItemStatus, SidebarAppConfig } from "./types";
export function useChat( export function useChat(
@ -42,11 +42,11 @@ export function useChat(
export function useGraphModule( export function useGraphModule(
graphKeys: Set<string>, graphKeys: Set<string>,
graphs: Graphs, graphs: Graphs,
graphUnreads: Record<string, number> graphUnreads: Record<string, Record<string, UnreadStats>>
): SidebarAppConfig { ): SidebarAppConfig {
const getStatus = useCallback( const getStatus = useCallback(
(s: string) => { (s: string) => {
if((graphUnreads[s] || 0) > 0) { if((graphUnreads?.[s]?.['/']?.unreads?.size || 0) > 0) {
return 'unread'; return 'unread';
} }
const [, , host, name] = s.split("/"); const [, , host, name] = s.split("/");
@ -57,18 +57,22 @@ export function useGraphModule(
} }
return undefined; return undefined;
}, },
[graphs, graphKeys] [graphs, graphKeys, graphUnreads]
); );
const lastUpdated = useCallback((s: string) => { const lastUpdated = useCallback((s: string) => {
// cant get link timestamps without loading posts // cant get link timestamps without loading posts
const last = graphUnreads?.[s]?.['/']?.last;
if(last) {
return last;
}
const stat = getStatus(s); const stat = getStatus(s);
if(stat === 'unsubscribed') { if(stat === 'unsubscribed') {
return 0; return 0;
} }
return 1; return 1;
}, [getStatus]); }, [getStatus, graphUnreads]);
return { getStatus, lastUpdated }; return { getStatus, lastUpdated };
} }

View File

@ -25,7 +25,6 @@ export default class Landscape extends Component<LandscapeProps, {}> {
document.title = 'OS1 - Landscape'; document.title = 'OS1 - Landscape';
this.props.subscription.startApp('groups'); this.props.subscription.startApp('groups');
this.props.subscription.startApp('chat');
this.props.subscription.startApp('graph'); this.props.subscription.startApp('graph');
} }