diff --git a/pkg/arvo/app/hark-chat-hook.hoon b/pkg/arvo/app/hark-chat-hook.hoon index dcd1449ab5..33a6695208 100644 --- a/pkg/arvo/app/hark-chat-hook.hoon +++ b/pkg/arvo/app/hark-chat-hook.hoon @@ -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 -- diff --git a/pkg/arvo/app/hark-graph-hook.hoon b/pkg/arvo/app/hark-graph-hook.hoon index edf87a6f86..3e38229834 100644 --- a/pkg/arvo/app/hark-graph-hook.hoon +++ b/pkg/arvo/app/hark-graph-hook.hoon @@ -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) :: -- -- diff --git a/pkg/arvo/app/hark-store.hoon b/pkg/arvo/app/hark-store.hoon index 476ea8d924..8b0eb55fd8 100644 --- a/pkg/arvo/app/hark-store.hoon +++ b/pkg/arvo/app/hark-store.hoon @@ -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) diff --git a/pkg/arvo/lib/hark/store.hoon b/pkg/arvo/lib/hark/store.hoon index f9448f1e8e..48affb1074 100644 --- a/pkg/arvo/lib/hark/store.hoon +++ b/pkg/arvo/lib/hark/store.hoon @@ -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) + == -- -- -- diff --git a/pkg/arvo/mar/graph/validator/link.hoon b/pkg/arvo/mar/graph/validator/link.hoon index a860b20155..9472c6cba5 100644 --- a/pkg/arvo/mar/graph/validator/link.hoon +++ b/pkg/arvo/mar/graph/validator/link.hoon @@ -5,8 +5,8 @@ ++ noun i ++ notification-kind ?+ index.p.i ~ - [@ ~] `[%link 0] - [@ @ @ ~] `[%comment 1] + [@ ~] `[%link 0 %each] + [@ @ @ ~] `[%comment 1 %since] == -- ++ grab diff --git a/pkg/arvo/mar/graph/validator/publish.hoon b/pkg/arvo/mar/graph/validator/publish.hoon index cddf69073e..4ef138ef13 100644 --- a/pkg/arvo/mar/graph/validator/publish.hoon +++ b/pkg/arvo/mar/graph/validator/publish.hoon @@ -8,8 +8,8 @@ :: ++ notification-kind ?+ index.p.i ~ - [@ %1 @ ~] `[%note 0] - [@ %2 @ @ ~] `[%comment 1] + [@ %1 @ ~] `[%note 0 %each] + [@ %2 @ @ ~] `[%comment 1 %since] == -- ++ grab diff --git a/pkg/arvo/sur/hark-store.hoon b/pkg/arvo/sur/hark-store.hoon index 6d2783709b..7d455e18fa 100644 --- a/pkg/arvo/sur/hark-store.hoon +++ b/pkg/arvo/sur/hark-store.hoon @@ -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])] == -- diff --git a/pkg/interface/src/logic/api/hark.ts b/pkg/interface/src/logic/api/hark.ts index 8f33fc4804..338fbd99cb 100644 --- a/pkg/interface/src/logic/api/hark.ts +++ b/pkg/interface/src/logic/api/hark.ts @@ -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 { 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 }); } diff --git a/pkg/interface/src/logic/lib/hark.ts b/pkg/interface/src/logic/lib/hark.ts new file mode 100644 index 0000000000..096a6bab5b --- /dev/null +++ b/pkg/interface/src/logic/lib/hark.ts @@ -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 + ); +} diff --git a/pkg/interface/src/logic/reducers/hark-update.ts b/pkg/interface/src/logic/reducers/hark-update.ts index 590afde13f..02eb7bf23a 100644 --- a/pkg/interface/src/logic/reducers/hark-update.ts +++ b/pkg/interface/src/logic/reducers/hark-update.ts @@ -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; -type HarkState = Pick; 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) => void) { + if(!('graph' in index)) { + return; + } + const unreads = _.get(state.unreads.graph, [index.graph.graph, index.graph.index, 'unreads'], new Set()); + 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 }) => ({ diff --git a/pkg/interface/src/logic/store/store.ts b/pkg/interface/src/logic/store/store.ts index 4b80ec83af..050913f572 100644 --- a/pkg/interface/src/logic/store/store.ts +++ b/pkg/interface/src/logic/store/store.ts @@ -106,12 +106,12 @@ export default class GlobalStore extends BaseStore { mentions: false, watching: [], }, - notificationsCount: 0, unreads: { - graph: {}, - group: {}, chat: {}, - } + graph: {}, + group: {} + }, + notificationsCount: 0 }; } diff --git a/pkg/interface/src/logic/store/type.ts b/pkg/interface/src/logic/store/type.ts index 88ef6d3ade..26538d499d 100644 --- a/pkg/interface/src/logic/store/type.ts +++ b/pkg/interface/src/logic/store/type.ts @@ -66,6 +66,7 @@ export interface StoreState { notificationsGroupConfig: GroupNotificationsConfig; notificationsChatConfig: string[]; notificationsCount: number, + unreads: Unreads; doNotDisturb: boolean; unreads: Unreads; } diff --git a/pkg/interface/src/logic/subscription/global.ts b/pkg/interface/src/logic/subscription/global.ts index 4e6d6e1e94..7584143790 100644 --- a/pkg/interface/src/logic/subscription/global.ts +++ b/pkg/interface/src/logic/subscription/global.ts @@ -54,7 +54,7 @@ export default class GlobalSubscription extends BaseSubscription { 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() { diff --git a/pkg/interface/src/types/chat-update.ts b/pkg/interface/src/types/chat-update.ts index 6cd382881b..34a844e322 100644 --- a/pkg/interface/src/types/chat-update.ts +++ b/pkg/interface/src/types/chat-update.ts @@ -55,7 +55,7 @@ export interface Inbox { [chatName: string]: Mailbox; } -interface Mailbox { +export interface Mailbox { config: MailboxConfig; envelopes: Envelope[]; } diff --git a/pkg/interface/src/types/hark-update.ts b/pkg/interface/src/types/hark-update.ts index 44935831f9..9a38250664 100644 --- a/pkg/interface/src/types/hark-update.ts +++ b/pkg/interface/src/types/hark-update.ts @@ -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; + 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; - group: Record; - graph: Record; + chat: Record; + graph: Record>; + group: Record; } interface WatchedIndex { diff --git a/pkg/interface/src/views/apps/launch/components/Groups.tsx b/pkg/interface/src/views/apps/launch/components/Groups.tsx index 98692be606..f7d531c9a8 100644 --- a/pkg/interface/src/views/apps/launch/components/Groups.tsx +++ b/pkg/interface/src/views/apps/launch/components/Groups.tsx @@ -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) => 0), // x?.['/']?.unreads?.size), f.reduce(f.add, 0) - ); + )(unreads); export default function Groups(props: GroupsProps & Parameters[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 ( [0]) { interface GroupProps { path: string; title: string; + updates?: number; unreads: number; } function Group(props: GroupProps) { diff --git a/pkg/interface/src/views/apps/links/LinkResource.tsx b/pkg/interface/src/views/apps/links/LinkResource.tsx index e269ac0122..b3764d108b 100644 --- a/pkg/interface/src/views/apps/links/LinkResource.tsx +++ b/pkg/interface/src/views/apps/links/LinkResource.tsx @@ -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 ( @@ -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) { { 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) => { {contents[0].text} {hostname} ↗ - - + - {showNickname ? nickname : cite(author) } - - {size} comments + {size} comments {(ourRole === 'admin' || node.post.author === window.ship) && ( api.graph.removeNodes(`~${ship}`, name, [node.post.index])}>Delete)} - + + ); diff --git a/pkg/interface/src/views/apps/links/components/link-preview.js b/pkg/interface/src/views/apps/links/components/link-preview.js index f66058f8b5..15c4a97a83 100644 --- a/pkg/interface/src/views/apps/links/components/link-preview.js +++ b/pkg/interface/src/views/apps/links/components/link-preview.js @@ -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 = ( {name} - + {dateFmt} {props.children} diff --git a/pkg/interface/src/views/apps/publish/components/Note.tsx b/pkg/interface/src/views/apps/publish/components/Note.tsx index be309a7398..33ae9ec795 100644 --- a/pkg/interface/src/views/apps/publish/components/Note.tsx +++ b/pkg/interface/src/views/apps/publish/components/Note.tsx @@ -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) { {name} - + {date} {commentDesc} diff --git a/pkg/interface/src/views/apps/publish/components/NoteRoutes.tsx b/pkg/interface/src/views/apps/publish/components/NoteRoutes.tsx index c628ca4b94..bba8172290 100644 --- a/pkg/interface/src/views/apps/publish/components/NoteRoutes.tsx +++ b/pkg/interface/src/views/apps/publish/components/NoteRoutes.tsx @@ -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; } diff --git a/pkg/interface/src/views/apps/publish/components/Notebook.tsx b/pkg/interface/src/views/apps/publish/components/Notebook.tsx index 7c9c4a721d..998e8beae6 100644 --- a/pkg/interface/src/views/apps/publish/components/Notebook.tsx +++ b/pkg/interface/src/views/apps/publish/components/Notebook.tsx @@ -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} /> diff --git a/pkg/interface/src/views/apps/publish/components/NotebookPosts.tsx b/pkg/interface/src/views/apps/publish/components/NotebookPosts.tsx index d44905a3b3..91dbaf2184 100644 --- a/pkg/interface/src/views/apps/publish/components/NotebookPosts.tsx +++ b/pkg/interface/src/views/apps/publish/components/NotebookPosts.tsx @@ -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} diff --git a/pkg/interface/src/views/apps/publish/components/NotebookRoutes.tsx b/pkg/interface/src/views/apps/publish/components/NotebookRoutes.tsx index e95f4fa7ec..385684d36b 100644 --- a/pkg/interface/src/views/apps/publish/components/NotebookRoutes.tsx +++ b/pkg/interface/src/views/apps/publish/components/NotebookRoutes.tsx @@ -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} diff --git a/pkg/interface/src/views/components/CommentItem.tsx b/pkg/interface/src/views/components/CommentItem.tsx index 38e59b8332..60b17e7583 100644 --- a/pkg/interface/src/views/components/CommentItem.tsx +++ b/pkg/interface/src/views/components/CommentItem.tsx @@ -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} > diff --git a/pkg/interface/src/views/components/Comments.tsx b/pkg/interface/src/views/components/Comments.tsx index 2fb629f417..735cd2a235 100644 --- a/pkg/interface/src/views/components/Comments.tsx +++ b/pkg/interface/src/views/components/Comments.tsx @@ -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 ( @@ -99,7 +114,7 @@ export function Comments(props: CommentsProps) { initial={commentContent} /> ) : null )} - {Array.from(comments.children).reverse() + {children.reverse() .map(([idx, comment]) => { return ( , graphs: Graphs, - graphUnreads: Record + graphUnreads: Record> ): 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 }; } diff --git a/pkg/interface/src/views/landscape/index.tsx b/pkg/interface/src/views/landscape/index.tsx index 906fa31e9e..622e4059f8 100644 --- a/pkg/interface/src/views/landscape/index.tsx +++ b/pkg/interface/src/views/landscape/index.tsx @@ -25,7 +25,6 @@ export default class Landscape extends Component { document.title = 'OS1 - Landscape'; this.props.subscription.startApp('groups'); - this.props.subscription.startApp('chat'); this.props.subscription.startApp('graph'); }