mirror of
https://github.com/ilyakooo0/urbit.git
synced 2024-12-14 17:41:33 +03:00
Merge pull request #3766 from urbit/lf/hark-redux
hark: notification store
This commit is contained in:
commit
fa71638abd
@ -5,7 +5,7 @@
|
|||||||
/- glob
|
/- glob
|
||||||
/+ default-agent, verb, dbug
|
/+ default-agent, verb, dbug
|
||||||
|%
|
|%
|
||||||
++ hash 0v3.29n7b.04srk.3pcv0.1ld5v.vl1io
|
++ hash 0v6.9vk2h.hr87m.nn63p.8kmo5.k4ljt
|
||||||
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
+$ state-0 [%0 hash=@uv glob=(unit (each glob:glob tid=@ta))]
|
||||||
+$ all-states
|
+$ all-states
|
||||||
$% state-0
|
$% state-0
|
||||||
@ -89,7 +89,7 @@
|
|||||||
=+ .^(=map=tube:clay %cc (weld home /map/mime))
|
=+ .^(=map=tube:clay %cc (weld home /map/mime))
|
||||||
=+ .^(arch %cy (weld home /app/landscape/js/bundle))
|
=+ .^(arch %cy (weld home /app/landscape/js/bundle))
|
||||||
=/ bundle-hash=@t
|
=/ bundle-hash=@t
|
||||||
%- need
|
%- need
|
||||||
^- (unit @t)
|
^- (unit @t)
|
||||||
%- ~(rep by dir)
|
%- ~(rep by dir)
|
||||||
|= [[file=@t ~] out=(unit @t)]
|
|= [[file=@t ~] out=(unit @t)]
|
||||||
|
@ -560,6 +560,14 @@
|
|||||||
|^
|
|^
|
||||||
?> (team:title our.bowl src.bowl)
|
?> (team:title our.bowl src.bowl)
|
||||||
?+ path (on-peek:def path)
|
?+ path (on-peek:def path)
|
||||||
|
[%x %graph-mark @ @ ~]
|
||||||
|
=/ =ship (slav %p i.t.t.path)
|
||||||
|
=/ =term i.t.t.t.path
|
||||||
|
=/ result=(unit marked-graph:store)
|
||||||
|
(~(get by graphs) [ship term])
|
||||||
|
?~ result [~ ~]
|
||||||
|
``noun+!>(q.u.result)
|
||||||
|
::
|
||||||
[%x %keys ~]
|
[%x %keys ~]
|
||||||
:- ~ :- ~ :- %graph-update
|
:- ~ :- ~ :- %graph-update
|
||||||
!>(`update:store`[%0 now.bowl [%keys ~(key by graphs)]])
|
!>(`update:store`[%0 now.bowl [%keys ~(key by graphs)]])
|
||||||
|
180
pkg/arvo/app/hark-chat-hook.hoon
Normal file
180
pkg/arvo/app/hark-chat-hook.hoon
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
:: 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
|
||||||
|
::
|
||||||
|
~% %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 /updates]
|
||||||
|
--
|
||||||
|
%- agent:dbug
|
||||||
|
^- agent:gall
|
||||||
|
~% %hark-chat-hook-agent ..card ~
|
||||||
|
|_ =bowl:gall
|
||||||
|
+* this .
|
||||||
|
ha ~(. +> bowl)
|
||||||
|
def ~(. (default-agent this %|) bowl)
|
||||||
|
met ~(. metadata bowl)
|
||||||
|
::
|
||||||
|
++ on-init
|
||||||
|
:_ this
|
||||||
|
~[watch-chat:ha]
|
||||||
|
::
|
||||||
|
++ on-save !>(state)
|
||||||
|
++ on-load
|
||||||
|
|= old=vase
|
||||||
|
^- (quip card _this)
|
||||||
|
`this(state !<(state-0 old))
|
||||||
|
::
|
||||||
|
++ 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)
|
||||||
|
:_ state
|
||||||
|
?+ -.update ~
|
||||||
|
%message (process-envelope path.update envelope.update)
|
||||||
|
::
|
||||||
|
%messages
|
||||||
|
%- zing
|
||||||
|
(turn envelopes.update (cury process-envelope path.update))
|
||||||
|
==
|
||||||
|
++ is-mention
|
||||||
|
|= =envelope:chat-store
|
||||||
|
?. ?=(%text -.letter.envelope) %.n
|
||||||
|
?& mentions
|
||||||
|
?= ^
|
||||||
|
(find (scow %p our.bowl) (trip text.letter.envelope))
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ is-notification
|
||||||
|
|= [=path =envelope:chat-store]
|
||||||
|
?& (~(has in watching) path)
|
||||||
|
!=(author.envelope our.bowl)
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ process-envelope
|
||||||
|
|= [=path =envelope:chat-store]
|
||||||
|
^- (list card)
|
||||||
|
=/ mention=?
|
||||||
|
(is-mention envelope)
|
||||||
|
?. ?|(mention (is-notification path envelope))
|
||||||
|
~
|
||||||
|
=/ =index:store
|
||||||
|
[%chat path mention]
|
||||||
|
=/ =contents:store
|
||||||
|
[%chat ~[envelope]]
|
||||||
|
~[(poke-store %add index when.envelope %.n contents)]
|
||||||
|
::
|
||||||
|
++ poke-store
|
||||||
|
|= =action:store
|
||||||
|
^- card
|
||||||
|
=- [%pass /store %agent [our.bowl %hark-store] %poke -]
|
||||||
|
hark-action+!>(action)
|
||||||
|
--
|
||||||
|
::
|
||||||
|
++ on-peek on-peek:def
|
||||||
|
::
|
||||||
|
++ on-leave on-leave:def
|
||||||
|
++ on-arvo on-arvo:def
|
||||||
|
++ on-fail on-fail:def
|
||||||
|
--
|
256
pkg/arvo/app/hark-graph-hook.hoon
Normal file
256
pkg/arvo/app/hark-graph-hook.hoon
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
:: hark-graph-hook: notifications for graph-store [landscape]
|
||||||
|
::
|
||||||
|
/- 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
|
||||||
|
+$ versioned-state
|
||||||
|
$% state-0
|
||||||
|
==
|
||||||
|
::
|
||||||
|
+$ state-0
|
||||||
|
$: %0
|
||||||
|
watching=(set resource)
|
||||||
|
mentions=_&
|
||||||
|
watch-on-self=_&
|
||||||
|
==
|
||||||
|
::
|
||||||
|
--
|
||||||
|
::
|
||||||
|
=| state-0
|
||||||
|
=* state -
|
||||||
|
::
|
||||||
|
=>
|
||||||
|
|_ =bowl:gall
|
||||||
|
::
|
||||||
|
++ 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:hook]
|
||||||
|
^- (list card)
|
||||||
|
[%give %fact paths hark-graph-hook-update+!>(update)]~
|
||||||
|
::
|
||||||
|
++ watch-graph
|
||||||
|
^- card
|
||||||
|
[%pass /graph %agent [our.bowl %graph-store] %watch /updates]
|
||||||
|
--
|
||||||
|
%- agent:dbug
|
||||||
|
^- agent:gall
|
||||||
|
~% %hark-graph-hook-agent ..card ~
|
||||||
|
|_ =bowl:gall
|
||||||
|
+* this .
|
||||||
|
ha ~(. +> bowl)
|
||||||
|
def ~(. (default-agent this %|) bowl)
|
||||||
|
met ~(. metadata bowl)
|
||||||
|
::
|
||||||
|
++ on-init
|
||||||
|
:_ this
|
||||||
|
~[watch-graph:ha]
|
||||||
|
::
|
||||||
|
++ on-save !>(state)
|
||||||
|
++ on-load
|
||||||
|
|= old=vase
|
||||||
|
^- (quip card _this)
|
||||||
|
`this(state !<(state-0 old))
|
||||||
|
::
|
||||||
|
++ on-watch
|
||||||
|
|= =path
|
||||||
|
^- (quip card _this)
|
||||||
|
=^ cards state
|
||||||
|
?+ path (on-watch:def path)
|
||||||
|
::
|
||||||
|
[%updates ~]
|
||||||
|
:_ state
|
||||||
|
%+ give:ha ~
|
||||||
|
:* %initial
|
||||||
|
watching
|
||||||
|
mentions
|
||||||
|
watch-on-self
|
||||||
|
==
|
||||||
|
==
|
||||||
|
[cards this]
|
||||||
|
::
|
||||||
|
++ on-poke
|
||||||
|
~/ %hark-graph-hook-poke
|
||||||
|
|= [=mark =vase]
|
||||||
|
^- (quip card _this)
|
||||||
|
|^
|
||||||
|
?> (team:title our.bowl src.bowl)
|
||||||
|
=^ cards state
|
||||||
|
?+ mark (on-poke:def mark vase)
|
||||||
|
%hark-graph-hook-action
|
||||||
|
(hark-graph-hook-action !<(action:hook vase))
|
||||||
|
==
|
||||||
|
[cards this]
|
||||||
|
::
|
||||||
|
++ hark-graph-hook-action
|
||||||
|
|= =action:hook
|
||||||
|
^- (quip card _state)
|
||||||
|
|^
|
||||||
|
:- (give:ha ~[/updates] action)
|
||||||
|
?- -.action
|
||||||
|
%listen (listen +.action)
|
||||||
|
%ignore (ignore +.action)
|
||||||
|
%set-mentions (set-mentions +.action)
|
||||||
|
%set-watch-on-self (set-watch-on-self +.action)
|
||||||
|
==
|
||||||
|
++ listen
|
||||||
|
|= graph=resource
|
||||||
|
^+ state
|
||||||
|
state(watching (~(put in watching) graph))
|
||||||
|
::
|
||||||
|
++ ignore
|
||||||
|
|= graph=resource
|
||||||
|
^+ state
|
||||||
|
state(watching (~(del in watching) graph))
|
||||||
|
::
|
||||||
|
++ set-mentions
|
||||||
|
|= ment=?
|
||||||
|
^+ state
|
||||||
|
state(mentions ment)
|
||||||
|
::
|
||||||
|
++ set-watch-on-self
|
||||||
|
|= self=?
|
||||||
|
^+ state
|
||||||
|
state(watch-on-self self)
|
||||||
|
--
|
||||||
|
--
|
||||||
|
::
|
||||||
|
++ on-agent
|
||||||
|
~/ %hark-graph-hook-agent
|
||||||
|
|= [=wire =sign:agent:gall]
|
||||||
|
^- (quip card _this)
|
||||||
|
|^
|
||||||
|
?+ -.sign (on-agent:def wire sign)
|
||||||
|
%kick
|
||||||
|
:_ this
|
||||||
|
?. ?=([%graph ~] wire)
|
||||||
|
~
|
||||||
|
~[watch-graph:ha]
|
||||||
|
::
|
||||||
|
%fact
|
||||||
|
?. ?=(%graph-update p.cage.sign)
|
||||||
|
(on-agent:def wire sign)
|
||||||
|
=^ cards state
|
||||||
|
(graph-update !<(update:graph-store q.cage.sign))
|
||||||
|
[cards this]
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ graph-update
|
||||||
|
|= =update:graph-store
|
||||||
|
^- (quip card _state)
|
||||||
|
?. ?=(%add-nodes -.q.update)
|
||||||
|
[~ state]
|
||||||
|
=/ group=resource
|
||||||
|
(need (group-from-app-resource:met %graph resource.q.update))
|
||||||
|
=/ =metadata:metadata-store
|
||||||
|
(need (peek-metadata:met %graph group resource.q.update))
|
||||||
|
=* rid resource.q.update
|
||||||
|
=+ %+ scry:ha
|
||||||
|
,mark=(unit mark)
|
||||||
|
/gx/graph-store/graph-mark/(scot %p entity.rid)/[name.rid]/noun
|
||||||
|
=+ %+ scry:ha
|
||||||
|
,=tube:clay
|
||||||
|
/cc/[q.byk.bowl]/[(fall mark %graph-validator-link)]/notification-kind
|
||||||
|
=/ nodes=(list [p=index:graph-store q=node:graph-store])
|
||||||
|
~(tap by nodes.q.update)
|
||||||
|
=| cards=(list card)
|
||||||
|
|^
|
||||||
|
?~ nodes
|
||||||
|
[cards state]
|
||||||
|
=* index p.i.nodes
|
||||||
|
=* node q.i.nodes
|
||||||
|
=^ node-cards state
|
||||||
|
(check-node node tube)
|
||||||
|
%_ $
|
||||||
|
nodes t.nodes
|
||||||
|
cards (weld node-cards cards)
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ check-node-children
|
||||||
|
|= [=node:graph-store =tube:clay]
|
||||||
|
^- (quip card _state)
|
||||||
|
?: ?=(%empty -.children.node)
|
||||||
|
[~ state]
|
||||||
|
=/ children=(list [=atom =node:graph-store])
|
||||||
|
(tap:orm:graph-store p.children.node)
|
||||||
|
=| cards=(list card)
|
||||||
|
|- ^- (quip card _state)
|
||||||
|
?~ children
|
||||||
|
[cards state]
|
||||||
|
=^ new-cards state
|
||||||
|
(check-node node.i.children tube)
|
||||||
|
%_ $
|
||||||
|
cards (weld cards new-cards)
|
||||||
|
children t.children
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ check-node
|
||||||
|
|= [=node:graph-store =tube:clay]
|
||||||
|
^- (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 @t) (tube !>([0 post.node])))
|
||||||
|
?~ notif-kind
|
||||||
|
[child-cards state]
|
||||||
|
=/ desc=@t
|
||||||
|
?: (is-mention contents.post.node)
|
||||||
|
%mention
|
||||||
|
u.notif-kind
|
||||||
|
?. ?| =(desc %mention)
|
||||||
|
(~(has in watching) rid)
|
||||||
|
==
|
||||||
|
[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])
|
||||||
|
::
|
||||||
|
++ is-mention
|
||||||
|
|= contents=(list content:post)
|
||||||
|
^- ?
|
||||||
|
?. mentions %.n
|
||||||
|
?~ contents %.n
|
||||||
|
?. ?=(%mention -.i.contents)
|
||||||
|
$(contents t.contents)
|
||||||
|
?: =(our.bowl ship.i.contents)
|
||||||
|
%.y
|
||||||
|
$(contents t.contents)
|
||||||
|
::
|
||||||
|
++ self-post
|
||||||
|
|= =node:graph-store
|
||||||
|
^- (quip card _state)
|
||||||
|
?. ?=(%.y watch-on-self)
|
||||||
|
[~ state]
|
||||||
|
`state(watching (~(put in watching) rid))
|
||||||
|
::
|
||||||
|
++ add-unread
|
||||||
|
|= [=index:store =notification:store]
|
||||||
|
^- card
|
||||||
|
=- [%pass / %agent [our.bowl %hark-store] %poke -]
|
||||||
|
hark-action+!>([%add index notification])
|
||||||
|
::
|
||||||
|
--
|
||||||
|
--
|
||||||
|
::
|
||||||
|
++ on-peek on-peek:def
|
||||||
|
::
|
||||||
|
++ on-leave on-leave:def
|
||||||
|
++ on-arvo on-arvo:def
|
||||||
|
++ on-fail on-fail:def
|
||||||
|
--
|
||||||
|
|
169
pkg/arvo/app/hark-group-hook.hoon
Normal file
169
pkg/arvo/app/hark-group-hook.hoon
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
:: hark-group-hook: notifications for groups [landscape]
|
||||||
|
::
|
||||||
|
/- store=hark-store, post, group-store, metadata-store, hook=hark-group-hook
|
||||||
|
/+ resource, metadata, default-agent, dbug, graph-store
|
||||||
|
::
|
||||||
|
~% %hark-group-hook-top ..is ~
|
||||||
|
|%
|
||||||
|
+$ card card:agent:gall
|
||||||
|
+$ versioned-state
|
||||||
|
$% state-0
|
||||||
|
==
|
||||||
|
::
|
||||||
|
+$ state-0
|
||||||
|
$: %0
|
||||||
|
watching=(set resource)
|
||||||
|
==
|
||||||
|
::
|
||||||
|
--
|
||||||
|
::
|
||||||
|
=| state-0
|
||||||
|
=* state -
|
||||||
|
::
|
||||||
|
=<
|
||||||
|
%- agent:dbug
|
||||||
|
^- agent:gall
|
||||||
|
~% %hark-group-hook-agent ..card ~
|
||||||
|
|_ =bowl:gall
|
||||||
|
+* this .
|
||||||
|
ha ~(. +> bowl)
|
||||||
|
def ~(. (default-agent this %|) bowl)
|
||||||
|
met ~(. metadata bowl)
|
||||||
|
::
|
||||||
|
++ on-init
|
||||||
|
:_ this
|
||||||
|
:~ watch-metadata:ha
|
||||||
|
watch-groups:ha
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ on-save !>(state)
|
||||||
|
++ on-load
|
||||||
|
|= old=vase
|
||||||
|
^- (quip card _this)
|
||||||
|
`this(state !<(state-0 old))
|
||||||
|
::
|
||||||
|
++ on-watch
|
||||||
|
|= =path
|
||||||
|
?. ?=([%updates ~] path)
|
||||||
|
(on-watch:def path)
|
||||||
|
:_ this
|
||||||
|
=; =cage
|
||||||
|
[%give %fact ~[/updates] cage]~
|
||||||
|
:- %hark-group-hook-update
|
||||||
|
!> ^- update:hook
|
||||||
|
[%initial watching]
|
||||||
|
::
|
||||||
|
++ on-poke
|
||||||
|
~/ %hark-group-hook-poke
|
||||||
|
|= [=mark =vase]
|
||||||
|
^- (quip card _this)
|
||||||
|
|^
|
||||||
|
?> (team:title our.bowl src.bowl)
|
||||||
|
=^ cards state
|
||||||
|
?+ mark (on-poke:def mark vase)
|
||||||
|
%hark-group-hook-action
|
||||||
|
(hark-group-hook-action !<(action:hook vase))
|
||||||
|
==
|
||||||
|
[cards this]
|
||||||
|
::
|
||||||
|
++ hark-group-hook-action
|
||||||
|
|= =action:hook
|
||||||
|
^- (quip card _state)
|
||||||
|
|^
|
||||||
|
?- -.action
|
||||||
|
%listen (listen +.action)
|
||||||
|
%ignore (ignore +.action)
|
||||||
|
==
|
||||||
|
++ listen
|
||||||
|
|= group=resource
|
||||||
|
^- (quip card _state)
|
||||||
|
:- (give %listen group)
|
||||||
|
state(watching (~(put in watching) group))
|
||||||
|
::
|
||||||
|
++ ignore
|
||||||
|
|= group=resource
|
||||||
|
^- (quip card _state)
|
||||||
|
:- (give %ignore group)
|
||||||
|
state(watching (~(del in watching) group))
|
||||||
|
::
|
||||||
|
++ give
|
||||||
|
|= =update:hook
|
||||||
|
^- (list card)
|
||||||
|
[%give %fact ~[/updates] %hark-group-hook-update !>(update)]~
|
||||||
|
--
|
||||||
|
--
|
||||||
|
::
|
||||||
|
++ on-agent
|
||||||
|
~/ %hark-group-hook-agent
|
||||||
|
|= [=wire =sign:agent:gall]
|
||||||
|
^- (quip card _this)
|
||||||
|
|^
|
||||||
|
?+ -.sign (on-agent:def wire sign)
|
||||||
|
%kick
|
||||||
|
:_ this
|
||||||
|
?+ wire ~
|
||||||
|
[%group ~] ~[watch-groups:ha]
|
||||||
|
[%metadata ~] ~[watch-metadata:ha]
|
||||||
|
==
|
||||||
|
::
|
||||||
|
%fact
|
||||||
|
?+ p.cage.sign (on-agent:def wire sign)
|
||||||
|
%group-update
|
||||||
|
=^ cards state
|
||||||
|
(group-update !<(update:group-store q.cage.sign))
|
||||||
|
[cards this]
|
||||||
|
::
|
||||||
|
%metadata-update
|
||||||
|
=^ cards state
|
||||||
|
(metadata-update !<(metadata-update:metadata-store q.cage.sign))
|
||||||
|
[cards this]
|
||||||
|
==
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ group-update
|
||||||
|
|= =update:group-store
|
||||||
|
^- (quip card _state)
|
||||||
|
?. ?=(?(%add-members %remove-members) -.update)
|
||||||
|
[~ state]
|
||||||
|
?. (~(has in watching) resource.update)
|
||||||
|
[~ state]
|
||||||
|
=/ =contents:store
|
||||||
|
[%group ~[update]]
|
||||||
|
=/ =notification:store [now.bowl %.n contents]
|
||||||
|
=/ =index:store
|
||||||
|
[%group resource.update -.update]
|
||||||
|
:_ state
|
||||||
|
~[(add-unread index notification)]
|
||||||
|
:: +metadata-update is stubbed for now, for the following reasons
|
||||||
|
:: - There's no semantic difference in metadata-store between
|
||||||
|
:: adding and editing a channel
|
||||||
|
:: - We have no way of retrieving old metadata to e.g. get a
|
||||||
|
:: channel's old name when it is renamed
|
||||||
|
++ metadata-update
|
||||||
|
|= update=metadata-update:metadata-store
|
||||||
|
^- (quip card _state)
|
||||||
|
[~ state]
|
||||||
|
::
|
||||||
|
++ add-unread
|
||||||
|
|= [=index:store =notification:store]
|
||||||
|
^- card
|
||||||
|
=- [%pass / %agent [our.bowl %hark-store] %poke -]
|
||||||
|
hark-action+!>([%add index notification])
|
||||||
|
--
|
||||||
|
::
|
||||||
|
++ on-peek on-peek:def
|
||||||
|
++ on-leave on-leave:def
|
||||||
|
++ on-arvo on-arvo:def
|
||||||
|
++ on-fail on-fail:def
|
||||||
|
--
|
||||||
|
|_ =bowl:gall
|
||||||
|
+* met ~(. metadata bowl)
|
||||||
|
::
|
||||||
|
++ watch-groups
|
||||||
|
^- card
|
||||||
|
[%pass /group %agent [our.bowl %group-store] %watch /groups]
|
||||||
|
::
|
||||||
|
++ watch-metadata
|
||||||
|
^- card
|
||||||
|
[%pass /metadata %agent [our.bowl %metadata-store] %watch /updates]
|
||||||
|
--
|
313
pkg/arvo/app/hark-store.hoon
Normal file
313
pkg/arvo/app/hark-store.hoon
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
:: hark-store: notifications [landscape]
|
||||||
|
::
|
||||||
|
/- 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-0
|
||||||
|
$: %0
|
||||||
|
=notifications:store
|
||||||
|
archive=notifications:store
|
||||||
|
last-seen=@da
|
||||||
|
dnd=?
|
||||||
|
==
|
||||||
|
+$ inflated-state
|
||||||
|
$: state-0
|
||||||
|
cache
|
||||||
|
==
|
||||||
|
:: $cache: useful to have precalculated, but can be derived from state
|
||||||
|
:: albeit expensively
|
||||||
|
+$ cache
|
||||||
|
$: unread-count=@ud
|
||||||
|
~
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ orm ((ordered-map @da timebox:store) lth)
|
||||||
|
--
|
||||||
|
::
|
||||||
|
=| inflated-state
|
||||||
|
=* state -
|
||||||
|
::
|
||||||
|
=<
|
||||||
|
%- agent:dbug
|
||||||
|
^- agent:gall
|
||||||
|
~% %hark-store-agent ..card ~
|
||||||
|
|_ =bowl:gall
|
||||||
|
+* this .
|
||||||
|
ha ~(. +> bowl)
|
||||||
|
def ~(. (default-agent this %|) bowl)
|
||||||
|
met ~(. metadata bowl)
|
||||||
|
::
|
||||||
|
++ on-init
|
||||||
|
:_ this
|
||||||
|
~[autoseen-timer]
|
||||||
|
::
|
||||||
|
++ on-save !>(-.state)
|
||||||
|
++ on-load
|
||||||
|
|= =old=vase
|
||||||
|
^- (quip card _this)
|
||||||
|
=/ old
|
||||||
|
!<(state-0 old-vase)
|
||||||
|
`this(-.state old, +.state (inflate-cache old))
|
||||||
|
::
|
||||||
|
++ on-watch
|
||||||
|
|= =path
|
||||||
|
^- (quip card _this)
|
||||||
|
|^
|
||||||
|
?+ path (on-watch:def path)
|
||||||
|
::
|
||||||
|
[%updates ~]
|
||||||
|
:_ this
|
||||||
|
[%give %fact ~ hark-update+!>(initial-updates)]~
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ initial-updates
|
||||||
|
^- update:store
|
||||||
|
:- %more
|
||||||
|
^- (list update:store)
|
||||||
|
:+ [%set-dnd dnd]
|
||||||
|
[%count unread-count]
|
||||||
|
%+ weld
|
||||||
|
%+ turn
|
||||||
|
(tap-nonempty archive)
|
||||||
|
(timebox-update &)
|
||||||
|
%+ turn
|
||||||
|
(tap-nonempty notifications)
|
||||||
|
(timebox-update |)
|
||||||
|
::
|
||||||
|
++ timebox-update
|
||||||
|
|= archived=?
|
||||||
|
|= [time=@da =timebox:store]
|
||||||
|
^- update:store
|
||||||
|
[%timebox time archived ~(tap by timebox)]
|
||||||
|
::
|
||||||
|
++ tap-nonempty
|
||||||
|
|= =notifications:store
|
||||||
|
^- (list [@da timebox:store])
|
||||||
|
%+ skip (tap:orm notifications)
|
||||||
|
|=([@da =timebox:store] =(0 ~(wyt by timebox)))
|
||||||
|
--
|
||||||
|
::
|
||||||
|
++ on-peek
|
||||||
|
|= =path
|
||||||
|
^- (unit (unit cage))
|
||||||
|
?+ path (on-peek:def path)
|
||||||
|
::
|
||||||
|
[%x %recent @ @ ~]
|
||||||
|
=/ offset=@ud
|
||||||
|
(slav %ud i.t.t.path)
|
||||||
|
=/ length=@ud
|
||||||
|
(slav %ud i.t.t.t.path)
|
||||||
|
:^ ~ ~ %noun
|
||||||
|
!> ^- update:store
|
||||||
|
:- %more
|
||||||
|
%+ turn
|
||||||
|
(scag length (slag offset (tap:orm notifications)))
|
||||||
|
|= [time=@da =timebox:store]
|
||||||
|
^- update:store
|
||||||
|
:^ %timebox time %.n
|
||||||
|
~(tap by timebox)
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ on-poke
|
||||||
|
~/ %hark-store-poke
|
||||||
|
|= [=mark =vase]
|
||||||
|
^- (quip card _this)
|
||||||
|
|^
|
||||||
|
?> (team:title our.bowl src.bowl)
|
||||||
|
=^ cards state
|
||||||
|
?+ mark (on-poke:def mark vase)
|
||||||
|
%hark-action (hark-action !<(action:store vase))
|
||||||
|
==
|
||||||
|
[cards this]
|
||||||
|
::
|
||||||
|
++ hark-action
|
||||||
|
|= =action:store
|
||||||
|
^- (quip card _state)
|
||||||
|
|^
|
||||||
|
?- -.action
|
||||||
|
%add (add +.action)
|
||||||
|
%archive (do-archive +.action)
|
||||||
|
%seen seen
|
||||||
|
%read (read +.action)
|
||||||
|
%unread (unread +.action)
|
||||||
|
%set-dnd (set-dnd +.action)
|
||||||
|
==
|
||||||
|
++ add
|
||||||
|
|= [=index:store =notification:store]
|
||||||
|
^- (quip card _state)
|
||||||
|
=/ =timebox:store
|
||||||
|
(gut-orm:ha notifications last-seen)
|
||||||
|
=/ existing-notif
|
||||||
|
(~(get by timebox) index)
|
||||||
|
=/ new=notification:store
|
||||||
|
?~ existing-notif
|
||||||
|
notification
|
||||||
|
(merge-notification:ha u.existing-notif notification)
|
||||||
|
=/ new-timebox=timebox:store
|
||||||
|
(~(put by timebox) index new)
|
||||||
|
:- (give:ha [/updates]~ %added last-seen index new)
|
||||||
|
%_ state
|
||||||
|
notifications (put:orm notifications last-seen new-timebox)
|
||||||
|
unread-count ?~(existing-notif +(unread-count) unread-count)
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ do-archive
|
||||||
|
|= [time=@da =index:store]
|
||||||
|
^- (quip card _state)
|
||||||
|
=/ =timebox:store
|
||||||
|
(gut-orm:ha notifications time)
|
||||||
|
=/ =notification:store
|
||||||
|
(~(got by timebox) index)
|
||||||
|
=/ new-timebox=timebox:store
|
||||||
|
(~(del by timebox) index)
|
||||||
|
:- (give:ha [/updates]~ %archive time index)
|
||||||
|
%_ state
|
||||||
|
unread-count ?.(read.notification (dec unread-count) unread-count)
|
||||||
|
::
|
||||||
|
notifications
|
||||||
|
(put:orm notifications time new-timebox)
|
||||||
|
::
|
||||||
|
archive
|
||||||
|
%^ jub-orm:ha archive time
|
||||||
|
|= archive-box=timebox:store
|
||||||
|
^- timebox:store
|
||||||
|
(~(put by archive-box) index notification(read %.y))
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ read
|
||||||
|
|= [time=@da =index:store]
|
||||||
|
^- (quip card _state)
|
||||||
|
:- (give:ha [/updates]~ %read time index)
|
||||||
|
%_ state
|
||||||
|
unread-count (dec unread-count)
|
||||||
|
notifications (change-read-status:ha time index %.y)
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ unread
|
||||||
|
|= [time=@da =index:store]
|
||||||
|
^- (quip card _state)
|
||||||
|
:- (give:ha [/updates]~ %unread time index)
|
||||||
|
%_ state
|
||||||
|
unread-count +(unread-count)
|
||||||
|
notifications (change-read-status:ha time index %.n)
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ seen
|
||||||
|
^- (quip card _state)
|
||||||
|
:_ state(last-seen now.bowl)
|
||||||
|
:~ cancel-autoseen:ha
|
||||||
|
autoseen-timer:ha
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ set-dnd
|
||||||
|
|= d=?
|
||||||
|
^- (quip card _state)
|
||||||
|
:_ state(dnd d)
|
||||||
|
(give:ha [/updates]~ %set-dnd d)
|
||||||
|
--
|
||||||
|
--
|
||||||
|
::
|
||||||
|
++ on-agent on-agent:def
|
||||||
|
::
|
||||||
|
++ on-leave on-leave:def
|
||||||
|
++ on-arvo
|
||||||
|
|= [=wire =sign-arvo]
|
||||||
|
^- (quip card _this)
|
||||||
|
?. ?=([%autoseen ~] wire)
|
||||||
|
(on-arvo:def wire sign-arvo)
|
||||||
|
?> ?=([%b %wake *] sign-arvo)
|
||||||
|
:_ this(last-seen now.bowl)
|
||||||
|
~[autoseen-timer:ha]
|
||||||
|
::
|
||||||
|
++ on-fail on-fail:def
|
||||||
|
--
|
||||||
|
|_ =bowl:gall
|
||||||
|
+* met ~(. metadata bowl)
|
||||||
|
::
|
||||||
|
++ merge-notification
|
||||||
|
|= [existing=notification:store new=notification:store]
|
||||||
|
^- notification:store
|
||||||
|
?- -.contents.existing
|
||||||
|
::
|
||||||
|
%chat
|
||||||
|
?> ?=(%chat -.contents.new)
|
||||||
|
existing(list.contents (weld list.contents.existing list.contents.new))
|
||||||
|
::
|
||||||
|
%graph
|
||||||
|
?> ?=(%graph -.contents.new)
|
||||||
|
existing(list.contents (weld list.contents.existing list.contents.new))
|
||||||
|
::
|
||||||
|
%group
|
||||||
|
?> ?=(%group -.contents.new)
|
||||||
|
existing(list.contents (weld list.contents.existing list.contents.new))
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ change-read-status
|
||||||
|
|= [time=@da =index:store read=?]
|
||||||
|
^+ notifications
|
||||||
|
%^ jub-orm notifications time
|
||||||
|
|= =timebox:store
|
||||||
|
%+ ~(jab by timebox) index
|
||||||
|
|= =notification:store
|
||||||
|
?> !=(read read.notification)
|
||||||
|
notification(read read)
|
||||||
|
:: +key-orm: +key:by for ordered maps
|
||||||
|
++ key-orm
|
||||||
|
|= =notifications:store
|
||||||
|
^- (list @da)
|
||||||
|
(turn (tap:orm notifications) |=([key=@da =timebox:store] key))
|
||||||
|
:: +jub-orm: combo +jab/+gut for ordered maps
|
||||||
|
:: TODO: move to zuse.hoon
|
||||||
|
++ jub-orm
|
||||||
|
|= [=notifications:store time=@da fun=$-(timebox:store timebox:store)]
|
||||||
|
^- notifications:store
|
||||||
|
=/ =timebox:store
|
||||||
|
(fun (gut-orm notifications time))
|
||||||
|
(put:orm notifications time timebox)
|
||||||
|
:: +gut-orm: +gut:by for ordered maps
|
||||||
|
:: TODO: move to zuse.hoon
|
||||||
|
++ gut-orm
|
||||||
|
|= [=notifications:store time=@da]
|
||||||
|
^- timebox:store
|
||||||
|
(fall (get:orm notifications time) ~)
|
||||||
|
::
|
||||||
|
++ autoseen-interval ~h3
|
||||||
|
++ cancel-autoseen
|
||||||
|
^- card
|
||||||
|
[%pass /autoseen %arvo %b %rest (add last-seen autoseen-interval)]
|
||||||
|
::
|
||||||
|
++ autoseen-timer
|
||||||
|
^- card
|
||||||
|
[%pass /autoseen %arvo %b %wait (add now.bowl autoseen-interval)]
|
||||||
|
::
|
||||||
|
++ give
|
||||||
|
|= [paths=(list path) update=update:store]
|
||||||
|
^- (list card)
|
||||||
|
[%give %fact paths [%hark-update !>(update)]]~
|
||||||
|
::
|
||||||
|
++ inflate-cache
|
||||||
|
|= state-0
|
||||||
|
^- cache
|
||||||
|
:_ ~
|
||||||
|
%+ roll
|
||||||
|
(tap:orm notifications)
|
||||||
|
|= [[time=@da =timebox:store] out=@ud]
|
||||||
|
=/ unreads ~(tap by timebox)
|
||||||
|
|-
|
||||||
|
?~ unreads out
|
||||||
|
=* notification q.i.unreads
|
||||||
|
?: read.notification
|
||||||
|
out
|
||||||
|
%_ $
|
||||||
|
unreads t.unreads
|
||||||
|
::
|
||||||
|
out +(out)
|
||||||
|
==
|
||||||
|
--
|
@ -24,6 +24,6 @@
|
|||||||
<div id="portal-root"></div>
|
<div id="portal-root"></div>
|
||||||
<script src="/~landscape/js/channel.js"></script>
|
<script src="/~landscape/js/channel.js"></script>
|
||||||
<script src="/~landscape/js/session.js"></script>
|
<script src="/~landscape/js/session.js"></script>
|
||||||
<script src="/~landscape/js/bundle/index.20fe6ee95061ade450fc.js"></script>
|
<script src="/~landscape/js/bundle/index.5f9890df6be59a4ff9c5.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -34,6 +34,78 @@
|
|||||||
++ enjs
|
++ enjs
|
||||||
=, enjs:format
|
=, enjs:format
|
||||||
|%
|
|%
|
||||||
|
::
|
||||||
|
++ signatures
|
||||||
|
|= s=^signatures
|
||||||
|
^- json
|
||||||
|
[%a (turn ~(tap in s) signature)]
|
||||||
|
::
|
||||||
|
++ signature
|
||||||
|
|= s=^signature
|
||||||
|
^- json
|
||||||
|
%- pairs
|
||||||
|
:~ [%signature s+(scot %ux p.s)]
|
||||||
|
[%ship (ship q.s)]
|
||||||
|
[%life (numb r.s)]
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ index
|
||||||
|
|= i=^index
|
||||||
|
^- json
|
||||||
|
=/ j=^tape ""
|
||||||
|
|-
|
||||||
|
?~ i [%s (crip j)]
|
||||||
|
=/ k=json (numb i.i)
|
||||||
|
?> ?=(%n -.k)
|
||||||
|
%_ $
|
||||||
|
i t.i
|
||||||
|
j (weld j (weld "/" (trip +.k)))
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ uid
|
||||||
|
|= u=^uid
|
||||||
|
^- json
|
||||||
|
%- pairs
|
||||||
|
:~ [%resource (enjs:res resource.u)]
|
||||||
|
[%index (index index.u)]
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ content
|
||||||
|
|= c=^content
|
||||||
|
^- json
|
||||||
|
?- -.c
|
||||||
|
%mention (frond %mention (ship ship.c))
|
||||||
|
%text (frond %text s+text.c)
|
||||||
|
%url (frond %url s+url.c)
|
||||||
|
%reference (frond %reference (uid uid.c))
|
||||||
|
%code
|
||||||
|
%+ frond %code
|
||||||
|
%- pairs
|
||||||
|
:- [%expression s+expression.c]
|
||||||
|
:_ ~
|
||||||
|
:- %output
|
||||||
|
:: virtualize output rendering, +tank:enjs:format might crash
|
||||||
|
::
|
||||||
|
=/ result=(each (list json) tang)
|
||||||
|
(mule |.((turn output.c tank)))
|
||||||
|
?- -.result
|
||||||
|
%& a+p.result
|
||||||
|
%| a+[a+[%s '[[output rendering error]]']~]~
|
||||||
|
==
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ post
|
||||||
|
|= p=^post
|
||||||
|
^- json
|
||||||
|
%- pairs
|
||||||
|
:~ [%author (ship author.p)]
|
||||||
|
[%index (index index.p)]
|
||||||
|
[%time-sent (time time-sent.p)]
|
||||||
|
[%contents [%a (turn contents.p content)]]
|
||||||
|
[%hash ?~(hash.p ~ s+(scot %ux u.hash.p))]
|
||||||
|
[%signatures (signatures signatures.p)]
|
||||||
|
==
|
||||||
|
::
|
||||||
++ update
|
++ update
|
||||||
|= upd=^update
|
|= upd=^update
|
||||||
^- json
|
^- json
|
||||||
@ -132,20 +204,6 @@
|
|||||||
:~ (index [a]~)
|
:~ (index [a]~)
|
||||||
(node n)
|
(node n)
|
||||||
==
|
==
|
||||||
::
|
|
||||||
++ index
|
|
||||||
|= i=^index
|
|
||||||
^- json
|
|
||||||
=/ j=^tape ""
|
|
||||||
|-
|
|
||||||
?~ i [%s (crip j)]
|
|
||||||
=/ k=json (numb i.i)
|
|
||||||
?> ?=(%n -.k)
|
|
||||||
%_ $
|
|
||||||
i t.i
|
|
||||||
j (weld j (weld "/" (trip +.k)))
|
|
||||||
==
|
|
||||||
::
|
|
||||||
++ node
|
++ node
|
||||||
|= n=^node
|
|= n=^node
|
||||||
^- json
|
^- json
|
||||||
@ -158,41 +216,7 @@
|
|||||||
==
|
==
|
||||||
==
|
==
|
||||||
::
|
::
|
||||||
++ post
|
::
|
||||||
|= p=^post
|
|
||||||
^- json
|
|
||||||
%- pairs
|
|
||||||
:~ [%author (ship author.p)]
|
|
||||||
[%index (index index.p)]
|
|
||||||
[%time-sent (time time-sent.p)]
|
|
||||||
[%contents [%a (turn contents.p content)]]
|
|
||||||
[%hash ?~(hash.p ~ s+(scot %ux u.hash.p))]
|
|
||||||
[%signatures (signatures signatures.p)]
|
|
||||||
==
|
|
||||||
::
|
|
||||||
++ content
|
|
||||||
|= c=^content
|
|
||||||
^- json
|
|
||||||
?- -.c
|
|
||||||
%text (frond %text s+text.c)
|
|
||||||
%url (frond %url s+url.c)
|
|
||||||
%reference (frond %reference (uid uid.c))
|
|
||||||
%code
|
|
||||||
%+ frond %code
|
|
||||||
%- pairs
|
|
||||||
:- [%expression s+expression.c]
|
|
||||||
:_ ~
|
|
||||||
:- %output
|
|
||||||
:: virtualize output rendering, +tank:enjs:format might crash
|
|
||||||
::
|
|
||||||
=/ result=(each (list json) tang)
|
|
||||||
(mule |.((turn output.c tank)))
|
|
||||||
?- -.result
|
|
||||||
%& a+p.result
|
|
||||||
%| a+[a+[%s '[[output rendering error]]']~]~
|
|
||||||
==
|
|
||||||
==
|
|
||||||
::
|
|
||||||
++ nodes
|
++ nodes
|
||||||
|= m=(map ^index ^node)
|
|= m=(map ^index ^node)
|
||||||
^- json
|
^- json
|
||||||
@ -210,27 +234,6 @@
|
|||||||
^- json
|
^- json
|
||||||
[%a (turn ~(tap in i) index)]
|
[%a (turn ~(tap in i) index)]
|
||||||
::
|
::
|
||||||
++ uid
|
|
||||||
|= u=^uid
|
|
||||||
^- json
|
|
||||||
%- pairs
|
|
||||||
:~ [%resource (enjs:res resource.u)]
|
|
||||||
[%index (index index.u)]
|
|
||||||
==
|
|
||||||
::
|
|
||||||
++ signatures
|
|
||||||
|= s=^signatures
|
|
||||||
^- json
|
|
||||||
[%a (turn ~(tap in s) signature)]
|
|
||||||
::
|
|
||||||
++ signature
|
|
||||||
|= s=^signature
|
|
||||||
^- json
|
|
||||||
%- pairs
|
|
||||||
:~ [%signature s+(scot %ux p.s)]
|
|
||||||
[%ship (ship q.s)]
|
|
||||||
[%life (numb r.s)]
|
|
||||||
==
|
|
||||||
--
|
--
|
||||||
--
|
--
|
||||||
::
|
::
|
||||||
@ -322,7 +325,8 @@
|
|||||||
::
|
::
|
||||||
++ content
|
++ content
|
||||||
%- of
|
%- of
|
||||||
:~ [%text so]
|
:~ [%mention (su ;~(pfix sig fed:ag))]
|
||||||
|
[%text so]
|
||||||
[%url so]
|
[%url so]
|
||||||
[%reference uid]
|
[%reference uid]
|
||||||
[%code eval]
|
[%code eval]
|
||||||
|
30
pkg/arvo/lib/hark/chat-hook.hoon
Normal file
30
pkg/arvo/lib/hark/chat-hook.hoon
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/- sur=hark-chat-hook
|
||||||
|
^?
|
||||||
|
=< [. sur]
|
||||||
|
=, sur
|
||||||
|
|%
|
||||||
|
++ dejs
|
||||||
|
=, dejs:format
|
||||||
|
|%
|
||||||
|
++ action
|
||||||
|
%- of
|
||||||
|
:~ listen+pa
|
||||||
|
ignore+pa
|
||||||
|
set-mentions+bo
|
||||||
|
==
|
||||||
|
--
|
||||||
|
::
|
||||||
|
++ enjs
|
||||||
|
=, enjs:format
|
||||||
|
|%
|
||||||
|
++ update
|
||||||
|
|= upd=^update
|
||||||
|
%+ frond -.upd
|
||||||
|
?- -.upd
|
||||||
|
?(%listen %ignore) (path chat.upd)
|
||||||
|
%set-mentions b+mentions.upd
|
||||||
|
%initial a+(turn ~(tap in watching.upd) path)
|
||||||
|
==
|
||||||
|
--
|
||||||
|
--
|
||||||
|
|
61
pkg/arvo/lib/hark/graph-hook.hoon
Normal file
61
pkg/arvo/lib/hark/graph-hook.hoon
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/- sur=hark-graph-hook
|
||||||
|
/+ graph-store, resource
|
||||||
|
^?
|
||||||
|
=< [. sur]
|
||||||
|
=, sur
|
||||||
|
|%
|
||||||
|
++ dejs
|
||||||
|
=, dejs:format
|
||||||
|
|%
|
||||||
|
++ graph-indices
|
||||||
|
%- ot
|
||||||
|
:~ graph+dejs-path:resource
|
||||||
|
indices+(as graph-index)
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ graph-index
|
||||||
|
^- $-(json index:graph-store)
|
||||||
|
(su ;~(pfix net (more net dem)))
|
||||||
|
::
|
||||||
|
++ action
|
||||||
|
%- of
|
||||||
|
:~ listen+dejs-path:resource
|
||||||
|
ignore+dejs-path:resource
|
||||||
|
set-mentions+bo
|
||||||
|
set-watch-on-self+bo
|
||||||
|
==
|
||||||
|
--
|
||||||
|
::
|
||||||
|
++ enjs
|
||||||
|
=, enjs:format
|
||||||
|
|%
|
||||||
|
++ graph-indices
|
||||||
|
|= [graph=resource indices=(set index:graph-store)]
|
||||||
|
%- pairs
|
||||||
|
:~ graph+s+(enjs-path:resource graph)
|
||||||
|
indices+a+(turn ~(tap in indices) index:enjs:graph-store)
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ action
|
||||||
|
|= act=^action
|
||||||
|
^- json
|
||||||
|
%+ frond -.act
|
||||||
|
?- -.act
|
||||||
|
%set-watch-on-self b+watch-on-self.act
|
||||||
|
%set-mentions b+mentions.act
|
||||||
|
?(%listen %ignore) s+(enjs-path:resource graph.act)
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ update
|
||||||
|
|= upd=^update
|
||||||
|
^- json
|
||||||
|
?. ?=(%initial -.upd)
|
||||||
|
(action upd)
|
||||||
|
%+ frond -.upd
|
||||||
|
%- pairs
|
||||||
|
:~ 'watchOnSelf'^b+watch-on-self.upd
|
||||||
|
'mentions'^b+mentions.upd
|
||||||
|
watching+a+(turn ~(tap in watching.upd) |=(r=resource s+(enjs-path:resource r)))
|
||||||
|
==
|
||||||
|
--
|
||||||
|
--
|
34
pkg/arvo/lib/hark/group-hook.hoon
Normal file
34
pkg/arvo/lib/hark/group-hook.hoon
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/- sur=hark-group-hook
|
||||||
|
/+ resource
|
||||||
|
^?
|
||||||
|
=< [. sur]
|
||||||
|
=, sur
|
||||||
|
|%
|
||||||
|
++ dejs
|
||||||
|
=, dejs:format
|
||||||
|
|%
|
||||||
|
++ action
|
||||||
|
%- of
|
||||||
|
:~ listen+dejs-path:resource
|
||||||
|
ignore+dejs-path:resource
|
||||||
|
==
|
||||||
|
--
|
||||||
|
::
|
||||||
|
++ enjs
|
||||||
|
=, enjs:format
|
||||||
|
|%
|
||||||
|
++ res
|
||||||
|
(cork enjs-path:resource (lead %s))
|
||||||
|
::
|
||||||
|
++ update
|
||||||
|
|= upd=^update
|
||||||
|
%+ frond -.upd
|
||||||
|
?- -.upd
|
||||||
|
?(%listen %ignore) (res group.upd)
|
||||||
|
::
|
||||||
|
%initial
|
||||||
|
:- %a
|
||||||
|
(turn ~(tap in watching.upd) res)
|
||||||
|
==
|
||||||
|
--
|
||||||
|
--
|
212
pkg/arvo/lib/hark/store.hoon
Normal file
212
pkg/arvo/lib/hark/store.hoon
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
/- sur=hark-store, post
|
||||||
|
/+ resource, graph-store, group-store, chat-store
|
||||||
|
^?
|
||||||
|
=< [. sur]
|
||||||
|
=, sur
|
||||||
|
|%
|
||||||
|
++ dejs
|
||||||
|
=, dejs:format
|
||||||
|
|%
|
||||||
|
++ index
|
||||||
|
%- of
|
||||||
|
:~ graph+graph-index
|
||||||
|
group+group-index
|
||||||
|
chat+chat-index
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ chat-index
|
||||||
|
%- ot
|
||||||
|
:~ chat+pa
|
||||||
|
mention+bo
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ group-index
|
||||||
|
%- ot
|
||||||
|
:~ group+dejs-path:resource
|
||||||
|
description+so
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ graph-index
|
||||||
|
%- ot
|
||||||
|
:~ group+dejs-path:resource
|
||||||
|
graph+dejs-path:resource
|
||||||
|
module+so
|
||||||
|
description+so
|
||||||
|
==
|
||||||
|
:: parse date as @ud
|
||||||
|
:: TODO: move to zuse
|
||||||
|
++ sd
|
||||||
|
|= jon=json
|
||||||
|
^- @da
|
||||||
|
?> ?=(%s -.jon)
|
||||||
|
`@da`(rash p.jon dem:ag)
|
||||||
|
|
||||||
|
::
|
||||||
|
++ notif-ref
|
||||||
|
^- $-(json [@da ^index])
|
||||||
|
%- ot
|
||||||
|
:~ time+sd
|
||||||
|
index+index
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ add
|
||||||
|
|= jon=json
|
||||||
|
[*^index *notification]
|
||||||
|
::
|
||||||
|
++ action
|
||||||
|
^- $-(json ^action)
|
||||||
|
%- of
|
||||||
|
:~ seen+ul
|
||||||
|
archive+notif-ref
|
||||||
|
unread+notif-ref
|
||||||
|
read+notif-ref
|
||||||
|
add+add
|
||||||
|
set-dnd+bo
|
||||||
|
==
|
||||||
|
--
|
||||||
|
::
|
||||||
|
++ enjs
|
||||||
|
=, enjs:format
|
||||||
|
|%
|
||||||
|
++ update
|
||||||
|
|= upd=^update
|
||||||
|
^- json
|
||||||
|
|^
|
||||||
|
%+ frond -.upd
|
||||||
|
?+ -.upd a+~
|
||||||
|
%added (added +.upd)
|
||||||
|
%timebox (timebox +.upd)
|
||||||
|
%set-dnd b+dnd.upd
|
||||||
|
%count (numb count.upd)
|
||||||
|
%more (more +.upd)
|
||||||
|
::
|
||||||
|
?(%archive %read %unread)
|
||||||
|
(notif-ref +.upd)
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ added
|
||||||
|
|= [tim=@da idx=^index not=^notification]
|
||||||
|
^- json
|
||||||
|
%- pairs
|
||||||
|
:~ time+s+(scot %ud tim)
|
||||||
|
index+(index idx)
|
||||||
|
notification+(notification not)
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ notif-ref
|
||||||
|
|= [tim=@da idx=^index]
|
||||||
|
^- json
|
||||||
|
%- pairs
|
||||||
|
:~ time+s+(scot %ud tim)
|
||||||
|
index+(index idx)
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ more
|
||||||
|
|= upds=(list ^update)
|
||||||
|
^- json
|
||||||
|
a+(turn upds update)
|
||||||
|
::
|
||||||
|
++ index
|
||||||
|
|= =^index
|
||||||
|
%+ frond -.index
|
||||||
|
|^
|
||||||
|
?- -.index
|
||||||
|
%graph (graph-index +.index)
|
||||||
|
%group (group-index +.index)
|
||||||
|
%chat (chat-index +.index)
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ chat-index
|
||||||
|
|= [chat=^path mention=?]
|
||||||
|
^- json
|
||||||
|
%- pairs
|
||||||
|
:~ chat+(path chat)
|
||||||
|
mention+b+mention
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ graph-index
|
||||||
|
|= [group=resource graph=resource module=@t description=@t]
|
||||||
|
^- json
|
||||||
|
%- pairs
|
||||||
|
:~ group+s+(enjs-path:resource group)
|
||||||
|
graph+s+(enjs-path:resource graph)
|
||||||
|
module+s+module
|
||||||
|
description+s+description
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ group-index
|
||||||
|
|= [group=resource description=@t]
|
||||||
|
^- json
|
||||||
|
%- pairs
|
||||||
|
:~ group+s+(enjs-path:resource group)
|
||||||
|
description+s+description
|
||||||
|
==
|
||||||
|
--
|
||||||
|
::
|
||||||
|
++ notification
|
||||||
|
|= ^notification
|
||||||
|
^- json
|
||||||
|
%- pairs
|
||||||
|
:~ time+(time date)
|
||||||
|
read+b+read
|
||||||
|
contents+(^contents contents)
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ contents
|
||||||
|
|= =^contents
|
||||||
|
^- json
|
||||||
|
%+ frond -.contents
|
||||||
|
|^
|
||||||
|
?- -.contents
|
||||||
|
%graph (graph-contents +.contents)
|
||||||
|
%group (group-contents +.contents)
|
||||||
|
%chat (chat-contents +.contents)
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ chat-contents
|
||||||
|
|= =(list envelope:chat-store)
|
||||||
|
^- json
|
||||||
|
:- %a
|
||||||
|
(turn list envelope:enjs:chat-store)
|
||||||
|
::
|
||||||
|
++ graph-contents
|
||||||
|
|= =(list post:post)
|
||||||
|
^- json
|
||||||
|
:- %a
|
||||||
|
(turn list post:enjs:graph-store)
|
||||||
|
::
|
||||||
|
++ group-contents
|
||||||
|
|= =(list ^group-contents)
|
||||||
|
^- json
|
||||||
|
:- %a
|
||||||
|
%+ murn list
|
||||||
|
|= =^group-contents
|
||||||
|
?. ?=(?(%add-members %remove-members) -.group-contents)
|
||||||
|
~
|
||||||
|
`(update:enjs:group-store group-contents)
|
||||||
|
--
|
||||||
|
::
|
||||||
|
++ indexed-notification
|
||||||
|
|= [=^index =^notification]
|
||||||
|
%- pairs
|
||||||
|
:~ index+(^index index)
|
||||||
|
notification+(^notification notification)
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ timebox
|
||||||
|
|= [tim=@da arch=? l=(list [^index ^notification])]
|
||||||
|
^- json
|
||||||
|
%- pairs
|
||||||
|
:~ time+s+(scot %ud tim)
|
||||||
|
archive+b+arch
|
||||||
|
:- %notifications
|
||||||
|
^- json
|
||||||
|
:- %a
|
||||||
|
%+ turn l
|
||||||
|
|= [=^index =^notification]
|
||||||
|
^- json
|
||||||
|
(indexed-notification index notification)
|
||||||
|
==
|
||||||
|
--
|
||||||
|
--
|
||||||
|
--
|
@ -107,6 +107,10 @@
|
|||||||
%graph-store
|
%graph-store
|
||||||
%graph-pull-hook
|
%graph-pull-hook
|
||||||
%graph-push-hook
|
%graph-push-hook
|
||||||
|
%hark-store
|
||||||
|
%hark-graph-hook
|
||||||
|
%hark-group-hook
|
||||||
|
%hark-chat-hook
|
||||||
%observe-hook
|
%observe-hook
|
||||||
==
|
==
|
||||||
::
|
::
|
||||||
@ -210,7 +214,7 @@
|
|||||||
==
|
==
|
||||||
::
|
::
|
||||||
++ on-load
|
++ on-load
|
||||||
|= [hood-version=?(%1 %2 %3 %4 %5 %6 %7 %8 %9 %10 %11) old=any-state]
|
|= [hood-version=@ud old=any-state]
|
||||||
=< se-abet =< se-view
|
=< se-abet =< se-view
|
||||||
=. sat old
|
=. sat old
|
||||||
=. dev (~(gut by bin) ost *source)
|
=. dev (~(gut by bin) ost *source)
|
||||||
@ -243,7 +247,11 @@
|
|||||||
=> (se-born | %home %graph-push-hook)
|
=> (se-born | %home %graph-push-hook)
|
||||||
(se-born | %home %graph-pull-hook)
|
(se-born | %home %graph-pull-hook)
|
||||||
=? ..on-load (lte hood-version %11)
|
=? ..on-load (lte hood-version %11)
|
||||||
(se-born | %home %observe-hook)
|
=> (se-born | %home %hark-graph-hook)
|
||||||
|
=> (se-born | %home %hark-group-hook)
|
||||||
|
=> (se-born | %home %hark-chat-hook)
|
||||||
|
=> (se-born | %home %hark-store)
|
||||||
|
(se-born | %home %observe-hook)
|
||||||
..on-load
|
..on-load
|
||||||
::
|
::
|
||||||
++ reap-phat :: ack connect
|
++ reap-phat :: ack connect
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
:: metadata: helpers for getting data from the metadata-store
|
:: metadata: helpers for getting data from the metadata-store
|
||||||
::
|
::
|
||||||
/- *metadata-store
|
/- *metadata-store
|
||||||
|
/+ res=resource
|
||||||
::
|
::
|
||||||
|_ =bowl:gall
|
|_ =bowl:gall
|
||||||
++ app-paths-from-group
|
++ app-paths-from-group
|
||||||
@ -21,6 +22,27 @@
|
|||||||
?. =(app-name.md-resource app-name) ~
|
?. =(app-name.md-resource app-name) ~
|
||||||
`app-path.md-resource
|
`app-path.md-resource
|
||||||
::
|
::
|
||||||
|
++ peek-metadata
|
||||||
|
|= [app-name=term =group=resource:res =app=resource:res]
|
||||||
|
^- (unit metadata)
|
||||||
|
=/ group-cord=cord (scot %t (spat (en-path:res group-resource)))
|
||||||
|
=/ app-cord=cord (scot %t (spat (en-path:res app-resource)))
|
||||||
|
=/ our=cord (scot %p our.bowl)
|
||||||
|
=/ now=cord (scot %da now.bowl)
|
||||||
|
.^ (unit metadata)
|
||||||
|
%gx (scot %p our.bowl) %metadata-store (scot %da now.bowl)
|
||||||
|
%metadata group-cord app-name app-cord /noun
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ group-from-app-resource
|
||||||
|
|= [app=term =app=resource:res]
|
||||||
|
^- (unit resource:res)
|
||||||
|
=/ app-path (en-path:res app-resource)
|
||||||
|
=/ group-paths (groups-from-resource app app-path)
|
||||||
|
?~ group-paths
|
||||||
|
~
|
||||||
|
`(de-path:res i.group-paths)
|
||||||
|
::
|
||||||
++ groups-from-resource
|
++ groups-from-resource
|
||||||
|= =md-resource
|
|= =md-resource
|
||||||
^- (list group-path)
|
^- (list group-path)
|
||||||
|
@ -37,6 +37,13 @@
|
|||||||
%- spat
|
%- spat
|
||||||
(en-path resource)
|
(en-path resource)
|
||||||
::
|
::
|
||||||
|
++ dejs-path
|
||||||
|
%- su:dejs:format
|
||||||
|
;~ pfix
|
||||||
|
(jest '/ship/')
|
||||||
|
;~((glue fas) ;~(pfix sig fed:ag) urs:ab)
|
||||||
|
==
|
||||||
|
::
|
||||||
++ dejs
|
++ dejs
|
||||||
=, dejs:format
|
=, dejs:format
|
||||||
^- $-(json resource)
|
^- $-(json resource)
|
||||||
|
@ -3,6 +3,11 @@
|
|||||||
++ grow
|
++ grow
|
||||||
|%
|
|%
|
||||||
++ noun i
|
++ noun i
|
||||||
|
++ notification-kind
|
||||||
|
?+ index.p.i ~
|
||||||
|
[@ ~] `%link
|
||||||
|
[@ @ ~] `%comment
|
||||||
|
==
|
||||||
--
|
--
|
||||||
++ grab
|
++ grab
|
||||||
|%
|
|%
|
||||||
@ -19,7 +24,7 @@
|
|||||||
:: comment on link post; comment text
|
:: comment on link post; comment text
|
||||||
::
|
::
|
||||||
[@ @ ~]
|
[@ @ ~]
|
||||||
?> ?=([[%text @] ~] contents.p.ip)
|
?> ?=(^ contents.p.ip)
|
||||||
ip
|
ip
|
||||||
==
|
==
|
||||||
--
|
--
|
||||||
|
@ -3,6 +3,14 @@
|
|||||||
++ grow
|
++ grow
|
||||||
|%
|
|%
|
||||||
++ noun i
|
++ noun i
|
||||||
|
:: +notification-kind
|
||||||
|
:: Ignore all containers, only notify on content
|
||||||
|
::
|
||||||
|
++ notification-kind
|
||||||
|
?+ index.p.i ~
|
||||||
|
[@ %1 @ ~] `%note
|
||||||
|
[@ %2 @ ~] `%comment
|
||||||
|
==
|
||||||
--
|
--
|
||||||
++ grab
|
++ grab
|
||||||
|%
|
|%
|
||||||
|
13
pkg/arvo/mar/hark/action.hoon
Normal file
13
pkg/arvo/mar/hark/action.hoon
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/+ *hark-store
|
||||||
|
|_ act=action
|
||||||
|
++ grad %noun
|
||||||
|
++ grow
|
||||||
|
|%
|
||||||
|
++ noun act
|
||||||
|
--
|
||||||
|
++ grab
|
||||||
|
|%
|
||||||
|
++ noun action
|
||||||
|
++ json action:dejs
|
||||||
|
--
|
||||||
|
--
|
13
pkg/arvo/mar/hark/chat-hook-action.hoon
Normal file
13
pkg/arvo/mar/hark/chat-hook-action.hoon
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/+ *hark-chat-hook
|
||||||
|
|_ act=action
|
||||||
|
++ grad %noun
|
||||||
|
++ grow
|
||||||
|
|%
|
||||||
|
++ noun act
|
||||||
|
--
|
||||||
|
++ grab
|
||||||
|
|%
|
||||||
|
++ noun action
|
||||||
|
++ json action:dejs
|
||||||
|
--
|
||||||
|
--
|
16
pkg/arvo/mar/hark/chat-hook-update.hoon
Normal file
16
pkg/arvo/mar/hark/chat-hook-update.hoon
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/+ *hark-chat-hook
|
||||||
|
|_ upd=update
|
||||||
|
++ grad %noun
|
||||||
|
++ grow
|
||||||
|
|%
|
||||||
|
++ noun upd
|
||||||
|
++ json
|
||||||
|
%+ frond:enjs:format
|
||||||
|
%hark-chat-hook-update
|
||||||
|
(update:enjs upd)
|
||||||
|
--
|
||||||
|
++ grab
|
||||||
|
|%
|
||||||
|
++ noun update
|
||||||
|
--
|
||||||
|
--
|
13
pkg/arvo/mar/hark/graph-hook-action.hoon
Normal file
13
pkg/arvo/mar/hark/graph-hook-action.hoon
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/+ *hark-graph-hook
|
||||||
|
|_ act=action
|
||||||
|
++ grad %noun
|
||||||
|
++ grow
|
||||||
|
|%
|
||||||
|
++ noun act
|
||||||
|
--
|
||||||
|
++ grab
|
||||||
|
|%
|
||||||
|
++ noun action
|
||||||
|
++ json action:dejs
|
||||||
|
--
|
||||||
|
--
|
17
pkg/arvo/mar/hark/graph-hook-update.hoon
Normal file
17
pkg/arvo/mar/hark/graph-hook-update.hoon
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/+ *hark-graph-hook
|
||||||
|
|_ upd=update
|
||||||
|
++ grad %noun
|
||||||
|
++ grow
|
||||||
|
|%
|
||||||
|
++ noun upd
|
||||||
|
++ json
|
||||||
|
%+ frond:enjs:format
|
||||||
|
%hark-graph-hook-update
|
||||||
|
(update:enjs upd)
|
||||||
|
--
|
||||||
|
++ grab
|
||||||
|
|%
|
||||||
|
++ noun update
|
||||||
|
++ json update:dejs
|
||||||
|
--
|
||||||
|
--
|
13
pkg/arvo/mar/hark/group-hook-action.hoon
Normal file
13
pkg/arvo/mar/hark/group-hook-action.hoon
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/+ *hark-group-hook
|
||||||
|
|_ act=action
|
||||||
|
++ grad %noun
|
||||||
|
++ grow
|
||||||
|
|%
|
||||||
|
++ noun act
|
||||||
|
--
|
||||||
|
++ grab
|
||||||
|
|%
|
||||||
|
++ noun action
|
||||||
|
++ json action:dejs
|
||||||
|
--
|
||||||
|
--
|
16
pkg/arvo/mar/hark/group-hook-update.hoon
Normal file
16
pkg/arvo/mar/hark/group-hook-update.hoon
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/+ *hark-group-hook
|
||||||
|
|_ upd=update
|
||||||
|
++ grad %noun
|
||||||
|
++ grow
|
||||||
|
|%
|
||||||
|
++ noun upd
|
||||||
|
++ json
|
||||||
|
%+ frond:enjs:format
|
||||||
|
%hark-group-hook-update
|
||||||
|
(update:enjs upd)
|
||||||
|
--
|
||||||
|
++ grab
|
||||||
|
|%
|
||||||
|
++ noun update
|
||||||
|
--
|
||||||
|
--
|
15
pkg/arvo/mar/hark/update.hoon
Normal file
15
pkg/arvo/mar/hark/update.hoon
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/+ *hark-store
|
||||||
|
|_ upd=update
|
||||||
|
++ grad %noun
|
||||||
|
++ grow
|
||||||
|
|%
|
||||||
|
++ noun upd
|
||||||
|
++ json
|
||||||
|
%+ frond:enjs:format 'harkUpdate'
|
||||||
|
(update:enjs upd)
|
||||||
|
--
|
||||||
|
++ grab
|
||||||
|
|%
|
||||||
|
++ noun update
|
||||||
|
--
|
||||||
|
--
|
15
pkg/arvo/sur/hark-chat-hook.hoon
Normal file
15
pkg/arvo/sur/hark-chat-hook.hoon
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
^?
|
||||||
|
|%
|
||||||
|
+$ action
|
||||||
|
$% [?(%listen %ignore) chat=path]
|
||||||
|
[%set-mentions mentions=?]
|
||||||
|
==
|
||||||
|
::
|
||||||
|
+$ update
|
||||||
|
$%
|
||||||
|
action
|
||||||
|
$: %initial
|
||||||
|
watching=(set path)
|
||||||
|
==
|
||||||
|
==
|
||||||
|
--
|
20
pkg/arvo/sur/hark-graph-hook.hoon
Normal file
20
pkg/arvo/sur/hark-graph-hook.hoon
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/- *resource, graph-store
|
||||||
|
^?
|
||||||
|
|%
|
||||||
|
+$ action
|
||||||
|
$%
|
||||||
|
[?(%listen %ignore) graph=resource]
|
||||||
|
[%set-mentions mentions=?]
|
||||||
|
[%set-watch-on-self watch-on-self=?]
|
||||||
|
==
|
||||||
|
::
|
||||||
|
+$ update
|
||||||
|
$%
|
||||||
|
action
|
||||||
|
$: %initial
|
||||||
|
watching=(set resource)
|
||||||
|
mentions=_&
|
||||||
|
watch-on-self=_&
|
||||||
|
==
|
||||||
|
==
|
||||||
|
--
|
11
pkg/arvo/sur/hark-group-hook.hoon
Normal file
11
pkg/arvo/sur/hark-group-hook.hoon
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/- *resource
|
||||||
|
^?
|
||||||
|
|%
|
||||||
|
+$ action
|
||||||
|
[?(%listen %ignore) group=resource]
|
||||||
|
::
|
||||||
|
+$ update
|
||||||
|
$% action
|
||||||
|
[%initial watching=(set resource)]
|
||||||
|
==
|
||||||
|
--
|
50
pkg/arvo/sur/hark-store.hoon
Normal file
50
pkg/arvo/sur/hark-store.hoon
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/- *resource, graph-store, post, group-store, metadata-store, chat-store
|
||||||
|
^?
|
||||||
|
|%
|
||||||
|
+$ index
|
||||||
|
$% [%graph group=resource graph=resource module=@t description=@t]
|
||||||
|
[%group group=resource description=@t]
|
||||||
|
[%chat chat=path mention=?]
|
||||||
|
==
|
||||||
|
::
|
||||||
|
+$ group-contents
|
||||||
|
$~ [%add-members *resource ~]
|
||||||
|
$% $>(?(%add-members %remove-members) update:group-store)
|
||||||
|
metadata-action:metadata-store
|
||||||
|
==
|
||||||
|
::
|
||||||
|
+$ notification
|
||||||
|
[date=@da read=? =contents]
|
||||||
|
::
|
||||||
|
+$ contents
|
||||||
|
$% [%graph =(list post:post)]
|
||||||
|
[%group =(list group-contents)]
|
||||||
|
[%chat =(list envelope:chat-store)]
|
||||||
|
==
|
||||||
|
::
|
||||||
|
+$ timebox
|
||||||
|
(map index notification)
|
||||||
|
::
|
||||||
|
+$ notifications
|
||||||
|
((mop @da timebox) lth)
|
||||||
|
::
|
||||||
|
+$ action
|
||||||
|
$% [%add =index =notification]
|
||||||
|
[%archive time=@da index]
|
||||||
|
[%read time=@da index]
|
||||||
|
[%unread time=@da index]
|
||||||
|
[%set-dnd dnd=?]
|
||||||
|
[%seen ~]
|
||||||
|
==
|
||||||
|
::
|
||||||
|
++ indexed-notification
|
||||||
|
[index notification]
|
||||||
|
::
|
||||||
|
+$ update
|
||||||
|
$% action
|
||||||
|
[%more =(list update)]
|
||||||
|
[%added time=@da =index =notification]
|
||||||
|
[%timebox time=@da archived=? =(list [index notification])]
|
||||||
|
[%count count=@ud]
|
||||||
|
==
|
||||||
|
--
|
@ -28,6 +28,7 @@
|
|||||||
::
|
::
|
||||||
+$ content
|
+$ content
|
||||||
$% [%text text=cord]
|
$% [%text text=cord]
|
||||||
|
[%mention =ship]
|
||||||
[%url url=cord]
|
[%url url=cord]
|
||||||
[%code expression=cord output=(list tank)]
|
[%code expression=cord output=(list tank)]
|
||||||
[%reference =uid]
|
[%reference =uid]
|
||||||
|
@ -11,6 +11,7 @@ import GroupsApi from './groups';
|
|||||||
import LaunchApi from './launch';
|
import LaunchApi from './launch';
|
||||||
import GraphApi from './graph';
|
import GraphApi from './graph';
|
||||||
import S3Api from './s3';
|
import S3Api from './s3';
|
||||||
|
import {HarkApi} from './hark';
|
||||||
|
|
||||||
export default class GlobalApi extends BaseApi<StoreState> {
|
export default class GlobalApi extends BaseApi<StoreState> {
|
||||||
chat = new ChatApi(this.ship, this.channel, this.store);
|
chat = new ChatApi(this.ship, this.channel, this.store);
|
||||||
@ -22,6 +23,7 @@ export default class GlobalApi extends BaseApi<StoreState> {
|
|||||||
launch = new LaunchApi(this.ship, this.channel, this.store);
|
launch = new LaunchApi(this.ship, this.channel, this.store);
|
||||||
s3 = new S3Api(this.ship, this.channel, this.store);
|
s3 = new S3Api(this.ship, this.channel, this.store);
|
||||||
graph = new GraphApi(this.ship, this.channel, this.store);
|
graph = new GraphApi(this.ship, this.channel, this.store);
|
||||||
|
hark = new HarkApi(this.ship, this.channel, this.store);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public ship: Patp,
|
public ship: Patp,
|
||||||
|
@ -3,10 +3,10 @@ import { StoreState } from '../store/type';
|
|||||||
import { Patp, Path, PatpNoSig } from '~/types/noun';
|
import { Patp, Path, PatpNoSig } from '~/types/noun';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import {makeResource, resourceFromPath} from '../lib/group';
|
import {makeResource, resourceFromPath} from '../lib/group';
|
||||||
import {GroupPolicy, Enc, Post, NodeMap} from '~/types';
|
import {GroupPolicy, Enc, Post, NodeMap, Content} from '~/types';
|
||||||
import { numToUd, unixToDa } from '~/logic/lib/util';
|
import { numToUd, unixToDa } from '~/logic/lib/util';
|
||||||
|
|
||||||
export const createPost = (contents: Object[], parentIndex: string = '') => {
|
export const createPost = (contents: Content[], parentIndex: string = '') => {
|
||||||
return {
|
return {
|
||||||
author: `~${window.ship}`,
|
author: `~${window.ship}`,
|
||||||
index: parentIndex + '/' + unixToDa(Date.now()).toString(),
|
index: parentIndex + '/' + unixToDa(Date.now()).toString(),
|
||||||
|
144
pkg/interface/src/logic/api/hark.ts
Normal file
144
pkg/interface/src/logic/api/hark.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import BaseApi from "./base";
|
||||||
|
import { StoreState } from "../store/type";
|
||||||
|
import { dateToDa, decToUd } from "../lib/util";
|
||||||
|
import {NotifIndex} from "~/types";
|
||||||
|
import { BigInteger } from 'big-integer';
|
||||||
|
|
||||||
|
export class HarkApi extends BaseApi<StoreState> {
|
||||||
|
private harkAction(action: any): Promise<any> {
|
||||||
|
return this.action("hark-store", "hark-action", action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private graphHookAction(action: any) {
|
||||||
|
return this.action("hark-graph-hook", "hark-graph-hook-action", action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private groupHookAction(action: any) {
|
||||||
|
return this.action("hark-group-hook", "hark-group-hook-action", action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private chatHookAction(action: any) {
|
||||||
|
return this.action("hark-chat-hook", "hark-chat-hook-action", action);
|
||||||
|
}
|
||||||
|
|
||||||
|
private actOnNotification(frond: string, intTime: BigInteger, index: NotifIndex) {
|
||||||
|
const time = decToUd(intTime.toString());
|
||||||
|
return this.harkAction({
|
||||||
|
[frond]: {
|
||||||
|
time,
|
||||||
|
index
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async setMentions(mentions: boolean) {
|
||||||
|
await this.graphHookAction({
|
||||||
|
'set-mentions': mentions
|
||||||
|
});
|
||||||
|
return this.chatHookAction({
|
||||||
|
'set-mentions': mentions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setWatchOnSelf(watchSelf: boolean) {
|
||||||
|
return this.graphHookAction({
|
||||||
|
'set-watch-on-self': watchSelf
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setDoNotDisturb(dnd: boolean) {
|
||||||
|
return this.harkAction({
|
||||||
|
'set-dnd': dnd
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
archive(time: BigInteger, index: NotifIndex) {
|
||||||
|
return this.actOnNotification('archive', time, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
read(time: BigInteger, index: NotifIndex) {
|
||||||
|
return this.actOnNotification('read', time, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
unread(time: BigInteger, index: NotifIndex) {
|
||||||
|
return this.actOnNotification('unread', time, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
seen() {
|
||||||
|
return this.harkAction({ seen: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
mute(index: NotifIndex) {
|
||||||
|
if('graph' in index) {
|
||||||
|
const { graph } = index.graph;
|
||||||
|
return this.ignoreGraph(graph);
|
||||||
|
}
|
||||||
|
if('group' in index) {
|
||||||
|
const { group } = index.group;
|
||||||
|
return this.ignoreGroup(group);
|
||||||
|
}
|
||||||
|
if('chat' in index) {
|
||||||
|
return this.ignoreChat(index.chat);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
unmute(index: NotifIndex) {
|
||||||
|
if('graph' in index) {
|
||||||
|
return this.listenGraph(index.graph.graph);
|
||||||
|
}
|
||||||
|
if('group' in index) {
|
||||||
|
return this.listenGroup(index.group.group);
|
||||||
|
}
|
||||||
|
if('chat' in index) {
|
||||||
|
return this.listenChat(index.chat);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreGroup(group: string) {
|
||||||
|
return this.groupHookAction({
|
||||||
|
ignore: group
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreGraph(graph: string) {
|
||||||
|
return this.graphHookAction({
|
||||||
|
ignore: graph
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreChat(chat: string) {
|
||||||
|
return this.chatHookAction({
|
||||||
|
ignore: chat
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
listenGroup(group: string) {
|
||||||
|
return this.groupHookAction({
|
||||||
|
listen: group
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
listenGraph(graph: string) {
|
||||||
|
return this.graphHookAction({
|
||||||
|
listen: graph
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
listenChat(chat: string) {
|
||||||
|
return this.chatHookAction({
|
||||||
|
listen: chat
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTimeSubset(start?: Date, end?: Date) {
|
||||||
|
const s = start ? dateToDa(start) : "-";
|
||||||
|
const e = end ? dateToDa(end) : "-";
|
||||||
|
const result = await this.scry("hark-hook", `/time-subset/${s}/${e}`);
|
||||||
|
this.store.handleEvent({
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
24
pkg/interface/src/logic/lib/graph.ts
Normal file
24
pkg/interface/src/logic/lib/graph.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Content } from "~/types";
|
||||||
|
import urbitOb from "urbit-ob";
|
||||||
|
|
||||||
|
export function scanForMentions(text: string) {
|
||||||
|
const regex = /~([a-z]|-)+/g;
|
||||||
|
let result: Content[] = [];
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
let lastPos = 0;
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
const newPos = match.index + match[0].length;
|
||||||
|
if (urbitOb.isValidPatp(match[0])) {
|
||||||
|
if (match.index !== lastPos) {
|
||||||
|
result.push({ text: text.slice(lastPos, match.index) });
|
||||||
|
}
|
||||||
|
result.push({ mention: match[0] });
|
||||||
|
}
|
||||||
|
lastPos = newPos;
|
||||||
|
}
|
||||||
|
const remainder = text.slice(lastPos, text.length);
|
||||||
|
if (remainder) {
|
||||||
|
result.push({ text: remainder });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
@ -55,6 +55,7 @@ const appIndex = function (apps) {
|
|||||||
const otherIndex = function() {
|
const otherIndex = function() {
|
||||||
const other = [];
|
const other = [];
|
||||||
other.push(result('DMs + Drafts', '/~landscape/home', 'home', null));
|
other.push(result('DMs + Drafts', '/~landscape/home', 'home', null));
|
||||||
|
other.push(result('Notifications', '/~notifications', 'inbox', null));
|
||||||
other.push(result('Profile and Settings', '/~profile/identity', 'profile', null));
|
other.push(result('Profile and Settings', '/~profile/identity', 'profile', null));
|
||||||
other.push(result('Log Out', '/~/logout', 'logout', null));
|
other.push(result('Log Out', '/~/logout', 'logout', null));
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ export const Sigil = memo(({ classes = '', color, foreground = '', ship, size, s
|
|||||||
display='inline-block'
|
display='inline-block'
|
||||||
height={size}
|
height={size}
|
||||||
width={size}
|
width={size}
|
||||||
|
className={classes}
|
||||||
/>) : (
|
/>) : (
|
||||||
<Box
|
<Box
|
||||||
display='inline-block'
|
display='inline-block'
|
||||||
|
@ -1,344 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import f from 'lodash/fp';
|
|
||||||
import bigInt from 'big-integer';
|
|
||||||
|
|
||||||
export const MOBILE_BROWSER_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i;
|
|
||||||
|
|
||||||
const DA_UNIX_EPOCH = bigInt("170141184475152167957503069145530368000"); // `@ud` ~1970.1.1
|
|
||||||
const DA_SECOND = bigInt("18446744073709551616"); // `@ud` ~s1
|
|
||||||
export function daToUnix(da) {
|
|
||||||
// ported from +time:enjs:format in hoon.hoon
|
|
||||||
const offset = DA_SECOND.divide(bigInt(2000));
|
|
||||||
const epochAdjusted = offset.add(da.subtract(DA_UNIX_EPOCH));
|
|
||||||
|
|
||||||
return Math.round(
|
|
||||||
epochAdjusted.multiply(bigInt(1000)).divide(DA_SECOND).toJSNumber()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unixToDa(unix) {
|
|
||||||
const timeSinceEpoch = bigInt(unix).multiply(DA_SECOND).divide(bigInt(1000));
|
|
||||||
return DA_UNIX_EPOCH.add(timeSinceEpoch);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function appIsGraph(app) {
|
|
||||||
return app === 'link' || app === 'publish';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parentPath(path) {
|
|
||||||
return _.dropRight(path.split('/'), 1).join('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clamp(x,min,max) {
|
|
||||||
return Math.max(min, Math.min(max, x));
|
|
||||||
}
|
|
||||||
|
|
||||||
// color is a #000000 color
|
|
||||||
export function adjustHex(color, amount) {
|
|
||||||
const res = f.flow(
|
|
||||||
f.split(''), f.chunk(2), // get individual color channels
|
|
||||||
f.map(c => parseInt(c.join(''), 16)), // as hex
|
|
||||||
f.map(c => clamp(c + amount, 0, 255).toString(16)), // adjust
|
|
||||||
f.join('')
|
|
||||||
)(color.slice(1))
|
|
||||||
return `#${res}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function resourceAsPath(resource) {
|
|
||||||
const { name, ship } = resource;
|
|
||||||
return `/ship/~${ship}/${name}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function uuid() {
|
|
||||||
let str = '0v';
|
|
||||||
str += Math.ceil(Math.random()*8)+'.';
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
let _str = Math.ceil(Math.random()*10000000).toString(32);
|
|
||||||
_str = ('00000'+_str).substr(-5,5);
|
|
||||||
str += _str+'.';
|
|
||||||
}
|
|
||||||
|
|
||||||
return str.slice(0,-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Goes from:
|
|
||||||
~2018.7.17..23.15.09..5be5 // urbit @da
|
|
||||||
To:
|
|
||||||
(javascript Date object)
|
|
||||||
*/
|
|
||||||
export function daToDate(st) {
|
|
||||||
const dub = function(n) {
|
|
||||||
return parseInt(n) < 10 ? '0' + parseInt(n) : n.toString();
|
|
||||||
};
|
|
||||||
const da = st.split('..');
|
|
||||||
const bigEnd = da[0].split('.');
|
|
||||||
const lilEnd = da[1].split('.');
|
|
||||||
const ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(lilEnd[0])}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
|
|
||||||
return new Date(ds);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Goes from:
|
|
||||||
(javascript Date object)
|
|
||||||
To:
|
|
||||||
~2018.7.17..23.15.09..5be5 // urbit @da
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function dateToDa(d, mil) {
|
|
||||||
const fil = function(n) {
|
|
||||||
return n >= 10 ? n : '0' + n;
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
`~${d.getUTCFullYear()}.` +
|
|
||||||
`${(d.getUTCMonth() + 1)}.` +
|
|
||||||
`${fil(d.getUTCDate())}..` +
|
|
||||||
`${fil(d.getUTCHours())}.` +
|
|
||||||
`${fil(d.getUTCMinutes())}.` +
|
|
||||||
`${fil(d.getUTCSeconds())}` +
|
|
||||||
`${mil ? '..0000' : ''}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deSig(ship) {
|
|
||||||
if(!ship) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return ship.replace('~', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function uxToHex(ux) {
|
|
||||||
if (ux.length > 2 && ux.substr(0,2) === '0x') {
|
|
||||||
const value = ux.substr(2).replace('.', '').padStart(6, '0');
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = ux.replace('.', '').padStart(6, '0');
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hexToUx(hex) {
|
|
||||||
const ux = f.flow(
|
|
||||||
f.chunk(4),
|
|
||||||
f.map(x => _.dropWhile(x, y => y === 0).join('')),
|
|
||||||
f.join('.')
|
|
||||||
)(hex.split(''))
|
|
||||||
return `0x${ux}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function writeText(str) {
|
|
||||||
return new Promise(((resolve, reject) => {
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(document.body);
|
|
||||||
document.getSelection().addRange(range);
|
|
||||||
|
|
||||||
let success = false;
|
|
||||||
function listener(e) {
|
|
||||||
e.clipboardData.setData('text/plain', str);
|
|
||||||
e.preventDefault();
|
|
||||||
success = true;
|
|
||||||
}
|
|
||||||
document.addEventListener('copy', listener);
|
|
||||||
document.execCommand('copy');
|
|
||||||
document.removeEventListener('copy', listener);
|
|
||||||
|
|
||||||
document.getSelection().removeAllRanges();
|
|
||||||
|
|
||||||
success ? resolve() : reject();
|
|
||||||
})).catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
});;
|
|
||||||
};
|
|
||||||
|
|
||||||
// trim patps to match dojo, chat-cli
|
|
||||||
export function cite(ship) {
|
|
||||||
let patp = ship, shortened = '';
|
|
||||||
if (patp === null || patp === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (patp.startsWith('~')) {
|
|
||||||
patp = patp.substr(1);
|
|
||||||
}
|
|
||||||
// comet
|
|
||||||
if (patp.length === 56) {
|
|
||||||
shortened = '~' + patp.slice(0, 6) + '_' + patp.slice(50, 56);
|
|
||||||
return shortened;
|
|
||||||
}
|
|
||||||
// moon
|
|
||||||
if (patp.length === 27) {
|
|
||||||
shortened = '~' + patp.slice(14, 20) + '^' + patp.slice(21, 27);
|
|
||||||
return shortened;
|
|
||||||
}
|
|
||||||
return `~${patp}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function alphabeticalOrder(a,b) {
|
|
||||||
return a.toLowerCase().localeCompare(b.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: deprecated
|
|
||||||
export function alphabetiseAssociations(associations) {
|
|
||||||
const result = {};
|
|
||||||
Object.keys(associations).sort((a, b) => {
|
|
||||||
let aName = a.substr(1);
|
|
||||||
let bName = b.substr(1);
|
|
||||||
if (associations[a].metadata && associations[a].metadata.title) {
|
|
||||||
aName = associations[a].metadata.title !== ''
|
|
||||||
? associations[a].metadata.title
|
|
||||||
: a.substr(1);
|
|
||||||
}
|
|
||||||
if (associations[b].metadata && associations[b].metadata.title) {
|
|
||||||
bName = associations[b].metadata.title !== ''
|
|
||||||
? associations[b].metadata.title
|
|
||||||
: b.substr(1);
|
|
||||||
}
|
|
||||||
return alphabeticalOrder(aName,bName);
|
|
||||||
}).map((each) => {
|
|
||||||
result[each] = associations[each];
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// encode the string into @ta-safe format, using logic from +wood.
|
|
||||||
// for example, 'some Chars!' becomes '~.some.~43.hars~21.'
|
|
||||||
//
|
|
||||||
export function stringToTa(string) {
|
|
||||||
let out = '';
|
|
||||||
for (let i = 0; i < string.length; i++) {
|
|
||||||
const char = string[i];
|
|
||||||
let add = '';
|
|
||||||
switch (char) {
|
|
||||||
case ' ':
|
|
||||||
add = '.';
|
|
||||||
break;
|
|
||||||
case '.':
|
|
||||||
add = '~.';
|
|
||||||
break;
|
|
||||||
case '~':
|
|
||||||
add = '~~';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
const charCode = string.charCodeAt(i);
|
|
||||||
if (
|
|
||||||
(charCode >= 97 && charCode <= 122) || // a-z
|
|
||||||
(charCode >= 48 && charCode <= 57) || // 0-9
|
|
||||||
char === '-'
|
|
||||||
) {
|
|
||||||
add = char;
|
|
||||||
} else {
|
|
||||||
// TODO behavior for unicode doesn't match +wood's,
|
|
||||||
// but we can probably get away with that for now.
|
|
||||||
add = '~' + charCode.toString(16) + '.';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out = out + add;
|
|
||||||
}
|
|
||||||
return '~.' + out;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function amOwnerOfGroup(groupPath) {
|
|
||||||
if (!groupPath)
|
|
||||||
return false;
|
|
||||||
const groupOwner = /(\/~)?\/~([a-z-]{3,})\/.*/.exec(groupPath)[2];
|
|
||||||
return window.ship === groupOwner;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getContactDetails(contact) {
|
|
||||||
const member = !contact;
|
|
||||||
contact = contact || {
|
|
||||||
nickname: '',
|
|
||||||
avatar: null,
|
|
||||||
color: '0x0'
|
|
||||||
};
|
|
||||||
const nickname = contact.nickname || '';
|
|
||||||
const color = uxToHex(contact.color || '0x0');
|
|
||||||
const avatar = contact.avatar || null;
|
|
||||||
return { nickname, color, member, avatar };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stringToSymbol(str) {
|
|
||||||
let result = '';
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
const n = str.charCodeAt(i);
|
|
||||||
if (((n >= 97) && (n <= 122)) ||
|
|
||||||
((n >= 48) && (n <= 57))) {
|
|
||||||
result += str[i];
|
|
||||||
} else if ((n >= 65) && (n <= 90)) {
|
|
||||||
result += String.fromCharCode(n + 32);
|
|
||||||
} else {
|
|
||||||
result += '-';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = result.replace(/^[\-\d]+|\-+/g, '-');
|
|
||||||
result = result.replace(/^\-+|\-+$/g, '');
|
|
||||||
if (result === '') {
|
|
||||||
return dateToDa(new Date());
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function scrollIsAtTop(container) {
|
|
||||||
if (
|
|
||||||
(navigator.userAgent.includes("Safari") &&
|
|
||||||
navigator.userAgent.includes("Chrome")) ||
|
|
||||||
navigator.userAgent.includes("Firefox")
|
|
||||||
) {
|
|
||||||
return container.scrollTop === 0;
|
|
||||||
} else if (navigator.userAgent.includes("Safari")) {
|
|
||||||
return (
|
|
||||||
container.scrollHeight + Math.round(container.scrollTop) <=
|
|
||||||
container.clientHeight + 10
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function scrollIsAtBottom(container) {
|
|
||||||
if (
|
|
||||||
(navigator.userAgent.includes("Safari") &&
|
|
||||||
navigator.userAgent.includes("Chrome")) ||
|
|
||||||
navigator.userAgent.includes("Firefox")
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
container.scrollHeight - Math.round(container.scrollTop) <=
|
|
||||||
container.clientHeight + 10
|
|
||||||
);
|
|
||||||
} else if (navigator.userAgent.includes("Safari")) {
|
|
||||||
return container.scrollTop === 0;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats a numbers as a `@ud` inserting dot where needed
|
|
||||||
*/
|
|
||||||
export function numToUd(num) {
|
|
||||||
return f.flow(
|
|
||||||
f.split(''),
|
|
||||||
f.reverse,
|
|
||||||
f.chunk(3),
|
|
||||||
f.reverse,
|
|
||||||
f.map(s => s.join('')),
|
|
||||||
f.join('.')
|
|
||||||
)(num.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
export function usePreventWindowUnload(shouldPreventDefault, message = "You have unsaved changes. Are you sure you want to exit?") {
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!shouldPreventDefault) return;
|
|
||||||
const handleBeforeUnload = event => {
|
|
||||||
event.preventDefault();
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
||||||
window.onbeforeunload = handleBeforeUnload;
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
||||||
window.onbeforeunload = undefined;
|
|
||||||
}
|
|
||||||
}, [shouldPreventDefault]);
|
|
||||||
}
|
|
360
pkg/interface/src/logic/lib/util.ts
Normal file
360
pkg/interface/src/logic/lib/util.ts
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import _ from "lodash";
|
||||||
|
import f from "lodash/fp";
|
||||||
|
import bigInt, { BigInteger } from "big-integer";
|
||||||
|
|
||||||
|
export const MOBILE_BROWSER_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i;
|
||||||
|
|
||||||
|
export const MOMENT_CALENDAR_DATE = {
|
||||||
|
sameDay: "[Today]",
|
||||||
|
nextDay: "[Tomorrow]",
|
||||||
|
nextWeek: "dddd",
|
||||||
|
lastDay: "[Yesterday]",
|
||||||
|
lastWeek: "[Last] dddd",
|
||||||
|
sameElse: "DD/MM/YYYY",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function appIsGraph(app: string) {
|
||||||
|
return app === 'publish' || app == 'link';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parentPath(path: string) {
|
||||||
|
return _.dropRight(path.split('/'), 1).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
const DA_UNIX_EPOCH = bigInt("170141184475152167957503069145530368000"); // `@ud` ~1970.1.1
|
||||||
|
const DA_SECOND = bigInt("18446744073709551616"); // `@ud` ~s1
|
||||||
|
export function daToUnix(da: BigInteger) {
|
||||||
|
// ported from +time:enjs:format in hoon.hoon
|
||||||
|
const offset = DA_SECOND.divide(bigInt(2000));
|
||||||
|
const epochAdjusted = offset.add(da.subtract(DA_UNIX_EPOCH));
|
||||||
|
|
||||||
|
return Math.round(
|
||||||
|
epochAdjusted.multiply(bigInt(1000)).divide(DA_SECOND).toJSNumber()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unixToDa(unix: number) {
|
||||||
|
const timeSinceEpoch = bigInt(unix).multiply(DA_SECOND).divide(bigInt(1000));
|
||||||
|
return DA_UNIX_EPOCH.add(timeSinceEpoch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makePatDa(patda: string) {
|
||||||
|
return bigInt(udToDec(patda));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function udToDec(ud: string): string {
|
||||||
|
return ud.replace(/\./g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decToUd(str: string): string {
|
||||||
|
return _.trimStart(
|
||||||
|
f.flow(
|
||||||
|
f.split(""),
|
||||||
|
f.reverse,
|
||||||
|
f.chunk(3),
|
||||||
|
f.map(f.flow(f.reverse, f.join(""))),
|
||||||
|
f.reverse,
|
||||||
|
f.join(".")
|
||||||
|
)(str),
|
||||||
|
"0."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clamp a number between a min and max
|
||||||
|
*/
|
||||||
|
export function clamp(x: number, min: number, max: number) {
|
||||||
|
return Math.max(min, Math.min(max, x));
|
||||||
|
}
|
||||||
|
|
||||||
|
// color is a #000000 color
|
||||||
|
export function adjustHex(color: string, amount: number): string {
|
||||||
|
return f.flow(
|
||||||
|
f.split(""),
|
||||||
|
f.chunk(2), // get RGB channels
|
||||||
|
f.map((c) => parseInt(c.join(""), 16)), // as hex
|
||||||
|
f.map((c) => clamp(c + amount, 0, 255).toString(16)), // adjust
|
||||||
|
f.join(""),
|
||||||
|
(res) => `#${res}` //format
|
||||||
|
)(color.slice(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resourceAsPath(resource: any) {
|
||||||
|
const { name, ship } = resource;
|
||||||
|
return `/ship/~${ship}/${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uuid() {
|
||||||
|
let str = "0v";
|
||||||
|
str += Math.ceil(Math.random() * 8) + ".";
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
let _str = Math.ceil(Math.random() * 10000000).toString(32);
|
||||||
|
_str = ("00000" + _str).substr(-5, 5);
|
||||||
|
str += _str + ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
return str.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Goes from:
|
||||||
|
~2018.7.17..23.15.09..5be5 // urbit @da
|
||||||
|
To:
|
||||||
|
(javascript Date object)
|
||||||
|
*/
|
||||||
|
export function daToDate(st: string) {
|
||||||
|
const dub = function (n: string) {
|
||||||
|
return parseInt(n) < 10 ? "0" + parseInt(n) : n.toString();
|
||||||
|
};
|
||||||
|
const da = st.split("..");
|
||||||
|
const bigEnd = da[0].split(".");
|
||||||
|
const lilEnd = da[1].split(".");
|
||||||
|
const ds = `${bigEnd[0].slice(1)}-${dub(bigEnd[1])}-${dub(bigEnd[2])}T${dub(
|
||||||
|
lilEnd[0]
|
||||||
|
)}:${dub(lilEnd[1])}:${dub(lilEnd[2])}Z`;
|
||||||
|
return new Date(ds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Goes from:
|
||||||
|
(javascript Date object)
|
||||||
|
To:
|
||||||
|
~2018.7.17..23.15.09..5be5 // urbit @da
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function dateToDa(d: Date, mil: boolean = false) {
|
||||||
|
const fil = function (n: number) {
|
||||||
|
return n >= 10 ? n : "0" + n;
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
`~${d.getUTCFullYear()}.` +
|
||||||
|
`${d.getUTCMonth() + 1}.` +
|
||||||
|
`${fil(d.getUTCDate())}..` +
|
||||||
|
`${fil(d.getUTCHours())}.` +
|
||||||
|
`${fil(d.getUTCMinutes())}.` +
|
||||||
|
`${fil(d.getUTCSeconds())}` +
|
||||||
|
`${mil ? "..0000" : ""}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deSig(ship: string) {
|
||||||
|
if (!ship) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ship.replace("~", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uxToHex(ux: string) {
|
||||||
|
if (ux.length > 2 && ux.substr(0, 2) === "0x") {
|
||||||
|
const value = ux.substr(2).replace(".", "").padStart(6, "0");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = ux.replace(".", "").padStart(6, "0");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hexToUx: (hex: string) => string = f.flow(
|
||||||
|
f.split(""),
|
||||||
|
f.chunk(4),
|
||||||
|
f.map(
|
||||||
|
f.flow(
|
||||||
|
f.dropWhile((y) => y === 0),
|
||||||
|
f.join
|
||||||
|
)
|
||||||
|
),
|
||||||
|
f.join("."),
|
||||||
|
(x) => `0x${x}`
|
||||||
|
);
|
||||||
|
|
||||||
|
export function writeText(str: string) {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(document.body);
|
||||||
|
document?.getSelection()?.addRange(range);
|
||||||
|
|
||||||
|
let success = false;
|
||||||
|
function listener(e) {
|
||||||
|
e.clipboardData.setData("text/plain", str);
|
||||||
|
e.preventDefault();
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
document.addEventListener("copy", listener);
|
||||||
|
document.execCommand("copy");
|
||||||
|
document.removeEventListener("copy", listener);
|
||||||
|
|
||||||
|
document?.getSelection()?.removeAllRanges();
|
||||||
|
|
||||||
|
success ? resolve() : reject();
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// trim patps to match dojo, chat-cli
|
||||||
|
export function cite(ship: string) {
|
||||||
|
let patp = ship,
|
||||||
|
shortened = "";
|
||||||
|
if (patp === null || patp === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (patp.startsWith("~")) {
|
||||||
|
patp = patp.substr(1);
|
||||||
|
}
|
||||||
|
// comet
|
||||||
|
if (patp.length === 56) {
|
||||||
|
shortened = "~" + patp.slice(0, 6) + "_" + patp.slice(50, 56);
|
||||||
|
return shortened;
|
||||||
|
}
|
||||||
|
// moon
|
||||||
|
if (patp.length === 27) {
|
||||||
|
shortened = "~" + patp.slice(14, 20) + "^" + patp.slice(21, 27);
|
||||||
|
return shortened;
|
||||||
|
}
|
||||||
|
return `~${patp}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function alphabeticalOrder(a: string, b: string) {
|
||||||
|
return a.toLowerCase().localeCompare(b.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: deprecated
|
||||||
|
export function alphabetiseAssociations(associations: any) {
|
||||||
|
const result = {};
|
||||||
|
Object.keys(associations)
|
||||||
|
.sort((a, b) => {
|
||||||
|
let aName = a.substr(1);
|
||||||
|
let bName = b.substr(1);
|
||||||
|
if (associations[a].metadata && associations[a].metadata.title) {
|
||||||
|
aName =
|
||||||
|
associations[a].metadata.title !== ""
|
||||||
|
? associations[a].metadata.title
|
||||||
|
: a.substr(1);
|
||||||
|
}
|
||||||
|
if (associations[b].metadata && associations[b].metadata.title) {
|
||||||
|
bName =
|
||||||
|
associations[b].metadata.title !== ""
|
||||||
|
? associations[b].metadata.title
|
||||||
|
: b.substr(1);
|
||||||
|
}
|
||||||
|
return alphabeticalOrder(aName, bName);
|
||||||
|
})
|
||||||
|
.map((each) => {
|
||||||
|
result[each] = associations[each];
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// encode the string into @ta-safe format, using logic from +wood.
|
||||||
|
// for example, 'some Chars!' becomes '~.some.~43.hars~21.'
|
||||||
|
//
|
||||||
|
export function stringToTa(str: string) {
|
||||||
|
let out = "";
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const char = str[i];
|
||||||
|
let add = "";
|
||||||
|
switch (char) {
|
||||||
|
case " ":
|
||||||
|
add = ".";
|
||||||
|
break;
|
||||||
|
case ".":
|
||||||
|
add = "~.";
|
||||||
|
break;
|
||||||
|
case "~":
|
||||||
|
add = "~~";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
const charCode = str.charCodeAt(i);
|
||||||
|
if (
|
||||||
|
(charCode >= 97 && charCode <= 122) || // a-z
|
||||||
|
(charCode >= 48 && charCode <= 57) || // 0-9
|
||||||
|
char === "-"
|
||||||
|
) {
|
||||||
|
add = char;
|
||||||
|
} else {
|
||||||
|
// TODO behavior for unicode doesn't match +wood's,
|
||||||
|
// but we can probably get away with that for now.
|
||||||
|
add = "~" + charCode.toString(16) + ".";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = out + add;
|
||||||
|
}
|
||||||
|
return "~." + out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function amOwnerOfGroup(groupPath: string) {
|
||||||
|
if (!groupPath) return false;
|
||||||
|
const groupOwner = /(\/~)?\/~([a-z-]{3,})\/.*/.exec(groupPath)?.[2];
|
||||||
|
return window.ship === groupOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getContactDetails(contact: any) {
|
||||||
|
const member = !contact;
|
||||||
|
contact = contact || {
|
||||||
|
nickname: "",
|
||||||
|
avatar: null,
|
||||||
|
color: "0x0",
|
||||||
|
};
|
||||||
|
const nickname = contact.nickname || "";
|
||||||
|
const color = uxToHex(contact.color || "0x0");
|
||||||
|
const avatar = contact.avatar || null;
|
||||||
|
return { nickname, color, member, avatar };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stringToSymbol(str: string) {
|
||||||
|
let result = "";
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const n = str.charCodeAt(i);
|
||||||
|
if ((n >= 97 && n <= 122) || (n >= 48 && n <= 57)) {
|
||||||
|
result += str[i];
|
||||||
|
} else if (n >= 65 && n <= 90) {
|
||||||
|
result += String.fromCharCode(n + 32);
|
||||||
|
} else {
|
||||||
|
result += "-";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = result.replace(/^[\-\d]+|\-+/g, "-");
|
||||||
|
result = result.replace(/^\-+|\-+$/g, "");
|
||||||
|
if (result === "") {
|
||||||
|
return dateToDa(new Date());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a numbers as a `@ud` inserting dot where needed
|
||||||
|
*/
|
||||||
|
export function numToUd(num: number) {
|
||||||
|
return f.flow(
|
||||||
|
f.split(''),
|
||||||
|
f.reverse,
|
||||||
|
f.chunk(3),
|
||||||
|
f.reverse,
|
||||||
|
f.map(s => s.join('')),
|
||||||
|
f.join('.')
|
||||||
|
)(num.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePreventWindowUnload(shouldPreventDefault: boolean, message = "You have unsaved changes. Are you sure you want to exit?") {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldPreventDefault) return;
|
||||||
|
const handleBeforeUnload = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||||
|
window.onbeforeunload = handleBeforeUnload;
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||||
|
// @ts-ignore
|
||||||
|
window.onbeforeunload = undefined;
|
||||||
|
}
|
||||||
|
}, [shouldPreventDefault]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pluralize(text: string, isPlural = false, vowel = false) {
|
||||||
|
return isPlural ? `${text}s`: `${vowel ? 'an' : 'a'} ${text}`;
|
||||||
|
}
|
285
pkg/interface/src/logic/reducers/hark-update.ts
Normal file
285
pkg/interface/src/logic/reducers/hark-update.ts
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
import {
|
||||||
|
Notifications,
|
||||||
|
NotifIndex,
|
||||||
|
NotificationGraphConfig,
|
||||||
|
GroupNotificationsConfig,
|
||||||
|
} from "~/types";
|
||||||
|
import { makePatDa } from "~/logic/lib/util";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
type HarkState = {
|
||||||
|
notifications: Notifications;
|
||||||
|
archivedNotifications: Notifications;
|
||||||
|
notificationsCount: number;
|
||||||
|
notificationsGraphConfig: NotificationGraphConfig;
|
||||||
|
notificationsGroupConfig: GroupNotificationsConfig;
|
||||||
|
notificationsChatConfig: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HarkReducer = (json: any, state: HarkState) => {
|
||||||
|
const data = _.get(json, "harkUpdate", false);
|
||||||
|
if (data) {
|
||||||
|
reduce(data, state);
|
||||||
|
}
|
||||||
|
const graphHookData = _.get(json, "hark-graph-hook-update", false);
|
||||||
|
if (graphHookData) {
|
||||||
|
console.log(graphHookData);
|
||||||
|
graphInitial(graphHookData, state);
|
||||||
|
graphIgnore(graphHookData, state);
|
||||||
|
graphListen(graphHookData, state);
|
||||||
|
graphWatchSelf(graphHookData, state);
|
||||||
|
graphMentions(graphHookData, state);
|
||||||
|
}
|
||||||
|
const groupHookData = _.get(json, "hark-group-hook-update", false);
|
||||||
|
if (groupHookData) {
|
||||||
|
groupInitial(groupHookData, state);
|
||||||
|
groupListen(groupHookData, state);
|
||||||
|
groupIgnore(groupHookData, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatHookData = _.get(json, "hark-chat-hook-update", false);
|
||||||
|
if(chatHookData) {
|
||||||
|
|
||||||
|
chatInitial(chatHookData, state);
|
||||||
|
chatListen(chatHookData, state);
|
||||||
|
chatIgnore(chatHookData, state);
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function chatInitial(json: any, state: HarkState) {
|
||||||
|
const data = _.get(json, "initial", false);
|
||||||
|
if (data) {
|
||||||
|
state.notificationsChatConfig = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function chatListen(json: any, state: HarkState) {
|
||||||
|
const data = _.get(json, "listen", false);
|
||||||
|
if (data) {
|
||||||
|
state.notificationsChatConfig = [...state.notificationsChatConfig, data];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function chatIgnore(json: any, state: HarkState) {
|
||||||
|
const data = _.get(json, "ignore", false);
|
||||||
|
if (data) {
|
||||||
|
state.notificationsChatConfig = state.notificationsChatConfig.filter(x => x !== data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupInitial(json: any, state: HarkState) {
|
||||||
|
const data = _.get(json, "initial", false);
|
||||||
|
if (data) {
|
||||||
|
state.notificationsGroupConfig = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function graphInitial(json: any, state: HarkState) {
|
||||||
|
const data = _.get(json, "initial", false);
|
||||||
|
if (data) {
|
||||||
|
state.notificationsGraphConfig = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function graphListen(json: any, state: HarkState) {
|
||||||
|
const data = _.get(json, "listen", false);
|
||||||
|
if (data) {
|
||||||
|
state.notificationsGraphConfig.watching = [
|
||||||
|
...state.notificationsGraphConfig.watching,
|
||||||
|
data,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function graphIgnore(json: any, state: HarkState) {
|
||||||
|
const data = _.get(json, "ignore", false);
|
||||||
|
if (data) {
|
||||||
|
state.notificationsGraphConfig.watching = state.notificationsGraphConfig.watching.filter(
|
||||||
|
(n) => n !== data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupListen(json: any, state: HarkState) {
|
||||||
|
const data = _.get(json, "listen", false);
|
||||||
|
if (data) {
|
||||||
|
state.notificationsGroupConfig = [...state.notificationsGroupConfig, data];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupIgnore(json: any, state: HarkState) {
|
||||||
|
const data = _.get(json, "ignore", false);
|
||||||
|
if (data) {
|
||||||
|
state.notificationsGroupConfig = state.notificationsGroupConfig.filter(
|
||||||
|
(n) => n !== data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function graphMentions(json: any, state: HarkState) {
|
||||||
|
const data = _.get(json, "set-mentions", undefined);
|
||||||
|
if (!_.isUndefined(data)) {
|
||||||
|
state.notificationsGraphConfig.mentions = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function graphWatchSelf(json: any, state: HarkState) {
|
||||||
|
const data = _.get(json, "set-watch-on-self", undefined);
|
||||||
|
if (!_.isUndefined(data)) {
|
||||||
|
state.notificationsGraphConfig.watchOnSelf = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reduce(data: any, state: HarkState) {
|
||||||
|
unread(data, state);
|
||||||
|
read(data, state);
|
||||||
|
archive(data, state);
|
||||||
|
timebox(data, state);
|
||||||
|
more(data, state);
|
||||||
|
dnd(data, state);
|
||||||
|
count(data, state);
|
||||||
|
added(data, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
function added(json: any, state: HarkState) {
|
||||||
|
const data = _.get(json, "added", false);
|
||||||
|
if (data) {
|
||||||
|
const { index, notification } = data;
|
||||||
|
const time = makePatDa(data.time);
|
||||||
|
const timebox = state.notifications.get(time) || [];
|
||||||
|
const arrIdx = timebox.findIndex((idxNotif) =>
|
||||||
|
notifIdxEqual(index, idxNotif.index)
|
||||||
|
);
|
||||||
|
if (arrIdx !== -1) {
|
||||||
|
timebox[arrIdx] = { index, notification };
|
||||||
|
state.notifications.set(time, timebox);
|
||||||
|
} else {
|
||||||
|
state.notifications.set(time, [...timebox, { index, notification }]);
|
||||||
|
state.notificationsCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function count(json: any, state: HarkState) {
|
||||||
|
const data = _.get(json, "count", false);
|
||||||
|
if (data !== false) {
|
||||||
|
state.notificationsCount = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dnd = (json: any, state: HarkState) => {
|
||||||
|
const data = _.get(json, "set-dnd", undefined);
|
||||||
|
if (!_.isUndefined(data)) {
|
||||||
|
state.doNotDisturb = data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
||||||
|
state.notifications.set(time, data.notifications);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function more(json: any, state: HarkState) {
|
||||||
|
const data = _.get(json, "more", false);
|
||||||
|
if (data) {
|
||||||
|
_.forEach(data, (d) => reduce(d, state));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifIdxEqual(a: NotifIndex, b: NotifIndex) {
|
||||||
|
if ("graph" in a && "graph" in b) {
|
||||||
|
return (
|
||||||
|
a.graph.graph === b.graph.graph &&
|
||||||
|
a.graph.group === b.graph.group &&
|
||||||
|
a.graph.module === b.graph.module &&
|
||||||
|
a.graph.description === b.graph.description
|
||||||
|
);
|
||||||
|
} else if ("group" in a && "group" in b) {
|
||||||
|
return (
|
||||||
|
a.group.group === b.group.group &&
|
||||||
|
a.group.description === b.group.description
|
||||||
|
);
|
||||||
|
} else if ("chat" in a && "chat" in b) {
|
||||||
|
return a.chat.chat === b.chat.chat &&
|
||||||
|
a.chat.mention === b.chat.mention;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRead(
|
||||||
|
time: string,
|
||||||
|
index: NotifIndex,
|
||||||
|
read: boolean,
|
||||||
|
state: HarkState
|
||||||
|
) {
|
||||||
|
const patDa = makePatDa(time);
|
||||||
|
const timebox = state.notifications.get(patDa);
|
||||||
|
if (_.isNull(timebox)) {
|
||||||
|
console.warn("Modifying nonexistent timebox");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const arrIdx = timebox.findIndex((idxNotif) =>
|
||||||
|
notifIdxEqual(index, idxNotif.index)
|
||||||
|
);
|
||||||
|
if (arrIdx === -1) {
|
||||||
|
console.warn("Modifying nonexistent index");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
timebox[arrIdx].notification.read = read;
|
||||||
|
state.notifications.set(patDa, timebox);
|
||||||
|
}
|
||||||
|
|
||||||
|
function read(json: any, state: HarkState) {
|
||||||
|
const data = _.get(json, "read", false);
|
||||||
|
if (data) {
|
||||||
|
const { time, index } = data;
|
||||||
|
state.notificationsCount--;
|
||||||
|
setRead(time, index, true, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unread(json: any, state: HarkState) {
|
||||||
|
const data = _.get(json, "unread", false);
|
||||||
|
if (data) {
|
||||||
|
const { time, index } = data;
|
||||||
|
state.notificationsCount++;
|
||||||
|
setRead(time, index, false, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function archive(json: any, state: HarkState) {
|
||||||
|
const data = _.get(json, "archive", false);
|
||||||
|
if (data) {
|
||||||
|
const { index } = data;
|
||||||
|
const time = makePatDa(data.time);
|
||||||
|
const timebox = state.notifications.get(time);
|
||||||
|
if (!timebox) {
|
||||||
|
console.warn("Modifying nonexistent timebox");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [archived, unarchived] = _.partition(timebox, (idxNotif) =>
|
||||||
|
notifIdxEqual(index, idxNotif.index)
|
||||||
|
);
|
||||||
|
state.notifications.set(time, unarchived);
|
||||||
|
const archiveBox = state.archivedNotifications.get(time) || [];
|
||||||
|
state.notificationsCount -= archived.filter(
|
||||||
|
({ notification }) => !notification.read
|
||||||
|
).length;
|
||||||
|
state.archivedNotifications.set(time, [
|
||||||
|
...archiveBox,
|
||||||
|
...archived.map(({ notification, index }) => ({
|
||||||
|
notification: { ...notification, read: true },
|
||||||
|
index,
|
||||||
|
})),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -5,13 +5,17 @@ import LocalReducer from '../reducers/local';
|
|||||||
import ChatReducer from '../reducers/chat-update';
|
import ChatReducer from '../reducers/chat-update';
|
||||||
|
|
||||||
import { StoreState } from './type';
|
import { StoreState } from './type';
|
||||||
|
import { Timebox } from '~/types';
|
||||||
import { Cage } from '~/types/cage';
|
import { Cage } from '~/types/cage';
|
||||||
import ContactReducer from '../reducers/contact-update';
|
import ContactReducer from '../reducers/contact-update';
|
||||||
import S3Reducer from '../reducers/s3-update';
|
import S3Reducer from '../reducers/s3-update';
|
||||||
import { GraphReducer } from '../reducers/graph-update';
|
import { GraphReducer } from '../reducers/graph-update';
|
||||||
|
import { HarkReducer } from '../reducers/hark-update';
|
||||||
import GroupReducer from '../reducers/group-update';
|
import GroupReducer from '../reducers/group-update';
|
||||||
import LaunchReducer from '../reducers/launch-update';
|
import LaunchReducer from '../reducers/launch-update';
|
||||||
import ConnectionReducer from '../reducers/connection';
|
import ConnectionReducer from '../reducers/connection';
|
||||||
|
import {OrderedMap} from '../lib/OrderedMap';
|
||||||
|
import { BigIntOrderedMap } from '../lib/BigIntOrderedMap';
|
||||||
|
|
||||||
export const homeAssociation = {
|
export const homeAssociation = {
|
||||||
"app-path": "/home",
|
"app-path": "/home",
|
||||||
@ -93,6 +97,16 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
|||||||
dark: false,
|
dark: false,
|
||||||
inbox: {},
|
inbox: {},
|
||||||
chatSynced: null,
|
chatSynced: null,
|
||||||
|
notifications: new BigIntOrderedMap<Timebox>(),
|
||||||
|
archivedNotifications: new BigIntOrderedMap<Timebox>(),
|
||||||
|
notificationsGroupConfig: [],
|
||||||
|
notificationsChatConfig: [],
|
||||||
|
notificationsGraphConfig: {
|
||||||
|
watchOnSelf: false,
|
||||||
|
mentions: false,
|
||||||
|
watching: [],
|
||||||
|
},
|
||||||
|
notificationsCount: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,5 +121,6 @@ export default class GlobalStore extends BaseStore<StoreState> {
|
|||||||
this.launchReducer.reduce(data, this.state);
|
this.launchReducer.reduce(data, this.state);
|
||||||
this.connReducer.reduce(data, this.state);
|
this.connReducer.reduce(data, this.state);
|
||||||
GraphReducer(data, this.state);
|
GraphReducer(data, this.state);
|
||||||
|
HarkReducer(data, this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,13 @@ import { S3State } from '~/types/s3-update';
|
|||||||
import { LaunchState, WeatherState } from '~/types/launch-update';
|
import { LaunchState, WeatherState } from '~/types/launch-update';
|
||||||
import { ConnectionStatus } from '~/types/connection';
|
import { ConnectionStatus } from '~/types/connection';
|
||||||
import {Graphs} from '~/types/graph-update';
|
import {Graphs} from '~/types/graph-update';
|
||||||
import { BackgroundConfig, LocalUpdateRemoteContentPolicy } from '~/types/local-update';
|
import {
|
||||||
|
Notifications,
|
||||||
|
NotificationGraphConfig,
|
||||||
|
GroupNotificationsConfig,
|
||||||
|
LocalUpdateRemoteContentPolicy,
|
||||||
|
BackgroundConfig
|
||||||
|
} from "~/types";
|
||||||
|
|
||||||
export interface StoreState {
|
export interface StoreState {
|
||||||
// local state
|
// local state
|
||||||
@ -45,11 +51,18 @@ export interface StoreState {
|
|||||||
userLocation: string | null;
|
userLocation: string | null;
|
||||||
|
|
||||||
// publish state
|
// publish state
|
||||||
notebooks: Notebooks;
|
notebooks: any;
|
||||||
|
|
||||||
// Chat state
|
// Chat state
|
||||||
chatInitialized: boolean;
|
chatInitialized: boolean;
|
||||||
chatSynced: ChatHookUpdate | null;
|
chatSynced: ChatHookUpdate | null;
|
||||||
inbox: Inbox;
|
inbox: Inbox;
|
||||||
pendingMessages: Map<Path, Envelope[]>;
|
pendingMessages: Map<Path, Envelope[]>;
|
||||||
|
|
||||||
|
notifications: Notifications;
|
||||||
|
notificationsGraphConfig: NotificationGraphConfig;
|
||||||
|
notificationsGroupConfig: GroupNotificationsConfig;
|
||||||
|
notificationsChatConfig: string[];
|
||||||
|
notificationsCount: number,
|
||||||
|
doNotDisturb: boolean;
|
||||||
}
|
}
|
||||||
|
@ -51,6 +51,10 @@ export default class GlobalSubscription extends BaseSubscription<StoreState> {
|
|||||||
this.subscribe('/all', 'launch');
|
this.subscribe('/all', 'launch');
|
||||||
this.subscribe('/all', 'weather');
|
this.subscribe('/all', 'weather');
|
||||||
this.subscribe('/keys', 'graph-store');
|
this.subscribe('/keys', 'graph-store');
|
||||||
|
this.subscribe('/updates', 'hark-store');
|
||||||
|
this.subscribe('/updates', 'hark-graph-hook');
|
||||||
|
this.subscribe('/updates', 'hark-group-hook');
|
||||||
|
this.subscribe('/updates', 'hark-chat-hook');
|
||||||
}
|
}
|
||||||
|
|
||||||
restart() {
|
restart() {
|
||||||
|
@ -1,12 +1,28 @@
|
|||||||
import { Patp } from "./noun";
|
import { Patp } from "./noun";
|
||||||
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
|
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
|
||||||
|
|
||||||
|
export interface TextContent {
|
||||||
export interface TextContent { text: string; };
|
text: string;
|
||||||
export interface UrlContent { url: string; }
|
}
|
||||||
export interface CodeContent { expresssion: string; output: string; };
|
export interface UrlContent {
|
||||||
export interface ReferenceContent { uid: string; }
|
url: string;
|
||||||
export type Content = TextContent | UrlContent | CodeContent | ReferenceContent;
|
}
|
||||||
|
export interface CodeContent {
|
||||||
|
expresssion: string;
|
||||||
|
output: string;
|
||||||
|
}
|
||||||
|
export interface ReferenceContent {
|
||||||
|
uid: string;
|
||||||
|
}
|
||||||
|
export interface MentionContent {
|
||||||
|
mention: string;
|
||||||
|
}
|
||||||
|
export type Content =
|
||||||
|
| TextContent
|
||||||
|
| UrlContent
|
||||||
|
| CodeContent
|
||||||
|
| ReferenceContent
|
||||||
|
| MentionContent;
|
||||||
|
|
||||||
export interface Post {
|
export interface Post {
|
||||||
author: Patp;
|
author: Patp;
|
||||||
@ -15,10 +31,9 @@ export interface Post {
|
|||||||
index: string;
|
index: string;
|
||||||
pending?: boolean;
|
pending?: boolean;
|
||||||
signatures: string[];
|
signatures: string[];
|
||||||
'time-sent': number;
|
"time-sent": number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface GraphNode {
|
export interface GraphNode {
|
||||||
children: Graph;
|
children: Graph;
|
||||||
post: Post;
|
post: Post;
|
||||||
@ -27,5 +42,3 @@ export interface GraphNode {
|
|||||||
export type Graph = BigIntOrderedMap<GraphNode>;
|
export type Graph = BigIntOrderedMap<GraphNode>;
|
||||||
|
|
||||||
export type Graphs = { [rid: string]: Graph };
|
export type Graphs = { [rid: string]: Graph };
|
||||||
|
|
||||||
|
|
||||||
|
63
pkg/interface/src/types/hark-update.ts
Normal file
63
pkg/interface/src/types/hark-update.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import _ from "lodash";
|
||||||
|
import { Post } from "./graph-update";
|
||||||
|
import { GroupUpdate } from "./group-update";
|
||||||
|
import { BigIntOrderedMap } from "~/logic/lib/BigIntOrderedMap";
|
||||||
|
import { Envelope } from './chat-update';
|
||||||
|
|
||||||
|
type GraphNotifDescription = "link" | "comment";
|
||||||
|
|
||||||
|
export interface GraphNotifIndex {
|
||||||
|
graph: string;
|
||||||
|
group: string;
|
||||||
|
description: GraphNotifDescription;
|
||||||
|
module: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupNotifIndex {
|
||||||
|
group: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatNotifIndex {
|
||||||
|
chat: string;
|
||||||
|
mention: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NotifIndex =
|
||||||
|
| { graph: GraphNotifIndex }
|
||||||
|
| { group: GroupNotifIndex }
|
||||||
|
| { chat: ChatNotifIndex };
|
||||||
|
|
||||||
|
export type GraphNotificationContents = Post[];
|
||||||
|
|
||||||
|
export type GroupNotificationContents = GroupUpdate[];
|
||||||
|
|
||||||
|
export type ChatNotificationContents = Envelope[];
|
||||||
|
|
||||||
|
export type NotificationContents =
|
||||||
|
| { graph: GraphNotificationContents }
|
||||||
|
| { group: GroupNotificationContents }
|
||||||
|
| { chat: ChatNotificationContents };
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
read: boolean;
|
||||||
|
time: number;
|
||||||
|
contents: NotificationContents;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IndexedNotification {
|
||||||
|
index: NotifIndex;
|
||||||
|
notification: Notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Timebox = IndexedNotification[];
|
||||||
|
|
||||||
|
export type Notifications = BigIntOrderedMap<Timebox>;
|
||||||
|
|
||||||
|
export interface NotificationGraphConfig {
|
||||||
|
watchOnSelf: boolean;
|
||||||
|
mentions: boolean;
|
||||||
|
watching: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GroupNotificationsConfig = string[];
|
@ -6,6 +6,7 @@ export * from './contact-update';
|
|||||||
export * from './global';
|
export * from './global';
|
||||||
export * from './group-update';
|
export * from './group-update';
|
||||||
export * from './graph-update';
|
export * from './graph-update';
|
||||||
|
export * from './hark-update';
|
||||||
export * from './invite-update';
|
export * from './invite-update';
|
||||||
export * from './launch-update';
|
export * from './launch-update';
|
||||||
export * from './local-update';
|
export * from './local-update';
|
||||||
|
@ -125,6 +125,9 @@ class App extends React.Component {
|
|||||||
const theme = state.dark ? dark : light;
|
const theme = state.dark ? dark : light;
|
||||||
const { background } = state;
|
const { background } = state;
|
||||||
|
|
||||||
|
const notificationsCount = state.notificationsCount || 0;
|
||||||
|
const doNotDisturb = state.doNotDisturb || false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@ -143,6 +146,8 @@ class App extends React.Component {
|
|||||||
connection={this.state.connection}
|
connection={this.state.connection}
|
||||||
subscription={this.subscription}
|
subscription={this.subscription}
|
||||||
ship={this.ship}
|
ship={this.ship}
|
||||||
|
doNotDisturb={doNotDisturb}
|
||||||
|
notificationsCount={notificationsCount}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useRef, useCallback } from 'react';
|
import React, { useRef, useCallback, useEffect } from 'react';
|
||||||
import { RouteComponentProps } from 'react-router-dom';
|
import { RouteComponentProps } from 'react-router-dom';
|
||||||
import { Col } from '@tlon/indigo-react';
|
import { Col } from '@tlon/indigo-react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@ -93,6 +93,15 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
station
|
station
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const scrollTo = new URLSearchParams(location.search).get('msg');
|
||||||
|
useEffect(() => {
|
||||||
|
const clear = () => {
|
||||||
|
props.history.replace(location.pathname);
|
||||||
|
};
|
||||||
|
setTimeout(clear, 10000);
|
||||||
|
return clear;
|
||||||
|
}, [station]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Col {...bind} height="100%" overflow="hidden" position="relative">
|
<Col {...bind} height="100%" overflow="hidden" position="relative">
|
||||||
{dragging && <SubmitDragger />}
|
{dragging && <SubmitDragger />}
|
||||||
@ -117,6 +126,7 @@ export function ChatResource(props: ChatResourceProps) {
|
|||||||
hideNicknames={props.hideNicknames}
|
hideNicknames={props.hideNicknames}
|
||||||
hideAvatars={props.hideAvatars}
|
hideAvatars={props.hideAvatars}
|
||||||
location={props.location}
|
location={props.location}
|
||||||
|
scrollTo={scrollTo ? parseInt(scrollTo, 10) : undefined}
|
||||||
/>
|
/>
|
||||||
<ChatInput
|
<ChatInput
|
||||||
ref={chatInput}
|
ref={chatInput}
|
||||||
|
@ -31,8 +31,8 @@ export const DayBreak = ({ when }) => (
|
|||||||
interface ChatMessageProps {
|
interface ChatMessageProps {
|
||||||
measure(element): void;
|
measure(element): void;
|
||||||
msg: Envelope | IMessage;
|
msg: Envelope | IMessage;
|
||||||
previousMsg: Envelope | IMessage | undefined;
|
previousMsg?: Envelope | IMessage;
|
||||||
nextMsg: Envelope | IMessage | undefined;
|
nextMsg?: Envelope | IMessage;
|
||||||
isLastRead: boolean;
|
isLastRead: boolean;
|
||||||
group: Group;
|
group: Group;
|
||||||
association: Association;
|
association: Association;
|
||||||
@ -48,6 +48,7 @@ interface ChatMessageProps {
|
|||||||
unreadMarkerRef: React.RefObject<HTMLDivElement>;
|
unreadMarkerRef: React.RefObject<HTMLDivElement>;
|
||||||
history: any;
|
history: any;
|
||||||
api: any;
|
api: any;
|
||||||
|
highlighted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ChatMessage extends Component<ChatMessageProps> {
|
export default class ChatMessage extends Component<ChatMessageProps> {
|
||||||
@ -84,7 +85,8 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
isLastMessage,
|
isLastMessage,
|
||||||
unreadMarkerRef,
|
unreadMarkerRef,
|
||||||
history,
|
history,
|
||||||
api
|
api,
|
||||||
|
highlighted
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const renderSigil = Boolean((nextMsg && msg.author !== nextMsg.author) || !nextMsg || msg.number === 1);
|
const renderSigil = Boolean((nextMsg && msg.author !== nextMsg.author) || !nextMsg || msg.number === 1);
|
||||||
@ -115,7 +117,8 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
isPending,
|
isPending,
|
||||||
history,
|
history,
|
||||||
api,
|
api,
|
||||||
scrollWindow
|
scrollWindow,
|
||||||
|
highlighted
|
||||||
};
|
};
|
||||||
|
|
||||||
const unreadContainerStyle = {
|
const unreadContainerStyle = {
|
||||||
@ -124,6 +127,7 @@ export default class ChatMessage extends Component<ChatMessageProps> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
bg={highlighted ? 'washedBlue' : 'white'}
|
||||||
width='100%'
|
width='100%'
|
||||||
display='flex'
|
display='flex'
|
||||||
flexWrap='wrap'
|
flexWrap='wrap'
|
||||||
@ -165,6 +169,8 @@ interface MessageProps {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class MessageWithSigil extends PureComponent<MessageProps> {
|
export class MessageWithSigil extends PureComponent<MessageProps> {
|
||||||
|
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
msg,
|
msg,
|
||||||
@ -176,8 +182,8 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
|
|||||||
hideAvatars,
|
hideAvatars,
|
||||||
remoteContentPolicy,
|
remoteContentPolicy,
|
||||||
measure,
|
measure,
|
||||||
history,
|
|
||||||
api,
|
api,
|
||||||
|
history,
|
||||||
scrollWindow
|
scrollWindow
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@ -185,8 +191,8 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
|
|||||||
const contact = msg.author in contacts ? contacts[msg.author] : false;
|
const contact = msg.author in contacts ? contacts[msg.author] : false;
|
||||||
const showNickname = !hideNicknames && contact && contact.nickname;
|
const showNickname = !hideNicknames && contact && contact.nickname;
|
||||||
const name = showNickname ? contact.nickname : cite(msg.author);
|
const name = showNickname ? contact.nickname : cite(msg.author);
|
||||||
const color = contact ? `#${uxToHex(contact.color)}` : '#000000';
|
const color = contact ? `#${uxToHex(contact.color)}` : this.isDark ? '#000000' :'#FFFFFF'
|
||||||
const sigilClass = contact ? '' : 'mix-blend-diff';
|
const sigilClass = contact ? '' : this.isDark ? 'mix-blend-diff' : 'mix-blend-darken';
|
||||||
|
|
||||||
let nameSpan = null;
|
let nameSpan = null;
|
||||||
|
|
||||||
@ -213,7 +219,7 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
|
|||||||
scrollWindow={scrollWindow}
|
scrollWindow={scrollWindow}
|
||||||
history={history}
|
history={history}
|
||||||
api={api}
|
api={api}
|
||||||
className="fl pr3 v-top bg-white bg-gray0-d pt1"
|
className="fl pr3 v-top pt1"
|
||||||
/>
|
/>
|
||||||
<Box flexGrow='1' display='block' className="clamp-message">
|
<Box flexGrow='1' display='block' className="clamp-message">
|
||||||
<Box
|
<Box
|
||||||
@ -239,7 +245,7 @@ export class MessageWithSigil extends PureComponent<MessageProps> {
|
|||||||
<Text flexShrink='0' gray mono className="v-mid">{timestamp}</Text>
|
<Text flexShrink='0' gray mono className="v-mid">{timestamp}</Text>
|
||||||
<Text gray mono ml={2} className="v-mid child dn-s">{datestamp}</Text>
|
<Text gray mono ml={2} className="v-mid child dn-s">{datestamp}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box fontSize='14px'><MessageContent content={msg.letter} remoteContentPolicy={remoteContentPolicy} measure={measure} /></Box>
|
<Box fontSize='14px'><MessageContent content={msg.letter} remoteContentPolicy={remoteContentPolicy} measure={measure} /></Box>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -43,6 +43,7 @@ type ChatWindowProps = RouteComponentProps<{
|
|||||||
hideNicknames: boolean;
|
hideNicknames: boolean;
|
||||||
hideAvatars: boolean;
|
hideAvatars: boolean;
|
||||||
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||||
|
scrollTo?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatWindowState {
|
interface ChatWindowState {
|
||||||
@ -84,6 +85,10 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
|||||||
window.addEventListener('focus', this.handleWindowFocus);
|
window.addEventListener('focus', this.handleWindowFocus);
|
||||||
this.initialFetch();
|
this.initialFetch();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
if(this.props.scrollTo) {
|
||||||
|
this.scrollToUnread();
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ initialized: true });
|
this.setState({ initialized: true });
|
||||||
}, this.INITIALIZATION_MAX_TIME);
|
}, this.INITIALIZATION_MAX_TIME);
|
||||||
}
|
}
|
||||||
@ -167,8 +172,9 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollToUnread() {
|
scrollToUnread() {
|
||||||
const { mailboxSize, unreadCount } = this.props;
|
const { mailboxSize, unreadCount, scrollTo } = this.props;
|
||||||
this.virtualList?.scrollToData(mailboxSize - unreadCount);
|
const target = scrollTo || (mailboxSize - unreadCount);
|
||||||
|
this.virtualList?.scrollToData(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
dismissUnread() {
|
dismissUnread() {
|
||||||
@ -297,7 +303,8 @@ export default class ChatWindow extends Component<ChatWindowProps, ChatWindowSta
|
|||||||
const isPending: boolean = 'pending' in msg && Boolean(msg.pending);
|
const isPending: boolean = 'pending' in msg && Boolean(msg.pending);
|
||||||
const isLastMessage: boolean = Boolean(index === lastMessage)
|
const isLastMessage: boolean = Boolean(index === lastMessage)
|
||||||
const isLastRead: boolean = Boolean(!isLastMessage && index === this.state.lastRead);
|
const isLastRead: boolean = Boolean(!isLastMessage && index === this.state.lastRead);
|
||||||
const props = { measure, scrollWindow, isPending, isLastRead, isLastMessage, msg, ...messageProps };
|
const highlighted = index === this.props.scrollTo;
|
||||||
|
const props = { measure, highlighted, scrollWindow, isPending, isLastRead, isLastMessage, msg, ...messageProps };
|
||||||
return (
|
return (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key={index}
|
key={index}
|
||||||
|
@ -87,6 +87,10 @@ h2 {
|
|||||||
mix-blend-mode: difference;
|
mix-blend-mode: difference;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mix-blend-darken {
|
||||||
|
mix-blend-mode: darken;
|
||||||
|
}
|
||||||
|
|
||||||
.placeholder-inter::placeholder {
|
.placeholder-inter::placeholder {
|
||||||
font-family: "Inter", sans-serif;
|
font-family: "Inter", sans-serif;
|
||||||
}
|
}
|
||||||
|
99
pkg/interface/src/views/apps/notifications/chat.tsx
Normal file
99
pkg/interface/src/views/apps/notifications/chat.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import React, { useCallback } from "react";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import GlobalApi from "~/logic/api/global";
|
||||||
|
import {
|
||||||
|
Rolodex,
|
||||||
|
Associations,
|
||||||
|
ChatNotifIndex,
|
||||||
|
ChatNotificationContents,
|
||||||
|
Groups,
|
||||||
|
} from "~/types";
|
||||||
|
import { BigInteger } from "big-integer";
|
||||||
|
import { Box, Col } from "@tlon/indigo-react";
|
||||||
|
import { Header } from "./header";
|
||||||
|
import { pluralize } from "~/logic/lib/util";
|
||||||
|
import ChatMessage from "../chat/components/ChatMessage";
|
||||||
|
|
||||||
|
function describeNotification(mention: boolean, lent: number) {
|
||||||
|
const msg = pluralize("message", lent !== 1);
|
||||||
|
if (mention) {
|
||||||
|
return `mentioned you in ${msg} in`;
|
||||||
|
}
|
||||||
|
return `sent ${msg} in`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatNotification(props: {
|
||||||
|
index: ChatNotifIndex;
|
||||||
|
contents: ChatNotificationContents;
|
||||||
|
archived: boolean;
|
||||||
|
read: boolean;
|
||||||
|
time: number;
|
||||||
|
timebox: BigInteger;
|
||||||
|
associations: Associations;
|
||||||
|
contacts: Rolodex;
|
||||||
|
groups: Groups;
|
||||||
|
api: GlobalApi;
|
||||||
|
}) {
|
||||||
|
const { index, contents, read, time, api, timebox } = props;
|
||||||
|
const authors = _.map(contents, "author");
|
||||||
|
|
||||||
|
const { chat, mention } = index;
|
||||||
|
const association = props.associations.chat[chat];
|
||||||
|
const groupPath = association["group-path"];
|
||||||
|
const appPath = association["app-path"];
|
||||||
|
|
||||||
|
const group = props.groups[groupPath];
|
||||||
|
|
||||||
|
const desc = describeNotification(mention, contents.length);
|
||||||
|
const groupContacts = props.contacts[groupPath];
|
||||||
|
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
if (props.archived) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const func = read ? "unread" : "read";
|
||||||
|
return api.hark[func](timebox, { chat: index });
|
||||||
|
}, [api, timebox, index, read]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col onClick={onClick} flexGrow="1" p="2">
|
||||||
|
<Header
|
||||||
|
chat
|
||||||
|
associations={props.associations}
|
||||||
|
read={read}
|
||||||
|
archived={props.archived}
|
||||||
|
time={time}
|
||||||
|
authors={authors}
|
||||||
|
moduleIcon="Chat"
|
||||||
|
channel={chat}
|
||||||
|
contacts={props.contacts}
|
||||||
|
group={groupPath}
|
||||||
|
description={desc}
|
||||||
|
/>
|
||||||
|
<Col pb="3" pl="5">
|
||||||
|
{_.map(_.take(contents, 5), (content, idx) => {
|
||||||
|
const to = `/~landscape${groupPath}/resource/chat${appPath}?msg=${content.number}`;
|
||||||
|
return (
|
||||||
|
<Link key={idx} to={to}>
|
||||||
|
<ChatMessage
|
||||||
|
measure={() => {}}
|
||||||
|
msg={content}
|
||||||
|
isLastRead={false}
|
||||||
|
group={group}
|
||||||
|
contacts={groupContacts}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{contents.length > 5 && (
|
||||||
|
<Box ml="4" mt="3" mb="2" color="gray" fontSize="14px">
|
||||||
|
and {contents.length - 5} other message
|
||||||
|
{contents.length > 6 ? "s" : ""}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
231
pkg/interface/src/views/apps/notifications/graph.tsx
Normal file
231
pkg/interface/src/views/apps/notifications/graph.tsx
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
import React, { ReactNode, useCallback } from "react";
|
||||||
|
import moment from "moment";
|
||||||
|
import { Row, Box, Col, Text, Anchor, Icon, Action } from "@tlon/indigo-react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import _ from "lodash";
|
||||||
|
import {
|
||||||
|
Post,
|
||||||
|
GraphNotifIndex,
|
||||||
|
GraphNotificationContents,
|
||||||
|
Associations,
|
||||||
|
Content,
|
||||||
|
Rolodex,
|
||||||
|
} from "~/types";
|
||||||
|
import { Header } from "./header";
|
||||||
|
import { cite, deSig, pluralize } from "~/logic/lib/util";
|
||||||
|
import { Sigil } from "~/logic/lib/sigil";
|
||||||
|
import RichText from "~/views/components/RichText";
|
||||||
|
import GlobalApi from "~/logic/api/global";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import { getSnippet } from "~/logic/lib/publish";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
function getGraphModuleIcon(module: string) {
|
||||||
|
if (module === "link") {
|
||||||
|
return "Links";
|
||||||
|
}
|
||||||
|
return _.capitalize(module);
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilterBox = styled(Box)`
|
||||||
|
background: linear-gradient(
|
||||||
|
${(p) => p.theme.colors.scales.white10} 0%,
|
||||||
|
${(p) => p.theme.colors.scales.white60} 40%,
|
||||||
|
${(p) => p.theme.colors.scales.white100} 100%
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
function describeNotification(description: string, plural: boolean) {
|
||||||
|
switch (description) {
|
||||||
|
case "link":
|
||||||
|
return `added ${pluralize("new link", plural)} to`;
|
||||||
|
case "comment":
|
||||||
|
return `left ${pluralize("comment", plural)} on`;
|
||||||
|
case "note":
|
||||||
|
return `posted ${pluralize("note", plural)} to`;
|
||||||
|
case "mention":
|
||||||
|
return "mentioned you on";
|
||||||
|
default:
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const GraphUrl = ({ url, title }) => (
|
||||||
|
<Box borderRadius="1" p="2" bg="washedGray">
|
||||||
|
<Anchor target="_blank" color="gray" href={url}>
|
||||||
|
<Icon verticalAlign="bottom" mr="2" icon="ArrowExternal" />
|
||||||
|
{title}
|
||||||
|
</Anchor>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
|
const GraphNodeContent = ({ contents, mod, description, index }) => {
|
||||||
|
const idx = index.slice(1).split("/");
|
||||||
|
if (mod === "link") {
|
||||||
|
if (idx.length === 1) {
|
||||||
|
const [{ text }, { url }] = contents;
|
||||||
|
return <GraphUrl title={text} url={url} />;
|
||||||
|
} else if (idx.length === 2) {
|
||||||
|
const [{ text }] = contents;
|
||||||
|
return <RichText>{text}</RichText>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (mod === "publish") {
|
||||||
|
if (idx.length !== 3) {
|
||||||
|
return null;
|
||||||
|
} else if (idx[1] === "2") {
|
||||||
|
const [{ text }] = contents;
|
||||||
|
return <RichText>{text}</RichText>;
|
||||||
|
} else if (idx[1] === "1") {
|
||||||
|
const [{ text: header }, { text: body }] = contents;
|
||||||
|
const snippet = getSnippet(body);
|
||||||
|
return (
|
||||||
|
<Col>
|
||||||
|
<Box mb="2" fontWeight="500">
|
||||||
|
{header}
|
||||||
|
</Box>
|
||||||
|
<Box overflow="hidden" maxHeight="400px">
|
||||||
|
{snippet}
|
||||||
|
<FilterBox
|
||||||
|
width="100%"
|
||||||
|
zIndex="1"
|
||||||
|
height="calc(100% - 2em)"
|
||||||
|
bottom="0px"
|
||||||
|
position="absolute"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getNodeUrl(mod: string, group: string, graph: string, index: string) {
|
||||||
|
const graphUrl = `/~landscape${group}/resource/${mod}${graph}`;
|
||||||
|
const idx = index.slice(1).split("/");
|
||||||
|
if (mod === "publish") {
|
||||||
|
const [noteId] = idx;
|
||||||
|
return `${graphUrl}/note/${noteId}`;
|
||||||
|
} else if (mod === "link") {
|
||||||
|
const [linkId] = idx;
|
||||||
|
return `${graphUrl}/-${linkId}`;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const GraphNode = ({
|
||||||
|
contents,
|
||||||
|
author,
|
||||||
|
mod,
|
||||||
|
description,
|
||||||
|
time,
|
||||||
|
index,
|
||||||
|
graph,
|
||||||
|
group,
|
||||||
|
}) => {
|
||||||
|
author = deSig(author);
|
||||||
|
|
||||||
|
const img = (
|
||||||
|
<Sigil
|
||||||
|
ship={`~${author}`}
|
||||||
|
size={16}
|
||||||
|
icon
|
||||||
|
color={`#000000`}
|
||||||
|
classes="mix-blend-diff"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodeUrl = getNodeUrl(mod, group, graph, index);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={nodeUrl}>
|
||||||
|
<Row gapX="2" py="2">
|
||||||
|
<Col>{img}</Col>
|
||||||
|
<Col alignItems="flex-start">
|
||||||
|
<Row
|
||||||
|
mb="2"
|
||||||
|
height="16px"
|
||||||
|
alignItems="center"
|
||||||
|
p="1"
|
||||||
|
backgroundColor="white"
|
||||||
|
>
|
||||||
|
<Text mono title={author}>
|
||||||
|
{cite(author)}
|
||||||
|
</Text>
|
||||||
|
<Text ml="2" gray>
|
||||||
|
{moment(time).format("HH:mm")}
|
||||||
|
</Text>
|
||||||
|
</Row>
|
||||||
|
<Row p="1">
|
||||||
|
<GraphNodeContent
|
||||||
|
contents={contents}
|
||||||
|
mod={mod}
|
||||||
|
description={description}
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function GraphNotification(props: {
|
||||||
|
index: GraphNotifIndex;
|
||||||
|
contents: GraphNotificationContents;
|
||||||
|
archived: boolean;
|
||||||
|
read: boolean;
|
||||||
|
time: number;
|
||||||
|
timebox: BigInteger;
|
||||||
|
associations: Associations;
|
||||||
|
contacts: Rolodex;
|
||||||
|
api: GlobalApi;
|
||||||
|
}) {
|
||||||
|
const { contents, index, read, time, api, timebox } = props;
|
||||||
|
|
||||||
|
const authors = _.map(contents, "author");
|
||||||
|
const { graph, group } = index;
|
||||||
|
const icon = getGraphModuleIcon(index.module);
|
||||||
|
const desc = describeNotification(index.description, contents.length !== 1);
|
||||||
|
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
if (props.archived) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const func = read ? "unread" : "read";
|
||||||
|
return api.hark[func](timebox, { graph: index });
|
||||||
|
}, [api, timebox, index, read]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col onClick={onClick} p="2">
|
||||||
|
<Header
|
||||||
|
archived={props.archived}
|
||||||
|
time={time}
|
||||||
|
read={read}
|
||||||
|
authors={authors}
|
||||||
|
moduleIcon={icon}
|
||||||
|
channel={graph}
|
||||||
|
contacts={props.contacts}
|
||||||
|
group={group}
|
||||||
|
description={desc}
|
||||||
|
associations={props.associations}
|
||||||
|
/>
|
||||||
|
<Col pl="5">
|
||||||
|
{_.map(contents, (content, idx) => (
|
||||||
|
<GraphNode
|
||||||
|
author={content.author}
|
||||||
|
contents={content.contents}
|
||||||
|
mod={index.module}
|
||||||
|
time={content["time-sent"]}
|
||||||
|
description={index.description}
|
||||||
|
index={content.index}
|
||||||
|
graph={graph}
|
||||||
|
group={group}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Col>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
90
pkg/interface/src/views/apps/notifications/group.tsx
Normal file
90
pkg/interface/src/views/apps/notifications/group.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import React, { ReactNode, useCallback } from "react";
|
||||||
|
import moment from "moment";
|
||||||
|
import { Row, Box, Col, Text, Anchor, Icon, Action } from "@tlon/indigo-react";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { NotificationProps } from "./types";
|
||||||
|
import {
|
||||||
|
Post,
|
||||||
|
GraphNotifIndex,
|
||||||
|
GraphNotificationContents,
|
||||||
|
Associations,
|
||||||
|
Content,
|
||||||
|
IndexedNotification,
|
||||||
|
GroupNotificationContents,
|
||||||
|
GroupNotifIndex,
|
||||||
|
GroupUpdate,
|
||||||
|
Rolodex,
|
||||||
|
} from "~/types";
|
||||||
|
import { Header } from "./header";
|
||||||
|
import { cite, deSig } from "~/logic/lib/util";
|
||||||
|
import { Sigil } from "~/logic/lib/sigil";
|
||||||
|
import RichText from "~/views/components/RichText";
|
||||||
|
import GlobalApi from "~/logic/api/global";
|
||||||
|
import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction";
|
||||||
|
|
||||||
|
|
||||||
|
function describeNotification(description: string, plural: boolean) {
|
||||||
|
switch (description) {
|
||||||
|
case "add-members":
|
||||||
|
return "joined";
|
||||||
|
case "remove-members":
|
||||||
|
return "left";
|
||||||
|
default:
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGroupUpdateParticipants(update: GroupUpdate) {
|
||||||
|
if ("addMembers" in update) {
|
||||||
|
return update.addMembers.ships;
|
||||||
|
}
|
||||||
|
if ("removeMembers" in update) {
|
||||||
|
return update.removeMembers.ships;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupNotificationProps {
|
||||||
|
index: GroupNotifIndex;
|
||||||
|
contents: GroupNotificationContents;
|
||||||
|
archived: boolean;
|
||||||
|
read: boolean;
|
||||||
|
time: number;
|
||||||
|
timebox: BigInteger;
|
||||||
|
associations: Associations;
|
||||||
|
contacts: Rolodex;
|
||||||
|
api: GlobalApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GroupNotification(props: GroupNotificationProps) {
|
||||||
|
const { contents, index, read, time, api, timebox, associations } = props;
|
||||||
|
|
||||||
|
const authors = _.flatten(_.map(contents, getGroupUpdateParticipants));
|
||||||
|
|
||||||
|
const { group } = index;
|
||||||
|
const desc = describeNotification(index.description, contents.length !== 1);
|
||||||
|
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
if (props.archived) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const func = read ? "unread" : "read";
|
||||||
|
return api.hark[func](timebox, { group: index });
|
||||||
|
}, [api, timebox, index, read]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col onClick={onClick} p="2">
|
||||||
|
<Header
|
||||||
|
archived={props.archived}
|
||||||
|
time={time}
|
||||||
|
read={read}
|
||||||
|
group={group}
|
||||||
|
contacts={props.contacts}
|
||||||
|
authors={authors}
|
||||||
|
description={desc}
|
||||||
|
associations={associations}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
107
pkg/interface/src/views/apps/notifications/header.tsx
Normal file
107
pkg/interface/src/views/apps/notifications/header.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Text as NormalText, Row, Icon } from "@tlon/indigo-react";
|
||||||
|
import f from "lodash/fp";
|
||||||
|
import _ from "lodash";
|
||||||
|
import moment from "moment";
|
||||||
|
import { PropFunc } from "~/types/util";
|
||||||
|
import { getContactDetails } from "~/logic/lib/util";
|
||||||
|
import { Associations, Contact, Contacts, Rolodex } from "~/types";
|
||||||
|
|
||||||
|
const Text = (props: PropFunc<typeof Text>) => (
|
||||||
|
<NormalText fontWeight="500" {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const Divider = (props: PropFunc<typeof Text>) => (
|
||||||
|
<Text lineHeight="tall" mx="1" fontWeight="bold" color="lightGray">
|
||||||
|
|
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
|
||||||
|
function Author(props: { patp: string; contacts: Contacts; last?: boolean }) {
|
||||||
|
const contact: Contact | undefined = props.contacts?.[props.patp];
|
||||||
|
|
||||||
|
const showNickname = !!contact?.nickname;
|
||||||
|
const name = contact?.nickname || `~${props.patp}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text mono={!showNickname}>
|
||||||
|
{name}
|
||||||
|
{!props.last && ", "}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Header(props: {
|
||||||
|
authors: string[];
|
||||||
|
archived?: boolean;
|
||||||
|
channel?: string;
|
||||||
|
group: string;
|
||||||
|
contacts: Rolodex;
|
||||||
|
description: string;
|
||||||
|
moduleIcon?: string;
|
||||||
|
time: number;
|
||||||
|
read: boolean;
|
||||||
|
associations: Associations;
|
||||||
|
chat?: boolean;
|
||||||
|
}) {
|
||||||
|
const { description, channel, group, moduleIcon, read } = props;
|
||||||
|
const contacts = props.contacts[group] || {};
|
||||||
|
|
||||||
|
const authors = _.uniq(props.authors);
|
||||||
|
|
||||||
|
const authorDesc = f.flow(
|
||||||
|
f.take(3),
|
||||||
|
f.entries,
|
||||||
|
f.map(([idx, p]: [string, string]) => {
|
||||||
|
const lent = Math.min(3, authors.length);
|
||||||
|
const last = lent - 1 === parseInt(idx, 10);
|
||||||
|
return <Author key={idx} contacts={contacts} patp={p} last={last} />;
|
||||||
|
}),
|
||||||
|
(auths) => (
|
||||||
|
<React.Fragment>
|
||||||
|
{auths}
|
||||||
|
|
||||||
|
{authors.length > 3 &&
|
||||||
|
` and ${authors.length - 3} other${authors.length === 4 ? "" : "s"}`}
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
|
)(authors);
|
||||||
|
|
||||||
|
const time = moment(props.time).format("HH:mm");
|
||||||
|
const groupTitle =
|
||||||
|
props.associations.contacts?.[props.group]?.metadata?.title || props.group;
|
||||||
|
|
||||||
|
const app = props.chat ? 'chat' : 'graph';
|
||||||
|
const channelTitle =
|
||||||
|
(channel && props.associations?.[app]?.[channel]?.metadata?.title) ||
|
||||||
|
channel;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row p="2" flexWrap="wrap" gapX="1" alignItems="center">
|
||||||
|
{!props.archived && (
|
||||||
|
<Icon
|
||||||
|
display="block"
|
||||||
|
mr="1"
|
||||||
|
icon={read ? "Circle" : "Bullet"}
|
||||||
|
color="blue"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Text mr="1" mono>
|
||||||
|
{authorDesc}
|
||||||
|
</Text>
|
||||||
|
<Text mr="1">{description}</Text>
|
||||||
|
{!!moduleIcon && <Icon icon={moduleIcon as any} />}
|
||||||
|
{!!channel && <Text fontWeight="500">{channelTitle}</Text>}
|
||||||
|
<Text mx="1" fontWeight="bold" color="lightGray">
|
||||||
|
|
|
||||||
|
</Text>
|
||||||
|
<Text fontWeight="500">{groupTitle}</Text>
|
||||||
|
<Text lineHeight="tall" mx="1" fontWeight="bold" color="lightGray">
|
||||||
|
|
|
||||||
|
</Text>
|
||||||
|
<Text fontWeight="regular" color="lightGray">
|
||||||
|
{time}
|
||||||
|
</Text>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
165
pkg/interface/src/views/apps/notifications/inbox.tsx
Normal file
165
pkg/interface/src/views/apps/notifications/inbox.tsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import f from "lodash/fp";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { Icon, Col, Row, Box, Text, Anchor } from "@tlon/indigo-react";
|
||||||
|
import moment from "moment";
|
||||||
|
import { Notifications, Rolodex, Timebox, IndexedNotification } from "~/types";
|
||||||
|
import { MOMENT_CALENDAR_DATE, daToUnix } from "~/logic/lib/util";
|
||||||
|
import { BigInteger } from "big-integer";
|
||||||
|
import GlobalApi from "~/logic/api/global";
|
||||||
|
import { Notification } from "./notification";
|
||||||
|
import { Associations } from "~/types";
|
||||||
|
|
||||||
|
type DatedTimebox = [BigInteger, Timebox];
|
||||||
|
|
||||||
|
function filterNotification(associations: Associations, groups: string[]) {
|
||||||
|
if (groups.length === 0) {
|
||||||
|
return () => true;
|
||||||
|
}
|
||||||
|
return (n: IndexedNotification) => {
|
||||||
|
if ("graph" in n.index) {
|
||||||
|
const { group } = n.index.graph;
|
||||||
|
return groups.findIndex((g) => group === g) !== -1;
|
||||||
|
} else if ("group" in n.index) {
|
||||||
|
const { group } = n.index.group;
|
||||||
|
return groups.findIndex((g) => group === g) !== -1;
|
||||||
|
} else if ("chat" in n.index) {
|
||||||
|
const group = associations.chat[n.index.chat]?.["group-path"];
|
||||||
|
return groups.findIndex((g) => group === g) !== -1;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Inbox(props: {
|
||||||
|
notifications: Notifications;
|
||||||
|
archive: Notifications;
|
||||||
|
showArchive?: boolean;
|
||||||
|
api: GlobalApi;
|
||||||
|
associations: Associations;
|
||||||
|
contacts: Rolodex;
|
||||||
|
filter: string[];
|
||||||
|
}) {
|
||||||
|
const { api, associations } = props;
|
||||||
|
useEffect(() => {
|
||||||
|
let seen = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
seen = true;
|
||||||
|
}, 3000);
|
||||||
|
return () => {
|
||||||
|
if (seen) {
|
||||||
|
api.hark.seen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [newNotifications, ...notifications] =
|
||||||
|
Array.from(props.showArchive ? props.archive : props.notifications) || [];
|
||||||
|
|
||||||
|
const notificationsByDay = f.flow(
|
||||||
|
f.map<DatedTimebox>(([date, nots]) => [
|
||||||
|
date,
|
||||||
|
nots.filter(filterNotification(associations, props.filter)),
|
||||||
|
]),
|
||||||
|
f.groupBy<DatedTimebox>(([date]) =>
|
||||||
|
moment(daToUnix(date)).format("DDMMYYYY")
|
||||||
|
),
|
||||||
|
f.values
|
||||||
|
)(notifications);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col overflowY="auto" flexGrow="1">
|
||||||
|
{newNotifications && (
|
||||||
|
<DaySection
|
||||||
|
latest
|
||||||
|
timeboxes={[newNotifications]}
|
||||||
|
contacts={props.contacts}
|
||||||
|
archive={!!props.showArchive}
|
||||||
|
associations={props.associations}
|
||||||
|
graphConfig={props.notificationsGraphConfig}
|
||||||
|
groupConfig={props.notificationsGroupConfig}
|
||||||
|
chatConfig={props.notificationsChatConfig}
|
||||||
|
api={api}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{_.map(
|
||||||
|
notificationsByDay,
|
||||||
|
(timeboxes, idx) =>
|
||||||
|
timeboxes.length > 0 && (
|
||||||
|
<DaySection
|
||||||
|
key={idx}
|
||||||
|
timeboxes={timeboxes}
|
||||||
|
contacts={props.contacts}
|
||||||
|
archive={!!props.showArchive}
|
||||||
|
associations={props.associations}
|
||||||
|
api={api}
|
||||||
|
graphConfig={props.notificationsGraphConfig}
|
||||||
|
groupConfig={props.notificationsGroupConfig}
|
||||||
|
chatConfig={props.notificationsChatConfig}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortTimeboxes([a]: DatedTimebox, [b]: DatedTimebox) {
|
||||||
|
return b.subtract(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortIndexedNotification(
|
||||||
|
{ notification: a }: IndexedNotification,
|
||||||
|
{ notification: b }: IndexedNotification
|
||||||
|
) {
|
||||||
|
return b.time - a.time;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DaySection({
|
||||||
|
contacts,
|
||||||
|
archive,
|
||||||
|
timeboxes,
|
||||||
|
latest = false,
|
||||||
|
associations,
|
||||||
|
api,
|
||||||
|
groupConfig,
|
||||||
|
graphConfig,
|
||||||
|
chatConfig,
|
||||||
|
}) {
|
||||||
|
const calendar = latest
|
||||||
|
? MOMENT_CALENDAR_DATE
|
||||||
|
: { ...MOMENT_CALENDAR_DATE, sameDay: "[Earlier Today]" };
|
||||||
|
if (timeboxes.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box position="sticky" zIndex="3" top="-1px" bg="white">
|
||||||
|
<Box p="2" bg="scales.black05">
|
||||||
|
{moment(daToUnix(timeboxes[0][0])).calendar(null, calendar)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{_.map(timeboxes.sort(sortTimeboxes), ([date, nots], i) =>
|
||||||
|
_.map(nots.sort(sortIndexedNotification), (not, j: number) => (
|
||||||
|
<React.Fragment key={j}>
|
||||||
|
{(i !== 0 || j !== 0) && (
|
||||||
|
<Box flexShrink="0" height="4px" bg="scales.black05" />
|
||||||
|
)}
|
||||||
|
<Notification
|
||||||
|
graphConfig={graphConfig}
|
||||||
|
groupConfig={groupConfig}
|
||||||
|
chatConfig={chatConfig}
|
||||||
|
api={api}
|
||||||
|
associations={associations}
|
||||||
|
notification={not}
|
||||||
|
archived={archive}
|
||||||
|
contacts={contacts}
|
||||||
|
time={date}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
43
pkg/interface/src/views/apps/notifications/metadata.tsx
Normal file
43
pkg/interface/src/views/apps/notifications/metadata.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Box } from "@tlon/indigo-react";
|
||||||
|
|
||||||
|
import { MetadataBody, NotificationProps } from "./types";
|
||||||
|
import { Header } from "./header";
|
||||||
|
|
||||||
|
function getInvolvedUsers(body: MetadataBody) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDescription(body: MetadataBody) {
|
||||||
|
const b = body.metadata;
|
||||||
|
if ("new" in b) {
|
||||||
|
return "created";
|
||||||
|
} else if ("changedTitle" in b) {
|
||||||
|
return "changed the title to";
|
||||||
|
} else if ("changedDescription" in b) {
|
||||||
|
return "changed the description to";
|
||||||
|
} else if ("changedColor" in b) {
|
||||||
|
return "changed the color to";
|
||||||
|
} else if ("deleted" in b) {
|
||||||
|
return "deleted";
|
||||||
|
} else {
|
||||||
|
throw new Error("bad metadata frond");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MetadataNotification(props: NotificationProps<"metadata">) {
|
||||||
|
const { unread } = props;
|
||||||
|
const description = getDescription(unread.unreads[0].body);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box p="2">
|
||||||
|
<Header
|
||||||
|
authors={[]}
|
||||||
|
description={description}
|
||||||
|
group={unread.group}
|
||||||
|
channel={unread.channel}
|
||||||
|
date={unread.date}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
175
pkg/interface/src/views/apps/notifications/notification.tsx
Normal file
175
pkg/interface/src/views/apps/notifications/notification.tsx
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import React, { ReactNode, useCallback, useMemo } from "react";
|
||||||
|
import { Row, Box, Col, Text, Anchor, Icon, Action } from "@tlon/indigo-react";
|
||||||
|
import _ from "lodash";
|
||||||
|
import {
|
||||||
|
GraphNotificationContents,
|
||||||
|
IndexedNotification,
|
||||||
|
GroupNotificationContents,
|
||||||
|
NotificationGraphConfig,
|
||||||
|
GroupNotificationsConfig,
|
||||||
|
NotifIndex,
|
||||||
|
Associations,
|
||||||
|
} from "~/types";
|
||||||
|
import GlobalApi from "~/logic/api/global";
|
||||||
|
import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction";
|
||||||
|
import { GroupNotification } from "./group";
|
||||||
|
import { GraphNotification } from "./graph";
|
||||||
|
import { ChatNotification } from "./chat";
|
||||||
|
import { BigInteger } from "big-integer";
|
||||||
|
|
||||||
|
interface NotificationProps {
|
||||||
|
notification: IndexedNotification;
|
||||||
|
time: BigInteger;
|
||||||
|
associations: Associations;
|
||||||
|
api: GlobalApi;
|
||||||
|
archived: boolean;
|
||||||
|
graphConfig: NotificationGraphConfig;
|
||||||
|
groupConfig: GroupNotificationsConfig;
|
||||||
|
chatConfig: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMuted(
|
||||||
|
idx: NotifIndex,
|
||||||
|
groups: GroupNotificationsConfig,
|
||||||
|
graphs: NotificationGraphConfig,
|
||||||
|
chat: string[]
|
||||||
|
) {
|
||||||
|
if ("graph" in idx) {
|
||||||
|
const { graph } = idx.graph;
|
||||||
|
return _.findIndex(graphs?.watching || [], (g) => g === graph) === -1;
|
||||||
|
}
|
||||||
|
if ("group" in idx) {
|
||||||
|
return _.findIndex(groups || [], (g) => g === idx.group.group) === -1;
|
||||||
|
}
|
||||||
|
if ("chat" in idx) {
|
||||||
|
return _.findIndex(chat || [], (c) => c === idx.chat) === -1;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NotificationWrapper(props: {
|
||||||
|
api: GlobalApi;
|
||||||
|
time: BigInteger;
|
||||||
|
notif: IndexedNotification;
|
||||||
|
children: ReactNode;
|
||||||
|
archived: boolean;
|
||||||
|
graphConfig: NotificationGraphConfig;
|
||||||
|
groupConfig: GroupNotificationsConfig;
|
||||||
|
chatConfig: string[];
|
||||||
|
}) {
|
||||||
|
const { api, time, notif, children } = props;
|
||||||
|
|
||||||
|
const onArchive = useCallback(async () => {
|
||||||
|
return api.hark.archive(time, notif.index);
|
||||||
|
}, [time, notif]);
|
||||||
|
|
||||||
|
const isMuted = getMuted(
|
||||||
|
notif.index,
|
||||||
|
props.groupConfig,
|
||||||
|
props.graphConfig,
|
||||||
|
props.chatConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeMute = useCallback(async () => {
|
||||||
|
const func = isMuted ? "unmute" : "mute";
|
||||||
|
return api.hark[func](notif.index);
|
||||||
|
}, [notif, api, isMuted]);
|
||||||
|
|
||||||
|
const changeMuteDesc = isMuted ? "Unmute" : "Mute";
|
||||||
|
return (
|
||||||
|
<Row alignItems="center" justifyContent="space-between">
|
||||||
|
{children}
|
||||||
|
<Row gapX="2" p="2" alignItems="center">
|
||||||
|
<StatelessAsyncAction name={changeMuteDesc} onClick={onChangeMute}>
|
||||||
|
{changeMuteDesc}
|
||||||
|
</StatelessAsyncAction>
|
||||||
|
{!props.archived && (
|
||||||
|
<StatelessAsyncAction name={time.toString()} onClick={onArchive}>
|
||||||
|
Archive
|
||||||
|
</StatelessAsyncAction>
|
||||||
|
)}
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Notification(props: NotificationProps) {
|
||||||
|
const { notification, associations, archived } = props;
|
||||||
|
const { read, contents, time } = notification.notification;
|
||||||
|
|
||||||
|
const Wrapper = ({ children }) => (
|
||||||
|
<NotificationWrapper
|
||||||
|
archived={archived}
|
||||||
|
notif={notification}
|
||||||
|
time={props.time}
|
||||||
|
api={props.api}
|
||||||
|
graphConfig={props.graphConfig}
|
||||||
|
groupConfig={props.groupConfig}
|
||||||
|
chatConfig={props.chatConfig}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</NotificationWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
if ("graph" in notification.index) {
|
||||||
|
const index = notification.index.graph;
|
||||||
|
const c: GraphNotificationContents = (contents as any).graph;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<GraphNotification
|
||||||
|
api={props.api}
|
||||||
|
index={index}
|
||||||
|
contents={c}
|
||||||
|
contacts={props.contacts}
|
||||||
|
read={read}
|
||||||
|
archived={archived}
|
||||||
|
timebox={props.time}
|
||||||
|
time={time}
|
||||||
|
associations={associations}
|
||||||
|
/>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ("group" in notification.index) {
|
||||||
|
const index = notification.index.group;
|
||||||
|
const c: GroupNotificationContents = (contents as any).group;
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<GroupNotification
|
||||||
|
api={props.api}
|
||||||
|
index={index}
|
||||||
|
contents={c}
|
||||||
|
contacts={props.contacts}
|
||||||
|
read={read}
|
||||||
|
timebox={props.time}
|
||||||
|
archived={archived}
|
||||||
|
time={time}
|
||||||
|
associations={associations}
|
||||||
|
/>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ("chat" in notification.index) {
|
||||||
|
const index = notification.index.chat;
|
||||||
|
const c: ChatNotificationContents = (contents as any).chat;
|
||||||
|
return (
|
||||||
|
<Wrapper>
|
||||||
|
<ChatNotification
|
||||||
|
api={props.api}
|
||||||
|
index={index}
|
||||||
|
contents={c}
|
||||||
|
contacts={props.contacts}
|
||||||
|
read={read}
|
||||||
|
archived={archived}
|
||||||
|
groups={{}}
|
||||||
|
timebox={props.time}
|
||||||
|
time={time}
|
||||||
|
associations={associations}
|
||||||
|
/>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
141
pkg/interface/src/views/apps/notifications/notifications.tsx
Normal file
141
pkg/interface/src/views/apps/notifications/notifications.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import React, { useCallback, useState } from "react";
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { Box, Col, Text, Row } from "@tlon/indigo-react";
|
||||||
|
import { Link, Switch, Route } from "react-router-dom";
|
||||||
|
|
||||||
|
import { Body } from "~/views/components/Body";
|
||||||
|
import { PropFunc } from "~/types/util";
|
||||||
|
import Inbox from "./inbox";
|
||||||
|
import NotificationPreferences from "./preferences";
|
||||||
|
import { Dropdown } from "~/views/components/Dropdown";
|
||||||
|
import { Formik } from "formik";
|
||||||
|
import { FormikOnBlur } from "~/views/components/FormikOnBlur";
|
||||||
|
import GroupSearch from "~/views/components/GroupSearch";
|
||||||
|
|
||||||
|
const baseUrl = "/~notifications";
|
||||||
|
|
||||||
|
const HeaderLink = (
|
||||||
|
props: PropFunc<typeof Text> & { view?: string; current: string }
|
||||||
|
) => {
|
||||||
|
const { current, view, ...textProps } = props;
|
||||||
|
const to = view ? `${baseUrl}/${view}` : baseUrl;
|
||||||
|
const active = view ? current === view : !current;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={to}>
|
||||||
|
<Text px="2" {...textProps} gray={!active} />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NotificationFilter {
|
||||||
|
groups: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NotificationsScreen(props: any) {
|
||||||
|
const relativePath = (p: string) => baseUrl + p;
|
||||||
|
|
||||||
|
const [filter, setFilter] = useState<NotificationFilter>({ groups: [] });
|
||||||
|
const onSubmit = async (values: { groups: string }) => {
|
||||||
|
setFilter({ groups: values.groups ? [values.groups] : [] });
|
||||||
|
};
|
||||||
|
const groupFilterDesc =
|
||||||
|
filter.groups.length === 0
|
||||||
|
? "All"
|
||||||
|
: filter.groups
|
||||||
|
.map((g) => props.associations?.contacts?.[g]?.metadata?.title)
|
||||||
|
.join(", ");
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Route
|
||||||
|
path={[relativePath("/:view"), relativePath("")]}
|
||||||
|
render={(routeProps) => {
|
||||||
|
const { view } = routeProps.match.params;
|
||||||
|
return (
|
||||||
|
<Body>
|
||||||
|
<Col height="100%">
|
||||||
|
<Row
|
||||||
|
p="3"
|
||||||
|
alignItems="center"
|
||||||
|
height="48px"
|
||||||
|
justifyContent="space-between"
|
||||||
|
width="100%"
|
||||||
|
borderBottom="1"
|
||||||
|
borderBottomColor="washedGray"
|
||||||
|
>
|
||||||
|
<Box>Updates </Box>
|
||||||
|
<Row>
|
||||||
|
<Box>
|
||||||
|
<HeaderLink current={view} view="">
|
||||||
|
Inbox
|
||||||
|
</HeaderLink>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<HeaderLink current={view} view="archive">
|
||||||
|
Archive
|
||||||
|
</HeaderLink>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<HeaderLink current={view} view="preferences">
|
||||||
|
Preferences
|
||||||
|
</HeaderLink>
|
||||||
|
</Box>
|
||||||
|
</Row>
|
||||||
|
<Dropdown
|
||||||
|
alignX="right"
|
||||||
|
alignY="top"
|
||||||
|
options={
|
||||||
|
<Col
|
||||||
|
p="2"
|
||||||
|
backgroundColor="white"
|
||||||
|
border={1}
|
||||||
|
borderRadius={1}
|
||||||
|
borderColor="lightGray"
|
||||||
|
gapY="2"
|
||||||
|
>
|
||||||
|
<FormikOnBlur
|
||||||
|
initialValues={filter}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
<GroupSearch
|
||||||
|
id="groups"
|
||||||
|
label="Filter Groups"
|
||||||
|
caption="Only show notifications from this group"
|
||||||
|
associations={props.associations}
|
||||||
|
/>
|
||||||
|
</FormikOnBlur>
|
||||||
|
</Col>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Text mr="1" gray>
|
||||||
|
Filter:
|
||||||
|
</Text>
|
||||||
|
{groupFilterDesc}
|
||||||
|
</Box>
|
||||||
|
</Dropdown>
|
||||||
|
</Row>
|
||||||
|
{view === "archive" && (
|
||||||
|
<Inbox
|
||||||
|
{...props}
|
||||||
|
archive={props.archivedNotifications}
|
||||||
|
showArchive
|
||||||
|
filter={filter.groups}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{view === "preferences" && (
|
||||||
|
<NotificationPreferences
|
||||||
|
graphConfig={props.notificationsGraphConfig}
|
||||||
|
api={props.api}
|
||||||
|
dnd={props.doNotDisturb}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!view && <Inbox {...props} filter={filter.groups} />}
|
||||||
|
</Col>
|
||||||
|
</Body>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
90
pkg/interface/src/views/apps/notifications/preferences.tsx
Normal file
90
pkg/interface/src/views/apps/notifications/preferences.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import React, { useCallback } from "react";
|
||||||
|
|
||||||
|
import { Box, Col, ManagedCheckboxField as Checkbox } from "@tlon/indigo-react";
|
||||||
|
import { Formik, Form, FormikHelpers } from "formik";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { AsyncButton } from "~/views/components/AsyncButton";
|
||||||
|
import { FormikOnBlur } from "~/views/components/FormikOnBlur";
|
||||||
|
import { NotificationGraphConfig } from "~/types";
|
||||||
|
import GlobalApi from "~/logic/api/global";
|
||||||
|
|
||||||
|
interface FormSchema {
|
||||||
|
mentions: boolean;
|
||||||
|
dnd: boolean;
|
||||||
|
watchOnSelf: boolean;
|
||||||
|
watching: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NotificationPreferencesProps {
|
||||||
|
graphConfig: NotificationGraphConfig;
|
||||||
|
dnd: boolean;
|
||||||
|
api: GlobalApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NotificationPreferences(
|
||||||
|
props: NotificationPreferencesProps
|
||||||
|
) {
|
||||||
|
const { graphConfig, api, dnd } = props;
|
||||||
|
|
||||||
|
const initialValues: FormSchema = {
|
||||||
|
mentions: graphConfig.mentions,
|
||||||
|
watchOnSelf: graphConfig.watchOnSelf,
|
||||||
|
dnd,
|
||||||
|
watching: graphConfig.watching,
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = useCallback(
|
||||||
|
async (values: FormSchema, actions: FormikHelpers<FormSchema>) => {
|
||||||
|
console.log(values);
|
||||||
|
try {
|
||||||
|
let promises: Promise<any>[] = [];
|
||||||
|
if (values.mentions !== graphConfig.mentions) {
|
||||||
|
promises.push(api.hark.setMentions(values.mentions));
|
||||||
|
}
|
||||||
|
if (values.watchOnSelf !== graphConfig.watchOnSelf) {
|
||||||
|
promises.push(api.hark.setWatchOnSelf(values.watchOnSelf));
|
||||||
|
}
|
||||||
|
if (values.dnd !== dnd && !_.isUndefined(values.dnd)) {
|
||||||
|
promises.push(api.hark.setDoNotDisturb(values.dnd))
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
actions.setStatus({ success: null });
|
||||||
|
actions.resetForm({ values: initialValues });
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
actions.setStatus({ error: e.message });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[api, graphConfig]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormikOnBlur
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<Col maxWidth="384px" p="3" gapY="4">
|
||||||
|
<Checkbox
|
||||||
|
label="Do not disturb"
|
||||||
|
id="dnd"
|
||||||
|
caption="You won't see the notification badge, but notifications will still appear in your inbox."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label="Watch for replies"
|
||||||
|
id="watchOnSelf"
|
||||||
|
caption="Automatically follow a post for notifications when it's yours"
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Watch for mentions"
|
||||||
|
id="mentions"
|
||||||
|
caption="Notify me if someone mentions my @p in a channel I've joined"
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Form>
|
||||||
|
</FormikOnBlur>
|
||||||
|
);
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { Comment, NoteId } from '~/types/publish-update';
|
|
||||||
import { Contacts } from '~/types/contact-update';
|
import { Contacts } from '~/types/contact-update';
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { Box, Row } from '@tlon/indigo-react';
|
import { Box, Row } from '@tlon/indigo-react';
|
||||||
@ -7,8 +6,8 @@ import styled from 'styled-components';
|
|||||||
import { Author } from '~/views/apps/publish/components/Author';
|
import { Author } from '~/views/apps/publish/components/Author';
|
||||||
import { GraphNode, TextContent } from '~/types/graph-update';
|
import { GraphNode, TextContent } from '~/types/graph-update';
|
||||||
import tokenizeMessage from '~/logic/lib/tokenizeMessage';
|
import tokenizeMessage from '~/logic/lib/tokenizeMessage';
|
||||||
import RichText from '~/views/components/RichText';
|
|
||||||
import { LocalUpdateRemoteContentPolicy } from '~/types';
|
import { LocalUpdateRemoteContentPolicy } from '~/types';
|
||||||
|
import { MentionText } from '~/views/components/MentionText';
|
||||||
|
|
||||||
const ClickBox = styled(Box)`
|
const ClickBox = styled(Box)`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -30,9 +29,7 @@ interface CommentItemProps {
|
|||||||
export function CommentItem(props: CommentItemProps) {
|
export function CommentItem(props: CommentItemProps) {
|
||||||
const { ship, contacts, name, api, remoteContentPolicy } = props;
|
const { ship, contacts, name, api, remoteContentPolicy } = props;
|
||||||
const commentData = props.comment?.post;
|
const commentData = props.comment?.post;
|
||||||
const comment = commentData.contents[0] as TextContent;
|
const comment = commentData.contents;
|
||||||
|
|
||||||
const content = tokenizeMessage(comment.text).flat().join(' ');
|
|
||||||
|
|
||||||
const disabled = props.pending || window.ship !== commentData.author;
|
const disabled = props.pending || window.ship !== commentData.author;
|
||||||
|
|
||||||
@ -61,7 +58,11 @@ export function CommentItem(props: CommentItemProps) {
|
|||||||
</Author>
|
</Author>
|
||||||
</Row>
|
</Row>
|
||||||
<Box mb={2}>
|
<Box mb={2}>
|
||||||
<RichText className="f9 white-d" remoteContentPolicy={remoteContentPolicy}>{content}</RichText>
|
<MentionText
|
||||||
|
contacts={contacts}
|
||||||
|
content={comment}
|
||||||
|
remoteContentPolicy={remoteContentPolicy}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Col } from '@tlon/indigo-react';
|
import { Col } from '@tlon/indigo-react';
|
||||||
import { CommentItem } from '~/views/components/CommentItem';
|
import { CommentItem } from './CommentItem';
|
||||||
import CommentInput from '~/views/components/CommentInput';
|
import CommentInput from './CommentInput';
|
||||||
import { Contacts } from '~/types/contact-update';
|
import { Contacts } from '~/types/contact-update';
|
||||||
import GlobalApi from '~/logic/api/global';
|
import GlobalApi from '~/logic/api/global';
|
||||||
import { FormikHelpers } from 'formik';
|
import { FormikHelpers } from 'formik';
|
||||||
import { GraphNode } from '~/types/graph-update';
|
import { GraphNode } from '~/types/graph-update';
|
||||||
import { createPost } from '~/logic/api/graph';
|
import { createPost } from '~/logic/api/graph';
|
||||||
import { LocalUpdateRemoteContentPolicy } from '~/types';
|
import { LocalUpdateRemoteContentPolicy } from '~/types';
|
||||||
|
import { scanForMentions } from '~/logic/lib/graph';
|
||||||
|
|
||||||
interface CommentsProps {
|
interface CommentsProps {
|
||||||
comments: GraphNode;
|
comments: GraphNode;
|
||||||
@ -28,7 +29,8 @@ export function Comments(props: CommentsProps) {
|
|||||||
actions: FormikHelpers<{ comment: string }>
|
actions: FormikHelpers<{ comment: string }>
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const post = createPost([{ text: comment }], comments?.post?.index);
|
const content = scanForMentions(comment);
|
||||||
|
const post = createPost(content, comments?.post?.index);
|
||||||
await api.graph.addPost(ship, name, post);
|
await api.graph.addPost(ship, name, post);
|
||||||
actions.resetForm();
|
actions.resetForm();
|
||||||
actions.setStatus({ success: null });
|
actions.setStatus({ success: null });
|
||||||
@ -44,7 +46,7 @@ export function Comments(props: CommentsProps) {
|
|||||||
{Array.from(comments.children).reverse().map(([idx, comment]) => (
|
{Array.from(comments.children).reverse().map(([idx, comment]) => (
|
||||||
<CommentItem
|
<CommentItem
|
||||||
comment={comment}
|
comment={comment}
|
||||||
key={idx}
|
key={idx.toString()}
|
||||||
contacts={props.contacts}
|
contacts={props.contacts}
|
||||||
api={api}
|
api={api}
|
||||||
name={name}
|
name={name}
|
||||||
|
@ -15,7 +15,7 @@ export function FormikOnBlur<
|
|||||||
) {
|
) {
|
||||||
const { values } = formikBag;
|
const { values } = formikBag;
|
||||||
formikBag.submitForm().then(() => {
|
formikBag.submitForm().then(() => {
|
||||||
formikBag.resetForm({ values });
|
formikBag.resetForm({ values, touched: {} });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
@ -79,7 +79,7 @@ export function GroupSearch(props: InviteSearchProps) {
|
|||||||
: Object.values(props.associations?.contacts || {});
|
: Object.values(props.associations?.contacts || {});
|
||||||
}, [props.associations?.contacts]);
|
}, [props.associations?.contacts]);
|
||||||
|
|
||||||
const [{ value }, meta, { setValue }] = useField(props.id);
|
const [{ value }, meta, { setValue, setTouched }] = useField(props.id);
|
||||||
|
|
||||||
const { title: groupTitle } =
|
const { title: groupTitle } =
|
||||||
props.associations.contacts?.[value]?.metadata || {};
|
props.associations.contacts?.[value]?.metadata || {};
|
||||||
@ -87,12 +87,14 @@ export function GroupSearch(props: InviteSearchProps) {
|
|||||||
const onSelect = useCallback(
|
const onSelect = useCallback(
|
||||||
(a: Association) => {
|
(a: Association) => {
|
||||||
setValue(a["group-path"]);
|
setValue(a["group-path"]);
|
||||||
|
setTouched(true);
|
||||||
},
|
},
|
||||||
[setValue]
|
[setValue]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onUnselect = useCallback(() => {
|
const onUnselect = useCallback(() => {
|
||||||
setValue(undefined);
|
setValue(undefined);
|
||||||
|
setTouched(true);
|
||||||
}, [setValue]);
|
}, [setValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
51
pkg/interface/src/views/components/MentionText.tsx
Normal file
51
pkg/interface/src/views/components/MentionText.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import React from "react";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { Text } from "@tlon/indigo-react";
|
||||||
|
import { Contacts, Content, LocalUpdateRemoteContentPolicy } from "~/types";
|
||||||
|
import RichText from "~/views/components/RichText";
|
||||||
|
import { cite } from "~/logic/lib/util";
|
||||||
|
|
||||||
|
interface MentionTextProps {
|
||||||
|
contacts: Contacts;
|
||||||
|
content: Content[];
|
||||||
|
remoteContentPolicy: LocalUpdateRemoteContentPolicy;
|
||||||
|
}
|
||||||
|
export function MentionText(props: MentionTextProps) {
|
||||||
|
const { content, contacts } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{_.map(content, (c, idx) => {
|
||||||
|
if ("text" in c) {
|
||||||
|
return (
|
||||||
|
<RichText
|
||||||
|
inline
|
||||||
|
key={idx}
|
||||||
|
remoteContentPolicy={props.remoteContentPolicy}
|
||||||
|
>
|
||||||
|
{c.text}
|
||||||
|
</RichText>
|
||||||
|
);
|
||||||
|
} else if ("mention" in c) {
|
||||||
|
return (
|
||||||
|
<Mention key={idx} contacts={contacts || {}} ship={c.mention} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Mention(props: { ship: string; contacts: Contacts }) {
|
||||||
|
const { contacts, ship } = props;
|
||||||
|
const contact = contacts[ship];
|
||||||
|
const showNickname = !!contact?.nickname;
|
||||||
|
const name = showNickname ? contact?.nickname : cite(ship);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text mx="2px" px="2px" bg="washedBlue" color="blue" mono={!showNickname}>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
@ -30,7 +30,7 @@ const RichText = React.memo(({ remoteContentPolicy, ...props }) => (
|
|||||||
return <BaseAnchor target='_blank' rel='noreferrer noopener' borderBottom='1px solid' {...props}>{props.children}</BaseAnchor>;
|
return <BaseAnchor target='_blank' rel='noreferrer noopener' borderBottom='1px solid' {...props}>{props.children}</BaseAnchor>;
|
||||||
},
|
},
|
||||||
paragraph: (paraProps) => {
|
paragraph: (paraProps) => {
|
||||||
return <Text display='block' mb='2' {...props}>{paraProps.children}</Text>;
|
return <Text display={props.inline ? 'inline' : 'block'} mb='2' {...props}>{paraProps.children}</Text>;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
plugins={[[
|
plugins={[[
|
||||||
|
@ -28,6 +28,11 @@ const StatusBar = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<StatusBarItem mr={2} onClick={() => props.api.local.setOmnibox()}>
|
<StatusBarItem mr={2} onClick={() => props.api.local.setOmnibox()}>
|
||||||
|
{ !props.doNotDisturb && props.notificationsCount > 0 &&
|
||||||
|
(<Box display="block" right="-8px" top="-8px" position="absolute" >
|
||||||
|
<Icon color="blue" icon="Bullet" />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
<Icon icon='LeapArrow'/>
|
<Icon icon='LeapArrow'/>
|
||||||
<Text ml={2} color='black'>
|
<Text ml={2} color='black'>
|
||||||
Leap
|
Leap
|
||||||
@ -42,7 +47,15 @@ const StatusBar = (props) => {
|
|||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row justifyContent="flex-end" collapse>
|
<Row justifyContent="flex-end" collapse>
|
||||||
|
{!props.doNotDisturb && (<StatusBarItem color="blue" px='12px' mr='2' onClick={() => props.history.push('/~notifications')}>
|
||||||
|
<Text
|
||||||
|
fontWeight={props.notificationsCount > 0 ? "500" : "400"}
|
||||||
|
fontSize='0'
|
||||||
|
color="blue"
|
||||||
|
>
|
||||||
|
{(props.notificationsCount > 99) ? "99+" : props.notificationsCount}
|
||||||
|
</Text>
|
||||||
|
</StatusBarItem>)}
|
||||||
<StatusBarItem px={'2'} flexShrink='0' onClick={() => props.history.push('/~profile')}>
|
<StatusBarItem px={'2'} flexShrink='0' onClick={() => props.history.push('/~profile')}>
|
||||||
<Sigil ship={props.ship} size={16} color='black' classes='mix-blend-diff' icon />
|
<Sigil ship={props.ship} size={16} color='black' classes='mix-blend-diff' icon />
|
||||||
<Text ml={2} display={["none", "inline"]} fontFamily="mono">~{props.ship}</Text>
|
<Text ml={2} display={["none", "inline"]} fontFamily="mono">~{props.ship}</Text>
|
||||||
|
@ -117,7 +117,7 @@ export class Omnibox extends Component {
|
|||||||
const { props } = this;
|
const { props } = this;
|
||||||
this.setState({ results: this.initialResults(), query: '' }, () => {
|
this.setState({ results: this.initialResults(), query: '' }, () => {
|
||||||
props.api.local.setOmnibox();
|
props.api.local.setOmnibox();
|
||||||
if (defaultApps.includes(app.toLowerCase()) || app === 'profile' || app === 'Links' || app === 'home') {
|
if (defaultApps.includes(app.toLowerCase()) || app === 'profile' || app === 'Links' || app === 'home' || app === 'inbox') {
|
||||||
props.history.push(link);
|
props.history.push(link);
|
||||||
} else {
|
} else {
|
||||||
window.location.href = link;
|
window.location.href = link;
|
||||||
|
@ -30,17 +30,20 @@ export class OmniboxResult extends Component {
|
|||||||
const sigilFill = (this.state.hovered || (selected === link)) ? '#3a8ff7' : '#ffffff';
|
const sigilFill = (this.state.hovered || (selected === link)) ? '#3a8ff7' : '#ffffff';
|
||||||
|
|
||||||
let graphic = <div />;
|
let graphic = <div />;
|
||||||
if (defaultApps.includes(icon.toLowerCase()) || icon.toLowerCase() === 'links') {
|
if (defaultApps.includes(icon.toLowerCase()) || icon.toLowerCase() === 'links' || icon === 'inbox') {
|
||||||
|
icon = (icon === 'inbox') ? 'Inbox' : icon;
|
||||||
icon = (icon === 'Link') ? 'Links' : icon;
|
icon = (icon === 'Link') ? 'Links' : icon;
|
||||||
graphic = <Icon display="inline-block" verticalAlign="middle" icon={icon} mr='2' size='16px' color={iconFill} />;
|
graphic = <Icon display="inline-block" verticalAlign="middle" icon={icon} mr='2' size='16px' color={iconFill} />;
|
||||||
} else if (icon === 'logout') {
|
} else if (icon === 'logout') {
|
||||||
graphic = <Icon display="inline-block" verticalAlign="middle" icon='ArrowWest' mr='2' size='16px' color={iconFill} />;
|
graphic = <Icon display="inline-block" verticalAlign="middle" icon='ArrowWest' mr='2' size='16px' color={iconFill} />;
|
||||||
} else if (icon === 'profile') {
|
} else if (icon === 'profile') {
|
||||||
graphic = <Sigil color={sigilFill} classes='dib v-mid mr2' ship={window.ship} size={16} icon padded />;
|
graphic = <Sigil color={sigilFill} classes='dib flex-shrink-0 v-mid mr2' ship={window.ship} size={16} icon padded />;
|
||||||
} else if (icon === 'home') {
|
} else if (icon === 'home') {
|
||||||
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Circle' mr='2' size='16px' color={iconFill} />;
|
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Circle' mr='2' size='16px' color={iconFill} />;
|
||||||
|
} else if (icon === 'notifications') {
|
||||||
|
graphic = <Icon display='inline-block' verticalAlign='middle' icon='Inbox' mr='2' size='16px' color={iconFill} />;
|
||||||
} else {
|
} else {
|
||||||
graphic = <Icon icon='NullIcon' verticalAlign="middle" mr='2' size="16px" color={iconFill} />;
|
graphic = <Icon display='inline-block' icon='NullIcon' verticalAlign="middle" mr='2' size="16px" color={iconFill} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return graphic;
|
return graphic;
|
||||||
|
@ -3,8 +3,10 @@ import { Link, useHistory } from "react-router-dom";
|
|||||||
|
|
||||||
import { Icon, Row, Col, Button, Text, Box, Action } from "@tlon/indigo-react";
|
import { Icon, Row, Col, Button, Text, Box, Action } from "@tlon/indigo-react";
|
||||||
import { Dropdown } from "~/views/components/Dropdown";
|
import { Dropdown } from "~/views/components/Dropdown";
|
||||||
import { Association } from "~/types";
|
import { Association, NotificationGraphConfig } from "~/types";
|
||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from "~/logic/api/global";
|
||||||
|
import { StatelessAsyncAction } from "~/views/components/StatelessAsyncAction";
|
||||||
|
import { appIsGraph } from "~/logic/lib/util";
|
||||||
|
|
||||||
const ChannelMenuItem = ({
|
const ChannelMenuItem = ({
|
||||||
icon,
|
icon,
|
||||||
@ -27,6 +29,8 @@ const ChannelMenuItem = ({
|
|||||||
interface ChannelMenuProps {
|
interface ChannelMenuProps {
|
||||||
association: Association;
|
association: Association;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
|
graphNotificationConfig: NotificationGraphConfig;
|
||||||
|
chatNotificationConfig: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChannelMenu(props: ChannelMenuProps) {
|
export function ChannelMenu(props: ChannelMenuProps) {
|
||||||
@ -34,8 +38,9 @@ export function ChannelMenu(props: ChannelMenuProps) {
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { metadata } = association;
|
const { metadata } = association;
|
||||||
const app = metadata.module || association["app-name"];
|
const app = metadata.module || association["app-name"];
|
||||||
const workspace = history.location.pathname.startsWith('/~landscape/home')
|
const workspace = history.location.pathname.startsWith("/~landscape/home")
|
||||||
? '/home' : association?.['group-path'];
|
? "/home"
|
||||||
|
: association?.["group-path"];
|
||||||
const baseUrl = `/~landscape${workspace}/resource/${app}${association["app-path"]}`;
|
const baseUrl = `/~landscape${workspace}/resource/${app}${association["app-path"]}`;
|
||||||
const appPath = association["app-path"];
|
const appPath = association["app-path"];
|
||||||
|
|
||||||
@ -44,6 +49,22 @@ export function ChannelMenu(props: ChannelMenuProps) {
|
|||||||
: appPath.split("/");
|
: appPath.split("/");
|
||||||
|
|
||||||
const isOurs = ship.slice(1) === window.ship;
|
const isOurs = ship.slice(1) === window.ship;
|
||||||
|
|
||||||
|
const isMuted = appIsGraph(app)
|
||||||
|
? props.graphNotificationConfig.watching.findIndex((a) => a === appPath) ===
|
||||||
|
-1
|
||||||
|
: props.chatNotificationConfig.findIndex((a) => a === appPath) === -1;
|
||||||
|
const onChangeMute = async () => {
|
||||||
|
const func =
|
||||||
|
association["app-name"] === "chat"
|
||||||
|
? isMuted
|
||||||
|
? "listenChat"
|
||||||
|
: "ignoreChat"
|
||||||
|
: isMuted
|
||||||
|
? "listenGraph"
|
||||||
|
: "ignoreGraph";
|
||||||
|
await api.hark[func](appPath);
|
||||||
|
};
|
||||||
const onUnsubscribe = useCallback(async () => {
|
const onUnsubscribe = useCallback(async () => {
|
||||||
const app = metadata.module || association["app-name"];
|
const app = metadata.module || association["app-name"];
|
||||||
switch (app) {
|
switch (app) {
|
||||||
@ -83,15 +104,35 @@ export function ChannelMenu(props: ChannelMenuProps) {
|
|||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
options={
|
options={
|
||||||
<Col backgroundColor="white" border={1} borderRadius={1} borderColor="lightGray">
|
<Col
|
||||||
|
backgroundColor="white"
|
||||||
|
border={1}
|
||||||
|
borderRadius={1}
|
||||||
|
borderColor="lightGray"
|
||||||
|
>
|
||||||
|
<ChannelMenuItem color="blue" icon="Inbox">
|
||||||
|
<StatelessAsyncAction
|
||||||
|
m="2"
|
||||||
|
bg="white"
|
||||||
|
name="notif"
|
||||||
|
onClick={onChangeMute}
|
||||||
|
>
|
||||||
|
{isMuted ? "Unmute" : "Mute"} this channel
|
||||||
|
</StatelessAsyncAction>
|
||||||
|
</ChannelMenuItem>
|
||||||
{isOurs ? (
|
{isOurs ? (
|
||||||
<>
|
<>
|
||||||
<ChannelMenuItem color="red" icon="TrashCan">
|
<ChannelMenuItem color="red" icon="TrashCan">
|
||||||
<Action m="2" backgroundColor='white' destructive onClick={onDelete}>
|
<Action
|
||||||
|
m="2"
|
||||||
|
backgroundColor="white"
|
||||||
|
destructive
|
||||||
|
onClick={onDelete}
|
||||||
|
>
|
||||||
Delete Channel
|
Delete Channel
|
||||||
</Action>
|
</Action>
|
||||||
</ChannelMenuItem>
|
</ChannelMenuItem>
|
||||||
<ChannelMenuItem bottom icon="Gear" color='black'>
|
<ChannelMenuItem bottom icon="Gear" color="black">
|
||||||
<Link to={`${baseUrl}/settings`}>
|
<Link to={`${baseUrl}/settings`}>
|
||||||
<Box fontSize={0} p="2">
|
<Box fontSize={0} p="2">
|
||||||
Channel Settings
|
Channel Settings
|
||||||
|
@ -8,6 +8,7 @@ import DojoApp from '~/views/apps/dojo/app';
|
|||||||
import Landscape from '~/views/landscape/index';
|
import Landscape from '~/views/landscape/index';
|
||||||
import Profile from '~/views/apps/profile/profile';
|
import Profile from '~/views/apps/profile/profile';
|
||||||
import ErrorComponent from '~/views/components/Error';
|
import ErrorComponent from '~/views/components/Error';
|
||||||
|
import Notifications from '~/views/apps/notifications/notifications';
|
||||||
|
|
||||||
|
|
||||||
export const Container = styled(Box)`
|
export const Container = styled(Box)`
|
||||||
@ -61,6 +62,12 @@ export const Content = (props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/~notifications"
|
||||||
|
render={ p => (
|
||||||
|
<Notifications {...props} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
render={p => (
|
render={p => (
|
||||||
<ErrorComponent
|
<ErrorComponent
|
||||||
|
@ -1,176 +0,0 @@
|
|||||||
import React, { useEffect } from "react";
|
|
||||||
import { AsyncButton } from "~/views/components/AsyncButton";
|
|
||||||
import * as Yup from "yup";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
ManagedTextInputField as Input,
|
|
||||||
ManagedCheckboxField as Checkbox,
|
|
||||||
Col,
|
|
||||||
Label,
|
|
||||||
Button,
|
|
||||||
} from "@tlon/indigo-react";
|
|
||||||
import { Formik, Form, useFormikContext, FormikHelpers } from "formik";
|
|
||||||
import { FormError } from "~/views/components/FormError";
|
|
||||||
import { Group, GroupPolicy } from "~/types/group-update";
|
|
||||||
import { Enc } from "~/types/noun";
|
|
||||||
import { Association } from "~/types/metadata-update";
|
|
||||||
import GlobalApi from "~/logic/api/global";
|
|
||||||
import { resourceFromPath, roleForShip } from "~/logic/lib/group";
|
|
||||||
import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton";
|
|
||||||
import { ColorInput } from "~/views/components/ColorInput";
|
|
||||||
import { useHistory } from "react-router-dom";
|
|
||||||
|
|
||||||
import { uxToHex } from '~/logic/lib/util';
|
|
||||||
|
|
||||||
|
|
||||||
interface FormSchema {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
color: string;
|
|
||||||
isPrivate: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formSchema = Yup.object({
|
|
||||||
title: Yup.string().required("Group must have a name"),
|
|
||||||
description: Yup.string(),
|
|
||||||
color: Yup.string(),
|
|
||||||
isPrivate: Yup.boolean(),
|
|
||||||
});
|
|
||||||
|
|
||||||
interface GroupSettingsProps {
|
|
||||||
group: Group;
|
|
||||||
association: Association;
|
|
||||||
api: GlobalApi;
|
|
||||||
}
|
|
||||||
export function GroupSettings(props: GroupSettingsProps) {
|
|
||||||
const { group, association } = props;
|
|
||||||
const { metadata } = association;
|
|
||||||
const history = useHistory();
|
|
||||||
const currentPrivate = "invite" in props.group.policy;
|
|
||||||
const initialValues: FormSchema = {
|
|
||||||
title: metadata?.title,
|
|
||||||
description: metadata?.description,
|
|
||||||
color: metadata?.color,
|
|
||||||
isPrivate: currentPrivate,
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (
|
|
||||||
values: FormSchema,
|
|
||||||
actions: FormikHelpers<FormSchema>
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const { title, description, color, isPrivate } = values;
|
|
||||||
const uxColor = uxToHex(color);
|
|
||||||
await props.api.metadata.update(props.association, {
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
color: uxColor
|
|
||||||
});
|
|
||||||
if (isPrivate !== currentPrivate) {
|
|
||||||
const resource = resourceFromPath(props.association["group-path"]);
|
|
||||||
const newPolicy: Enc<GroupPolicy> = isPrivate
|
|
||||||
? { invite: { pending: [] } }
|
|
||||||
: { open: { banRanks: [], banned: [] } };
|
|
||||||
const diff = { replace: newPolicy };
|
|
||||||
await props.api.groups.changePolicy(resource, diff);
|
|
||||||
}
|
|
||||||
|
|
||||||
actions.setStatus({ success: null });
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
actions.setStatus({ error: e.message });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDelete = async () => {
|
|
||||||
const name = association['group-path'].split('/').pop();
|
|
||||||
if (prompt(`To confirm deleting this group, type ${name}`) === name) {
|
|
||||||
await props.api.contacts.delete(association["group-path"]);
|
|
||||||
history.push("/");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const disabled =
|
|
||||||
resourceFromPath(association["group-path"]).ship.slice(1) !== window.ship &&
|
|
||||||
roleForShip(group, window.ship) !== "admin";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box height="100%" overflowY="auto">
|
|
||||||
<Formik
|
|
||||||
validationSchema={formSchema}
|
|
||||||
initialValues={initialValues}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
<Form style={{ display: "contents" }}>
|
|
||||||
<Box
|
|
||||||
maxWidth="300px"
|
|
||||||
gridTemplateColumns="1fr"
|
|
||||||
gridAutoRows="auto"
|
|
||||||
display="grid"
|
|
||||||
gridRowGap={4}
|
|
||||||
my={3}
|
|
||||||
mx={4}
|
|
||||||
>
|
|
||||||
{!disabled ? (
|
|
||||||
<Col>
|
|
||||||
<Label>Delete Group</Label>
|
|
||||||
<Label gray mt="2">
|
|
||||||
Permanently delete this group. (All current members will no
|
|
||||||
longer see this group.)
|
|
||||||
</Label>
|
|
||||||
<StatelessAsyncButton onClick={onDelete} mt={2} destructive>
|
|
||||||
Delete this group
|
|
||||||
</StatelessAsyncButton>
|
|
||||||
</Col>
|
|
||||||
) : (
|
|
||||||
<Col>
|
|
||||||
<Label>Leave Group</Label>
|
|
||||||
<Label gray mt="2">
|
|
||||||
Leave this group. You can rejoin if it is an open group, or if
|
|
||||||
you are reinvited
|
|
||||||
</Label>
|
|
||||||
<StatelessAsyncButton onClick={onDelete} mt={2} destructive>
|
|
||||||
Leave this group
|
|
||||||
</StatelessAsyncButton>
|
|
||||||
</Col>
|
|
||||||
)}
|
|
||||||
<Box borderBottom={1} borderBottomColor="washedGray" />
|
|
||||||
<Input
|
|
||||||
id="title"
|
|
||||||
label="Group Name"
|
|
||||||
caption="The name for your group to be called by"
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
id="description"
|
|
||||||
label="Group Description"
|
|
||||||
caption="The description of your group"
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
<ColorInput
|
|
||||||
id="color"
|
|
||||||
label="Group color"
|
|
||||||
caption="A color to represent your group"
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
id="isPrivate"
|
|
||||||
label="Private group"
|
|
||||||
caption="If enabled, users must be invited to join the group"
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
<AsyncButton
|
|
||||||
disabled={disabled}
|
|
||||||
primary
|
|
||||||
loadingText="Updating.."
|
|
||||||
border
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</AsyncButton>
|
|
||||||
<FormError message="Failed to update settings" />
|
|
||||||
</Box>
|
|
||||||
</Form>
|
|
||||||
</Formik>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
@ -0,0 +1,134 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { AsyncButton } from "~/views/components/AsyncButton";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
ManagedTextInputField as Input,
|
||||||
|
ManagedToggleSwitchField as Checkbox,
|
||||||
|
Col,
|
||||||
|
Label,
|
||||||
|
Button,
|
||||||
|
} from "@tlon/indigo-react";
|
||||||
|
import { Formik, Form, useFormikContext, FormikHelpers } from "formik";
|
||||||
|
import { FormError } from "~/views/components/FormError";
|
||||||
|
import { Group, GroupPolicy } from "~/types/group-update";
|
||||||
|
import { Enc } from "~/types/noun";
|
||||||
|
import { Association } from "~/types/metadata-update";
|
||||||
|
import GlobalApi from "~/logic/api/global";
|
||||||
|
import { resourceFromPath, roleForShip } from "~/logic/lib/group";
|
||||||
|
import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton";
|
||||||
|
import { ColorInput } from "~/views/components/ColorInput";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
|
import { uxToHex } from "~/logic/lib/util";
|
||||||
|
|
||||||
|
interface FormSchema {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
color: string;
|
||||||
|
isPrivate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formSchema = Yup.object({
|
||||||
|
title: Yup.string().required("Group must have a name"),
|
||||||
|
description: Yup.string(),
|
||||||
|
color: Yup.string(),
|
||||||
|
isPrivate: Yup.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface GroupAdminSettingsProps {
|
||||||
|
group: Group;
|
||||||
|
association: Association;
|
||||||
|
api: GlobalApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GroupAdminSettings(props: GroupAdminSettingsProps) {
|
||||||
|
const { group, association } = props;
|
||||||
|
const { metadata } = association;
|
||||||
|
const history = useHistory();
|
||||||
|
const currentPrivate = "invite" in props.group.policy;
|
||||||
|
const initialValues: FormSchema = {
|
||||||
|
title: metadata?.title,
|
||||||
|
description: metadata?.description,
|
||||||
|
color: metadata?.color,
|
||||||
|
isPrivate: currentPrivate,
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (
|
||||||
|
values: FormSchema,
|
||||||
|
actions: FormikHelpers<FormSchema>
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const { title, description, color, isPrivate } = values;
|
||||||
|
const uxColor = uxToHex(color);
|
||||||
|
await props.api.metadata.update(props.association, {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
color: uxColor,
|
||||||
|
});
|
||||||
|
if (isPrivate !== currentPrivate) {
|
||||||
|
const resource = resourceFromPath(props.association["group-path"]);
|
||||||
|
const newPolicy: Enc<GroupPolicy> = isPrivate
|
||||||
|
? { invite: { pending: [] } }
|
||||||
|
: { open: { banRanks: [], banned: [] } };
|
||||||
|
const diff = { replace: newPolicy };
|
||||||
|
await props.api.groups.changePolicy(resource, diff);
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.setStatus({ success: null });
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
actions.setStatus({ error: e.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const disabled =
|
||||||
|
resourceFromPath(association["group-path"]).ship.slice(1) !== window.ship &&
|
||||||
|
roleForShip(group, window.ship) !== "admin";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
validationSchema={formSchema}
|
||||||
|
initialValues={initialValues}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<Col gapY={4}>
|
||||||
|
<Input
|
||||||
|
id="title"
|
||||||
|
label="Group Name"
|
||||||
|
caption="The name for your group to be called by"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
id="description"
|
||||||
|
label="Group Description"
|
||||||
|
caption="The description of your group"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<ColorInput
|
||||||
|
id="color"
|
||||||
|
label="Group color"
|
||||||
|
caption="A color to represent your group"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
id="isPrivate"
|
||||||
|
label="Private group"
|
||||||
|
caption="If enabled, users must be invited to join the group"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<AsyncButton
|
||||||
|
disabled={disabled}
|
||||||
|
primary
|
||||||
|
loadingText="Updating.."
|
||||||
|
border
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</AsyncButton>
|
||||||
|
<FormError message="Failed to update settings" />
|
||||||
|
</Col>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { AsyncButton } from "~/views/components/AsyncButton";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
ManagedTextInputField as Input,
|
||||||
|
ManagedToggleSwitchField as Checkbox,
|
||||||
|
Col,
|
||||||
|
Label,
|
||||||
|
Button,
|
||||||
|
} from "@tlon/indigo-react";
|
||||||
|
import { Formik, Form, useFormikContext, FormikHelpers } from "formik";
|
||||||
|
import { FormError } from "~/views/components/FormError";
|
||||||
|
import { Group, GroupPolicy } from "~/types/group-update";
|
||||||
|
import { Enc } from "~/types/noun";
|
||||||
|
import { Association } from "~/types/metadata-update";
|
||||||
|
import GlobalApi from "~/logic/api/global";
|
||||||
|
import { resourceFromPath, roleForShip } from "~/logic/lib/group";
|
||||||
|
import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton";
|
||||||
|
import { ColorInput } from "~/views/components/ColorInput";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
|
import { uxToHex } from "~/logic/lib/util";
|
||||||
|
import { GroupAdminSettings } from "./Admin";
|
||||||
|
import { GroupPersonalSettings } from "./Personal";
|
||||||
|
import {GroupNotificationsConfig} from "~/types";
|
||||||
|
|
||||||
|
interface GroupSettingsProps {
|
||||||
|
group: Group;
|
||||||
|
association: Association;
|
||||||
|
api: GlobalApi;
|
||||||
|
notificationsGroupConfig: GroupNotificationsConfig;
|
||||||
|
}
|
||||||
|
export function GroupSettings(props: GroupSettingsProps) {
|
||||||
|
return (
|
||||||
|
<Box height="100%" overflowY="auto">
|
||||||
|
<Col maxWidth="384px" p="4" gapY="4">
|
||||||
|
<GroupPersonalSettings {...props} />
|
||||||
|
<Box borderBottom="1" borderBottomColor="washedGray" />
|
||||||
|
<GroupAdminSettings {...props} />
|
||||||
|
</Col>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,97 @@
|
|||||||
|
import React, { useCallback } from "react";
|
||||||
|
|
||||||
|
import { AsyncButton } from "~/views/components/AsyncButton";
|
||||||
|
import * as Yup from "yup";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
ManagedTextInputField as Input,
|
||||||
|
ManagedToggleSwitchField as Toggle,
|
||||||
|
Col,
|
||||||
|
Label,
|
||||||
|
Button,
|
||||||
|
} from "@tlon/indigo-react";
|
||||||
|
import { Formik, Form, useFormikContext, FormikHelpers } from "formik";
|
||||||
|
import { FormError } from "~/views/components/FormError";
|
||||||
|
import { Group, GroupPolicy } from "~/types/group-update";
|
||||||
|
import { Enc } from "~/types/noun";
|
||||||
|
import { Association } from "~/types/metadata-update";
|
||||||
|
import GlobalApi from "~/logic/api/global";
|
||||||
|
import { resourceFromPath, roleForShip } from "~/logic/lib/group";
|
||||||
|
import { StatelessAsyncButton } from "~/views/components/StatelessAsyncButton";
|
||||||
|
import { ColorInput } from "~/views/components/ColorInput";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
|
import { uxToHex } from "~/logic/lib/util";
|
||||||
|
import { FormikOnBlur } from "~/views/components/FormikOnBlur";
|
||||||
|
import {GroupNotificationsConfig} from "~/types";
|
||||||
|
|
||||||
|
function DeleteGroup(props: {
|
||||||
|
owner: boolean;
|
||||||
|
api: GlobalApi;
|
||||||
|
association: Association;
|
||||||
|
}) {
|
||||||
|
const history = useHistory();
|
||||||
|
const onDelete = async () => {
|
||||||
|
const name = props.association['group-path'].split('/').pop();
|
||||||
|
if (prompt(`To confirm deleting this group, type ${name}`) === name) {
|
||||||
|
await props.api.contacts.delete(props.association["group-path"]);
|
||||||
|
history.push("/");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = props.owner ? "Delete" : "Leave";
|
||||||
|
const description = props.owner
|
||||||
|
? "Permanently delete this group. (All current members will no longer see this group.)"
|
||||||
|
: "Leave this group. You can rejoin if it is an open group, or if you are reinvited";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col>
|
||||||
|
<Label>{action} Group</Label>
|
||||||
|
<Label gray mt="2">
|
||||||
|
{description}
|
||||||
|
</Label>
|
||||||
|
<StatelessAsyncButton onClick={onDelete} mt={2} destructive>
|
||||||
|
{action} this group
|
||||||
|
</StatelessAsyncButton>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FormSchema {
|
||||||
|
watching: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GroupPersonalSettings(props: {
|
||||||
|
api: GlobalApi;
|
||||||
|
association: Association;
|
||||||
|
notificationsGroupConfig: GroupNotificationsConfig;
|
||||||
|
}) {
|
||||||
|
|
||||||
|
const groupPath = props.association['group-path'];
|
||||||
|
|
||||||
|
const watching = props.notificationsGroupConfig.findIndex(g => g === groupPath) !== -1;
|
||||||
|
|
||||||
|
const initialValues: FormSchema = {
|
||||||
|
watching
|
||||||
|
};
|
||||||
|
const onSubmit = async (values: FormSchema) => {
|
||||||
|
if(values.watching === watching) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const func = values.watching ? 'listenGroup' : 'ignoreGroup';
|
||||||
|
await props.api.hark[func](groupPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Col gapY="4">
|
||||||
|
<FormikOnBlur initialValues={initialValues} onSubmit={onSubmit}>
|
||||||
|
<Toggle
|
||||||
|
id="watching"
|
||||||
|
label="Notify me on group activity"
|
||||||
|
caption="Send me notifications when this group changes"
|
||||||
|
/>
|
||||||
|
</FormikOnBlur>
|
||||||
|
<DeleteGroup association={props.association} owner api={props.api} />
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
|
}
|
@ -68,6 +68,8 @@ export function GroupsPane(props: GroupsPaneProps) {
|
|||||||
s3={props.s3}
|
s3={props.s3}
|
||||||
hideAvatars={props.hideAvatars}
|
hideAvatars={props.hideAvatars}
|
||||||
hideNicknames={props.hideNicknames}
|
hideNicknames={props.hideNicknames}
|
||||||
|
notificationsGroupConfig={props.notificationsGroupConfig}
|
||||||
|
|
||||||
{...routeProps}
|
{...routeProps}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
/>)}
|
/>)}
|
||||||
|
@ -7,10 +7,10 @@ import { Contacts } from "~/types/contact-update";
|
|||||||
import { Group } from "~/types/group-update";
|
import { Group } from "~/types/group-update";
|
||||||
import { Association } from "~/types/metadata-update";
|
import { Association } from "~/types/metadata-update";
|
||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from "~/logic/api/global";
|
||||||
import {S3State} from "~/types";
|
import {GroupNotificationsConfig, S3State} from "~/types";
|
||||||
|
|
||||||
import { ContactCard } from "./ContactCard";
|
import { ContactCard } from "./ContactCard";
|
||||||
import { GroupSettings } from "./GroupSettings";
|
import { GroupSettings } from "./GroupSettings/GroupSettings";
|
||||||
import { Participants } from "./Participants";
|
import { Participants } from "./Participants";
|
||||||
|
|
||||||
|
|
||||||
@ -41,6 +41,7 @@ export function PopoverRoutes(
|
|||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
hideAvatars: boolean;
|
hideAvatars: boolean;
|
||||||
hideNicknames: boolean;
|
hideNicknames: boolean;
|
||||||
|
notificationsGroupConfig: GroupNotificationsConfig;
|
||||||
} & RouteComponentProps
|
} & RouteComponentProps
|
||||||
) {
|
) {
|
||||||
const relativeUrl = (url: string) => `${props.baseUrl}/popover${url}`;
|
const relativeUrl = (url: string) => `${props.baseUrl}/popover${url}`;
|
||||||
@ -125,6 +126,7 @@ export function PopoverRoutes(
|
|||||||
group={props.group}
|
group={props.group}
|
||||||
association={props.association}
|
association={props.association}
|
||||||
api={props.api}
|
api={props.api}
|
||||||
|
notificationsGroupConfig={props.notificationsGroupConfig}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{view === "participants" && (
|
{view === "participants" && (
|
||||||
|
@ -57,7 +57,13 @@ export function Resource(props: ResourceProps) {
|
|||||||
<Route
|
<Route
|
||||||
path={relativePath("")}
|
path={relativePath("")}
|
||||||
render={(routeProps) => (
|
render={(routeProps) => (
|
||||||
<ResourceSkeleton baseUrl={props.baseUrl} {...skelProps} atRoot>
|
<ResourceSkeleton
|
||||||
|
notificationsGraphConfig={props.notificationsGraphConfig}
|
||||||
|
notificationsChatConfig={props.notificationsChatConfig}
|
||||||
|
baseUrl={props.baseUrl}
|
||||||
|
{...skelProps}
|
||||||
|
atRoot
|
||||||
|
>
|
||||||
{app === "chat" ? (
|
{app === "chat" ? (
|
||||||
<ChatResource {...props} />
|
<ChatResource {...props} />
|
||||||
) : app === "publish" ? (
|
) : app === "publish" ? (
|
||||||
|
@ -6,13 +6,14 @@ import { Link } from "react-router-dom";
|
|||||||
import { ChatResource } from "~/views/apps/chat/ChatResource";
|
import { ChatResource } from "~/views/apps/chat/ChatResource";
|
||||||
import { PublishResource } from "~/views/apps/publish/PublishResource";
|
import { PublishResource } from "~/views/apps/publish/PublishResource";
|
||||||
|
|
||||||
import RichText from '~/views/components/RichText';
|
import RichText from "~/views/components/RichText";
|
||||||
|
|
||||||
import { Association } from "~/types/metadata-update";
|
import { Association } from "~/types/metadata-update";
|
||||||
import GlobalApi from "~/logic/api/global";
|
import GlobalApi from "~/logic/api/global";
|
||||||
import { RouteComponentProps, Route, Switch } from "react-router-dom";
|
import { RouteComponentProps, Route, Switch } from "react-router-dom";
|
||||||
import { ChannelSettings } from "./ChannelSettings";
|
import { ChannelSettings } from "./ChannelSettings";
|
||||||
import { ChannelMenu } from "./ChannelMenu";
|
import { ChannelMenu } from "./ChannelMenu";
|
||||||
|
import { NotificationGraphConfig } from "~/types";
|
||||||
|
|
||||||
const TruncatedBox = styled(Box)`
|
const TruncatedBox = styled(Box)`
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -22,6 +23,7 @@ const TruncatedBox = styled(Box)`
|
|||||||
|
|
||||||
type ResourceSkeletonProps = {
|
type ResourceSkeletonProps = {
|
||||||
association: Association;
|
association: Association;
|
||||||
|
notificationsGraphConfig: NotificationGraphConfig;
|
||||||
api: GlobalApi;
|
api: GlobalApi;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@ -33,13 +35,14 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) {
|
|||||||
const { association, api, baseUrl, children, atRoot } = props;
|
const { association, api, baseUrl, children, atRoot } = props;
|
||||||
const app = association?.metadata?.module || association["app-name"];
|
const app = association?.metadata?.module || association["app-name"];
|
||||||
const appPath = association["app-path"];
|
const appPath = association["app-path"];
|
||||||
const workspace = (baseUrl === '/~landscape/home') ? '/home' : association["group-path"];
|
const workspace =
|
||||||
|
baseUrl === "/~landscape/home" ? "/home" : association["group-path"];
|
||||||
const title = props.title || association?.metadata?.title;
|
const title = props.title || association?.metadata?.title;
|
||||||
const disableRemoteContent = {
|
const disableRemoteContent = {
|
||||||
audioShown: false,
|
audioShown: false,
|
||||||
imageShown: false,
|
imageShown: false,
|
||||||
oembedShown: false,
|
oembedShown: false,
|
||||||
videoShown: false
|
videoShown: false,
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Col width="100%" height="100%" overflowY="hidden">
|
<Col width="100%" height="100%" overflowY="hidden">
|
||||||
@ -64,11 +67,7 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) {
|
|||||||
<Link to={`/~landscape${workspace}`}> {"<- Back"}</Link>
|
<Link to={`/~landscape${workspace}`}> {"<- Back"}</Link>
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Box
|
<Box color="blue" pr={2} mr={2}>
|
||||||
color="blue"
|
|
||||||
pr={2}
|
|
||||||
mr={2}
|
|
||||||
>
|
|
||||||
<Link to={`/~landscape${workspace}/resource/${app}${appPath}`}>
|
<Link to={`/~landscape${workspace}/resource/${app}${appPath}`}>
|
||||||
<Text color="blue">Go back to channel</Text>
|
<Text color="blue">Go back to channel</Text>
|
||||||
</Link>
|
</Link>
|
||||||
@ -78,22 +77,34 @@ export function ResourceSkeleton(props: ResourceSkeletonProps) {
|
|||||||
{atRoot && (
|
{atRoot && (
|
||||||
<>
|
<>
|
||||||
<Box pr={1} mr={2}>
|
<Box pr={1} mr={2}>
|
||||||
<Text display='inline-block' verticalAlign='middle'>{title}</Text>
|
<Text display="inline-block" verticalAlign="middle">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<TruncatedBox
|
<TruncatedBox
|
||||||
display={["none", "block"]}
|
display={["none", "block"]}
|
||||||
maxWidth="60%"
|
maxWidth="60%"
|
||||||
verticalAlign='middle'
|
verticalAlign="middle"
|
||||||
flexShrink={1}
|
flexShrink={1}
|
||||||
title={association?.metadata?.description}
|
title={association?.metadata?.description}
|
||||||
color="gray"
|
color="gray"
|
||||||
>
|
>
|
||||||
<RichText color='gray' remoteContentPolicy={disableRemoteContent} mb='0' display='inline-block'>
|
<RichText
|
||||||
{association?.metadata?.description}
|
color="gray"
|
||||||
|
remoteContentPolicy={disableRemoteContent}
|
||||||
|
mb="0"
|
||||||
|
display="inline-block"
|
||||||
|
>
|
||||||
|
{association?.metadata?.description}
|
||||||
</RichText>
|
</RichText>
|
||||||
</TruncatedBox>
|
</TruncatedBox>
|
||||||
<Box flexGrow={1} />
|
<Box flexGrow={1} />
|
||||||
<ChannelMenu association={association} api={api} />
|
<ChannelMenu
|
||||||
|
graphNotificationConfig={props.notificationsGraphConfig}
|
||||||
|
chatNotificationConfig={props.notificationsChatConfig}
|
||||||
|
association={association}
|
||||||
|
api={api}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -20,6 +20,7 @@ const scales = {
|
|||||||
white80: "rgba(255,255,255,0.8)",
|
white80: "rgba(255,255,255,0.8)",
|
||||||
white90: "rgba(255,255,255,0.9)",
|
white90: "rgba(255,255,255,0.9)",
|
||||||
white100: "rgba(255,255,255,1)",
|
white100: "rgba(255,255,255,1)",
|
||||||
|
black05: "rgba(0,0,0,0.05)",
|
||||||
black10: "rgba(0,0,0,0.1)",
|
black10: "rgba(0,0,0,0.1)",
|
||||||
black20: "rgba(0,0,0,0.2)",
|
black20: "rgba(0,0,0,0.2)",
|
||||||
black30: "rgba(0,0,0,0.3)",
|
black30: "rgba(0,0,0,0.3)",
|
||||||
|
Loading…
Reference in New Issue
Block a user