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]
::
/- store=hark-store, post, group-store, metadata-store, hook=hark-chat-hook
/+ resource, metadata, default-agent, dbug, chat-store, grpl=group
/+ default-agent
::
~% %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
~% %hark-chat-hook-agent ..card ~
|_ =bowl:gall
+* this .
ha ~(. +> bowl)
def ~(. (default-agent this %|) bowl)
met ~(. metadata bowl)
grp ~(. grpl bowl)
::
++ on-init
:_ this
~[watch-chat:ha]
::
++ on-save !>(state)
++ on-init [~ this]
++ on-save !>(~)
++ on-load
|= old=vase
^- (quip card _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
::
|= =vase
`this
++ 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-arvo on-arvo:def
++ on-fail on-fail:def
--

View File

@ -3,6 +3,7 @@
/- store=hark-store, post, group-store, metadata-store, hook=hark-graph-hook
/+ resource, metadata, default-agent, dbug, graph-store
::
::
~% %hark-graph-hook-top ..is ~
|%
+$ card card:agent:gall
@ -57,7 +58,10 @@
++ on-load
|= old=vase
^- (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
|= =path
@ -204,12 +208,7 @@
^- (quip card _state)
=^ child-cards state
(check-node-children node tube)
?: =(our.bowl author.post.node)
=^ self-cards state
(self-post node)
:_ state
(weld child-cards self-cards)
=+ !< notif-kind=(unit [name=@t parent-lent=@ud])
=+ !< notif-kind=(unit [name=@t parent-lent=@ud mode=?(%each %since)])
(tube !>([0 post.node]))
?~ notif-kind
[child-cards state]
@ -219,17 +218,25 @@
name.u.notif-kind
=/ parent=index:post
(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)
(~(has in watching) [rid parent])
==
[child-cards state]
=/ notif-index=index:store
[%graph group rid module.metadata desc]
~
=/ =contents:store
[%graph (limo post.node ~)]
:_ state
%+ snoc child-cards
(add-unread notif-index [time-sent.post.node %.n contents])
~[(add-unread notif-index [time-sent.post.node %.n contents])]
::
++ is-mention
|= contents=(list content:post)
@ -243,17 +250,35 @@
$(contents t.contents)
::
++ self-post
|= =node:graph-store
|= $: =node:graph-store
=index:store
mode=?(%since %each)
==
^- (quip card _state)
=| cards=(list card)
=? cards ?=(%since mode)
:_ cards
(poke-hark %read-since index index.post.node)
?. ?=(%.y watch-on-self)
[~ state]
`state(watching (~(put in watching) [rid index.post.node]))
[cards state]
:- 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
|= [=index:store =notification:store]
^- card
=- [%pass / %agent [our.bowl %hark-store] %poke -]
hark-action+!>([%add index notification])
(poke-hark %add-note 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
/+ resource, metadata, default-agent, dbug, graph-store
::
::
~% %hark-store-top ..is ~
|%
+$ card card:agent:gall
+$ versioned-state
$% state-0
$% state:state-zero:store
state-1
==
+$ unread-stats
[indices=(set index:graph-store) last=@da]
::
+$ state-0
$: %0
+$ state-1
$: %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
archive=notifications:store
last-seen=@da
current-timebox=@da
dnd=_|
==
+$ inflated-state
$: state-0
$: state-1
cache
==
:: $cache: useful to have precalculated, but can be derived from state
:: albeit expensively
+$ cache
$: unread-count=@ud
by-index=(jug index:store @da)
$: by-index=(jug index:store @da)
~
==
::
@ -54,16 +70,26 @@
|= =old=vase
^- (quip card _this)
=/ old
!<(state-0 old-vase)
=. notifications.old
(gas:orm *notifications:store (tap:orm notifications.old))
=. archive.old
(gas:orm *notifications:store (tap:orm archive.old))
`this(-.state old, +.state (inflate-cache old))
!<(versioned-state old-vase)
=| cards=(list card)
|-
?- -.old
%1
[cards this(+.state (inflate-cache:ha old), -.state old)]
::
%0
%_ $
::
old
*state-1
==
==
::
++ on-watch
|= =path
^- (quip card _this)
?> (team:title [src our]:bowl)
|^
?+ path (on-watch:def path)
::
@ -76,26 +102,41 @@
^- update:store
:- %more
^- (list update:store)
:- unreads
:+ [%set-dnd dnd]
[%count unread-count]
:+ give-unreads
[%set-dnd dnd]
%+ weld
%+ turn
%+ scag 3
(tap-nonempty:ha archive)
(timebox-update &)
%+ turn
%+ scag 3
(tap-nonempty:ha notifications)
(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
:- %unreads
^- (list [index:store @ud])
%+ turn
~(tap by by-index)
|=([=index:store =(set @da)] [index ~(wyt in set)])
(weld give-each-unreads give-since-unreads)
::
++ timebox-update
|= archived=?
@ -139,6 +180,7 @@
=^ cards state
?+ mark (on-poke:def mark vase)
%hark-action (hark-action !<(action:store vase))
%noun ~& +.state [~ state]
==
[cards this]
::
@ -146,46 +188,44 @@
|= =action:store
^- (quip card _state)
|^
?- -.action
%add (add +.action)
%archive (do-archive +.action)
%seen seen
%read (read +.action)
%read-index (read-index +.action)
%unread (unread +.action)
%set-dnd (set-dnd +.action)
?- -.action
%add-note (add-note +.action)
%archive (do-archive +.action)
::
%read-each (read-each +.action)
%unread-each (unread-each +.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)
%seen seen
==
++ add
::
++ add-note
|= [=index:store =notification:store]
^- (quip card _state)
=/ =timebox:store
(gut-orm:ha notifications last-seen)
(gut-orm:ha notifications current-timebox)
=/ existing-notif
(~(get by timebox) index)
=/ new=notification:store
?~ existing-notif
notification
(merge-notification:ha u.existing-notif notification)
=. read.new %.y
=/ new-timebox=timebox:store
(~(put by timebox) index new)
:- (give:ha [/updates]~ %added last-seen index new)
:- (give:ha [/updates]~ %added current-timebox index new)
%_ state
+ ?~(existing-notif (upd-unreads:ha index last-seen %.n) +.state)
notifications (put:orm notifications last-seen new-timebox)
+ ?~(existing-notif (upd-unreads:ha index current-timebox %.n) +.state)
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
|= [time=@da =index:store]
@ -210,29 +250,118 @@
(~(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]
^- (quip card _state)
:- (give:ha [/updates]~ %read time index)
%_ state
+ (upd-unreads:ha index time %.y)
unread-count (dec unread-count)
notifications (change-read-status:ha time index %.y)
==
::
++ unread
++ unread-note
|= [time=@da =index:store]
^- (quip card _state)
:- (give:ha [/updates]~ %unread time index)
:- (give:ha [/updates]~ %unread-note time index)
%_ state
+ (upd-unreads:ha index time %.n)
unread-count +(unread-count)
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
^- (quip card _state)
:_ state(last-seen now.bowl)
:_ state(current-timebox now.bowl)
:~ cancel-autoseen:ha
autoseen-timer:ha
==
@ -254,7 +383,7 @@
?. ?=([%autoseen ~] wire)
(on-arvo:def wire sign-arvo)
?> ?=([%b %wake *] sign-arvo)
:_ this(last-seen now.bowl)
:_ this(current-timebox now.bowl)
~[autoseen-timer:ha]
::
++ on-fail on-fail:def
@ -262,12 +391,6 @@
|_ =bowl:gall
+* met ~(. metadata bowl)
::
++ tap-nonempty
|= =notifications:store
^- (list [@da timebox:store])
%+ skip (tap:orm notifications)
|=([@da =timebox:store] =(0 ~(wyt by timebox)))
::
++ merge-notification
|= [existing=notification:store new=notification:store]
^- notification:store
@ -275,15 +398,15 @@
::
%chat
?> ?=(%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 -.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 -.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
@ -318,12 +441,18 @@
++ autoseen-interval ~h3
++ cancel-autoseen
^- card
[%pass /autoseen %arvo %b %rest (add last-seen autoseen-interval)]
[%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)
@ -341,8 +470,29 @@
~(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
|= state-0
|= state-1
^+ +.state
=/ nots=(list [p=@da =timebox:store])
(tap:orm notifications)

View File

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

View File

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

View File

@ -1,8 +1,37 @@
/- *resource, graph-store, post, group-store, metadata-store, chat-store
/- chat-store, graph-store, post, *resource, group-store, metadata-store
^?
|%
++ 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]
[%group group=resource description=@t]
[%chat chat=path mention=?]
==
--
::
+$ index
$% [%graph group=resource graph=resource module=@t description=@t]
$% $: %graph
group=resource
graph=resource
module=@t
description=@t
=index:graph-store
==
[%group group=resource description=@t]
[%chat chat=path mention=?]
==
@ -29,24 +58,42 @@
((mop @da timebox) gth)
::
+$ action
$% [%add =index =notification]
$% [%add-note =index =notification]
[%archive time=@da index]
[%read time=@da index]
[%read-index index]
[%unread time=@da index]
::
[%unread-since =index time=@da]
[%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=?]
[%seen ~]
==
::
++ indexed-notification
+$ indexed-notification
[index notification]
::
+$ index-stats
[notifications=@ud =unreads last-seen=@da]
::
+$ unreads
$% [%since =index:graph-store]
[%each indices=(set index:graph-store)]
==
::
+$ update
$% action
[%more more=(list update)]
[%added time=@da =index =notification]
[%read-index =index]
[%read time=@da =index]
[%timebox time=@da archived=? =(list [index notification])]
[%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 { StoreState } from "../store/type";
import { dateToDa, decToUd } from "../lib/util";
import {NotifIndex, IndexedNotification} from "~/types";
import {NotifIndex, IndexedNotification, Association, GraphNotifDescription} from "~/types";
import { BigInteger } from 'big-integer';
import {getParentIndex} from "../lib/notification";
@ -71,6 +71,48 @@ export class HarkApi extends BaseApi<StoreState> {
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() {
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,
NotificationGraphConfig,
GroupNotificationsConfig,
UnreadStats,
} from "~/types";
import { makePatDa } from "~/logic/lib/util";
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) => {
const data = _.get(json, "harkUpdate", false);
@ -141,29 +136,90 @@ function reduce(data: any, state: HarkState) {
dnd(data, state);
added(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) {
const data = _.get(json, 'unreads');
if(data) {
data.forEach(({ index, unread }) => {
updateUnreads(state, index, x => x + unread);
data.forEach(({ index, stats }) => {
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);
if('graph' in index) {
const curr = state.unreads.graph[index.graph.graph] || 0;
state.unreads.graph[index.graph.graph] = f(curr);
} else if('group' in index) {
const curr = state.unreads.group[index.group.group] || 0;
state.unreads.group[index.group.group] = f(curr);
} else if('chat' in index) {
const curr = state.unreads.chat[index.chat.chat] || 0
state.unreads.chat[index.chat.chat] = f(curr);
}
}
if('graph' in index) {
const curr = _.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 = _.get(state.unreads.group, [index.group.group, statField], 0);
_.set(state.unreads.group, [index.group.group, statField], f(curr));
} else if('chat' in index) {
const curr = _.get(state.unreads.chat, [index.chat.chat, statField], 0);
_.set(state.unreads.chat, [index.chat.chat, statField], f(curr));
}
}
function added(json: any, state: HarkState) {
@ -178,12 +234,12 @@ function added(json: any, state: HarkState) {
);
if (arrIdx !== -1) {
if(timebox[arrIdx]?.notification?.read) {
updateUnreads(state, index, x => x+1);
updateNotificationStats(state, index, 'notifications', x => x+1);
}
timebox[arrIdx] = { index, notification };
state.notifications.set(time, timebox);
} else {
updateUnreads(state, index, x => x+1);
updateNotificationStats(state, index, 'notifications', x => x+1);
state.notifications.set(time, [...timebox, { index, notification }]);
}
}
@ -200,9 +256,7 @@ const timebox = (json: any, state: HarkState) => {
const data = _.get(json, "timebox", false);
if (data) {
const time = makePatDa(data.time);
if (data.archive) {
state.archivedNotifications.set(time, data.notifications);
} else {
if (!data.archive) {
state.notifications.set(time, data.notifications);
}
}
@ -262,7 +316,7 @@ function read(json: any, state: HarkState) {
const data = _.get(json, "read", false);
if (data) {
const { time, index } = data;
updateUnreads(state, index, x => x-1);
updateNotificationStats(state, index, 'notifications', x => x-1);
setRead(time, index, true, state);
}
}
@ -271,7 +325,7 @@ function unread(json: any, state: HarkState) {
const data = _.get(json, "unread", false);
if (data) {
const { time, index } = data;
updateUnreads(state, index, x => x+1);
updateNotificationStats(state, index, 'notifications', x => x+1);
setRead(time, index, false, state);
}
}
@ -294,7 +348,7 @@ function archive(json: any, state: HarkState) {
const readCount = archived.filter(
({ notification }) => !notification.read
).length;
updateUnreads(state, index, x => x - readCount);
updateNotificationStats(state, index, 'notifications', x => x - readCount);
state.archivedNotifications.set(time, [
...archiveBox,
...archived.map(({ notification, index }) => ({

View File

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

View File

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

View File

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

View File

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

View File

@ -4,13 +4,20 @@ import { GroupUpdate } from "./group-update";
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
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 {
graph: string;
group: string;
description: GraphNotifDescription;
module: string;
index: string;
}
export interface GroupNotifIndex {
@ -61,9 +68,9 @@ export interface NotificationGraphConfig {
}
export interface Unreads {
chat: Record<string, number>;
group: Record<string, number>;
graph: Record<string, number>;
chat: Record<string, UnreadStats>;
graph: Record<string, Record<string, UnreadStats>>;
group: Record<string, UnreadStats>;
}
interface WatchedIndex {

View File

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

View File

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

View File

@ -1,9 +1,10 @@
import React from 'react';
import React from 'react';
import { Row, Col, Anchor, Box, Text, BaseImage } from '@tlon/indigo-react';
import { Sigil } from '~/logic/lib/sigil';
import { Link } from 'react-router-dom';
import { cite } from '~/logic/lib/util';
import { Author } from "~/views/apps/publish/components/Author";
import { roleForShip } from '~/logic/lib/group';
@ -12,6 +13,8 @@ export const LinkItem = (props) => {
node,
nickname,
avatar,
contacts,
unread,
resource,
hideAvatars,
hideNicknames,
@ -26,6 +29,7 @@ export const LinkItem = (props) => {
const author = node.post.author;
const index = node.post.index.split('/')[1];
const size = node.children ? node.children.size : 0;
const date = node.post['time-sent'];
const contents = node.post.contents;
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 ml="2" color="gray" display='inline-block' flexShrink='0'>{hostname} </Text>
</Anchor>
<Box width="100%">
<Text
fontFamily={showNickname ? 'sans' : 'mono'} pr={2}
<Row alignItems="center" width="100%">
<Author
contacts={contacts}
ship={author}
hideAvatars={hideAvatars}
hideNicknames={hideNicknames}
unread={unread}
date={date}
>
{showNickname ? nickname : cite(author) }
</Text>
<Link to={`${baseUrl}/${index}`}>
<Text color="gray">{size} comments</Text>
<Text ml="2" color="gray">{size} comments</Text>
</Link>
{(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>)}
</Box>
</Author>
</Row>
</Col>
</Row>
);

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import { cite } from '~/logic/lib/util';
import RemoteContent from '~/views/components/RemoteContent';
@ -20,6 +20,10 @@ export const LinkPreview = (props) => {
const timeSent =
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 = (
<RemoteContent
unfold={true}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,14 +6,15 @@ import CommentInput from './CommentInput';
import { Contacts } from '~/types/contact-update';
import GlobalApi from '~/logic/api/global';
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 { getLatestCommentRevision } from '~/logic/lib/publish';
import { LocalUpdateRemoteContentPolicy } from '~/types';
import { scanForMentions } from '~/logic/lib/graph';
import { getLastSeen } from '~/logic/lib/hark';
interface CommentsProps {
comments: GraphNode;
association: Association;
name: string;
ship: string;
editCommentId: string;
@ -26,7 +27,7 @@ interface 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 (
{ comment },
@ -87,6 +88,20 @@ export function Comments(props: CommentsProps) {
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 (
<Col>
@ -99,7 +114,7 @@ export function Comments(props: CommentsProps) {
initial={commentContent}
/>
) : null )}
{Array.from(comments.children).reverse()
{children.reverse()
.map(([idx, comment]) => {
return (
<CommentItem
@ -109,6 +124,7 @@ export function Comments(props: CommentsProps) {
api={api}
name={name}
ship={ship}
unread={lastSeen.lt(idx)}
hideNicknames={props.hideNicknames}
hideAvatars={props.hideAvatars}
remoteContentPolicy={props.remoteContentPolicy}

View File

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

View File

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