mirror of
https://github.com/urbit/shrub.git
synced 2024-12-20 01:01:37 +03:00
hark: separate unread tracking from notifications
This commit is contained in:
parent
5d7fa0463c
commit
b7b4000986
@ -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
|
||||
::
|
||||
++ on-leave on-leave: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-fail on-fail:def
|
||||
--
|
||||
|
@ -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)
|
||||
::
|
||||
--
|
||||
--
|
||||
|
@ -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]
|
||||
::
|
||||
@ -147,45 +189,43 @@
|
||||
^- (quip card _state)
|
||||
|^
|
||||
?- -.action
|
||||
%add (add +.action)
|
||||
%add-note (add-note +.action)
|
||||
%archive (do-archive +.action)
|
||||
%seen seen
|
||||
%read (read +.action)
|
||||
%read-index (read-index +.action)
|
||||
%unread (unread +.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)
|
||||
|
@ -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)
|
||||
==
|
||||
--
|
||||
--
|
||||
--
|
||||
|
@ -5,8 +5,8 @@
|
||||
++ noun i
|
||||
++ notification-kind
|
||||
?+ index.p.i ~
|
||||
[@ ~] `[%link 0]
|
||||
[@ @ @ ~] `[%comment 1]
|
||||
[@ ~] `[%link 0 %each]
|
||||
[@ @ @ ~] `[%comment 1 %since]
|
||||
==
|
||||
--
|
||||
++ grab
|
||||
|
@ -8,8 +8,8 @@
|
||||
::
|
||||
++ notification-kind
|
||||
?+ index.p.i ~
|
||||
[@ %1 @ ~] `[%note 0]
|
||||
[@ %2 @ @ ~] `[%comment 1]
|
||||
[@ %1 @ ~] `[%note 0 %each]
|
||||
[@ %2 @ @ ~] `[%comment 1 %since]
|
||||
==
|
||||
--
|
||||
++ grab
|
||||
|
@ -1,11 +1,40 @@
|
||||
/- *resource, graph-store, post, group-store, metadata-store, chat-store
|
||||
/- chat-store, graph-store, post, *resource, group-store, metadata-store
|
||||
^?
|
||||
|%
|
||||
+$ index
|
||||
++ state-zero
|
||||
|%
|
||||
+$ state
|
||||
$: %0
|
||||
notifications=notifications
|
||||
archive=notifications
|
||||
last-seen=@da
|
||||
dnd=_|
|
||||
==
|
||||
::
|
||||
+$ notifications
|
||||
((mop @da timebox) gth)
|
||||
::
|
||||
+$ timebox
|
||||
(map index notification)
|
||||
::
|
||||
+$ index
|
||||
$% [%graph group=resource graph=resource module=@t description=@t]
|
||||
[%group group=resource description=@t]
|
||||
[%chat chat=path mention=?]
|
||||
==
|
||||
--
|
||||
::
|
||||
+$ index
|
||||
$% $: %graph
|
||||
group=resource
|
||||
graph=resource
|
||||
module=@t
|
||||
description=@t
|
||||
=index:graph-store
|
||||
==
|
||||
[%group group=resource description=@t]
|
||||
[%chat chat=path mention=?]
|
||||
==
|
||||
::
|
||||
+$ group-contents
|
||||
$~ [%add-members *resource ~]
|
||||
@ -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])]
|
||||
==
|
||||
--
|
||||
|
@ -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 });
|
||||
}
|
||||
|
17
pkg/interface/src/logic/lib/hark.ts
Normal file
17
pkg/interface/src/logic/lib/hark.ts
Normal 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
|
||||
);
|
||||
}
|
@ -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,28 +136,89 @@ 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);
|
||||
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 = state.unreads.group[index.group.group] || 0;
|
||||
state.unreads.group[index.group.group] = f(curr);
|
||||
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 = state.unreads.chat[index.chat.chat] || 0
|
||||
state.unreads.chat[index.chat.chat] = f(curr);
|
||||
const curr = _.get(state.unreads.chat, [index.chat.chat, statField], 0);
|
||||
_.set(state.unreads.chat, [index.chat.chat, statField], f(curr));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 }) => ({
|
||||
|
@ -106,12 +106,12 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
||||
mentions: false,
|
||||
watching: [],
|
||||
},
|
||||
notificationsCount: 0,
|
||||
unreads: {
|
||||
graph: {},
|
||||
group: {},
|
||||
chat: {},
|
||||
}
|
||||
graph: {},
|
||||
group: {}
|
||||
},
|
||||
notificationsCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -66,6 +66,7 @@ export interface StoreState {
|
||||
notificationsGroupConfig: GroupNotificationsConfig;
|
||||
notificationsChatConfig: string[];
|
||||
notificationsCount: number,
|
||||
unreads: Unreads;
|
||||
doNotDisturb: boolean;
|
||||
unreads: Unreads;
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -55,7 +55,7 @@ export interface Inbox {
|
||||
[chatName: string]: Mailbox;
|
||||
}
|
||||
|
||||
interface Mailbox {
|
||||
export interface Mailbox {
|
||||
config: MailboxConfig;
|
||||
envelopes: Envelope[];
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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}
|
||||
|
@ -4,6 +4,7 @@ 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>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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}
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user