From ca25bbda5e09c106e464259142a1e27a7c4afcad Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Tue, 12 Nov 2024 10:28:20 -0600 Subject: [PATCH 01/52] wip: refactoring hooks --- desk/app/channels-server.hoon | 277 ++++++++++++++++++++++++++++++++-- desk/lib/channel-utils.hoon | 59 +++++++- desk/sur/hooks.hoon | 156 +++++++++++++++++++ 3 files changed, 477 insertions(+), 15 deletions(-) create mode 100644 desk/sur/hooks.hoon diff --git a/desk/app/channels-server.hoon b/desk/app/channels-server.hoon index 8ba00a11d6..a16ac4b1bf 100644 --- a/desk/app/channels-server.hoon +++ b/desk/app/channels-server.hoon @@ -2,7 +2,7 @@ :: :: this is the server-side from which /app/channels gets its data. :: -/- c=channels, g=groups +/- c=channels, g=groups, h=hooks /+ utils=channel-utils, imp=import-aid /+ default-agent, verb, dbug, neg=negotiate :: @@ -16,8 +16,9 @@ |% +$ card card:agent:gall +$ current-state - $: %6 + $: %7 =v-channels:c + hooks=(map nest:c hooks:h) =pimp:imp == -- @@ -92,12 +93,22 @@ =? old ?=(%3 -.old) (state-3-to-4 old) =? old ?=(%4 -.old) (state-4-to-5 old) =? old ?=(%5 -.old) (state-5-to-6 old) - ?> ?=(%6 -.old) + =? old ?=(%6 -.old) (state-6-to-7 old) + ?> ?=(%7 -.old) =. state old inflate-io :: - +$ versioned-state $%(state-6 state-5 state-4 state-3 state-2 state-1 state-0) - +$ state-6 current-state + +$ versioned-state $%(state-7 state-6 state-5 state-4 state-3 state-2 state-1 state-0) + +$ state-7 current-state + +$ state-6 + $: %6 + =v-channels:c + =pimp:imp + == + ++ state-6-to-7 + |= state-6 + ^- state-7 + [%7 v-channels ~ pimp] +$ state-5 $: %5 =v-channels:v6:old:c @@ -321,6 +332,10 @@ [~ %| *] ~& [dap.bowl %overwriting-pending-import] cor(pimp `|+egg-any) == + :: + %hook-action-0 + =+ !<([=nest:c =action:h] vase) + ho-abet:(ho-action:(ho-abed:ho-core nest) action) == :: ++ run-import @@ -646,16 +661,26 @@ |= =c-post:c ^- [(unit u-channel:c) _posts.channel] ?> (can-write:ca-perms src.bowl writers.perm.perm.channel) + =/ =context:h (get-context channel) ?- -.c-post %add ?> =(src.bowl author.essay.c-post) ?> =(kind.nest -.kind-data.essay.c-post) + =^ result=(each event:h tang) cor + =/ =event:h [%on-post %add essay.c-post] + %- ho-run:(ho-abed:ho-core nest) + [event context 'post blocked'] + ?: ?=(%.n -.result) + ((slog p.result) [~ posts.channel]) + =/ =essay:c + ?> ?=([%on-post %add *] p.result) + essay.p.result =/ id=id-post:c |- =/ post (get:on-v-posts:c posts.channel now.bowl) ?~ post now.bowl $(now.bowl `@da`(add now.bowl ^~((div ~s1 (bex 16))))) - =/ new=v-post:c [[id ~ ~] 0 essay.c-post] + =/ new=v-post:c [[id ~ ~] 0 essay] :- `[%post id %set ~ new] (put:on-v-posts:c posts.channel id ~ new) :: @@ -666,8 +691,17 @@ ?~ post `posts.channel ?~ u.post `posts.channel ?> |(=(src.bowl author.u.u.post) (is-admin:ca-perms src.bowl)) + =^ result=(each event:h tang) cor + =/ =event:h [%on-post %edit u.u.post essay.c-post] + %- ho-run:(ho-abed:ho-core nest) + [event context 'edit blocked'] + ?: ?=(%.n -.result) + ((slog p.result) [~ posts.channel]) + =/ =essay:c + ?> ?=([%on-post %edit *] p.result) + essay.p.result ::TODO could optimize and no-op if the edit is identical to current - =/ new=v-post:c [-.u.u.post +(rev.u.u.post) essay.c-post] + =/ new=v-post:c [-.u.u.post +(rev.u.u.post) essay] :- `[%post id.c-post %set ~ new] (put:on-v-posts:c posts.channel id.c-post ~ new) :: @@ -676,6 +710,11 @@ ?~ post `(put:on-v-posts:c posts.channel id.c-post ~) ?~ u.post `posts.channel ?> |(=(src.bowl author.u.u.post) (is-admin:ca-perms src.bowl)) + =^ result=(each event:h tang) cor + =/ =event:h [%on-post %del u.u.post] + %- ho-run:(ho-abed:ho-core nest) + [event context 'delete blocked'] + ?> =(& -.result) :- `[%post id.c-post %set ~] (put:on-v-posts:c posts.channel id.c-post ~) :: @@ -683,7 +722,23 @@ =/ post (get:on-v-posts:c posts.channel id.c-post) ?~ post `posts.channel ?~ u.post `posts.channel - =/ [update=? reacts=v-reacts:c] (ca-c-react reacts.u.u.post c-post) + =^ result=(each event:h tang) cor + =/ =event:h + :* %on-post %react u.u.post + ?: ?=(%del-react -.c-post) [p.c-post ~] + [p `q]:c-post + == + %- ho-run:(ho-abed:ho-core nest) + [event context 'react action blocked'] + ?: ?=(%.n -.result) + ((slog p.result) [~ posts.channel]) + =/ new=c-post:c + ?> ?=([%on-post %react *] p.result) + ?~ react.p.result [%del-react id.c-post ship.p.result] + [%add-react id.c-post [ship u.react]:p.result] + =/ [update=? reacts=v-reacts:c] + %+ ca-c-react reacts.u.u.post + ?>(?=(?(%add-react %del-react) -.new) new) ?. update `posts.channel :- `[%post id.c-post %reacts reacts] (put:on-v-posts:c posts.channel id.c-post ~ u.u.post(reacts reacts)) @@ -693,25 +748,35 @@ ?~ post `posts.channel ?~ u.post `posts.channel =^ update=(unit u-post:c) replies.u.u.post - (ca-c-reply replies.u.u.post c-reply.c-post) + (ca-c-reply u.u.post c-reply.c-post context) ?~ update `posts.channel :- `[%post id.c-post u.update] (put:on-v-posts:c posts.channel id.c-post ~ u.u.post) == :: ++ ca-c-reply - |= [replies=v-replies:c =c-reply:c] - ^- [(unit u-post:c) _replies] + |= [parent=v-post:c =c-reply:c =context:h] + ^- [(unit u-post:c) v-replies:c] + =* replies replies.parent ?- -.c-reply %add ?> =(src.bowl author.memo.c-reply) + =^ result=(each event:h tang) cor + =/ =event:h [%on-reply %add parent memo.c-reply] + %- ho-run:(ho-abed:ho-core nest) + [event context 'reply blocked'] + ?: ?=(%.n -.result) + ((slog p.result) [~ replies]) + =/ =memo:c + ?> ?=([%on-reply %add *] p.result) + memo.p.result =/ id=id-reply:c |- =/ reply (get:on-v-replies:c replies now.bowl) ?~ reply now.bowl $(now.bowl `@da`(add now.bowl ^~((div ~s1 (bex 16))))) =/ reply-seal=v-reply-seal:c [id ~] - =/ new=v-reply:c [reply-seal 0 memo.c-reply] + =/ new=v-reply:c [reply-seal 0 memo] :- `[%reply id %set ~ new] (put:on-v-replies:c replies id ~ new) :: @@ -720,8 +785,17 @@ ?~ reply `replies ?~ u.reply `replies ?> =(src.bowl author.u.u.reply) + =^ result=(each event:h tang) cor + =/ =event:h [%on-reply %edit parent u.u.reply memo.c-reply] + %- ho-run:(ho-abed:ho-core nest) + [event context 'edit blocked'] + ?: ?=(%.n -.result) + ((slog p.result) [~ replies]) + =/ =memo:c + ?> ?=([%on-reply %edit *] p.result) + memo.p.result ::TODO could optimize and no-op if the edit is identical to current - =/ new=v-reply:c [-.u.u.reply +(rev.u.u.reply) memo.c-reply] + =/ new=v-reply:c [-.u.u.reply +(rev.u.u.reply) memo] :- `[%reply id.c-reply %set ~ new] (put:on-v-replies:c replies id.c-reply ~ new) :: @@ -730,6 +804,11 @@ ?~ reply `(put:on-v-replies:c replies id.c-reply ~) ?~ u.reply `replies ?> |(=(src.bowl author.u.u.reply) (is-admin:ca-perms src.bowl)) + =^ result=(each event:h tang) cor + =/ =event:h [%on-reply %del parent u.u.reply] + %- ho-run:(ho-abed:ho-core nest) + [event context 'delete blocked'] + ?> =(& -.result) :- `[%reply id.c-reply %set ~] (put:on-v-replies:c replies id.c-reply ~) :: @@ -737,7 +816,23 @@ =/ reply (get:on-v-replies:c replies id.c-reply) ?~ reply `replies ?~ u.reply `replies - =/ [update=? reacts=v-reacts:c] (ca-c-react reacts.u.u.reply c-reply) + =^ result=(each event:h tang) cor + =/ =event:h + :* %on-reply %react parent u.u.reply + ?: ?=(%del-react -.c-reply) [p.c-reply ~] + [p `q]:c-reply + == + %- ho-run:(ho-abed:ho-core nest) + [event context 'delete blocked'] + ?: ?=(%.n -.result) + ((slog p.result) [~ replies]) + =/ new=c-reply:c + ?> ?=([%on-reply %react *] p.result) + ?~ react.p.result [%del-react id.c-reply ship.p.result] + [%add-react id.c-reply [ship u.react]:p.result] + =/ [update=? reacts=v-reacts:c] + %+ ca-c-react reacts.u.u.reply + ?>(?=(?(%add-react %del-react) -.new) new) ?. update `replies :- `[%reply id.c-reply %reacts reacts] (put:on-v-replies:c replies id.c-reply ~ u.u.reply(reacts reacts)) @@ -836,4 +931,158 @@ (said:utils nest plan posts.channel) (give %kick ~ ~) -- +++ scry-path + |= [=dude:gall =path] + %+ welp + /(scot %p our.bowl)/[dude]/(scot %da now.bowl) + path +++ get-context + |= =v-channel:c + ^- context:h + =* flag group.perm.perm.v-channel + =/ =group:g + ?. .^(? %gu (scry-path %groups /$)) *group:g + ?. .^(? %gx (scry-path %groups /exists/(scot %p p.flag)/[q.flag]/noun)) + *group:g + .^(group:g %gx (scry-path %groups /groups/(scot %p p.flag)/[q.flag]/v1/noun)) + :* v-channel + v-channels + group + !>(~) :: we default this because each hook will replace with its own + [now our src eny]:bowl + == +:: +++ ho-core + |_ [=nest:c hks=hooks:h ctx=context:h gone=_|] + ++ ho-core . + ++ emit |=(=card ho-core(cor (^emit card))) + ++ emil |=(caz=(list card) ho-core(cor (^emil caz))) + ++ give |=(=gift:agent:gall ho-core(cor (^give gift))) + ++ ho-abet + %_ cor + hooks + ?:(gone (~(del by hooks) nest) (~(put by hooks) nest hks)) + == + :: + ++ ho-abed + |= n=nest:c + ho-core(nest n, hks (~(gut by hooks) n *hooks:h)) + :: + ++ ho-action + |= =action:h + ^+ ho-core + ?> (is-admin:ca-perms:(ca-abed:ca-core nest) src.bowl) + ?- -.action + %add + ~& "adding hook {}" + =/ =id:h (rsh [3 48] eny.bowl) + =/ src=(rev:c (unit @t)) [0 `src.action] + =/ result=(each nock tang) + ~& "compiling hook" + ((compile:utils args:h (return:h *)) `src.action) + ~& "compilation result: {}" + =/ compiled + ?: ?=(%| -.result) ~ + `p.result + =. order.hks + +:(next-rev:c order.hks (snoc +.order.hks id)) + =. hooks.hks + %+ ~(put by hooks.hks) id + [id name.action & src compiled cron.action !>(~)] + ho-core + :: + %edit + ?~ old-hook=(~(get by hooks.hks) id.action) ho-core + =/ hook u.old-hook + =^ src-changed src.hook + (next-rev:c src.hook `src.action) + =/ name-changed !=(name.action name.hook) + =/ cron-changed !=(cron.action cron.hook) + ?. |(src-changed name-changed cron-changed) ho-core + =. name.hook name.action + =. cron.hook cron.action + =. compiled.hook + ?~ +.src.hook ~ + =/ result=(each nock tang) + ((compile:utils args:h return:h) +.src.hook) + ?: ?=(%| -.result) ~ + `p.result + =. hooks.hks (~(put by hooks.hks) id.action hook) + ho-core + :: + %del + :: TODO: make more CRDT + =. hooks.hks (~(del by hooks.hks) id.action) + =/ [* new-order=_order.hks] + %+ next-rev:c order.hks + %+ skim +.order.hks + |= =id:h + !=(id id.action) + =. order.hks new-order + ho-core + :: + %enable + =/ hook (~(got by hooks.hks) id.action) + =. hooks.hks (~(put by hooks.hks) id.action hook(enabled &)) + ho-core + :: + %disable + =/ hook (~(got by hooks.hks) id.action) + =. hooks.hks (~(put by hooks.hks) id.action hook(enabled |)) + ho-core + :: + %order + =^ changed order.hks + (next-rev:c order.hks seq.action) + ho-core + == + ++ ho-run + |= [=event:h =context:h default=cord] + =^ [result=(each event:h tang) effects=(list effect:h)] hks + (run-hooks:utils event context default hks) + =. hooks (~(put by hooks) nest hks) + [result (ho-run-effects effects)] + ++ ho-run-effects + |= effects=(list effect:h) + ^+ cor + |- + ?~ effects cor + =/ =effect:h i.effects + =; new-cor + =. cor new-cor + $(effects t.effects) + ?- -.effect + %channels + =/ =cage channel-action+!>(a-channels.effect) + (emit [%pass /hooks/effect %agent [our.bowl %channels] %poke cage]) + :: + %groups + =/ =cage group-action-3+!>(action.effect) + (emit [%pass /hooks/effect %agent [our.bowl %groups] %poke cage]) + :: + %activity + =/ =cage activity-action+!>(action.effect) + (emit [%pass /hooks/effect %agent [our.bowl %activity] %poke cage]) + :: + %dm + =/ =cage chat-dm-action+!>(action.effect) + (emit [%pass /hooks/effect %agent [our.bowl %chat] %poke cage]) + :: + %club + =/ =cage chat-club-action+!>(action.effect) + (emit [%pass /hooks/effect %agent [our.bowl %chat] %poke cage]) + :: + %contacts + =/ =cage contacts-action-1+!>(action.effect) + (emit [%pass /hooks/effect %agent [our.bowl %contacts] %poke cage]) + :: + %delay + =/ fires-at (add now.bowl wait.effect) + =. delayed.hks + %+ ~(put by delayed.hks) id.effect + +:effect(data [data.effect fires-at]) + =/ =wire /hooks/delayed/(scot %uv id.effect) + (emit [%pass wire %arvo %b %wait fires-at]) + == + -- -- diff --git a/desk/lib/channel-utils.hoon b/desk/lib/channel-utils.hoon index 26e070491c..b41dd9419d 100644 --- a/desk/lib/channel-utils.hoon +++ b/desk/lib/channel-utils.hoon @@ -1,4 +1,4 @@ -/- c=channels, g=groups, ci=cite +/- c=channels, g=groups, ci=cite, h=hooks :: convert a post to a preview for a "said" response :: |% @@ -676,4 +676,61 @@ ;br; == -- +++ subject ^~(!>([..subject ..zuse])) +++ compile + |* [args=mold return=mold] + |= src=(unit @t) + ^- (each nock tang) + ?~ src |+~['no src'] + ~& %a + =/ tonk=(each (pair type nock) tang) + ~& %b + =/ vex=(like hoon) ((full vest) [0 0] (trip u.src)) + ~& %c + ?~ q.vex |+~[leaf+"\{{} {}}" 'syntax error'] + ~& %d + %- mule + |.((~(mint ut -:subject) %noun p.u.q.vex)) + ~& %e + ~& "parsed hoon: {<-.tonk>}" + ~& %f + ?: ?=(%| -.tonk) + ~& "returning error" + tonk + &+q.p.tonk +++ execute + |* prod=mold + |= [=nock simp=*] + ^- (unit prod) + %- (soft prod) + (slum .*(+:subject nock) simp) +:: +++ run-hooks + |= [=event:h =context:h default=cord hks=hooks:h] + ^- [[(each event:h tang) (list effect:h)] hooks:h] + =/ current-event event + =| effects=(list effect:h) + =* order +.order.hks + |- + ?~ order + [[&+current-event effects] hks] + =* next $(order t.order) + =/ hook (~(got by hooks.hks) i.order) + ?~ compiled.hook next + =/ =args:h [current-event context(state state.hook)] + =/ outcome=(unit outcome:h) + ((execute outcome:h) u.compiled.hook args) + ~& "{(trip name.hook)} hook run: {}" + ?~ outcome next + ?: ?=(%.n -.u.outcome) + ~& "hook failed:" + %- (slog p.u.outcome) + next + =* result result.p.u.outcome + =. effects (weld effects effects.p.u.outcome) + =. hooks.hks (~(put by hooks.hks) i.order hook(state new-state.p.u.outcome)) + ?: ?=(%denied -.result) + [[|+~[(fall msg.result default)] effects] hks] + =. current-event new.result + next -- diff --git a/desk/sur/hooks.hoon b/desk/sur/hooks.hoon new file mode 100644 index 0000000000..77a0bf2efc --- /dev/null +++ b/desk/sur/hooks.hoon @@ -0,0 +1,156 @@ +/- *channels, g=groups, a=activity, ch=chat, co=contacts +|% +:: $id: a unique identifier for a hook ++$ id @uv +:: +:: $hook: a pure function that runs on triggers in a channel +:: +:: $id: a unique identifier for the hook +:: $name: a human-readable name for the hook +:: $origin: whether or not this hook was added by us or came through an update +:: $src: the source code of the hook +:: $compiled: the compiled nock of the hook +:: $cron: the cron schedule for the hook if it has one +:: $state: the current state of the hook +:: +++ hook + $: =id + name=@t + enabled=? + src=(rev src=(unit @t)) + compiled=(unit nock) + cron=(unit @dr) + state=vase + == +:: $hooks: collection of hooks, the order they should be run in, and +:: any delayed hooks that need to be run +++ hooks + $: hooks=(map id hook) + order=(rev (list id)) + delayed=(map id delayed-hook) + == +:: $delayed-hook: metadata for when a delayed hook fires from the timer ++$ delayed-hook + $: =id + hook=id + wait=@dr + data=vase + fires-at=time + == +:: ++$ action + $% [%add name=@t src=@t cron=(unit @dr)] + [%edit =id name=@t src=@t cron=(unit @dr)] + [%del =id] + [%enable =id] + [%disable =id] + [%order seq=(list id)] + == ++$ response + $% [%set =id name=@t src=(unit @t) error=(unit tang)] + [%order seq=(list id)] + == +:: $context: ambient state that a hook should know about not +:: necessarily tied to a specific event +:: +:: $channel: the channel that the hook is operating on +:: $channels: all the channels in the group +:: $group: the group that the channel belongs to +:: $state: the current state of the hook +:: $now: the current time +:: $our: the ship that the hook is running on +:: $src: the ship that triggered the hook +:: $eny: entropy for random number generation or key derivation +:: ++$ context + $: v-channel + channels=v-channels + =group:g + state=vase + now=time + our=ship + src=ship + eny=@ + == +:: +:: $on-post: a hook event that fires when posts are interacted with ++$ on-post + $% [%add =essay] + [%edit original=v-post =essay] + [%del original=v-post] + [%react post=v-post =ship react=(unit react)] + == +:: +:: $on-reply: a hook event that fires when replies are interacted with ++$ on-reply + $% [%add parent=v-post =memo] + [%edit parent=v-post original=v-reply =memo] + [%del parent=v-post original=v-reply] + [%react parent=v-post reply=v-reply =ship react=(unit react)] + == +:: $event-type: the type of event that triggers a hook ++$ event-type ?(%on-post %on-reply %cron %delay) +:: +:: $event: the data associated with the trigger of a hook +:: +:: $on-post: a post was added, edited, deleted, or reacted to +:: $on-reply: a reply was added, edited, deleted, or reacted to +:: $cron: a scheduled wake-up +:: $delay: a delayed invocation of the hook called with metadata about +:: when it fired, its id, and the event it should run with +:: ++$ event + $% [%on-post on-post] + [%on-reply on-reply] + [%cron ~] + [%delay delayed-hook] + == +:: +:: $args: the arguments passed to a hook ++$ args + $: event=event + context=context + == +:: +:: $result: the result of a hook running +:: +:: $allowed: represents the action being allowed to go through, and the +:: new value of the action +:: $denied: represents the action being denied along with the reason +:: that the action was denied +:: $error: represents an error that occurred while running the hook +:: ++$ result + $% [%allowed new=event] + [%denied msg=(unit cord)] + == +:: +:: $effect: an effect that a hook can have, limited to agents in +:: the %groups desk. $delay is a special effect that will wake up the +:: same hook at a later time. ++$ effect + $% [%channels =a-channels] + [%groups =action:g] + [%activity =action:a] + [%dm =action:dm:ch] + [%club =action:club:ch] + [%contacts =action:co] + [%delay =id hook=id wait=@dr data=vase] + == +:: +:: $return: the data returned from a hook +:: +:: $result: whether the action was allowed or denied, any new values, +:: or an error message if something went wrong +:: $actions: any actions that should be taken on other agents or delay +:: $new-state: the new state of the hook after running +:: ++$ return + $: $: =result + effects=(list effect) + == + new-state=vase + == +:: ++$ outcome (each return tang) +-- \ No newline at end of file From fb4daf2a5bedbe40d87d591996a109d792e1113e Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Tue, 12 Nov 2024 11:31:33 -0600 Subject: [PATCH 02/52] channels-server: fix compile issue --- desk/app/channels-server.hoon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desk/app/channels-server.hoon b/desk/app/channels-server.hoon index a16ac4b1bf..71d54ad770 100644 --- a/desk/app/channels-server.hoon +++ b/desk/app/channels-server.hoon @@ -1048,8 +1048,8 @@ |- ?~ effects cor =/ =effect:h i.effects - =; new-cor - =. cor new-cor + =; new-cor=_ho-core + =. ho-core new-cor $(effects t.effects) ?- -.effect %channels From 724ea8cf78875a14c3159295e76e43865e74d1c7 Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Thu, 14 Nov 2024 14:47:24 -0600 Subject: [PATCH 03/52] channels-server: hooks mostly working, delay needs testing --- desk/app/channels-server.hoon | 189 ++++++++++++++++++++++++++-------- desk/gen/hooks/create.hoon | 13 +++ desk/gen/hooks/truncate.hoon | 97 +++++++++++++++++ desk/lib/channel-utils.hoon | 36 ++++--- desk/mar/hook/action-0.hoon | 12 +++ desk/sur/hooks.hoon | 2 +- 6 files changed, 290 insertions(+), 59 deletions(-) create mode 100644 desk/gen/hooks/create.hoon create mode 100644 desk/gen/hooks/truncate.hoon create mode 100644 desk/mar/hook/action-0.hoon diff --git a/desk/app/channels-server.hoon b/desk/app/channels-server.hoon index 71d54ad770..ac95188c32 100644 --- a/desk/app/channels-server.hoon +++ b/desk/app/channels-server.hoon @@ -67,7 +67,12 @@ abet:(agent:cor wire sign) [cards this] :: - ++ on-arvo on-arvo:def + ++ on-arvo + |= [=wire sign=sign-arvo] + ^- (quip card _this) + =^ cards state + abet:(arvo:cor wire sign) + [cards this] -- :: |_ [=bowl:gall cards=(list card)] @@ -287,7 +292,20 @@ inflate-io :: ++ inflate-io - (safe-watch /groups [our.bowl %groups] /groups) + =. cor (safe-watch /groups [our.bowl %groups] /groups) + %+ roll ~(tap by hooks) + |= [[=nest:c hks=hooks:h] cr=_cor] + %+ roll ~(tap by hooks.hks) + |= [[=id:h =hook:h] co=_cr] + ?~ cron.hook co + ?~ compiled.hook co + ?. enabled.hook co + ?^ delay=(~(get by delayed.hks) id) co + :: only start timers for crons that haven't already been started + =/ fires-at (add now.bowl u.cron.hook) + =- ho-abet.- + %- ho-schedule:(ho-abed:ho-core:co nest) + [%cron id id u.cron.hook !>(~) fires-at] :: ++ poke |= [=mark =vase] @@ -437,6 +455,14 @@ %- (slog 'diary-server: poke failure' >wire< u.p.sign) cor == + :: + [%hooks %effect ~] + ?+ -.sign !! + %poke-ack + ?~ p.sign cor + %- (slog 'hook effect: poke failure' >wire< u.p.sign) + cor + == :: [%groups ~] ?+ -.sign !! @@ -474,6 +500,16 @@ == == :: +++ arvo + |= [=(pole knot) sign=sign-arvo] + ^+ cor + ?+ pole ~|(bad-arvo-take/pole !!) + [%hooks =kind:c ship=@ name=@ rest=*] + =/ ship (slav %p ship.pole) + =/ =nest:c [kind.pole ship name.pole] + ho-abet:(ho-arvo:(ho-abed:ho-core nest) rest.pole) + == +:: ++ watch-groups (safe-watch /groups [our.bowl %groups] /groups) ++ take-groups |= =action:g @@ -651,7 +687,7 @@ (ca-update %perm perm.channel) :: %post - =^ update=(unit u-channel:c) posts.channel + =^ update=(unit u-channel:c) ca-core (ca-c-post c-post.c-channel) ?~ update ca-core (ca-update u.update) @@ -659,9 +695,10 @@ :: ++ ca-c-post |= =c-post:c - ^- [(unit u-channel:c) _posts.channel] + ^- [(unit u-channel:c) _ca-core] ?> (can-write:ca-perms src.bowl writers.perm.perm.channel) =/ =context:h (get-context channel) + =* no-op `ca-core ?- -.c-post %add ?> =(src.bowl author.essay.c-post) @@ -671,7 +708,7 @@ %- ho-run:(ho-abed:ho-core nest) [event context 'post blocked'] ?: ?=(%.n -.result) - ((slog p.result) [~ posts.channel]) + ((slog p.result) [~ ca-core]) =/ =essay:c ?> ?=([%on-post %add *] p.result) essay.p.result @@ -682,33 +719,33 @@ $(now.bowl `@da`(add now.bowl ^~((div ~s1 (bex 16))))) =/ new=v-post:c [[id ~ ~] 0 essay] :- `[%post id %set ~ new] - (put:on-v-posts:c posts.channel id ~ new) + ca-core(posts.channel (put:on-v-posts:c posts.channel id ~ new)) :: %edit ?> |(=(src.bowl author.essay.c-post) (is-admin:ca-perms src.bowl)) ?> =(kind.nest -.kind-data.essay.c-post) =/ post (get:on-v-posts:c posts.channel id.c-post) - ?~ post `posts.channel - ?~ u.post `posts.channel + ?~ post no-op + ?~ u.post no-op ?> |(=(src.bowl author.u.u.post) (is-admin:ca-perms src.bowl)) =^ result=(each event:h tang) cor =/ =event:h [%on-post %edit u.u.post essay.c-post] %- ho-run:(ho-abed:ho-core nest) [event context 'edit blocked'] ?: ?=(%.n -.result) - ((slog p.result) [~ posts.channel]) + ((slog p.result) no-op) =/ =essay:c ?> ?=([%on-post %edit *] p.result) essay.p.result ::TODO could optimize and no-op if the edit is identical to current =/ new=v-post:c [-.u.u.post +(rev.u.u.post) essay] :- `[%post id.c-post %set ~ new] - (put:on-v-posts:c posts.channel id.c-post ~ new) + ca-core(posts.channel (put:on-v-posts:c posts.channel id.c-post ~ new)) :: %del =/ post (get:on-v-posts:c posts.channel id.c-post) - ?~ post `(put:on-v-posts:c posts.channel id.c-post ~) - ?~ u.post `posts.channel + ?~ post `ca-core(posts.channel (put:on-v-posts:c posts.channel id.c-post ~)) + ?~ u.post no-op ?> |(=(src.bowl author.u.u.post) (is-admin:ca-perms src.bowl)) =^ result=(each event:h tang) cor =/ =event:h [%on-post %del u.u.post] @@ -716,12 +753,12 @@ [event context 'delete blocked'] ?> =(& -.result) :- `[%post id.c-post %set ~] - (put:on-v-posts:c posts.channel id.c-post ~) + ca-core(posts.channel (put:on-v-posts:c posts.channel id.c-post ~)) :: ?(%add-react %del-react) =/ post (get:on-v-posts:c posts.channel id.c-post) - ?~ post `posts.channel - ?~ u.post `posts.channel + ?~ post no-op + ?~ u.post no-op =^ result=(each event:h tang) cor =/ =event:h :* %on-post %react u.u.post @@ -731,7 +768,7 @@ %- ho-run:(ho-abed:ho-core nest) [event context 'react action blocked'] ?: ?=(%.n -.result) - ((slog p.result) [~ posts.channel]) + ((slog p.result) no-op) =/ new=c-post:c ?> ?=([%on-post %react *] p.result) ?~ react.p.result [%del-react id.c-post ship.p.result] @@ -739,19 +776,25 @@ =/ [update=? reacts=v-reacts:c] %+ ca-c-react reacts.u.u.post ?>(?=(?(%add-react %del-react) -.new) new) - ?. update `posts.channel + ?. update no-op :- `[%post id.c-post %reacts reacts] - (put:on-v-posts:c posts.channel id.c-post ~ u.u.post(reacts reacts)) + %= ca-core + posts.channel + (put:on-v-posts:c posts.channel id.c-post ~ u.u.post(reacts reacts)) + == :: %reply =/ post (get:on-v-posts:c posts.channel id.c-post) - ?~ post `posts.channel - ?~ u.post `posts.channel + ?~ post no-op + ?~ u.post no-op =^ update=(unit u-post:c) replies.u.u.post (ca-c-reply u.u.post c-reply.c-post context) - ?~ update `posts.channel + ?~ update no-op :- `[%post id.c-post u.update] - (put:on-v-posts:c posts.channel id.c-post ~ u.u.post) + %= ca-core + posts.channel + (put:on-v-posts:c posts.channel id.c-post ~ u.u.post) + == == :: ++ ca-c-reply @@ -940,15 +983,15 @@ |= =v-channel:c ^- context:h =* flag group.perm.perm.v-channel - =/ =group:g - ?. .^(? %gu (scry-path %groups /$)) *group:g + =/ =group-ui:g + ?. .^(? %gu (scry-path %groups /$)) *group-ui:g ?. .^(? %gx (scry-path %groups /exists/(scot %p p.flag)/[q.flag]/noun)) - *group:g - .^(group:g %gx (scry-path %groups /groups/(scot %p p.flag)/[q.flag]/v1/noun)) + *group-ui:g + .^(group-ui:g %gx (scry-path %groups /groups/(scot %p p.flag)/[q.flag]/v1/noun)) :* v-channel v-channels - group - !>(~) :: we default this because each hook will replace with its own + group-ui + *hook:h :: we default this because each hook will replace with itself [now our src eny]:bowl == :: @@ -979,11 +1022,17 @@ =/ src=(rev:c (unit @t)) [0 `src.action] =/ result=(each nock tang) ~& "compiling hook" - ((compile:utils args:h (return:h *)) `src.action) - ~& "compilation result: {}" + ((compile:utils args:h outcome:h) `src.action) =/ compiled - ?: ?=(%| -.result) ~ + ?: ?=(%| -.result) + ((slog 'compilation result:' p.result) ~) + ~& "compilation result: {}" `p.result + =. ho-core + ?~ cron.action ho-core + =/ fires-at (add now.bowl u.cron.action) + =/ dh [id id u.cron.action !>(~) fires-at] + (ho-schedule %cron dh) =. order.hks +:(next-rev:c order.hks (snoc +.order.hks id)) =. hooks.hks @@ -1008,10 +1057,17 @@ ?: ?=(%| -.result) ~ `p.result =. hooks.hks (~(put by hooks.hks) id.action hook) - ho-core + ?. cron-changed ho-core + ?~ cron.action ho-core + =. ho-core (ho-unschedule id.action %cron) + =/ fires-at (add now.bowl u.cron.action) + =/ dh [id.action id.action u.cron.action !>(~) fires-at] + (ho-schedule %cron dh) :: %del :: TODO: make more CRDT + ?~ hook=(~(get by hooks.hks) id.action) ho-core + =. ho-core (ho-unschedule id.action %cron) =. hooks.hks (~(del by hooks.hks) id.action) =/ [* new-order=_order.hks] %+ next-rev:c order.hks @@ -1024,12 +1080,15 @@ %enable =/ hook (~(got by hooks.hks) id.action) =. hooks.hks (~(put by hooks.hks) id.action hook(enabled &)) - ho-core + ?~ cron.hook ho-core + =/ fires-at (add now.bowl u.cron.hook) + =/ dh [id.action id.action u.cron.hook !>(~) fires-at] + (ho-schedule %cron dh) :: %disable =/ hook (~(got by hooks.hks) id.action) =. hooks.hks (~(put by hooks.hks) id.action hook(enabled |)) - ho-core + (ho-unschedule id.action %cron) :: %order =^ changed order.hks @@ -1040,13 +1099,26 @@ |= [=event:h =context:h default=cord] =^ [result=(each event:h tang) effects=(list effect:h)] hks (run-hooks:utils event context default hks) - =. hooks (~(put by hooks) nest hks) - [result (ho-run-effects effects)] + [result ho-abet:(ho-run-effects effects)] + ++ ho-run-single + |= [=event:h prefix=tape =hook:h] + ?~ channel=(~(get by v-channels) nest) ho-core + =/ =context:h (get-context u.channel) + =/ return=(unit return:h) + (run-hook:utils event context hook) + ?~ return + ~& "{prefix} {} failed" + ho-core + ~& "{prefix} {} ran" + =. hooks.hks + (~(put by hooks.hks) id.hook hook(state new-state.u.return)) + (ho-run-effects effects.u.return) ++ ho-run-effects |= effects=(list effect:h) - ^+ cor + ^+ ho-core |- - ?~ effects cor + ?~ effects + ho-core =/ =effect:h i.effects =; new-cor=_ho-core =. ho-core new-cor @@ -1078,11 +1150,42 @@ :: %delay =/ fires-at (add now.bowl wait.effect) - =. delayed.hks - %+ ~(put by delayed.hks) id.effect - +:effect(data [data.effect fires-at]) - =/ =wire /hooks/delayed/(scot %uv id.effect) - (emit [%pass wire %arvo %b %wait fires-at]) + =/ dh +:effect(data [data.effect fires-at]) + =. ho-core (ho-unschedule id.effect %delayed) + (ho-schedule %delayed dh) + == + ++ ho-schedule + |= [type=@tas dh=delayed-hook:h] + ^+ ho-core + ~& "scheduling hook" + =. delayed.hks (~(put by delayed.hks) id.dh dh) + =/ =wire (welp ho-prefix /[type]/(scot %uv id.dh)) + (emit [%pass wire %arvo %b %wait fires-at.dh]) + ++ ho-unschedule + |= [=id:h type=@tas] + ?~ previous=(~(get by delayed.hks) id) ho-core + =/ =wire (welp ho-prefix /[type]/(scot %uv id)) + (emit [%pass wire %arvo %b %rest fires-at.u.previous]) + ++ ho-arvo + |= =(pole knot) + ^+ ho-core + ?+ pole ~|(bad-arvo-take/pole !!) + [%delayed id=@ ~] + =/ =id:h (slav %uv id.pole) + ?~ delay=(~(get by delayed.hks) id) ho-core + ?~ hook=(~(get by hooks.hks) hook.u.delay) ho-core + (ho-run-single [%delay u.delay] "delayed hook" u.hook) + :: + [%cron id=@ ~] + =/ =id:h (slav %uv id.pole) + ?~ delay=(~(get by delayed.hks) id) ho-core + ?~ hook=(~(get by hooks.hks) id) ho-core + :: if unscheduled, ignore + ?~ cron.u.hook ho-core + =/ next (add now.bowl u.cron.u.hook) + =. ho-core (ho-schedule %cron u.delay(fires-at next)) + (ho-run-single [%cron ~] "cron job" u.hook) == + ++ ho-prefix /hooks/[kind.nest]/(scot %p ship.nest)/[name.nest] -- -- diff --git a/desk/gen/hooks/create.hoon b/desk/gen/hooks/create.hoon new file mode 100644 index 0000000000..3b02b7d70e --- /dev/null +++ b/desk/gen/hooks/create.hoon @@ -0,0 +1,13 @@ +/- c=channels, h=hooks +:- %say +|= $: [now=@da eny=@uvJ =beak] + [[=nest:c name=@t src=@t cron=(unit @dr) ~] ~] + == +:- %hook-action-0 +^- [nest:c action:h] +:* nest + %add + name + src + cron +== \ No newline at end of file diff --git a/desk/gen/hooks/truncate.hoon b/desk/gen/hooks/truncate.hoon new file mode 100644 index 0000000000..af66032416 --- /dev/null +++ b/desk/gen/hooks/truncate.hoon @@ -0,0 +1,97 @@ +/- h=hooks, c=channels +:- %say +|= $: [now=@da eny=@uvJ =beak] + [[=event:h ~] ~] + == +:- %noun +^- outcome:h +=| count=@ud +=/ max 140 +=/ new-content=story:c ~ +=* no-op &+[[[%allowed event] ~] !>(~)] +?. ?=(%on-post -.event) no-op +=* on-post +.event +?. ?=(?(%add %edit) -.on-post) no-op +=/ verses + ?- -.on-post + %add content.essay.on-post + %edit content.essay.on-post + == +|^ +:: made it to the end +=* return + =- &+[[[%allowed -] ~] !>(~)] + ?- event + [%on-post %add *] event(content.essay new-content) + [%on-post %edit *] event(content.essay new-content) + == +?~ verses return +?: (gte count max) return +=* next $(verses t.verses) +=/ verse i.verses +:: remove blocks +?: ?=(%block -.verse) next +=/ [new-inlines=(list inline:c) new-count=@ud] + (run-list p.verse count) +$(new-content (snoc new-content [%inline new-inlines]), verses t.verses, count new-count) +++ run-list + |= [inlines=(list inline:c) count=@ud] + ^- [(list inline:c) @ud] + =/ new-inlines=(list inline:c) ~ + |- + ?~ inlines + :: made it all the way through + [new-inlines count] + =* next $(inlines t.inlines) + =/ inline i.inlines + ?: (gte count max) + [new-inlines count] + ?@ inline + =/ new-string (trim-cord inline count) + ?~ new-string $(inlines ~) ::done + $(new-inlines (snoc new-inlines u.new-string), inlines t.inlines, count (add count (met 3 u.new-string))) + =/ [new-inline=(unit inline:c) new-count=@ud] + (run-special-inlines inline count) + ?~ new-inline $(inlines ~) ::done + $(new-inlines (snoc new-inlines u.new-inline), inlines t.inlines, count new-count) +++ run-special-inlines + |= [=inline:c count=@ud] + ^- [(unit inline:c) @ud] + ?+ -.inline [~ count] + %break [`inline +(count)] + :: + %ship + ?: (gth (add count 14) max) [~ count] + [`inline (add count 14)] + :: + %link + =/ new-string=(unit cord) (trim-cord q.inline count) + ?~ new-string [~ count] + =/ new-inline=inline:c inline(q u.new-string) + [(some new-inline) (add count (met 3 u.new-string))] + :: + %inline-code + =/ new-string=(unit cord) (trim-cord p.inline count) + ?~ new-string [~ count] + [(some inline(p u.new-string)) (add count (met 3 u.new-string))] + :: + ?(%italics %bold %strike %blockquote) + =/ [new-inlines=(list inline:c) new-count=@ud] (run-list p.inline count) + ?~ new-inlines [~ count] + [(some inline(p new-inlines)) new-count] + == +++ trim-cord + |= [=cord count=@ud] + ^- (unit ^cord) + =/ string (trip cord) + =/ length (lent string) + =/ total (add length count) + ?: (gth total max) + :: truncate + =/ remainder (sub total max) + :: no room for anything + ?: =(length remainder) ~ + =/ new-length (sub length remainder) + `(crip (scag new-length string)) + `cord +-- \ No newline at end of file diff --git a/desk/lib/channel-utils.hoon b/desk/lib/channel-utils.hoon index b41dd9419d..a1a49fc1e5 100644 --- a/desk/lib/channel-utils.hoon +++ b/desk/lib/channel-utils.hoon @@ -695,7 +695,7 @@ ~& "parsed hoon: {<-.tonk>}" ~& %f ?: ?=(%| -.tonk) - ~& "returning error" + %- (slog 'returning error' p.tonk) tonk &+q.p.tonk ++ execute @@ -705,30 +705,36 @@ %- (soft prod) (slum .*(+:subject nock) simp) :: +++ run-hook + |= [=event:h =context:h =hook:h] + ^- (unit return:h) + ?. enabled.hook ~ + ?~ compiled.hook ~ + =/ =args:h [event context(state state.hook)] + =/ outcome=(unit outcome:h) + ((execute outcome:h) u.compiled.hook args) + ~& "{(trip name.hook)} hook run: {}" + ?~ outcome ~ + ?: ?=(%.y -.u.outcome) `p.u.outcome + ~& "hook failed:" + ((slog p.u.outcome) ~) ++ run-hooks |= [=event:h =context:h default=cord hks=hooks:h] ^- [[(each event:h tang) (list effect:h)] hooks:h] =/ current-event event =| effects=(list effect:h) - =* order +.order.hks + =/ order +.order.hks |- ?~ order [[&+current-event effects] hks] =* next $(order t.order) =/ hook (~(got by hooks.hks) i.order) - ?~ compiled.hook next - =/ =args:h [current-event context(state state.hook)] - =/ outcome=(unit outcome:h) - ((execute outcome:h) u.compiled.hook args) - ~& "{(trip name.hook)} hook run: {}" - ?~ outcome next - ?: ?=(%.n -.u.outcome) - ~& "hook failed:" - %- (slog p.u.outcome) - next - =* result result.p.u.outcome - =. effects (weld effects effects.p.u.outcome) - =. hooks.hks (~(put by hooks.hks) i.order hook(state new-state.p.u.outcome)) + =/ return=(unit return:h) + (run-hook current-event context hook) + ?~ return next + =* result result.u.return + =. effects (weld effects effects.u.return) + =. hooks.hks (~(put by hooks.hks) i.order hook(state new-state.u.return)) ?: ?=(%denied -.result) [[|+~[(fall msg.result default)] effects] hks] =. current-event new.result diff --git a/desk/mar/hook/action-0.hoon b/desk/mar/hook/action-0.hoon new file mode 100644 index 0000000000..325b647171 --- /dev/null +++ b/desk/mar/hook/action-0.hoon @@ -0,0 +1,12 @@ +/- h=hooks, c=channels +|_ [=nest:c =action:h] +++ grad %noun +++ grow + |% + ++ noun [nest action] + -- +++ grab + |% + ++ noun [=nest:c =action:h] + -- +-- diff --git a/desk/sur/hooks.hoon b/desk/sur/hooks.hoon index 77a0bf2efc..1a3696ec38 100644 --- a/desk/sur/hooks.hoon +++ b/desk/sur/hooks.hoon @@ -65,7 +65,7 @@ +$ context $: v-channel channels=v-channels - =group:g + =group-ui:g state=vase now=time our=ship From 938383bea818940ab69aeb99e41aab2a192b9d6c Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Thu, 14 Nov 2024 17:35:10 -0600 Subject: [PATCH 04/52] wip: trying to use delay hooks --- desk/app/channels-server.hoon | 40 +++++++++++++++++------------------ desk/gen/hooks/confirm.hoon | 19 +++++++++++++++++ desk/lib/channel-utils.hoon | 3 ++- desk/sur/hooks.hoon | 10 ++++----- 4 files changed, 46 insertions(+), 26 deletions(-) create mode 100644 desk/gen/hooks/confirm.hoon diff --git a/desk/app/channels-server.hoon b/desk/app/channels-server.hoon index ac95188c32..ae398bde7b 100644 --- a/desk/app/channels-server.hoon +++ b/desk/app/channels-server.hoon @@ -703,21 +703,21 @@ %add ?> =(src.bowl author.essay.c-post) ?> =(kind.nest -.kind-data.essay.c-post) + =/ id=id-post:c + |- + =/ post (get:on-v-posts:c posts.channel now.bowl) + ?~ post now.bowl + $(now.bowl `@da`(add now.bowl ^~((div ~s1 (bex 16))))) + =/ new=v-post:c [[id ~ ~] 0 essay.c-post] =^ result=(each event:h tang) cor - =/ =event:h [%on-post %add essay.c-post] + =/ =event:h [%on-post %add new] %- ho-run:(ho-abed:ho-core nest) [event context 'post blocked'] ?: ?=(%.n -.result) ((slog p.result) [~ ca-core]) - =/ =essay:c + =. new ?> ?=([%on-post %add *] p.result) - essay.p.result - =/ id=id-post:c - |- - =/ post (get:on-v-posts:c posts.channel now.bowl) - ?~ post now.bowl - $(now.bowl `@da`(add now.bowl ^~((div ~s1 (bex 16))))) - =/ new=v-post:c [[id ~ ~] 0 essay] + post.p.result :- `[%post id %set ~ new] ca-core(posts.channel (put:on-v-posts:c posts.channel id ~ new)) :: @@ -804,22 +804,22 @@ ?- -.c-reply %add ?> =(src.bowl author.memo.c-reply) - =^ result=(each event:h tang) cor - =/ =event:h [%on-reply %add parent memo.c-reply] - %- ho-run:(ho-abed:ho-core nest) - [event context 'reply blocked'] - ?: ?=(%.n -.result) - ((slog p.result) [~ replies]) - =/ =memo:c - ?> ?=([%on-reply %add *] p.result) - memo.p.result =/ id=id-reply:c |- =/ reply (get:on-v-replies:c replies now.bowl) ?~ reply now.bowl $(now.bowl `@da`(add now.bowl ^~((div ~s1 (bex 16))))) =/ reply-seal=v-reply-seal:c [id ~] - =/ new=v-reply:c [reply-seal 0 memo] + =/ new=v-reply:c [reply-seal 0 memo.c-reply] + =^ result=(each event:h tang) cor + =/ =event:h [%on-reply %add parent new] + %- ho-run:(ho-abed:ho-core nest) + [event context 'reply blocked'] + ?: ?=(%.n -.result) + ((slog p.result) [~ replies]) + =. new + ?> ?=([%on-reply %add *] p.result) + reply.p.result :- `[%reply id %set ~ new] (put:on-v-replies:c replies id ~ new) :: @@ -1023,10 +1023,10 @@ =/ result=(each nock tang) ~& "compiling hook" ((compile:utils args:h outcome:h) `src.action) + ~& "compilation result: {<-.result>}" =/ compiled ?: ?=(%| -.result) ((slog 'compilation result:' p.result) ~) - ~& "compilation result: {}" `p.result =. ho-core ?~ cron.action ho-core diff --git a/desk/gen/hooks/confirm.hoon b/desk/gen/hooks/confirm.hoon new file mode 100644 index 0000000000..ea6c1f228c --- /dev/null +++ b/desk/gen/hooks/confirm.hoon @@ -0,0 +1,19 @@ +/- h=hooks, c=channels +:- %say +|= $: [now=@da eny=@uvJ =beak] + [[=event:h =context:h ~] ~] + == +:- %noun +^- outcome:h +=- &+[[[%allowed event] -] state.hook.context] +^- (list effect:h) +?. ?=(?(%delay %on-post) -.event) ~ +?: ?=(%delay -.event) + =/ =nest:c [%chat ~bospur-davmyl-nocsyx-lassul %welcome-8458] + =+ !<(trigger=event:h data.event) + ?. ?=([%on-post %add *] trigger) ~ + =* post post.trigger + =/ =c-react:c [%add-react id.post author.post ':thumbs-up:'] + ~[[%channels %channel nest %post c-react]] +=/ id (rsh [3 48] eny.context) +~[[%delay id id.hook.context ~s30 !>(event)]] \ No newline at end of file diff --git a/desk/lib/channel-utils.hoon b/desk/lib/channel-utils.hoon index a1a49fc1e5..054cd30205 100644 --- a/desk/lib/channel-utils.hoon +++ b/desk/lib/channel-utils.hoon @@ -710,7 +710,8 @@ ^- (unit return:h) ?. enabled.hook ~ ?~ compiled.hook ~ - =/ =args:h [event context(state state.hook)] + ~& "running hook: {} {}" + =/ =args:h [event context(hook hook)] =/ outcome=(unit outcome:h) ((execute outcome:h) u.compiled.hook args) ~& "{(trip name.hook)} hook run: {}" diff --git a/desk/sur/hooks.hoon b/desk/sur/hooks.hoon index 1a3696ec38..9373ffa43b 100644 --- a/desk/sur/hooks.hoon +++ b/desk/sur/hooks.hoon @@ -66,7 +66,7 @@ $: v-channel channels=v-channels =group-ui:g - state=vase + =hook now=time our=ship src=ship @@ -75,7 +75,7 @@ :: :: $on-post: a hook event that fires when posts are interacted with +$ on-post - $% [%add =essay] + $% [%add post=v-post] [%edit original=v-post =essay] [%del original=v-post] [%react post=v-post =ship react=(unit react)] @@ -83,7 +83,7 @@ :: :: $on-reply: a hook event that fires when replies are interacted with +$ on-reply - $% [%add parent=v-post =memo] + $% [%add parent=v-post reply=v-reply] [%edit parent=v-post original=v-reply =memo] [%del parent=v-post original=v-reply] [%react parent=v-post reply=v-reply =ship react=(unit react)] @@ -108,8 +108,8 @@ :: :: $args: the arguments passed to a hook +$ args - $: event=event - context=context + $: =event + =context == :: :: $result: the result of a hook running From 5c60aee2a657792feaf00b4c8d996c59ece5f6ee Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Thu, 21 Nov 2024 16:03:56 -0600 Subject: [PATCH 05/52] hooks: responses and usage threads --- desk/app/channels-server.hoon | 500 +++++++++++++++++++--------------- desk/app/channels.hoon | 3 + desk/gen/hooks/create.hoon | 13 - desk/gen/hooks/truncate.hoon | 4 +- desk/lib/channel-utils.hoon | 60 +--- desk/mar/hook/action-0.hoon | 6 +- desk/mar/hook/response-0.hoon | 12 + desk/sur/hooks.hoon | 50 ++-- desk/ted/hooks/add.hoon | 21 ++ desk/ted/hooks/del.hoon | 19 ++ desk/ted/hooks/edit.hoon | 23 ++ desk/ted/hooks/order.hoon | 19 ++ desk/ted/hooks/run.hoon | 65 +++++ 13 files changed, 487 insertions(+), 308 deletions(-) delete mode 100644 desk/gen/hooks/create.hoon create mode 100644 desk/mar/hook/response-0.hoon create mode 100644 desk/ted/hooks/add.hoon create mode 100644 desk/ted/hooks/del.hoon create mode 100644 desk/ted/hooks/edit.hoon create mode 100644 desk/ted/hooks/order.hoon create mode 100644 desk/ted/hooks/run.hoon diff --git a/desk/app/channels-server.hoon b/desk/app/channels-server.hoon index ae398bde7b..9e705c24f4 100644 --- a/desk/app/channels-server.hoon +++ b/desk/app/channels-server.hoon @@ -18,7 +18,7 @@ +$ current-state $: %7 =v-channels:c - hooks=(map nest:c hooks:h) + =hooks:h =pimp:imp == -- @@ -113,7 +113,7 @@ ++ state-6-to-7 |= state-6 ^- state-7 - [%7 v-channels ~ pimp] + [%7 v-channels *hooks:h pimp] +$ state-5 $: %5 =v-channels:v6:old:c @@ -293,19 +293,17 @@ :: ++ inflate-io =. cor (safe-watch /groups [our.bowl %groups] /groups) - %+ roll ~(tap by hooks) - |= [[=nest:c hks=hooks:h] cr=_cor] - %+ roll ~(tap by hooks.hks) - |= [[=id:h =hook:h] co=_cr] - ?~ cron.hook co - ?~ compiled.hook co - ?. enabled.hook co - ?^ delay=(~(get by delayed.hks) id) co + %+ roll ~(tap by crons.hooks) + |= [[=id:h schedules=(map origin:h cron:h)] cr=_cor] + %+ roll ~(tap by schedules) + |= [[=origin:h cron:h] co=_cr] + ?~ hook=(~(get by hooks.hooks) id) co + ?~ compiled.u.hook co + ?^ delay=(~(get by delayed.hooks) delay-id) co :: only start timers for crons that haven't already been started - =/ fires-at (add now.bowl u.cron.hook) - =- ho-abet.- - %- ho-schedule:(ho-abed:ho-core:co nest) - [%cron id id u.cron.hook !>(~) fires-at] + =/ fires-at (add now.bowl schedule) + =/ =wire (hook-cron-wire id origin) + (schedule-hook wire origin delay-id id !>(~) fires-at) :: ++ poke |= [=mark =vase] @@ -352,8 +350,33 @@ == :: %hook-action-0 - =+ !<([=nest:c =action:h] vase) - ho-abet:(ho-action:(ho-abed:ho-core nest) action) + =+ !<(=action:h vase) + ?- -.action + %add + ho-abet:(ho-add:ho-core [name src]:action) + :: + %edit + ho-abet:(ho-edit:(ho-abed:ho-core id.action) [name src]:action) + :: + %del + ho-abet:ho-del:(ho-abed:ho-core id.action) + :: + %order + =/ seq + %+ skim + seq.action + |= =id:h + (~(has by hooks.hooks) id) + =. order.hooks (~(put by order.hooks) nest.action seq) + (give-hook-response %order nest.action seq) + :: + %wait + =/ args [origin schedule]:action + ho-abet:(ho-wait:(ho-abed:ho-core id.action) args) + :: + %rest + ho-abet:(ho-rest:(ho-abed:ho-core id.action) origin.action) + == == :: ++ run-import @@ -399,6 +422,8 @@ ^+ cor ~| watch-path=`path`pole ?+ pole ~|(%bad-watch-path !!) + [%hooks %v0 ~] cor + :: [=kind:c name=@ %create ~] ?> =(our src):bowl =* nest [kind.pole our.bowl name.pole] @@ -504,10 +529,8 @@ |= [=(pole knot) sign=sign-arvo] ^+ cor ?+ pole ~|(bad-arvo-take/pole !!) - [%hooks =kind:c ship=@ name=@ rest=*] - =/ ship (slav %p ship.pole) - =/ =nest:c [kind.pole ship name.pole] - ho-abet:(ho-arvo:(ho-abed:ho-core nest) rest.pole) + [%hooks rest=*] + (wakeup-hook rest.pole) == :: ++ watch-groups (safe-watch /groups [our.bowl %groups] /groups) @@ -651,6 +674,7 @@ |= =c-channel:c ^+ ca-core ?> am-host:ca-perms + ~& "received command {}" ?- -.c-channel %view ?> (is-admin:ca-perms src.bowl) @@ -689,6 +713,7 @@ %post =^ update=(unit u-channel:c) ca-core (ca-c-post c-post.c-channel) + ~& "received post update {}" ?~ update ca-core (ca-update u.update) == @@ -697,12 +722,13 @@ |= =c-post:c ^- [(unit u-channel:c) _ca-core] ?> (can-write:ca-perms src.bowl writers.perm.perm.channel) - =/ =context:h (get-context channel) + ~& "running post command" + =/ =context:h (get-hook-context `[nest channel]) =* no-op `ca-core ?- -.c-post %add - ?> =(src.bowl author.essay.c-post) - ?> =(kind.nest -.kind-data.essay.c-post) + ~& "adding post" + ?> |(=(src.bowl our.bowl) =(src.bowl author.essay.c-post)) =/ id=id-post:c |- =/ post (get:on-v-posts:c posts.channel now.bowl) @@ -711,8 +737,8 @@ =/ new=v-post:c [[id ~ ~] 0 essay.c-post] =^ result=(each event:h tang) cor =/ =event:h [%on-post %add new] - %- ho-run:(ho-abed:ho-core nest) - [event context 'post blocked'] + ~& "running post hooks" + (run-hooks event context nest 'post blocked') ?: ?=(%.n -.result) ((slog p.result) [~ ca-core]) =. new @@ -723,15 +749,13 @@ :: %edit ?> |(=(src.bowl author.essay.c-post) (is-admin:ca-perms src.bowl)) - ?> =(kind.nest -.kind-data.essay.c-post) =/ post (get:on-v-posts:c posts.channel id.c-post) ?~ post no-op ?~ u.post no-op ?> |(=(src.bowl author.u.u.post) (is-admin:ca-perms src.bowl)) =^ result=(each event:h tang) cor =/ =event:h [%on-post %edit u.u.post essay.c-post] - %- ho-run:(ho-abed:ho-core nest) - [event context 'edit blocked'] + (run-hooks event context nest 'edit blocked') ?: ?=(%.n -.result) ((slog p.result) no-op) =/ =essay:c @@ -749,8 +773,7 @@ ?> |(=(src.bowl author.u.u.post) (is-admin:ca-perms src.bowl)) =^ result=(each event:h tang) cor =/ =event:h [%on-post %del u.u.post] - %- ho-run:(ho-abed:ho-core nest) - [event context 'delete blocked'] + (run-hooks event context nest 'delete blocked') ?> =(& -.result) :- `[%post id.c-post %set ~] ca-core(posts.channel (put:on-v-posts:c posts.channel id.c-post ~)) @@ -765,8 +788,7 @@ ?: ?=(%del-react -.c-post) [p.c-post ~] [p `q]:c-post == - %- ho-run:(ho-abed:ho-core nest) - [event context 'react action blocked'] + (run-hooks event context nest 'react action blocked') ?: ?=(%.n -.result) ((slog p.result) no-op) =/ new=c-post:c @@ -813,8 +835,7 @@ =/ new=v-reply:c [reply-seal 0 memo.c-reply] =^ result=(each event:h tang) cor =/ =event:h [%on-reply %add parent new] - %- ho-run:(ho-abed:ho-core nest) - [event context 'reply blocked'] + (run-hooks event context nest 'reply blocked') ?: ?=(%.n -.result) ((slog p.result) [~ replies]) =. new @@ -830,8 +851,7 @@ ?> =(src.bowl author.u.u.reply) =^ result=(each event:h tang) cor =/ =event:h [%on-reply %edit parent u.u.reply memo.c-reply] - %- ho-run:(ho-abed:ho-core nest) - [event context 'edit blocked'] + (run-hooks event context nest 'edit blocked') ?: ?=(%.n -.result) ((slog p.result) [~ replies]) =/ =memo:c @@ -849,8 +869,7 @@ ?> |(=(src.bowl author.u.u.reply) (is-admin:ca-perms src.bowl)) =^ result=(each event:h tang) cor =/ =event:h [%on-reply %del parent u.u.reply] - %- ho-run:(ho-abed:ho-core nest) - [event context 'delete blocked'] + (run-hooks event context nest 'delete blocked') ?> =(& -.result) :- `[%reply id.c-reply %set ~] (put:on-v-replies:c replies id.c-reply ~) @@ -865,8 +884,7 @@ ?: ?=(%del-react -.c-reply) [p.c-reply ~] [p `q]:c-reply == - %- ho-run:(ho-abed:ho-core nest) - [event context 'delete blocked'] + (run-hooks event context nest 'delete blocked') ?: ?=(%.n -.result) ((slog p.result) [~ replies]) =/ new=c-reply:c @@ -885,7 +903,7 @@ |= [reacts=v-reacts:c =c-react:c] ^- [changed=? v-reacts:c] =/ =ship ?:(?=(%add-react -.c-react) p.c-react p.c-react) - ?> =(src.bowl ship) + ?> |(=(src.bowl our.bowl) =(src.bowl ship)) =/ new-react ?:(?=(%add-react -.c-react) `q.c-react ~) =/ [changed=? new-rev=@ud] =/ old-react (~(get by reacts) ship) @@ -979,213 +997,251 @@ %+ welp /(scot %p our.bowl)/[dude]/(scot %da now.bowl) path -++ get-context - |= =v-channel:c +++ get-hook-context + |= channel=(unit [nest:c v-channel:c]) ^- context:h - =* flag group.perm.perm.v-channel - =/ =group-ui:g + =/ group + ?~ channel ~ + =* flag group.perm.perm.+.u.channel + %- some ?. .^(? %gu (scry-path %groups /$)) *group-ui:g ?. .^(? %gx (scry-path %groups /exists/(scot %p p.flag)/[q.flag]/noun)) *group-ui:g .^(group-ui:g %gx (scry-path %groups /groups/(scot %p p.flag)/[q.flag]/v1/noun)) - :* v-channel + :* channel + group v-channels - group-ui *hook:h :: we default this because each hook will replace with itself [now our src eny]:bowl == :: +++ give-hook-response + |= =response:h + ^+ cor + (give %fact ~[/hooks/v0] hook-response-0+!>(response)) ++ ho-core - |_ [=nest:c hks=hooks:h ctx=context:h gone=_|] + |_ [=id:h =hook:h gone=_|] ++ ho-core . ++ emit |=(=card ho-core(cor (^emit card))) ++ emil |=(caz=(list card) ho-core(cor (^emil caz))) ++ give |=(=gift:agent:gall ho-core(cor (^give gift))) ++ ho-abet %_ cor - hooks - ?:(gone (~(del by hooks) nest) (~(put by hooks) nest hks)) + hooks.hooks + ?:(gone (~(del by hooks.hooks) id) (~(put by hooks.hooks) id hook)) == :: ++ ho-abed - |= n=nest:c - ho-core(nest n, hks (~(gut by hooks) n *hooks:h)) + |= i=id:h + ho-core(id i, hook (~(got by hooks.hooks) i)) :: - ++ ho-action - |= =action:h + ++ ho-add + |= [name=@t src=@t] ^+ ho-core - ?> (is-admin:ca-perms:(ca-abed:ca-core nest) src.bowl) - ?- -.action - %add - ~& "adding hook {}" - =/ =id:h (rsh [3 48] eny.bowl) - =/ src=(rev:c (unit @t)) [0 `src.action] - =/ result=(each nock tang) - ~& "compiling hook" - ((compile:utils args:h outcome:h) `src.action) - ~& "compilation result: {<-.result>}" - =/ compiled - ?: ?=(%| -.result) - ((slog 'compilation result:' p.result) ~) - `p.result - =. ho-core - ?~ cron.action ho-core - =/ fires-at (add now.bowl u.cron.action) - =/ dh [id id u.cron.action !>(~) fires-at] - (ho-schedule %cron dh) - =. order.hks - +:(next-rev:c order.hks (snoc +.order.hks id)) - =. hooks.hks - %+ ~(put by hooks.hks) id - [id name.action & src compiled cron.action !>(~)] - ho-core - :: - %edit - ?~ old-hook=(~(get by hooks.hks) id.action) ho-core - =/ hook u.old-hook - =^ src-changed src.hook - (next-rev:c src.hook `src.action) - =/ name-changed !=(name.action name.hook) - =/ cron-changed !=(cron.action cron.hook) - ?. |(src-changed name-changed cron-changed) ho-core - =. name.hook name.action - =. cron.hook cron.action - =. compiled.hook - ?~ +.src.hook ~ - =/ result=(each nock tang) - ((compile:utils args:h return:h) +.src.hook) - ?: ?=(%| -.result) ~ - `p.result - =. hooks.hks (~(put by hooks.hks) id.action hook) - ?. cron-changed ho-core - ?~ cron.action ho-core - =. ho-core (ho-unschedule id.action %cron) - =/ fires-at (add now.bowl u.cron.action) - =/ dh [id.action id.action u.cron.action !>(~) fires-at] - (ho-schedule %cron dh) - :: - %del - :: TODO: make more CRDT - ?~ hook=(~(get by hooks.hks) id.action) ho-core - =. ho-core (ho-unschedule id.action %cron) - =. hooks.hks (~(del by hooks.hks) id.action) - =/ [* new-order=_order.hks] - %+ next-rev:c order.hks - %+ skim +.order.hks - |= =id:h - !=(id id.action) - =. order.hks new-order - ho-core - :: - %enable - =/ hook (~(got by hooks.hks) id.action) - =. hooks.hks (~(put by hooks.hks) id.action hook(enabled &)) - ?~ cron.hook ho-core - =/ fires-at (add now.bowl u.cron.hook) - =/ dh [id.action id.action u.cron.hook !>(~) fires-at] - (ho-schedule %cron dh) - :: - %disable - =/ hook (~(got by hooks.hks) id.action) - =. hooks.hks (~(put by hooks.hks) id.action hook(enabled |)) - (ho-unschedule id.action %cron) - :: - %order - =^ changed order.hks - (next-rev:c order.hks seq.action) - ho-core - == - ++ ho-run - |= [=event:h =context:h default=cord] - =^ [result=(each event:h tang) effects=(list effect:h)] hks - (run-hooks:utils event context default hks) - [result ho-abet:(ho-run-effects effects)] + ~& "adding hook {}" + =. id (rsh [3 48] eny.bowl) + =/ result=(each vase tang) + ~& "compiling hook" + (compile:utils src) + ~& "compilation result: {<-.result>}" + =/ compiled + ?: ?=(%| -.result) + ((slog 'compilation result:' p.result) ~) + `p.result + =. hook [id name %0 src compiled !>(~)] + =. cor + =/ error=(unit tang) + ?:(?=(%& -.result) ~ `p.result) + (give-hook-response [%set id name src error]) + ho-core + ++ ho-edit + |= [name=@t src=@t] + =. src.hook src + =. name.hook name + =/ result=(each vase tang) + (compile:utils src.hook) + =. compiled.hook + ?: ?=(%| -.result) ~ + `p.result + =. cor + =/ error=(unit tang) + ?:(?=(%& -.result) ~ `p.result) + (give-hook-response [%set id name src error]) + ho-core + :: + ++ ho-del + =. gone & + =. cor + %+ roll + ~(tap by (~(gut by crons.hooks) id *(map origin:h cron:h))) + |= [[=origin:h cron:h] cr=_cor] + =/ =wire (hook-cron-wire id origin) + (unschedule-hook:cr delay-id wire) + =. crons.hooks (~(del by crons.hooks) id) + =. order.hooks + %+ roll + ~(tap by order.hooks) + |= [[=nest:c ids=(list id:h)] or=(map nest:c (list id:h))] + =- (~(put by or) nest -) + (skip ids |=(i=id:h =(id i))) + =. delayed.hooks + %+ roll + ~(tap by delayed.hooks) + |= [[=delay-id:h d=[* delayed-hook:h]] dh=_delayed.hooks] + ?. =(id hook.d) dh + (~(del by dh) delay-id) + =. cor (give-hook-response [%gone id]) + ho-core + ++ ho-wait + |= [=origin:h schedule=@dr] + ^+ ho-core + =/ d-id (rsh [3 48] eny.bowl) + =/ crons (~(gut by crons.hooks) id *(map origin:h cron:h)) + =. crons.hooks + =- (~(put by crons.hooks) id.hook -) + (~(put by crons) origin [d-id schedule]) + =/ fires-at (add now.bowl schedule) + =/ dh [d-id id !>(~) fires-at] + =/ =wire (hook-cron-wire id origin) + =. cor (schedule-hook wire origin dh) + =. cor (give-hook-response [%wait id origin schedule]) + ho-core + ++ ho-rest + |= =origin:h + ^+ ho-core + =/ crons (~(got by crons.hooks) id) + =/ cron (~(got by crons) origin) + =. crons.hooks + (~(put by crons.hooks) id (~(del by crons) origin)) + =/ =wire (hook-cron-wire id origin) + =. cor (unschedule-hook delay-id.cron wire) + =. cor (give-hook-response [%rest id origin]) + ho-core ++ ho-run-single - |= [=event:h prefix=tape =hook:h] - ?~ channel=(~(get by v-channels) nest) ho-core - =/ =context:h (get-context u.channel) + |= [=event:h prefix=tape =origin:h] + =/ channel + ?@ origin ~ + ?~ ch=(~(get by v-channels) origin) ~ + `[origin u.ch] + =/ =context:h (get-hook-context channel) =/ return=(unit return:h) (run-hook:utils event context hook) ?~ return - ~& "{prefix} {} failed" - ho-core - ~& "{prefix} {} ran" - =. hooks.hks - (~(put by hooks.hks) id.hook hook(state new-state.u.return)) - (ho-run-effects effects.u.return) - ++ ho-run-effects - |= effects=(list effect:h) - ^+ ho-core - |- - ?~ effects + ~& "{prefix} {} failed" ho-core - =/ =effect:h i.effects - =; new-cor=_ho-core - =. ho-core new-cor - $(effects t.effects) - ?- -.effect - %channels - =/ =cage channel-action+!>(a-channels.effect) - (emit [%pass /hooks/effect %agent [our.bowl %channels] %poke cage]) - :: - %groups - =/ =cage group-action-3+!>(action.effect) - (emit [%pass /hooks/effect %agent [our.bowl %groups] %poke cage]) - :: - %activity - =/ =cage activity-action+!>(action.effect) - (emit [%pass /hooks/effect %agent [our.bowl %activity] %poke cage]) - :: - %dm - =/ =cage chat-dm-action+!>(action.effect) - (emit [%pass /hooks/effect %agent [our.bowl %chat] %poke cage]) - :: - %club - =/ =cage chat-club-action+!>(action.effect) - (emit [%pass /hooks/effect %agent [our.bowl %chat] %poke cage]) - :: - %contacts - =/ =cage contacts-action-1+!>(action.effect) - (emit [%pass /hooks/effect %agent [our.bowl %contacts] %poke cage]) - :: - %delay - =/ fires-at (add now.bowl wait.effect) - =/ dh +:effect(data [data.effect fires-at]) - =. ho-core (ho-unschedule id.effect %delayed) - (ho-schedule %delayed dh) - == - ++ ho-schedule - |= [type=@tas dh=delayed-hook:h] - ^+ ho-core - ~& "scheduling hook" - =. delayed.hks (~(put by delayed.hks) id.dh dh) - =/ =wire (welp ho-prefix /[type]/(scot %uv id.dh)) - (emit [%pass wire %arvo %b %wait fires-at.dh]) - ++ ho-unschedule - |= [=id:h type=@tas] - ?~ previous=(~(get by delayed.hks) id) ho-core - =/ =wire (welp ho-prefix /[type]/(scot %uv id)) - (emit [%pass wire %arvo %b %rest fires-at.u.previous]) - ++ ho-arvo - |= =(pole knot) - ^+ ho-core - ?+ pole ~|(bad-arvo-take/pole !!) - [%delayed id=@ ~] - =/ =id:h (slav %uv id.pole) - ?~ delay=(~(get by delayed.hks) id) ho-core - ?~ hook=(~(get by hooks.hks) hook.u.delay) ho-core - (ho-run-single [%delay u.delay] "delayed hook" u.hook) - :: - [%cron id=@ ~] - =/ =id:h (slav %uv id.pole) - ?~ delay=(~(get by delayed.hks) id) ho-core - ?~ hook=(~(get by hooks.hks) id) ho-core - :: if unscheduled, ignore - ?~ cron.u.hook ho-core - =/ next (add now.bowl u.cron.u.hook) - =. ho-core (ho-schedule %cron u.delay(fires-at next)) - (ho-run-single [%cron ~] "cron job" u.hook) - == - ++ ho-prefix /hooks/[kind.nest]/(scot %p ship.nest)/[name.nest] + ~& "{prefix} {} ran" + =. hook hook(state new-state.u.return) + =. cor (run-hook-effects effects.u.return origin) + ho-core -- +++ run-hooks + |= [=event:h =context:h =nest:c default=cord] + ^- [(each event:h tang) _cor] + =; [result=(each event:h tang) effects=(list effect:h)] + [result (run-hook-effects effects nest)] + =/ current-event event + =| effects=(list effect:h) + =/ order (~(got by order.hooks) nest) + ~& "got orders {}" + |- + ?~ order + [&+current-event effects] + =* next $(order t.order) + ~& "getting hook" + =/ hook (~(got by hooks.hooks) i.order) + =/ return=(unit return:h) + (run-hook:utils current-event context hook) + ?~ return next + =* result result.u.return + =. effects (weld effects effects.u.return) + =. hooks.hooks (~(put by hooks.hooks) i.order hook(state new-state.u.return)) + ?: ?=(%denied -.result) + [|+~[(fall msg.result default)] effects] + =. current-event new.result + next +++ wakeup-hook + |= =(pole knot) + ^+ cor + ?+ pole ~|(bad-arvo-take/pole !!) + [%delayed id=@ ~] + =/ =id:h (slav %uv id.pole) + ?~ delay=(~(get by delayed.hooks) id) cor + :: make sure we clean up + =. delayed.hooks (~(del by delayed.hooks) id) + =/ args [[%wake +.u.delay] "delayed hook" origin.u.delay] + ho-abet:(ho-run-single:(ho-abed:ho-core hook.u.delay) args) + :: + [%cron id=@ kind=?(%chat %diary %heap) ship=@ name=@ ~] + =/ =id:h (slav %uv id.pole) + =/ =origin:h [kind.pole (slav %p ship.pole) name.pole] + :: if unscheduled, ignore + ?~ crons=(~(get by crons.hooks) id) cor + ?~ cron=(~(get by u.crons) origin) cor + ?~ delay=(~(get by delayed.hooks) delay-id.u.cron) cor + =. delayed.hooks (~(del by delayed.hooks) delay-id.u.cron) + =/ next (add now.bowl schedule.u.cron) + =/ =wire (hook-cron-wire id origin) + =. cor + (schedule-hook wire origin +.u.delay(fires-at next)) + =/ args [[%cron ~] "cron job" origin] + ho-abet:(ho-run-single:(ho-abed:ho-core hook.u.delay) args) + == +++ hook-cron-wire + |= [=id:h =origin:h] + ^- wire + %+ welp /hooks/cron/(scot %uv id) + ?@ origin ~ + /[kind.origin]/(scot %p ship.origin)/[name.origin] +++ schedule-hook + |= [=wire =origin:h dh=delayed-hook:h] + ^+ cor + ~& "scheduling hook" + =. delayed.hooks (~(put by delayed.hooks) id.dh [origin dh]) + (emit [%pass wire %arvo %b %wait fires-at.dh]) +++ unschedule-hook + |= [=id:h =wire] + ^+ cor + ?~ previous=(~(get by delayed.hooks) id) cor + ~& "unscheduling hook" + (emit [%pass wire %arvo %b %rest fires-at.u.previous]) +++ run-hook-effects + |= [effects=(list effect:h) =origin:h] + ^+ cor + |- + ?~ effects + cor + =/ =effect:h i.effects + =; new-cor=_cor + =. cor new-cor + $(effects t.effects) + ?- -.effect + %channels + =/ =cage channel-action+!>(a-channels.effect) + (emit [%pass /hooks/effect %agent [our.bowl %channels] %poke cage]) + :: + %groups + =/ =cage group-action-3+!>(action.effect) + (emit [%pass /hooks/effect %agent [our.bowl %groups] %poke cage]) + :: + %activity + =/ =cage activity-action+!>(action.effect) + (emit [%pass /hooks/effect %agent [our.bowl %activity] %poke cage]) + :: + %dm + =/ =cage chat-dm-action+!>(action.effect) + (emit [%pass /hooks/effect %agent [our.bowl %chat] %poke cage]) + :: + %club + =/ =cage chat-club-action+!>(action.effect) + (emit [%pass /hooks/effect %agent [our.bowl %chat] %poke cage]) + :: + %contacts + =/ =cage contacts-action-1+!>(action.effect) + (emit [%pass /hooks/effect %agent [our.bowl %contacts] %poke cage]) + :: + %wait + =/ =wire /hooks/delayed/(scot %uv id.effect) + =. cor (unschedule-hook id.effect wire) + (schedule-hook wire origin +:effect) + == -- diff --git a/desk/app/channels.hoon b/desk/app/channels.hoon index 364f628953..2c8b4f38e5 100644 --- a/desk/app/channels.hoon +++ b/desk/app/channels.hoon @@ -734,6 +734,9 @@ :: [%x %v2 %channels full=?(~ [%full ~])] ``channels-2+!>((uv-channels-2:utils v-channels ?=(^ full.pole))) + :: + [%x %v3 %v-channels ~] + ``noun+!>(v-channels) :: [%x ?(%v0 %v1) %init ~] ``noun+!>([unreads (uv-channels-1:utils v-channels)]) [%x %v2 %init ~] ``noun+!>([unreads (uv-channels-2:utils v-channels |)]) diff --git a/desk/gen/hooks/create.hoon b/desk/gen/hooks/create.hoon deleted file mode 100644 index 3b02b7d70e..0000000000 --- a/desk/gen/hooks/create.hoon +++ /dev/null @@ -1,13 +0,0 @@ -/- c=channels, h=hooks -:- %say -|= $: [now=@da eny=@uvJ =beak] - [[=nest:c name=@t src=@t cron=(unit @dr) ~] ~] - == -:- %hook-action-0 -^- [nest:c action:h] -:* nest - %add - name - src - cron -== \ No newline at end of file diff --git a/desk/gen/hooks/truncate.hoon b/desk/gen/hooks/truncate.hoon index af66032416..538738a432 100644 --- a/desk/gen/hooks/truncate.hoon +++ b/desk/gen/hooks/truncate.hoon @@ -14,7 +14,7 @@ ?. ?=(?(%add %edit) -.on-post) no-op =/ verses ?- -.on-post - %add content.essay.on-post + %add content.essay.post.on-post %edit content.essay.on-post == |^ @@ -22,7 +22,7 @@ =* return =- &+[[[%allowed -] ~] !>(~)] ?- event - [%on-post %add *] event(content.essay new-content) + [%on-post %add *] event(content.essay.post new-content) [%on-post %edit *] event(content.essay new-content) == ?~ verses return diff --git a/desk/lib/channel-utils.hoon b/desk/lib/channel-utils.hoon index 054cd30205..1fa0dae925 100644 --- a/desk/lib/channel-utils.hoon +++ b/desk/lib/channel-utils.hoon @@ -676,68 +676,38 @@ ;br; == -- -++ subject ^~(!>([..subject ..zuse])) +++ subject ^~(!>(..compile)) ++ compile - |* [args=mold return=mold] - |= src=(unit @t) - ^- (each nock tang) - ?~ src |+~['no src'] + |= src=@t + ^- (each vase tang) ~& %a - =/ tonk=(each (pair type nock) tang) + =/ tonk=(each vase tang) ~& %b - =/ vex=(like hoon) ((full vest) [0 0] (trip u.src)) + =/ vex=(like hoon) ((full vest) [0 0] (trip src)) ~& %c ?~ q.vex |+~[leaf+"\{{} {}}" 'syntax error'] ~& %d %- mule - |.((~(mint ut -:subject) %noun p.u.q.vex)) + |.((slap subject p.u.q.vex)) ~& %e ~& "parsed hoon: {<-.tonk>}" ~& %f ?: ?=(%| -.tonk) %- (slog 'returning error' p.tonk) tonk - &+q.p.tonk -++ execute - |* prod=mold - |= [=nock simp=*] - ^- (unit prod) - %- (soft prod) - (slum .*(+:subject nock) simp) -:: + &+p.tonk ++ run-hook |= [=event:h =context:h =hook:h] ^- (unit return:h) - ?. enabled.hook ~ - ?~ compiled.hook ~ ~& "running hook: {} {}" + ?~ compiled.hook + ~&("hook not compiled" ~) + :: ~& "nock: {}" =/ =args:h [event context(hook hook)] - =/ outcome=(unit outcome:h) - ((execute outcome:h) u.compiled.hook args) - ~& "{(trip name.hook)} hook run: {}" - ?~ outcome ~ - ?: ?=(%.y -.u.outcome) `p.u.outcome + =+ !<(=outcome:h (slam u.compiled.hook !>(args))) + ~& "{(trip name.hook)} hook run:" + ~& outcome + ?: ?=(%.y -.outcome) `p.outcome ~& "hook failed:" - ((slog p.u.outcome) ~) -++ run-hooks - |= [=event:h =context:h default=cord hks=hooks:h] - ^- [[(each event:h tang) (list effect:h)] hooks:h] - =/ current-event event - =| effects=(list effect:h) - =/ order +.order.hks - |- - ?~ order - [[&+current-event effects] hks] - =* next $(order t.order) - =/ hook (~(got by hooks.hks) i.order) - =/ return=(unit return:h) - (run-hook current-event context hook) - ?~ return next - =* result result.u.return - =. effects (weld effects effects.u.return) - =. hooks.hks (~(put by hooks.hks) i.order hook(state new-state.u.return)) - ?: ?=(%denied -.result) - [[|+~[(fall msg.result default)] effects] hks] - =. current-event new.result - next + ((slog p.outcome) ~) -- diff --git a/desk/mar/hook/action-0.hoon b/desk/mar/hook/action-0.hoon index 325b647171..bab332196b 100644 --- a/desk/mar/hook/action-0.hoon +++ b/desk/mar/hook/action-0.hoon @@ -1,12 +1,12 @@ /- h=hooks, c=channels -|_ [=nest:c =action:h] +|_ =action:h ++ grad %noun ++ grow |% - ++ noun [nest action] + ++ noun action -- ++ grab |% - ++ noun [=nest:c =action:h] + ++ noun action:h -- -- diff --git a/desk/mar/hook/response-0.hoon b/desk/mar/hook/response-0.hoon new file mode 100644 index 0000000000..88f7887db3 --- /dev/null +++ b/desk/mar/hook/response-0.hoon @@ -0,0 +1,12 @@ +/- h=hooks, c=channels +|_ =response:h +++ grad %noun +++ grow + |% + ++ noun response + -- +++ grab + |% + ++ noun response:h + -- +-- diff --git a/desk/sur/hooks.hoon b/desk/sur/hooks.hoon index 9373ffa43b..3ada49b788 100644 --- a/desk/sur/hooks.hoon +++ b/desk/sur/hooks.hoon @@ -7,48 +7,52 @@ :: :: $id: a unique identifier for the hook :: $name: a human-readable name for the hook -:: $origin: whether or not this hook was added by us or came through an update +:: $version: the version the hook was compiled with :: $src: the source code of the hook -:: $compiled: the compiled nock of the hook -:: $cron: the cron schedule for the hook if it has one +:: $compiled: the compiled version of the hook :: $state: the current state of the hook :: ++ hook $: =id name=@t - enabled=? - src=(rev src=(unit @t)) - compiled=(unit nock) - cron=(unit @dr) + version=%0 + src=@t + compiled=(unit vase) state=vase == :: $hooks: collection of hooks, the order they should be run in, and :: any delayed hooks that need to be run ++ hooks $: hooks=(map id hook) - order=(rev (list id)) - delayed=(map id delayed-hook) + order=(map nest (list id)) + crons=(map id (map origin cron)) + delayed=(map delay-id [=origin delayed-hook]) == ++$ origin $@(~ nest) ++$ delay-id id ++$ cron [=delay-id schedule=@dr] :: $delayed-hook: metadata for when a delayed hook fires from the timer +$ delayed-hook - $: =id + $: id=delay-id hook=id - wait=@dr data=vase fires-at=time == :: +$ action - $% [%add name=@t src=@t cron=(unit @dr)] - [%edit =id name=@t src=@t cron=(unit @dr)] + $% [%add name=@t src=@t] + [%edit =id name=@t src=@t] [%del =id] - [%enable =id] - [%disable =id] - [%order seq=(list id)] + [%order =nest seq=(list id)] + [%wait =id =origin schedule=@dr] + [%rest =id =origin] == +$ response - $% [%set =id name=@t src=(unit @t) error=(unit tang)] - [%order seq=(list id)] + $% [%set =id name=@t src=@t error=(unit tang)] + [%gone =id] + [%order =nest seq=(list id)] + [%wait =id =origin schedule=@dr] + [%rest =id =origin] == :: $context: ambient state that a hook should know about not :: necessarily tied to a specific event @@ -63,9 +67,9 @@ :: $eny: entropy for random number generation or key derivation :: +$ context - $: v-channel + $: channel=(unit [=nest v-channel]) + group=(unit group-ui:g) channels=v-channels - =group-ui:g =hook now=time our=ship @@ -96,14 +100,14 @@ :: $on-post: a post was added, edited, deleted, or reacted to :: $on-reply: a reply was added, edited, deleted, or reacted to :: $cron: a scheduled wake-up -:: $delay: a delayed invocation of the hook called with metadata about +:: $wake: a delayed invocation of the hook called with metadata about :: when it fired, its id, and the event it should run with :: +$ event $% [%on-post on-post] [%on-reply on-reply] [%cron ~] - [%delay delayed-hook] + [%wake delayed-hook] == :: :: $args: the arguments passed to a hook @@ -135,7 +139,7 @@ [%dm =action:dm:ch] [%club =action:club:ch] [%contacts =action:co] - [%delay =id hook=id wait=@dr data=vase] + [%wait delayed-hook] == :: :: $return: the data returned from a hook diff --git a/desk/ted/hooks/add.hoon b/desk/ted/hooks/add.hoon new file mode 100644 index 0000000000..cafb458e84 --- /dev/null +++ b/desk/ted/hooks/add.hoon @@ -0,0 +1,21 @@ +/- spider, h=hooks +/+ s=strandio +=, strand=strand:spider +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<([~ name=@t src=@t] arg) +;< our=@p bind:m get-our:s +;< ~ bind:m (watch:s /responses [our %channels-server] /hooks/v0) +=/ =cage hook-action-0+!>(`action:h`[%add name src]) +;< ~ bind:m (poke-our:s %channels-server cage) +;< =^cage bind:m (take-fact:s /responses) +?> ?=(%hook-response-0 p.cage) +=+ !<(=response:h q.cage) +?> ?=(%set -.response) +~& "hook {} added with id {}" +?~ error.response (pure:m !>(~)) +~& "compilation error:" +%- (slog u.error.response) +(pure:m !>(~)) \ No newline at end of file diff --git a/desk/ted/hooks/del.hoon b/desk/ted/hooks/del.hoon new file mode 100644 index 0000000000..0ffc680d72 --- /dev/null +++ b/desk/ted/hooks/del.hoon @@ -0,0 +1,19 @@ +/- spider, h=hooks +/+ s=strandio +=, strand=strand:spider +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<([~ =id:h] arg) +;< our=@p bind:m get-our:s +;< ~ bind:m (watch:s /responses [our %channels-server] /hooks/v0) +=/ =cage hook-action-0+!>(`action:h`[%del id]) +;< ~ bind:m (poke-our:s %channels-server cage) +;< =^cage bind:m (take-fact:s /responses) +?> ?=(%hook-response-0 p.cage) +=+ !<(=response:h q.cage) +?> ?=(%gone -.response) +?> =(id id.response) +~& "hook {} deleted" +(pure:m !>(~)) \ No newline at end of file diff --git a/desk/ted/hooks/edit.hoon b/desk/ted/hooks/edit.hoon new file mode 100644 index 0000000000..05e1090f95 --- /dev/null +++ b/desk/ted/hooks/edit.hoon @@ -0,0 +1,23 @@ +/- spider, h=hooks +/+ s=strandio +=, strand=strand:spider +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<([~ =id:h name=@t src=@t] arg) +;< our=@p bind:m get-our:s +;< ~ bind:m (watch:s /responses [our %channels-server] /hooks/v0) +=/ =cage hook-action-0+!>(`action:h`[%edit id name src]) +;< ~ bind:m (poke-our:s %channels-server cage) +;< =^cage bind:m (take-fact:s /responses) +?> ?=(%hook-response-0 p.cage) +=+ !<(=response:h q.cage) +?> ?=(%set -.response) +?~ error.response + ~& "hook {} edited successfully" + (pure:m !>(~)) +~& "hook {} edited" +~& "compilation error:" +%- (slog u.error.response) +(pure:m !>(~)) \ No newline at end of file diff --git a/desk/ted/hooks/order.hoon b/desk/ted/hooks/order.hoon new file mode 100644 index 0000000000..e9980c1994 --- /dev/null +++ b/desk/ted/hooks/order.hoon @@ -0,0 +1,19 @@ +/- spider, h=hooks, c=channels +/+ s=strandio +=, strand=strand:spider +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<([~ =nest:c seq=(list id:h)] arg) +;< our=@p bind:m get-our:s +;< ~ bind:m (watch:s /responses [our %channels-server] /hooks/v0) +=/ =cage hook-action-0+!>(`action:h`[%order nest seq]) +;< ~ bind:m (poke-our:s %channels-server cage) +;< =^cage bind:m (take-fact:s /responses) +?> ?=(%hook-response-0 p.cage) +=+ !<(=response:h q.cage) +?> ?=(%order -.response) +~& "new hook order for {}" +~& seq.response +(pure:m !>(~)) \ No newline at end of file diff --git a/desk/ted/hooks/run.hoon b/desk/ted/hooks/run.hoon new file mode 100644 index 0000000000..1f9ea6f685 --- /dev/null +++ b/desk/ted/hooks/run.hoon @@ -0,0 +1,65 @@ +/- spider, h=hooks, c=channels, g=groups +/+ s=strandio, utils=channel-utils +=, strand=strand:spider +^- thread:spider +|= arg=vase +|^ +=/ m (strand ,vase) +^- form:m +=+ !<([~ =event:h =context-option src=@t] arg) +;< our=@p bind:m get-our:s +=/ compiled=(each vase tang) (compile:utils src) +?. ?=(%& -.compiled) + ~& "compilation error:" + %- (slog p.compiled) + (pure:m !>(~)) +~& "compiled successfully" +;< ctx=context:h bind:m (get-context context-option) +=+ !<(=outcome:h (slam p.compiled !>([event ctx]))) +?: ?=(%.y -.outcome) + ~& "hook ran successfully" + (pure:m !>(p.outcome)) +~& "hook failed:" +%- (slog p.outcome) +(pure:m !>(~)) ++$ event-option + $% [%ref path=@t] + [%event event:h] + == ++$ context-option + $% [%origin =origin:h state=(unit vase)] + [%context =context:h] + == +++ get-context + |= =context-option + =/ m (strand ,context:h) + ^- form:m + ?: ?=(%context -.context-option) (pure:m context.context-option) + =/ [=origin:h state=(unit vase)] +.context-option + ;< =v-channels:c bind:m + (scry:s v-channels:c /gx/channels/v3/v-channels/noun) + =/ channel=(unit [=nest:c vc=v-channel:c]) + ?~ origin ~ + `[origin (~(gut by v-channels) origin *v-channel:c)] + ;< group=(unit group-ui:g) bind:m + =/ n (strand (unit group-ui:g)) + ?~ channel (pure:n ~) + =* flag group.perm.perm.vc.u.channel + ;< live=? bind:n (scry:s ? /gu/groups/$) + ?. live (pure:n `*group-ui:g) + ;< exists=? bind:n + (scry:s ? /gx/groups/exists/(scot %p p.flag)/[q.flag]/noun) + ?. exists (pure:n `*group-ui:g) + ;< =group-ui:g bind:n + (scry:s group-ui:g /groups/groups/(scot %p p.flag)/[q.flag]/v1/noun) + (pure:n (some group-ui)) + ;< =bowl:spider bind:m get-bowl:s + =/ hook *hook:h + %- pure:m + :* channel + group + v-channels + hook(state ?~(state !>(~) u.state)) + [now our src eny]:bowl + == +-- From 87ec95d71487671ba38ec21f9d00a3f6db4e303d Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Fri, 22 Nov 2024 12:07:28 -0600 Subject: [PATCH 06/52] hooks: adding configuration --- desk/app/channels-server.hoon | 64 +++++++++++++++++++++-------------- desk/lib/channel-utils.hoon | 3 +- desk/sur/hooks.hoon | 19 ++++++++--- desk/ted/hooks/configure.hoon | 18 ++++++++++ 4 files changed, 73 insertions(+), 31 deletions(-) create mode 100644 desk/ted/hooks/configure.hoon diff --git a/desk/app/channels-server.hoon b/desk/app/channels-server.hoon index 9e705c24f4..100e52b4b4 100644 --- a/desk/app/channels-server.hoon +++ b/desk/app/channels-server.hoon @@ -369,10 +369,12 @@ (~(has by hooks.hooks) id) =. order.hooks (~(put by order.hooks) nest.action seq) (give-hook-response %order nest.action seq) + :: + %configure + ho-abet:(ho-configure:(ho-abed:ho-core id.action) +.+.action) :: %wait - =/ args [origin schedule]:action - ho-abet:(ho-wait:(ho-abed:ho-core id.action) args) + ho-abet:(ho-wait:(ho-abed:ho-core id.action) +.+.action) :: %rest ho-abet:(ho-rest:(ho-abed:ho-core id.action) origin.action) @@ -723,7 +725,6 @@ ^- [(unit u-channel:c) _ca-core] ?> (can-write:ca-perms src.bowl writers.perm.perm.channel) ~& "running post command" - =/ =context:h (get-hook-context `[nest channel]) =* no-op `ca-core ?- -.c-post %add @@ -738,7 +739,7 @@ =^ result=(each event:h tang) cor =/ =event:h [%on-post %add new] ~& "running post hooks" - (run-hooks event context nest 'post blocked') + (run-hooks event nest 'post blocked') ?: ?=(%.n -.result) ((slog p.result) [~ ca-core]) =. new @@ -755,7 +756,7 @@ ?> |(=(src.bowl author.u.u.post) (is-admin:ca-perms src.bowl)) =^ result=(each event:h tang) cor =/ =event:h [%on-post %edit u.u.post essay.c-post] - (run-hooks event context nest 'edit blocked') + (run-hooks event nest 'edit blocked') ?: ?=(%.n -.result) ((slog p.result) no-op) =/ =essay:c @@ -773,7 +774,7 @@ ?> |(=(src.bowl author.u.u.post) (is-admin:ca-perms src.bowl)) =^ result=(each event:h tang) cor =/ =event:h [%on-post %del u.u.post] - (run-hooks event context nest 'delete blocked') + (run-hooks event nest 'delete blocked') ?> =(& -.result) :- `[%post id.c-post %set ~] ca-core(posts.channel (put:on-v-posts:c posts.channel id.c-post ~)) @@ -788,7 +789,7 @@ ?: ?=(%del-react -.c-post) [p.c-post ~] [p `q]:c-post == - (run-hooks event context nest 'react action blocked') + (run-hooks event nest 'react action blocked') ?: ?=(%.n -.result) ((slog p.result) no-op) =/ new=c-post:c @@ -810,7 +811,7 @@ ?~ post no-op ?~ u.post no-op =^ update=(unit u-post:c) replies.u.u.post - (ca-c-reply u.u.post c-reply.c-post context) + (ca-c-reply u.u.post c-reply.c-post) ?~ update no-op :- `[%post id.c-post u.update] %= ca-core @@ -820,7 +821,7 @@ == :: ++ ca-c-reply - |= [parent=v-post:c =c-reply:c =context:h] + |= [parent=v-post:c =c-reply:c] ^- [(unit u-post:c) v-replies:c] =* replies replies.parent ?- -.c-reply @@ -835,7 +836,7 @@ =/ new=v-reply:c [reply-seal 0 memo.c-reply] =^ result=(each event:h tang) cor =/ =event:h [%on-reply %add parent new] - (run-hooks event context nest 'reply blocked') + (run-hooks event nest 'reply blocked') ?: ?=(%.n -.result) ((slog p.result) [~ replies]) =. new @@ -851,7 +852,7 @@ ?> =(src.bowl author.u.u.reply) =^ result=(each event:h tang) cor =/ =event:h [%on-reply %edit parent u.u.reply memo.c-reply] - (run-hooks event context nest 'edit blocked') + (run-hooks event nest 'edit blocked') ?: ?=(%.n -.result) ((slog p.result) [~ replies]) =/ =memo:c @@ -869,7 +870,7 @@ ?> |(=(src.bowl author.u.u.reply) (is-admin:ca-perms src.bowl)) =^ result=(each event:h tang) cor =/ =event:h [%on-reply %del parent u.u.reply] - (run-hooks event context nest 'delete blocked') + (run-hooks event nest 'delete blocked') ?> =(& -.result) :- `[%reply id.c-reply %set ~] (put:on-v-replies:c replies id.c-reply ~) @@ -884,7 +885,7 @@ ?: ?=(%del-react -.c-reply) [p.c-reply ~] [p `q]:c-reply == - (run-hooks event context nest 'delete blocked') + (run-hooks event nest 'delete blocked') ?: ?=(%.n -.result) ((slog p.result) [~ replies]) =/ new=c-reply:c @@ -998,7 +999,7 @@ /(scot %p our.bowl)/[dude]/(scot %da now.bowl) path ++ get-hook-context - |= channel=(unit [nest:c v-channel:c]) + |= [channel=(unit [nest:c v-channel:c]) =config:h] ^- context:h =/ group ?~ channel ~ @@ -1012,6 +1013,7 @@ group v-channels *hook:h :: we default this because each hook will replace with itself + config [now our src eny]:bowl == :: @@ -1048,7 +1050,7 @@ ?: ?=(%| -.result) ((slog 'compilation result:' p.result) ~) `p.result - =. hook [id name %0 src compiled !>(~)] + =. hook [id name %0 src compiled !>(~) ~] =. cor =/ error=(unit tang) ?:(?=(%& -.result) ~ `p.result) @@ -1092,19 +1094,25 @@ (~(del by dh) delay-id) =. cor (give-hook-response [%gone id]) ho-core + ++ ho-configure + |= [=nest:c =config:h] + ^+ ho-core + =. config.hook (~(put by config.hook) nest config) + =. cor (give-hook-response [%configure id nest config]) + ho-core ++ ho-wait - |= [=origin:h schedule=@dr] + |= [=origin:h schedule=@dr =config:h] ^+ ho-core =/ d-id (rsh [3 48] eny.bowl) =/ crons (~(gut by crons.hooks) id *(map origin:h cron:h)) =. crons.hooks =- (~(put by crons.hooks) id.hook -) - (~(put by crons) origin [d-id schedule]) + (~(put by crons) origin [d-id schedule config]) =/ fires-at (add now.bowl schedule) =/ dh [d-id id !>(~) fires-at] =/ =wire (hook-cron-wire id origin) =. cor (schedule-hook wire origin dh) - =. cor (give-hook-response [%wait id origin schedule]) + =. cor (give-hook-response [%wait id origin schedule config]) ho-core ++ ho-rest |= =origin:h @@ -1118,14 +1126,14 @@ =. cor (give-hook-response [%rest id origin]) ho-core ++ ho-run-single - |= [=event:h prefix=tape =origin:h] + |= [=event:h prefix=tape =origin:h =config:h] =/ channel ?@ origin ~ ?~ ch=(~(get by v-channels) origin) ~ `[origin u.ch] - =/ =context:h (get-hook-context channel) + =/ =context:h (get-hook-context channel config) =/ return=(unit return:h) - (run-hook:utils event context hook) + (run-hook:utils [event context(hook hook)] hook) ?~ return ~& "{prefix} {} failed" ho-core @@ -1135,7 +1143,7 @@ ho-core -- ++ run-hooks - |= [=event:h =context:h =nest:c default=cord] + |= [=event:h =nest:c default=cord] ^- [(each event:h tang) _cor] =; [result=(each event:h tang) effects=(list effect:h)] [result (run-hook-effects effects nest)] @@ -1143,14 +1151,17 @@ =| effects=(list effect:h) =/ order (~(got by order.hooks) nest) ~& "got orders {}" + =/ channel `[nest (~(got by v-channels) nest)] + =/ =context:h (get-hook-context channel *config:h) |- ?~ order [&+current-event effects] =* next $(order t.order) ~& "getting hook" =/ hook (~(got by hooks.hooks) i.order) + =/ ctx context(hook hook, config (~(gut by config.hook) nest ~)) =/ return=(unit return:h) - (run-hook:utils current-event context hook) + (run-hook:utils [current-event ctx] hook) ?~ return next =* result result.u.return =. effects (weld effects effects.u.return) @@ -1168,7 +1179,10 @@ ?~ delay=(~(get by delayed.hooks) id) cor :: make sure we clean up =. delayed.hooks (~(del by delayed.hooks) id) - =/ args [[%wake +.u.delay] "delayed hook" origin.u.delay] + =* origin origin.u.delay + =/ hook (~(got by hooks.hooks) hook.u.delay) + =/ config ?@(origin ~ (~(gut by config.hook) origin ~)) + =/ args [[%wake +.u.delay] "delayed hook" origin config] ho-abet:(ho-run-single:(ho-abed:ho-core hook.u.delay) args) :: [%cron id=@ kind=?(%chat %diary %heap) ship=@ name=@ ~] @@ -1183,7 +1197,7 @@ =/ =wire (hook-cron-wire id origin) =. cor (schedule-hook wire origin +.u.delay(fires-at next)) - =/ args [[%cron ~] "cron job" origin] + =/ args [[%cron ~] "cron job" origin config.u.cron] ho-abet:(ho-run-single:(ho-abed:ho-core hook.u.delay) args) == ++ hook-cron-wire diff --git a/desk/lib/channel-utils.hoon b/desk/lib/channel-utils.hoon index 1fa0dae925..fa645b5932 100644 --- a/desk/lib/channel-utils.hoon +++ b/desk/lib/channel-utils.hoon @@ -697,13 +697,12 @@ tonk &+p.tonk ++ run-hook - |= [=event:h =context:h =hook:h] + |= [=args:h =hook:h] ^- (unit return:h) ~& "running hook: {} {}" ?~ compiled.hook ~&("hook not compiled" ~) :: ~& "nock: {}" - =/ =args:h [event context(hook hook)] =+ !<(=outcome:h (slam u.compiled.hook !>(args))) ~& "{(trip name.hook)} hook run:" ~& outcome diff --git a/desk/sur/hooks.hoon b/desk/sur/hooks.hoon index 3ada49b788..cf17d92462 100644 --- a/desk/sur/hooks.hoon +++ b/desk/sur/hooks.hoon @@ -11,6 +11,7 @@ :: $src: the source code of the hook :: $compiled: the compiled version of the hook :: $state: the current state of the hook +:: $config: any configuration data for the hook :: ++ hook $: =id @@ -19,6 +20,7 @@ src=@t compiled=(unit vase) state=vase + config=(map nest config) == :: $hooks: collection of hooks, the order they should be run in, and :: any delayed hooks that need to be run @@ -30,7 +32,11 @@ == +$ origin $@(~ nest) +$ delay-id id -+$ cron [=delay-id schedule=@dr] ++$ cron + $: =delay-id + schedule=@dr + =config + == :: $delayed-hook: metadata for when a delayed hook fires from the timer +$ delayed-hook $: id=delay-id @@ -39,19 +45,22 @@ fires-at=time == :: ++$ config (map @t vase) +$ action $% [%add name=@t src=@t] [%edit =id name=@t src=@t] [%del =id] [%order =nest seq=(list id)] - [%wait =id =origin schedule=@dr] + [%configure =id =nest =config] + [%wait =id =origin schedule=@dr =config] [%rest =id =origin] == +$ response $% [%set =id name=@t src=@t error=(unit tang)] [%gone =id] [%order =nest seq=(list id)] - [%wait =id =origin schedule=@dr] + [%configure =id =nest =config] + [%wait =id =origin schedule=@dr =config] [%rest =id =origin] == :: $context: ambient state that a hook should know about not @@ -60,7 +69,8 @@ :: $channel: the channel that the hook is operating on :: $channels: all the channels in the group :: $group: the group that the channel belongs to -:: $state: the current state of the hook +:: $hook: the hook that's running +:: $config: the configuration data for this instance of the hook :: $now: the current time :: $our: the ship that the hook is running on :: $src: the ship that triggered the hook @@ -71,6 +81,7 @@ group=(unit group-ui:g) channels=v-channels =hook + =config now=time our=ship src=ship diff --git a/desk/ted/hooks/configure.hoon b/desk/ted/hooks/configure.hoon new file mode 100644 index 0000000000..72754085d5 --- /dev/null +++ b/desk/ted/hooks/configure.hoon @@ -0,0 +1,18 @@ +/- spider, h=hooks, c=channels +/+ s=strandio +=, strand=strand:spider +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +=+ !<([~ =id:h =nest:c =config:h] arg) +;< our=@p bind:m get-our:s +;< ~ bind:m (watch:s /responses [our %channels-server] /hooks/v0) +=/ =cage hook-action-0+!>(`action:h`[%configure id nest config]) +;< ~ bind:m (poke-our:s %channels-server cage) +;< =^cage bind:m (take-fact:s /responses) +?> ?=(%hook-response-0 p.cage) +=+ !<(=response:h q.cage) +?> ?=(%configure -.response) +~& "hook {} running on {} configured" +(pure:m !>(~)) \ No newline at end of file From bc4abcb3d1e05790e859b05dc35a60d2dc8f67ff Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Fri, 22 Nov 2024 16:57:56 -0600 Subject: [PATCH 07/52] hooks: scheduling thread --- desk/ted/hooks/schedule.hoon | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 desk/ted/hooks/schedule.hoon diff --git a/desk/ted/hooks/schedule.hoon b/desk/ted/hooks/schedule.hoon new file mode 100644 index 0000000000..30eafe4df9 --- /dev/null +++ b/desk/ted/hooks/schedule.hoon @@ -0,0 +1,34 @@ +/- spider, h=hooks +/+ s=strandio +=, strand=strand:spider +^- thread:spider +|= arg=vase +=/ m (strand ,vase) +^- form:m +|^ +=+ !<([~ =id:h =origin:h =action] arg) +;< our=@p bind:m get-our:s +;< ~ bind:m (watch:s /responses [our %channels-server] /hooks/v0) +=/ =cage + :- %hook-action-0 + !> + ^- action:h + ?: ?=(%stop -.action) [%rest id origin] + [%wait id origin +.action] +;< ~ bind:m (poke-our:s %channels-server cage) +;< =^cage bind:m (take-fact:s /responses) +?> ?=(%hook-response-0 p.cage) +=+ !<(=response:h q.cage) +?> ?=(?(%wait %rest) -.response) +?: ?=(%rest -.response) + ~& "stopped scheduled hook {} running on {}" + (pure:m !>(~)) +;< now=time bind:m get-time:s +=/ fires-at (add now schedule.response) +~& "starting hook {}, scheduled to run on {} at {}" +(pure:m !>(~)) ++$ action + $% [%stop ~] + [%start schedule=@dr =config:h] + == +-- \ No newline at end of file From e58131c427730fdac2ae3182efe206a9218a3c83 Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Mon, 25 Nov 2024 14:13:49 -0600 Subject: [PATCH 08/52] hooks: refactor cron to be separate, clean up types --- desk/app/channels-server.hoon | 91 ++++++++++++++++--------------- desk/lib/channel-utils.hoon | 9 +-- desk/sur/hooks.hoon | 100 +++++++++++++++++----------------- 3 files changed, 97 insertions(+), 103 deletions(-) diff --git a/desk/app/channels-server.hoon b/desk/app/channels-server.hoon index 100e52b4b4..ba027de722 100644 --- a/desk/app/channels-server.hoon +++ b/desk/app/channels-server.hoon @@ -292,18 +292,7 @@ inflate-io :: ++ inflate-io - =. cor (safe-watch /groups [our.bowl %groups] /groups) - %+ roll ~(tap by crons.hooks) - |= [[=id:h schedules=(map origin:h cron:h)] cr=_cor] - %+ roll ~(tap by schedules) - |= [[=origin:h cron:h] co=_cr] - ?~ hook=(~(get by hooks.hooks) id) co - ?~ compiled.u.hook co - ?^ delay=(~(get by delayed.hooks) delay-id) co - :: only start timers for crons that haven't already been started - =/ fires-at (add now.bowl schedule) - =/ =wire (hook-cron-wire id origin) - (schedule-hook wire origin delay-id id !>(~) fires-at) + (safe-watch /groups [our.bowl %groups] /groups) :: ++ poke |= [=mark =vase] @@ -370,7 +359,7 @@ =. order.hooks (~(put by order.hooks) nest.action seq) (give-hook-response %order nest.action seq) :: - %configure + %config ho-abet:(ho-configure:(ho-abed:ho-core id.action) +.+.action) :: %wait @@ -1076,9 +1065,8 @@ =. cor %+ roll ~(tap by (~(gut by crons.hooks) id *(map origin:h cron:h))) - |= [[=origin:h cron:h] cr=_cor] - =/ =wire (hook-cron-wire id origin) - (unschedule-hook:cr delay-id wire) + |= [[=origin:h =cron:h] cr=_cor] + (unschedule-cron:cr origin cron) =. crons.hooks (~(del by crons.hooks) id) =. order.hooks %+ roll @@ -1098,20 +1086,20 @@ |= [=nest:c =config:h] ^+ ho-core =. config.hook (~(put by config.hook) nest config) - =. cor (give-hook-response [%configure id nest config]) + =. cor (give-hook-response [%config id nest config]) ho-core ++ ho-wait - |= [=origin:h schedule=@dr =config:h] + |= [=origin:h =schedule:h =config:h] ^+ ho-core - =/ d-id (rsh [3 48] eny.bowl) + =/ fires-at + ?@ schedule (add now.bowl schedule) + start.schedule =/ crons (~(gut by crons.hooks) id *(map origin:h cron:h)) + =/ =cron:h [id schedule config fires-at] =. crons.hooks =- (~(put by crons.hooks) id.hook -) - (~(put by crons) origin [d-id schedule config]) - =/ fires-at (add now.bowl schedule) - =/ dh [d-id id !>(~) fires-at] - =/ =wire (hook-cron-wire id origin) - =. cor (schedule-hook wire origin dh) + (~(put by crons) origin cron) + =. cor (schedule-cron origin cron) =. cor (give-hook-response [%wait id origin schedule config]) ho-core ++ ho-rest @@ -1121,8 +1109,7 @@ =/ cron (~(got by crons) origin) =. crons.hooks (~(put by crons.hooks) id (~(del by crons) origin)) - =/ =wire (hook-cron-wire id origin) - =. cor (unschedule-hook delay-id.cron wire) + =. cor (unschedule-cron origin cron) =. cor (give-hook-response [%rest id origin]) ho-core ++ ho-run-single @@ -1191,32 +1178,46 @@ :: if unscheduled, ignore ?~ crons=(~(get by crons.hooks) id) cor ?~ cron=(~(get by u.crons) origin) cor - ?~ delay=(~(get by delayed.hooks) delay-id.u.cron) cor - =. delayed.hooks (~(del by delayed.hooks) delay-id.u.cron) - =/ next (add now.bowl schedule.u.cron) - =/ =wire (hook-cron-wire id origin) + =/ next + ?@ schedule.u.cron + (add now.bowl schedule.u.cron) + (add now.bowl repeat.schedule.u.cron) + =/ new-cron u.cron(fires-at next) + =. crons.hooks + %+ ~(put by crons.hooks) id + (~(put by u.crons) origin new-cron) =. cor - (schedule-hook wire origin +.u.delay(fires-at next)) + (schedule-cron origin new-cron) =/ args [[%cron ~] "cron job" origin config.u.cron] - ho-abet:(ho-run-single:(ho-abed:ho-core hook.u.delay) args) + ho-abet:(ho-run-single:(ho-abed:ho-core hook.u.cron) args) == -++ hook-cron-wire - |= [=id:h =origin:h] - ^- wire - %+ welp /hooks/cron/(scot %uv id) - ?@ origin ~ - /[kind.origin]/(scot %p ship.origin)/[name.origin] -++ schedule-hook - |= [=wire =origin:h dh=delayed-hook:h] +++ schedule-cron + |= [=origin:h =cron:h] ^+ cor ~& "scheduling hook" - =. delayed.hooks (~(put by delayed.hooks) id.dh [origin dh]) + =/ wire + %+ welp /hooks/cron/(scot %uv hook.cron) + ?@ origin ~ + /[kind.origin]/(scot %p ship.origin)/[name.origin] + (emit [%pass wire %arvo %b %wait fires-at.cron]) +++ unschedule-cron + |= [=origin:h =cron:h] + ~& "unscheduling hook" + =/ wire + %+ welp /hooks/cron/(scot %uv hook.cron) + ?@ origin ~ + /[kind.origin]/(scot %p ship.origin)/[name.origin] + (emit [%pass wire %arvo %b %rest fires-at.cron]) +++ schedule-delay + |= dh=delayed-hook:h + =/ =wire /hooks/delayed/(scot %uv id.dh) (emit [%pass wire %arvo %b %wait fires-at.dh]) -++ unschedule-hook - |= [=id:h =wire] +++ unschedule-delay + |= =id:h ^+ cor ?~ previous=(~(get by delayed.hooks) id) cor ~& "unscheduling hook" + =/ =wire /hooks/delayed/(scot %uv id.u.previous) (emit [%pass wire %arvo %b %rest fires-at.u.previous]) ++ run-hook-effects |= [effects=(list effect:h) =origin:h] @@ -1255,7 +1256,7 @@ :: %wait =/ =wire /hooks/delayed/(scot %uv id.effect) - =. cor (unschedule-hook id.effect wire) - (schedule-hook wire origin +:effect) + =. cor (unschedule-delay id.effect) + (schedule-delay +:effect) == -- diff --git a/desk/lib/channel-utils.hoon b/desk/lib/channel-utils.hoon index fa645b5932..44df979919 100644 --- a/desk/lib/channel-utils.hoon +++ b/desk/lib/channel-utils.hoon @@ -680,18 +680,12 @@ ++ compile |= src=@t ^- (each vase tang) - ~& %a =/ tonk=(each vase tang) - ~& %b =/ vex=(like hoon) ((full vest) [0 0] (trip src)) - ~& %c ?~ q.vex |+~[leaf+"\{{} {}}" 'syntax error'] - ~& %d %- mule |.((slap subject p.u.q.vex)) - ~& %e ~& "parsed hoon: {<-.tonk>}" - ~& %f ?: ?=(%| -.tonk) %- (slog 'returning error' p.tonk) tonk @@ -707,6 +701,5 @@ ~& "{(trip name.hook)} hook run:" ~& outcome ?: ?=(%.y -.outcome) `p.outcome - ~& "hook failed:" - ((slog p.outcome) ~) + ((slog 'hook failed:' p.outcome) ~) -- diff --git a/desk/sur/hooks.hoon b/desk/sur/hooks.hoon index cf17d92462..40be172626 100644 --- a/desk/sur/hooks.hoon +++ b/desk/sur/hooks.hoon @@ -23,7 +23,7 @@ config=(map nest config) == :: $hooks: collection of hooks, the order they should be run in, and -:: any delayed hooks that need to be run +:: any delayed hooks that need to be run ++ hooks $: hooks=(map id hook) order=(map nest (list id)) @@ -32,10 +32,12 @@ == +$ origin $@(~ nest) +$ delay-id id ++$ schedule $@(@dr [start=@da repeat=@dr]) +$ cron - $: =delay-id - schedule=@dr + $: hook=id + =schedule =config + fires-at=time == :: $delayed-hook: metadata for when a delayed hook fires from the timer +$ delayed-hook @@ -45,30 +47,30 @@ fires-at=time == :: -+$ config (map @t vase) ++$ config (map @t *) +$ action $% [%add name=@t src=@t] [%edit =id name=@t src=@t] [%del =id] [%order =nest seq=(list id)] - [%configure =id =nest =config] - [%wait =id =origin schedule=@dr =config] + [%config =id =nest =config] + [%wait =id =origin =schedule =config] [%rest =id =origin] == +$ response $% [%set =id name=@t src=@t error=(unit tang)] [%gone =id] [%order =nest seq=(list id)] - [%configure =id =nest =config] - [%wait =id =origin schedule=@dr =config] + [%config =id =nest =config] + [%wait =id =origin =schedule =config] [%rest =id =origin] == :: $context: ambient state that a hook should know about not -:: necessarily tied to a specific event +:: necessarily tied to a specific event :: :: $channel: the channel that the hook is operating on -:: $channels: all the channels in the group :: $group: the group that the channel belongs to +:: $channels: all the channels in the group :: $hook: the hook that's running :: $config: the configuration data for this instance of the hook :: $now: the current time @@ -88,6 +90,21 @@ eny=@ == :: +:: $event: the data associated with the trigger of a hook +:: +:: $on-post: a post was added, edited, deleted, or reacted to +:: $on-reply: a reply was added, edited, deleted, or reacted to +:: $cron: a scheduled wake-up +:: $wake: a delayed invocation of the hook called with metadata about +:: when it fired, its id, and the event it should run with +:: ++$ event + $% [%on-post on-post] + [%on-reply on-reply] + [%cron ~] + [%wake delayed-hook] + == +:: :: $on-post: a hook event that fires when posts are interacted with +$ on-post $% [%add post=v-post] @@ -103,37 +120,36 @@ [%del parent=v-post original=v-reply] [%react parent=v-post reply=v-reply =ship react=(unit react)] == -:: $event-type: the type of event that triggers a hook -+$ event-type ?(%on-post %on-reply %cron %delay) -:: -:: $event: the data associated with the trigger of a hook -:: -:: $on-post: a post was added, edited, deleted, or reacted to -:: $on-reply: a reply was added, edited, deleted, or reacted to -:: $cron: a scheduled wake-up -:: $wake: a delayed invocation of the hook called with metadata about -:: when it fired, its id, and the event it should run with -:: -+$ event - $% [%on-post on-post] - [%on-reply on-reply] - [%cron ~] - [%wake delayed-hook] - == :: :: $args: the arguments passed to a hook +$ args $: =event =context == +:: $outcome: the result of a hook running ++$ outcome (each return tang) +:: +:: $return: the data returned from a hook +:: +:: $result: whether the action was allowed or denied and any +:: transformed values +:: $actions: any actions that should be taken on other agents or delay +:: $new-state: the new state of the hook after running +:: ++$ return + $: $: =result + effects=(list effect) + == + new-state=vase + == :: -:: $result: the result of a hook running +:: $result: whether to allow the action, and any transformations to +:: the event :: -:: $allowed: represents the action being allowed to go through, and the -:: new value of the action +:: $allowed: represents the action being allowed to go through, and +:: the new value of the action :: $denied: represents the action being denied along with the reason -:: that the action was denied -:: $error: represents an error that occurred while running the hook +:: that the action was denied :: +$ result $% [%allowed new=event] @@ -141,8 +157,8 @@ == :: :: $effect: an effect that a hook can have, limited to agents in -:: the %groups desk. $delay is a special effect that will wake up the -:: same hook at a later time. +:: the %groups desk. $delay is a special effect that will wake +:: up the same hook at a later time. +$ effect $% [%channels =a-channels] [%groups =action:g] @@ -152,20 +168,4 @@ [%contacts =action:co] [%wait delayed-hook] == -:: -:: $return: the data returned from a hook -:: -:: $result: whether the action was allowed or denied, any new values, -:: or an error message if something went wrong -:: $actions: any actions that should be taken on other agents or delay -:: $new-state: the new state of the hook after running -:: -+$ return - $: $: =result - effects=(list effect) - == - new-state=vase - == -:: -+$ outcome (each return tang) -- \ No newline at end of file From 5285523d467aa72752ee0091255d1ca7213c5dd5 Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Mon, 25 Nov 2024 21:30:03 -0600 Subject: [PATCH 09/52] hooks: cleaning up marks, adding metadata, making edit unitized --- desk/app/channels-server.hoon | 41 +++-- desk/lib/hooks-json.hoon | 216 +++++++++++++++++++++++ desk/mar/{hook => hooks}/action-0.hoon | 2 + desk/mar/hooks/full.hoon | 14 ++ desk/mar/{hook => hooks}/response-0.hoon | 2 + desk/sur/hooks.hoon | 10 +- desk/ted/hooks/edit.hoon | 6 +- 7 files changed, 272 insertions(+), 19 deletions(-) create mode 100644 desk/lib/hooks-json.hoon rename desk/mar/{hook => hooks}/action-0.hoon (75%) create mode 100644 desk/mar/hooks/full.hoon rename desk/mar/{hook => hooks}/response-0.hoon (70%) diff --git a/desk/app/channels-server.hoon b/desk/app/channels-server.hoon index ba027de722..910c3fc790 100644 --- a/desk/app/channels-server.hoon +++ b/desk/app/channels-server.hoon @@ -2,9 +2,10 @@ :: :: this is the server-side from which /app/channels gets its data. :: -/- c=channels, g=groups, h=hooks +/- c=channels, g=groups, h=hooks, m=meta /+ utils=channel-utils, imp=import-aid /+ default-agent, verb, dbug, neg=negotiate +/+ hj=hooks-json :: %- %- agent:neg [| [~.channels^%1 ~ ~] ~] @@ -57,7 +58,7 @@ abet:(watch:cor path) [cards this] :: - ++ on-peek on-peek:def + ++ on-peek peek:cor ++ on-leave on-leave:def ++ on-fail on-fail:def ++ on-agent @@ -338,14 +339,14 @@ cor(pimp `|+egg-any) == :: - %hook-action-0 + %hooks-action-0 =+ !<(=action:h vase) ?- -.action %add ho-abet:(ho-add:ho-core [name src]:action) :: %edit - ho-abet:(ho-edit:(ho-abed:ho-core id.action) [name src]:action) + ho-abet:(ho-edit:(ho-abed:ho-core id.action) +.+.action) :: %del ho-abet:ho-del:(ho-abed:ho-core id.action) @@ -413,7 +414,11 @@ ^+ cor ~| watch-path=`path`pole ?+ pole ~|(%bad-watch-path !!) - [%hooks %v0 ~] cor + [%v0 %hooks ~] cor + :: + [%v0 %hooks %full ~] + =. cor (give %fact ~ hooks-full+!>(hooks)) + (give %kick ~ ~) :: [=kind:c name=@ %create ~] ?> =(our src):bowl @@ -516,6 +521,16 @@ == == :: +++ peek + |= =(pole knot) + ^- (unit (unit cage)) + =? +.pole !?=([%v0 *] +.pole) + [%v0 +.pole] + ?+ pole [~ ~] + [%x %v0 %hooks ~] + ``hooks-full+!>(hooks) + == +:: ++ arvo |= [=(pole knot) sign=sign-arvo] ^+ cor @@ -1009,7 +1024,7 @@ ++ give-hook-response |= =response:h ^+ cor - (give %fact ~[/hooks/v0] hook-response-0+!>(response)) + (give %fact ~[/hooks/v0] hooks-response-0+!>(response)) ++ ho-core |_ [=id:h =hook:h gone=_|] ++ ho-core . @@ -1039,16 +1054,17 @@ ?: ?=(%| -.result) ((slog 'compilation result:' p.result) ~) `p.result - =. hook [id name %0 src compiled !>(~) ~] + =. hook [id %0 name *data:m src compiled !>(~) ~] =. cor =/ error=(unit tang) ?:(?=(%& -.result) ~ `p.result) - (give-hook-response [%set id name src error]) + (give-hook-response [%set id name src meta.hook error]) ho-core ++ ho-edit - |= [name=@t src=@t] - =. src.hook src - =. name.hook name + |= [name=(unit @t) src=(unit @t) meta=(unit data:m)] + =? src.hook ?=(^ src) u.src + =? name.hook ?=(^ name) u.name + =? meta.hook ?=(^ meta) u.meta =/ result=(each vase tang) (compile:utils src.hook) =. compiled.hook @@ -1057,7 +1073,8 @@ =. cor =/ error=(unit tang) ?:(?=(%& -.result) ~ `p.result) - (give-hook-response [%set id name src error]) + %- give-hook-response + [%set id name.hook src.hook meta.hook error] ho-core :: ++ ho-del diff --git a/desk/lib/hooks-json.hoon b/desk/lib/hooks-json.hoon new file mode 100644 index 0000000000..bf725d7696 --- /dev/null +++ b/desk/lib/hooks-json.hoon @@ -0,0 +1,216 @@ +/- h=hooks, c=channels, m=meta +/+ cj=channel-json, gj=groups-json +=* z ..zuse +|% +++ enjs + =, enjs:format + |% + ++ id + |= i=id:h + s+(scot %uv i) + ++ hooks + |= hks=hooks:h + %- pairs + :~ hooks+(hook-map hooks.hks) + order+(order order.hks) + crons+(crons crons.hks) + == + :: + ++ hook-map + |= hks=(map id:h hook:h) + %- pairs + %+ turn + ~(tap by hks) + |= [=id:h hk=hook:h] + [(scot %uv id) (hook hk)] + :: + ++ hook + |= hk=hook:h + %- pairs + :~ id+(id id.hk) + version+s+`@tas`version.hk + name+s+name.hk + meta+(meta:enjs:gj meta.hk) + src+s+src.hk + compiled+b+?=(^ compiled.hk) + config+(config-map config.hk) + == + :: + ++ config-map + |= cfg=(map nest:c config:h) + %- pairs + %+ turn + ~(tap by cfg) + |= [=nest:c con=config:h] + [(nest-cord:enjs:cj nest) (config con)] + ++ config + |= con=config:h + %- pairs + %+ turn + ~(tap by con) + |= [key=@t noun=*] + [key s+(scot %uw `@uw`(jam noun))] + ++ order + |= ord=(map nest:c (list id:h)) + %- pairs + %+ turn + ~(tap by ord) + |= [=nest:c seq=(list id:h)] + [(nest-cord:enjs:cj nest) a+(turn seq id)] + ++ crons + |= crs=(map id:h (map origin:h cron:h)) + %- pairs + %+ turn + ~(tap by crs) + |= [=id:h cr=(map origin:h cron:h)] + [(scot %uv id) (cron-map cr)] + ++ cron-map + |= cr=(map origin:h cron:h) + %- pairs + %+ turn + ~(tap by cr) + |= [=origin:h crn=cron:h] + :_ (cron crn) + ?@(origin 'global' (nest-cord:enjs:cj origin)) + ++ cron + |= crn=cron:h + %- pairs + :~ hook+(id hook.crn) + schedule+(schedule schedule.crn) + config+(config config.crn) + fires-at+s+(scot %da fires-at.crn) + == + ++ schedule + |= sch=schedule:h + ?@ sch s+(scot %dr sch) + %- pairs + :~ start+s+(scot %da start.sch) + repeat+s+(scot %dr repeat.sch) + == + ++ response + |= r=response:h + %+ frond -.r + ?- -.r + %set (set-rsp +.r) + %gone (id id.r) + %order (order-rsp +.r) + %config (config-rsp +.r) + %wait (wait-rsp +.r) + %rest (rest-rsp +.r) + == + ++ set-rsp + |= [i=id:h name=@t src=@t meta=data:m error=(unit ^tang)] + %- pairs + :~ id+(id i) + name+s+name + src+s+src + meta+(meta:enjs:gj meta) + error+?~(error ~ (tang u.error)) + == + ++ order-rsp + |= [=nest:c seq=(list id:h)] + %- pairs + :~ nest+(nest:enjs:cj nest) + seq+a+(turn seq id) + == + ++ config-rsp + |= [i=id:h =nest:c con=config:h] + %- pairs + :~ id+(id i) + nest+(nest:enjs:cj nest) + config+(config con) + == + ++ wait-rsp + |= [i=id:h or=origin:h sch=schedule:h con=config:h] + %- pairs + :~ id+(id i) + origin+s+?~(or 'global' (nest-cord:enjs:cj or)) + schedule+(schedule sch) + config+(config con) + == + ++ rest-rsp + |= [i=id:h or=origin:h] + %- pairs + :~ id+(id i) + origin+s+?~(or 'global' (nest-cord:enjs:cj or)) + == + ++ tang + |= t=^tang + :- %s + %- crip + %+ roll t + |= [tk=^tank tp=^tape] + ~! tk + =/ next=^tape ~(ram re tk) + (welp (snoc tp '\0a') next) + -- +:: +++ dejs + =, dejs:format + |% + ++ id (se %uv) + ++ action + %- of + :~ add/add + edit/edit + del/id + order/order + config/config + wait/wait + rest/rest + == + ++ add + %- ot + :~ name/so + src/so + == + ++ edit + %- ot + :~ id/id + name/(mu so) + src/(mu so) + meta/(mu meta:dejs:gj) + == + ++ order + %- ot + :~ nest/nest:dejs:cj + seq/(ar id) + == + ++ config + %- ot + :~ id/id + nest/nest:dejs:cj + config/(om noun) + == + ++ noun + |= j=json + (cue ((se %uw) j)) + ++ origin + |= j=json + ?~(j ~ (nest:dejs:cj j)) + ++ wait + %- ot + :~ id/id + origin/origin + schedule/schedule + config/(om noun) + == + ++ rest + %- ot + :~ id/id + origin/origin + == + ++ schedule + |= j=json + ?+ j !! + [%s *] ((se %dr) j) + :: + [%o *] + %. j + %- ot + :~ start/(se %da) + repeat/(se %dr) + == + == + -- +-- diff --git a/desk/mar/hook/action-0.hoon b/desk/mar/hooks/action-0.hoon similarity index 75% rename from desk/mar/hook/action-0.hoon rename to desk/mar/hooks/action-0.hoon index bab332196b..45838b60b0 100644 --- a/desk/mar/hook/action-0.hoon +++ b/desk/mar/hooks/action-0.hoon @@ -1,4 +1,5 @@ /- h=hooks, c=channels +/+ hj=hooks-json |_ =action:h ++ grad %noun ++ grow @@ -8,5 +9,6 @@ ++ grab |% ++ noun action:h + ++ json action:dejs:hj -- -- diff --git a/desk/mar/hooks/full.hoon b/desk/mar/hooks/full.hoon new file mode 100644 index 0000000000..6d9dd00ba5 --- /dev/null +++ b/desk/mar/hooks/full.hoon @@ -0,0 +1,14 @@ +/- h=hooks, c=channels +/+ hj=hooks-json +|_ =hooks:h +++ grad %noun +++ grow + |% + ++ noun hooks + ++ json (hooks:enjs:hj hooks) + -- +++ grab + |% + ++ noun hooks:h + -- +-- diff --git a/desk/mar/hook/response-0.hoon b/desk/mar/hooks/response-0.hoon similarity index 70% rename from desk/mar/hook/response-0.hoon rename to desk/mar/hooks/response-0.hoon index 88f7887db3..e4eac8ff34 100644 --- a/desk/mar/hook/response-0.hoon +++ b/desk/mar/hooks/response-0.hoon @@ -1,9 +1,11 @@ /- h=hooks, c=channels +/+ hj=hooks-json |_ =response:h ++ grad %noun ++ grow |% ++ noun response + ++ json (response:enjs:hj response) -- ++ grab |% diff --git a/desk/sur/hooks.hoon b/desk/sur/hooks.hoon index 40be172626..de00bf9d4a 100644 --- a/desk/sur/hooks.hoon +++ b/desk/sur/hooks.hoon @@ -1,4 +1,4 @@ -/- *channels, g=groups, a=activity, ch=chat, co=contacts +/- *channels, g=groups, a=activity, ch=chat, co=contacts, m=meta |% :: $id: a unique identifier for a hook +$ id @uv @@ -15,8 +15,9 @@ :: ++ hook $: =id - name=@t version=%0 + name=@t + meta=data:m src=@t compiled=(unit vase) state=vase @@ -50,7 +51,7 @@ +$ config (map @t *) +$ action $% [%add name=@t src=@t] - [%edit =id name=@t src=@t] + [%edit =id name=(unit @t) src=(unit @t) meta=(unit data:m)] [%del =id] [%order =nest seq=(list id)] [%config =id =nest =config] @@ -58,7 +59,7 @@ [%rest =id =origin] == +$ response - $% [%set =id name=@t src=@t error=(unit tang)] + $% [%set =id name=@t src=@t meta=data:m error=(unit tang)] [%gone =id] [%order =nest seq=(list id)] [%config =id =nest =config] @@ -168,4 +169,5 @@ [%contacts =action:co] [%wait delayed-hook] == +:: -- \ No newline at end of file diff --git a/desk/ted/hooks/edit.hoon b/desk/ted/hooks/edit.hoon index 05e1090f95..8730b0cfc0 100644 --- a/desk/ted/hooks/edit.hoon +++ b/desk/ted/hooks/edit.hoon @@ -1,14 +1,14 @@ -/- spider, h=hooks +/- spider, h=hooks, m=meta /+ s=strandio =, strand=strand:spider ^- thread:spider |= arg=vase =/ m (strand ,vase) ^- form:m -=+ !<([~ =id:h name=@t src=@t] arg) +=+ !<([~ =id:h name=(unit @t) src=(unit @t) meta=(unit data:m)] arg) ;< our=@p bind:m get-our:s ;< ~ bind:m (watch:s /responses [our %channels-server] /hooks/v0) -=/ =cage hook-action-0+!>(`action:h`[%edit id name src]) +=/ =cage hook-action-0+!>(`action:h`[%edit id name src meta]) ;< ~ bind:m (poke-our:s %channels-server cage) ;< =^cage bind:m (take-fact:s /responses) ?> ?=(%hook-response-0 p.cage) From 840fa02247b93e9aa28e87e92912b27050343627 Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Tue, 26 Nov 2024 10:09:13 -0600 Subject: [PATCH 10/52] hooks: depluralizing folders --- desk/app/channels-server.hoon | 8 ++++---- desk/mar/{hooks => hook}/action-0.hoon | 0 desk/mar/{hooks => hook}/full.hoon | 0 desk/mar/{hooks => hook}/response-0.hoon | 0 desk/ted/{hooks => hook}/add.hoon | 0 desk/ted/{hooks => hook}/configure.hoon | 0 desk/ted/{hooks => hook}/del.hoon | 0 desk/ted/{hooks => hook}/edit.hoon | 0 desk/ted/{hooks => hook}/order.hoon | 0 desk/ted/{hooks => hook}/run.hoon | 0 desk/ted/{hooks => hook}/schedule.hoon | 0 11 files changed, 4 insertions(+), 4 deletions(-) rename desk/mar/{hooks => hook}/action-0.hoon (100%) rename desk/mar/{hooks => hook}/full.hoon (100%) rename desk/mar/{hooks => hook}/response-0.hoon (100%) rename desk/ted/{hooks => hook}/add.hoon (100%) rename desk/ted/{hooks => hook}/configure.hoon (100%) rename desk/ted/{hooks => hook}/del.hoon (100%) rename desk/ted/{hooks => hook}/edit.hoon (100%) rename desk/ted/{hooks => hook}/order.hoon (100%) rename desk/ted/{hooks => hook}/run.hoon (100%) rename desk/ted/{hooks => hook}/schedule.hoon (100%) diff --git a/desk/app/channels-server.hoon b/desk/app/channels-server.hoon index 910c3fc790..182a437f47 100644 --- a/desk/app/channels-server.hoon +++ b/desk/app/channels-server.hoon @@ -339,7 +339,7 @@ cor(pimp `|+egg-any) == :: - %hooks-action-0 + %hook-action-0 =+ !<(=action:h vase) ?- -.action %add @@ -417,7 +417,7 @@ [%v0 %hooks ~] cor :: [%v0 %hooks %full ~] - =. cor (give %fact ~ hooks-full+!>(hooks)) + =. cor (give %fact ~ hook-full+!>(hooks)) (give %kick ~ ~) :: [=kind:c name=@ %create ~] @@ -528,7 +528,7 @@ [%v0 +.pole] ?+ pole [~ ~] [%x %v0 %hooks ~] - ``hooks-full+!>(hooks) + ``hook-full+!>(hooks) == :: ++ arvo @@ -1024,7 +1024,7 @@ ++ give-hook-response |= =response:h ^+ cor - (give %fact ~[/hooks/v0] hooks-response-0+!>(response)) + (give %fact ~[/hooks/v0] hook-response-0+!>(response)) ++ ho-core |_ [=id:h =hook:h gone=_|] ++ ho-core . diff --git a/desk/mar/hooks/action-0.hoon b/desk/mar/hook/action-0.hoon similarity index 100% rename from desk/mar/hooks/action-0.hoon rename to desk/mar/hook/action-0.hoon diff --git a/desk/mar/hooks/full.hoon b/desk/mar/hook/full.hoon similarity index 100% rename from desk/mar/hooks/full.hoon rename to desk/mar/hook/full.hoon diff --git a/desk/mar/hooks/response-0.hoon b/desk/mar/hook/response-0.hoon similarity index 100% rename from desk/mar/hooks/response-0.hoon rename to desk/mar/hook/response-0.hoon diff --git a/desk/ted/hooks/add.hoon b/desk/ted/hook/add.hoon similarity index 100% rename from desk/ted/hooks/add.hoon rename to desk/ted/hook/add.hoon diff --git a/desk/ted/hooks/configure.hoon b/desk/ted/hook/configure.hoon similarity index 100% rename from desk/ted/hooks/configure.hoon rename to desk/ted/hook/configure.hoon diff --git a/desk/ted/hooks/del.hoon b/desk/ted/hook/del.hoon similarity index 100% rename from desk/ted/hooks/del.hoon rename to desk/ted/hook/del.hoon diff --git a/desk/ted/hooks/edit.hoon b/desk/ted/hook/edit.hoon similarity index 100% rename from desk/ted/hooks/edit.hoon rename to desk/ted/hook/edit.hoon diff --git a/desk/ted/hooks/order.hoon b/desk/ted/hook/order.hoon similarity index 100% rename from desk/ted/hooks/order.hoon rename to desk/ted/hook/order.hoon diff --git a/desk/ted/hooks/run.hoon b/desk/ted/hook/run.hoon similarity index 100% rename from desk/ted/hooks/run.hoon rename to desk/ted/hook/run.hoon diff --git a/desk/ted/hooks/schedule.hoon b/desk/ted/hook/schedule.hoon similarity index 100% rename from desk/ted/hooks/schedule.hoon rename to desk/ted/hook/schedule.hoon From 75abc7290c5397e89526030e7a49f86cbfac043c Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Tue, 26 Nov 2024 10:44:45 -0600 Subject: [PATCH 11/52] hooks: better scheduling --- desk/app/channels-server.hoon | 37 +++++++++++++++++++---------------- desk/lib/hooks-json.hoon | 10 ++++------ desk/sur/hooks.hoon | 7 +++---- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/desk/app/channels-server.hoon b/desk/app/channels-server.hoon index 182a437f47..2a0e003c45 100644 --- a/desk/app/channels-server.hoon +++ b/desk/app/channels-server.hoon @@ -346,7 +346,7 @@ ho-abet:(ho-add:ho-core [name src]:action) :: %edit - ho-abet:(ho-edit:(ho-abed:ho-core id.action) +.+.action) + ho-abet:(ho-edit:(ho-abed:ho-core id.action) +>.action) :: %del ho-abet:ho-del:(ho-abed:ho-core id.action) @@ -361,10 +361,10 @@ (give-hook-response %order nest.action seq) :: %config - ho-abet:(ho-configure:(ho-abed:ho-core id.action) +.+.action) + ho-abet:(ho-configure:(ho-abed:ho-core id.action) +>.action) :: %wait - ho-abet:(ho-wait:(ho-abed:ho-core id.action) +.+.action) + ho-abet:(ho-wait:(ho-abed:ho-core id.action) +>.action) :: %rest ho-abet:(ho-rest:(ho-abed:ho-core id.action) origin.action) @@ -1106,13 +1106,13 @@ =. cor (give-hook-response [%config id nest config]) ho-core ++ ho-wait - |= [=origin:h =schedule:h =config:h] + |= [=origin:h schedule=$@(@dr schedule:h) =config:h] ^+ ho-core - =/ fires-at - ?@ schedule (add now.bowl schedule) - start.schedule + =/ schedule + ?: ?=(@ schedule) [now.bowl schedule] + schedule =/ crons (~(gut by crons.hooks) id *(map origin:h cron:h)) - =/ =cron:h [id schedule config fires-at] + =/ =cron:h [id schedule config] =. crons.hooks =- (~(put by crons.hooks) id.hook -) (~(put by crons) origin cron) @@ -1195,16 +1195,19 @@ :: if unscheduled, ignore ?~ crons=(~(get by crons.hooks) id) cor ?~ cron=(~(get by u.crons) origin) cor - =/ next - ?@ schedule.u.cron - (add now.bowl schedule.u.cron) - (add now.bowl repeat.schedule.u.cron) - =/ new-cron u.cron(fires-at next) + =. next.schedule.u.cron + :: we don't want to run the cron for every iteration it would + :: have run 'offline', so we check here to make sure that the + :: next fire time is in the future + =/ next (add [next repeat]:schedule.u.cron) + |- + ?: (gte next now.bowl) next + $(next (add next repeat.schedule.u.cron)) =. crons.hooks %+ ~(put by crons.hooks) id - (~(put by u.crons) origin new-cron) + (~(put by u.crons) origin u.cron) =. cor - (schedule-cron origin new-cron) + (schedule-cron origin u.cron) =/ args [[%cron ~] "cron job" origin config.u.cron] ho-abet:(ho-run-single:(ho-abed:ho-core hook.u.cron) args) == @@ -1216,7 +1219,7 @@ %+ welp /hooks/cron/(scot %uv hook.cron) ?@ origin ~ /[kind.origin]/(scot %p ship.origin)/[name.origin] - (emit [%pass wire %arvo %b %wait fires-at.cron]) + (emit [%pass wire %arvo %b %wait next.schedule.cron]) ++ unschedule-cron |= [=origin:h =cron:h] ~& "unscheduling hook" @@ -1224,7 +1227,7 @@ %+ welp /hooks/cron/(scot %uv hook.cron) ?@ origin ~ /[kind.origin]/(scot %p ship.origin)/[name.origin] - (emit [%pass wire %arvo %b %rest fires-at.cron]) + (emit [%pass wire %arvo %b %rest next.schedule.cron]) ++ schedule-delay |= dh=delayed-hook:h =/ =wire /hooks/delayed/(scot %uv id.dh) diff --git a/desk/lib/hooks-json.hoon b/desk/lib/hooks-json.hoon index bf725d7696..ece631a223 100644 --- a/desk/lib/hooks-json.hoon +++ b/desk/lib/hooks-json.hoon @@ -78,13 +78,11 @@ :~ hook+(id hook.crn) schedule+(schedule schedule.crn) config+(config config.crn) - fires-at+s+(scot %da fires-at.crn) == ++ schedule |= sch=schedule:h - ?@ sch s+(scot %dr sch) %- pairs - :~ start+s+(scot %da start.sch) + :~ next+s+(scot %da next.sch) repeat+s+(scot %dr repeat.sch) == ++ response @@ -121,11 +119,11 @@ config+(config con) == ++ wait-rsp - |= [i=id:h or=origin:h sch=schedule:h con=config:h] + |= [i=id:h or=origin:h sch=$@(@dr schedule:h) con=config:h] %- pairs :~ id+(id i) origin+s+?~(or 'global' (nest-cord:enjs:cj or)) - schedule+(schedule sch) + schedule+?@(sch s+(scot %dr sch) (schedule sch)) config+(config con) == ++ rest-rsp @@ -208,7 +206,7 @@ [%o *] %. j %- ot - :~ start/(se %da) + :~ next/(se %da) repeat/(se %dr) == == diff --git a/desk/sur/hooks.hoon b/desk/sur/hooks.hoon index de00bf9d4a..984c08f4d1 100644 --- a/desk/sur/hooks.hoon +++ b/desk/sur/hooks.hoon @@ -33,12 +33,11 @@ == +$ origin $@(~ nest) +$ delay-id id -+$ schedule $@(@dr [start=@da repeat=@dr]) ++$ schedule [next=@da repeat=@dr] +$ cron $: hook=id =schedule =config - fires-at=time == :: $delayed-hook: metadata for when a delayed hook fires from the timer +$ delayed-hook @@ -55,7 +54,7 @@ [%del =id] [%order =nest seq=(list id)] [%config =id =nest =config] - [%wait =id =origin =schedule =config] + [%wait =id =origin schedule=$@(@dr schedule) =config] [%rest =id =origin] == +$ response @@ -63,7 +62,7 @@ [%gone =id] [%order =nest seq=(list id)] [%config =id =nest =config] - [%wait =id =origin =schedule =config] + [%wait =id =origin schedule=$@(@dr schedule) =config] [%rest =id =origin] == :: $context: ambient state that a hook should know about not From 83727c369556a066a01d9d0c5b5290278660e278 Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Tue, 26 Nov 2024 10:59:54 -0600 Subject: [PATCH 12/52] channels-server: restoring kind check for now --- desk/app/channels-server.hoon | 1 + 1 file changed, 1 insertion(+) diff --git a/desk/app/channels-server.hoon b/desk/app/channels-server.hoon index 2a0e003c45..6c013029c7 100644 --- a/desk/app/channels-server.hoon +++ b/desk/app/channels-server.hoon @@ -734,6 +734,7 @@ %add ~& "adding post" ?> |(=(src.bowl our.bowl) =(src.bowl author.essay.c-post)) + ?> =(kind.nest -.kind-data.essay.c-post) =/ id=id-post:c |- =/ post (get:on-v-posts:c posts.channel now.bowl) From b8f8d021fca20b233136c073687cd16c173a7d8b Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Tue, 26 Nov 2024 11:42:00 -0600 Subject: [PATCH 13/52] hooks: cleaning up logging and ids --- desk/app/channels-server.hoon | 16 +++++----------- desk/lib/channel-utils.hoon | 19 ++++++++++--------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/desk/app/channels-server.hoon b/desk/app/channels-server.hoon index 6c013029c7..f4c5275e91 100644 --- a/desk/app/channels-server.hoon +++ b/desk/app/channels-server.hoon @@ -1045,12 +1045,11 @@ ++ ho-add |= [name=@t src=@t] ^+ ho-core - ~& "adding hook {}" - =. id (rsh [3 48] eny.bowl) + =. id + =+ i=(end 7 eny.bowl) + |-(?:((~(has by hooks.hooks) i) $(i +(i)) i)) =/ result=(each vase tang) - ~& "compiling hook" (compile:utils src) - ~& "compilation result: {<-.result>}" =/ compiled ?: ?=(%| -.result) ((slog 'compilation result:' p.result) ~) @@ -1140,9 +1139,9 @@ =/ return=(unit return:h) (run-hook:utils [event context(hook hook)] hook) ?~ return - ~& "{prefix} {} failed" + %- (slog (crip "{prefix} {} failed, running on {}") ~) ho-core - ~& "{prefix} {} ran" + %- (slog (crip "{prefix} {} ran on {}") ~) =. hook hook(state new-state.u.return) =. cor (run-hook-effects effects.u.return origin) ho-core @@ -1155,14 +1154,12 @@ =/ current-event event =| effects=(list effect:h) =/ order (~(got by order.hooks) nest) - ~& "got orders {}" =/ channel `[nest (~(got by v-channels) nest)] =/ =context:h (get-hook-context channel *config:h) |- ?~ order [&+current-event effects] =* next $(order t.order) - ~& "getting hook" =/ hook (~(got by hooks.hooks) i.order) =/ ctx context(hook hook, config (~(gut by config.hook) nest ~)) =/ return=(unit return:h) @@ -1215,7 +1212,6 @@ ++ schedule-cron |= [=origin:h =cron:h] ^+ cor - ~& "scheduling hook" =/ wire %+ welp /hooks/cron/(scot %uv hook.cron) ?@ origin ~ @@ -1223,7 +1219,6 @@ (emit [%pass wire %arvo %b %wait next.schedule.cron]) ++ unschedule-cron |= [=origin:h =cron:h] - ~& "unscheduling hook" =/ wire %+ welp /hooks/cron/(scot %uv hook.cron) ?@ origin ~ @@ -1237,7 +1232,6 @@ |= =id:h ^+ cor ?~ previous=(~(get by delayed.hooks) id) cor - ~& "unscheduling hook" =/ =wire /hooks/delayed/(scot %uv id.u.previous) (emit [%pass wire %arvo %b %rest fires-at.u.previous]) ++ run-hook-effects diff --git a/desk/lib/channel-utils.hoon b/desk/lib/channel-utils.hoon index 44df979919..1d3d8f79c3 100644 --- a/desk/lib/channel-utils.hoon +++ b/desk/lib/channel-utils.hoon @@ -684,8 +684,8 @@ =/ vex=(like hoon) ((full vest) [0 0] (trip src)) ?~ q.vex |+~[leaf+"\{{} {}}" 'syntax error'] %- mule - |.((slap subject p.u.q.vex)) - ~& "parsed hoon: {<-.tonk>}" + |.((~(mint ut p:subject) %noun p.u.q.vex)) + %- (slog (crip "parsed hoon: {<-.tonk>}") ~) ?: ?=(%| -.tonk) %- (slog 'returning error' p.tonk) tonk @@ -693,13 +693,14 @@ ++ run-hook |= [=args:h =hook:h] ^- (unit return:h) - ~& "running hook: {} {}" - ?~ compiled.hook - ~&("hook not compiled" ~) - :: ~& "nock: {}" - =+ !<(=outcome:h (slam u.compiled.hook !>(args))) - ~& "{(trip name.hook)} hook run:" - ~& outcome + %- (slog (crip "running hook: {} {}") ~) + %- ?~ channel.context.args same + (slog (crip "on channel: {}") ~) + ?~ compiled.hook ~ + =/ gate [p.u.compiled.hook .*(q:subject q.u.compiled.hook)] + =+ !<(=outcome:h (slam gate !>(args))) + %- (slog (crip "{(trip name.hook)} {} hook run:") ~) + %- (slog (crip "{}") ~) ?: ?=(%.y -.outcome) `p.outcome ((slog 'hook failed:' p.outcome) ~) -- From 9f85a72532f5d66aa94d4be8fedd4df33a5e5796 Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Tue, 26 Nov 2024 11:49:06 -0600 Subject: [PATCH 14/52] hooks: fail edit on bad compile --- desk/app/channels-server.hoon | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/desk/app/channels-server.hoon b/desk/app/channels-server.hoon index f4c5275e91..bc817fcb7a 100644 --- a/desk/app/channels-server.hoon +++ b/desk/app/channels-server.hoon @@ -1063,18 +1063,19 @@ ++ ho-edit |= [name=(unit @t) src=(unit @t) meta=(unit data:m)] =? src.hook ?=(^ src) u.src - =? name.hook ?=(^ name) u.name - =? meta.hook ?=(^ meta) u.meta =/ result=(each vase tang) (compile:utils src.hook) - =. compiled.hook - ?: ?=(%| -.result) ~ - `p.result + ?: ?=(%| -.result) + =. cor + %- give-hook-response + [%set id name.hook src.hook meta.hook `p.result] + ho-core + =? name.hook ?=(^ name) u.name + =? meta.hook ?=(^ meta) u.meta + =. compiled.hook `p.result =. cor - =/ error=(unit tang) - ?:(?=(%& -.result) ~ `p.result) %- give-hook-response - [%set id name.hook src.hook meta.hook error] + [%set id name.hook src.hook meta.hook ~] ho-core :: ++ ho-del From 424ee7fcba68d8b7793475e673050e6f102d7263 Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Tue, 26 Nov 2024 13:00:00 -0600 Subject: [PATCH 15/52] hooks: ignore premature firing --- desk/app/channels-server.hoon | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/desk/app/channels-server.hoon b/desk/app/channels-server.hoon index bc817fcb7a..9506a2030d 100644 --- a/desk/app/channels-server.hoon +++ b/desk/app/channels-server.hoon @@ -1182,6 +1182,8 @@ ?~ delay=(~(get by delayed.hooks) id) cor :: make sure we clean up =. delayed.hooks (~(del by delayed.hooks) id) + :: ignore premature fires + ?: (lth now.bowl fires-at.u.delay) cor =* origin origin.u.delay =/ hook (~(got by hooks.hooks) hook.u.delay) =/ config ?@(origin ~ (~(gut by config.hook) origin ~)) @@ -1194,6 +1196,8 @@ :: if unscheduled, ignore ?~ crons=(~(get by crons.hooks) id) cor ?~ cron=(~(get by u.crons) origin) cor + :: ignore premature fires + ?: (lth now.bowl next.schedule.u.cron) cor =. next.schedule.u.cron :: we don't want to run the cron for every iteration it would :: have run 'offline', so we check here to make sure that the From 05792c8d90e4fcdd6666e8102095270e981b771b Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Tue, 26 Nov 2024 13:22:56 -0600 Subject: [PATCH 16/52] channel-utils: simpler log --- desk/lib/channel-utils.hoon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desk/lib/channel-utils.hoon b/desk/lib/channel-utils.hoon index 1d3d8f79c3..7bdcc0987b 100644 --- a/desk/lib/channel-utils.hoon +++ b/desk/lib/channel-utils.hoon @@ -700,7 +700,7 @@ =/ gate [p.u.compiled.hook .*(q:subject q.u.compiled.hook)] =+ !<(=outcome:h (slam gate !>(args))) %- (slog (crip "{(trip name.hook)} {} hook run:") ~) - %- (slog (crip "{}") ~) + %- (slog >outcome< ~) ?: ?=(%.y -.outcome) `p.outcome ((slog 'hook failed:' p.outcome) ~) -- From 8923a79a338107c486eda16a3ac16f0cf08fb45c Mon Sep 17 00:00:00 2001 From: Patrick O'Sullivan Date: Tue, 26 Nov 2024 14:59:54 -0600 Subject: [PATCH 17/52] desktop: fix ChatList/ChannelList height/space weirdness --- .../components/ListItem/ChannelListItem.tsx | 9 +++---- .../src/components/ListItem/GroupListItem.tsx | 6 ++--- .../ui/src/components/ListItem/ListItem.tsx | 27 ++++++++++--------- packages/ui/src/components/TextV2/Text.tsx | 2 +- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/ui/src/components/ListItem/ChannelListItem.tsx b/packages/ui/src/components/ListItem/ChannelListItem.tsx index 279763bef8..4d6aef6943 100644 --- a/packages/ui/src/components/ListItem/ChannelListItem.tsx +++ b/packages/ui/src/components/ListItem/ChannelListItem.tsx @@ -3,7 +3,6 @@ import * as logic from '@tloncorp/shared/logic'; import { useMemo } from 'react'; import { View } from 'tamagui'; import { isWeb } from 'tamagui'; -import { getVariableValue } from 'tamagui'; import * as utils from '../../utils'; import { capitalize } from '../../utils'; @@ -102,9 +101,7 @@ export function ChannelListItem({ )} @@ -112,13 +109,13 @@ export function ChannelListItem({ {isWeb && ( - + diff --git a/packages/ui/src/components/ListItem/GroupListItem.tsx b/packages/ui/src/components/ListItem/GroupListItem.tsx index ab897d5d0b..5a5a550dc8 100644 --- a/packages/ui/src/components/ListItem/GroupListItem.tsx +++ b/packages/ui/src/components/ListItem/GroupListItem.tsx @@ -68,7 +68,7 @@ export const GroupListItem = ({ )} @@ -77,13 +77,13 @@ export const GroupListItem = ({ {isWeb && !isPending && ( - + diff --git a/packages/ui/src/components/ListItem/ListItem.tsx b/packages/ui/src/components/ListItem/ListItem.tsx index b243fb237f..8edd5989fe 100644 --- a/packages/ui/src/components/ListItem/ListItem.tsx +++ b/packages/ui/src/components/ListItem/ListItem.tsx @@ -3,12 +3,11 @@ import * as db from '@tloncorp/shared/db'; import { ComponentProps, ReactElement, useMemo } from 'react'; import { getVariableValue, - isWeb, styled, useTheme, withStaticProperties, } from 'tamagui'; -import { SizableText, Stack, View, XStack, YStack } from 'tamagui'; +import { Stack, View, XStack, YStack } from 'tamagui'; import { numberWithMax } from '../../utils'; import { @@ -20,6 +19,7 @@ import { } from '../Avatar'; import ContactName from '../ContactName'; import { Icon, IconType } from '../Icon'; +import { Text } from '../TextV2'; export interface BaseListItemProps { model: T; @@ -80,14 +80,16 @@ const ListItemImageIcon = ImageAvatar; const ListItemMainContent = styled(YStack, { name: 'ListItemMainContent', flex: 1, - justifyContent: 'space-evenly', - height: isWeb ? '$5xl' : '$4xl', + justifyContent: 'space-around', + height: '$4xl', }); -const ListItemTitle = styled(SizableText, { +const ListItemTitle = styled(Text, { name: 'ListItemTitle', color: '$primaryText', numberOfLines: 1, + size: '$label/l', + paddingBottom: 1, variants: { dimmed: { true: { @@ -121,19 +123,19 @@ const ListItemSubtitleIcon = styled(Icon, { size: '$s', }); -const ListItemSubtitle = styled(SizableText, { +const ListItemSubtitle = styled(Text, { name: 'ListItemSubtitle', numberOfLines: 1, - size: '$s', + size: '$label/m', color: '$tertiaryText', }); -export const ListItemTimeText = styled(SizableText, { +export const ListItemTimeText = styled(Text, { name: 'ListItemTimeText', numberOfLines: 1, color: '$tertiaryText', - size: '$s', - lineHeight: '$xs', + size: '$label/m', + paddingBottom: '$xs', }); const ListItemTime = ListItemTimeText.styleable<{ @@ -185,9 +187,9 @@ const ListItemCount = ({ {muted && ( )} - + {numberWithMax(count, 99)} - + ); @@ -197,6 +199,7 @@ const ListItemCountNumber = styled(XStack, { name: 'ListItemCountNumber', gap: '$s', alignItems: 'center', + paddingVertical: '$s', variants: { hidden: { true: { diff --git a/packages/ui/src/components/TextV2/Text.tsx b/packages/ui/src/components/TextV2/Text.tsx index 8ac9b42e0b..995f376269 100644 --- a/packages/ui/src/components/TextV2/Text.tsx +++ b/packages/ui/src/components/TextV2/Text.tsx @@ -59,7 +59,7 @@ export const typeStyles = { fontSize: 16, lineHeight: 24, letterSpacing: -0.2, - fontWeight: '500', + fontWeight: '400', }, '$label/xl': { fontSize: 17, From 2379d807efd008c8476875d2b572d724f0f6616d Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Tue, 26 Nov 2024 15:17:23 -0600 Subject: [PATCH 18/52] hooks: cleanup threads and run everything --- desk/app/channels-server.hoon | 6 ++++-- desk/ted/hook/add.hoon | 7 +++---- desk/ted/hook/configure.hoon | 8 ++++---- desk/ted/hook/del.hoon | 4 ++-- desk/ted/hook/edit.hoon | 13 ++++++------- desk/ted/hook/order.hoon | 4 ++-- desk/ted/hook/run.hoon | 26 ++++++++++++++------------ desk/ted/hook/schedule.hoon | 12 +++++++----- 8 files changed, 42 insertions(+), 38 deletions(-) diff --git a/desk/app/channels-server.hoon b/desk/app/channels-server.hoon index 875df5066b..edf10cdfec 100644 --- a/desk/app/channels-server.hoon +++ b/desk/app/channels-server.hoon @@ -1031,7 +1031,7 @@ ++ give-hook-response |= =response:h ^+ cor - (give %fact ~[/hooks/v0] hook-response-0+!>(response)) + (give %fact ~[/v0/hooks] hook-response-0+!>(response)) ++ ho-core |_ [=id:h =hook:h gone=_|] ++ ho-core . @@ -1283,6 +1283,8 @@ %wait =/ =wire /hooks/delayed/(scot %uv id.effect) =. cor (unschedule-delay id.effect) - (schedule-delay +:effect) + =. delayed.hooks + (~(put by delayed.hooks) id.effect [origin +.effect]) + (schedule-delay +.effect) == -- diff --git a/desk/ted/hook/add.hoon b/desk/ted/hook/add.hoon index cafb458e84..2e66c7b2ed 100644 --- a/desk/ted/hook/add.hoon +++ b/desk/ted/hook/add.hoon @@ -7,15 +7,14 @@ ^- form:m =+ !<([~ name=@t src=@t] arg) ;< our=@p bind:m get-our:s -;< ~ bind:m (watch:s /responses [our %channels-server] /hooks/v0) +;< ~ bind:m (watch:s /responses [our %channels-server] /v0/hooks) =/ =cage hook-action-0+!>(`action:h`[%add name src]) ;< ~ bind:m (poke-our:s %channels-server cage) ;< =^cage bind:m (take-fact:s /responses) ?> ?=(%hook-response-0 p.cage) =+ !<(=response:h q.cage) ?> ?=(%set -.response) -~& "hook {} added with id {}" +%- (slog (crip "hook {} added with id {}") ~) ?~ error.response (pure:m !>(~)) -~& "compilation error:" -%- (slog u.error.response) +%- (slog 'compilation error:' u.error.response) (pure:m !>(~)) \ No newline at end of file diff --git a/desk/ted/hook/configure.hoon b/desk/ted/hook/configure.hoon index 72754085d5..20927f9f85 100644 --- a/desk/ted/hook/configure.hoon +++ b/desk/ted/hook/configure.hoon @@ -7,12 +7,12 @@ ^- form:m =+ !<([~ =id:h =nest:c =config:h] arg) ;< our=@p bind:m get-our:s -;< ~ bind:m (watch:s /responses [our %channels-server] /hooks/v0) -=/ =cage hook-action-0+!>(`action:h`[%configure id nest config]) +;< ~ bind:m (watch:s /responses [our %channels-server] /v0/hooks) +=/ =cage hook-action-0+!>(`action:h`[%config id nest config]) ;< ~ bind:m (poke-our:s %channels-server cage) ;< =^cage bind:m (take-fact:s /responses) ?> ?=(%hook-response-0 p.cage) =+ !<(=response:h q.cage) -?> ?=(%configure -.response) -~& "hook {} running on {} configured" +?> ?=(%config -.response) +%- (slog (crip "hook {} running on {} configured") ~) (pure:m !>(~)) \ No newline at end of file diff --git a/desk/ted/hook/del.hoon b/desk/ted/hook/del.hoon index 0ffc680d72..b955c86621 100644 --- a/desk/ted/hook/del.hoon +++ b/desk/ted/hook/del.hoon @@ -7,7 +7,7 @@ ^- form:m =+ !<([~ =id:h] arg) ;< our=@p bind:m get-our:s -;< ~ bind:m (watch:s /responses [our %channels-server] /hooks/v0) +;< ~ bind:m (watch:s /responses [our %channels-server] /v0/hooks) =/ =cage hook-action-0+!>(`action:h`[%del id]) ;< ~ bind:m (poke-our:s %channels-server cage) ;< =^cage bind:m (take-fact:s /responses) @@ -15,5 +15,5 @@ =+ !<(=response:h q.cage) ?> ?=(%gone -.response) ?> =(id id.response) -~& "hook {} deleted" +%- (slog (crip "hook {} deleted") ~) (pure:m !>(~)) \ No newline at end of file diff --git a/desk/ted/hook/edit.hoon b/desk/ted/hook/edit.hoon index 8730b0cfc0..04fbbdc369 100644 --- a/desk/ted/hook/edit.hoon +++ b/desk/ted/hook/edit.hoon @@ -1,13 +1,13 @@ -/- spider, h=hooks, m=meta +/- spider, h=hooks, meta /+ s=strandio =, strand=strand:spider ^- thread:spider |= arg=vase =/ m (strand ,vase) ^- form:m -=+ !<([~ =id:h name=(unit @t) src=(unit @t) meta=(unit data:m)] arg) +=+ !<([~ =id:h name=(unit @t) src=(unit @t) meta=(unit data:meta)] arg) ;< our=@p bind:m get-our:s -;< ~ bind:m (watch:s /responses [our %channels-server] /hooks/v0) +;< ~ bind:m (watch:s /responses [our %channels-server] /v0/hooks) =/ =cage hook-action-0+!>(`action:h`[%edit id name src meta]) ;< ~ bind:m (poke-our:s %channels-server cage) ;< =^cage bind:m (take-fact:s /responses) @@ -15,9 +15,8 @@ =+ !<(=response:h q.cage) ?> ?=(%set -.response) ?~ error.response - ~& "hook {} edited successfully" + %- (slog (crip "hook {} edited successfully") ~) (pure:m !>(~)) -~& "hook {} edited" -~& "compilation error:" -%- (slog u.error.response) +%- (slog (crip "hook {} edited") ~) +%- (slog 'compilation error:' u.error.response) (pure:m !>(~)) \ No newline at end of file diff --git a/desk/ted/hook/order.hoon b/desk/ted/hook/order.hoon index e9980c1994..d31736f891 100644 --- a/desk/ted/hook/order.hoon +++ b/desk/ted/hook/order.hoon @@ -7,13 +7,13 @@ ^- form:m =+ !<([~ =nest:c seq=(list id:h)] arg) ;< our=@p bind:m get-our:s -;< ~ bind:m (watch:s /responses [our %channels-server] /hooks/v0) +;< ~ bind:m (watch:s /responses [our %channels-server] /v0/hooks) =/ =cage hook-action-0+!>(`action:h`[%order nest seq]) ;< ~ bind:m (poke-our:s %channels-server cage) ;< =^cage bind:m (take-fact:s /responses) ?> ?=(%hook-response-0 p.cage) =+ !<(=response:h q.cage) ?> ?=(%order -.response) -~& "new hook order for {}" +%- (slog (crip "new hook order for {}") ~) ~& seq.response (pure:m !>(~)) \ No newline at end of file diff --git a/desk/ted/hook/run.hoon b/desk/ted/hook/run.hoon index 1f9ea6f685..1efcc6e1d5 100644 --- a/desk/ted/hook/run.hoon +++ b/desk/ted/hook/run.hoon @@ -10,24 +10,22 @@ ;< our=@p bind:m get-our:s =/ compiled=(each vase tang) (compile:utils src) ?. ?=(%& -.compiled) - ~& "compilation error:" - %- (slog p.compiled) + %- (slog 'compilation error:' p.compiled) (pure:m !>(~)) -~& "compiled successfully" ;< ctx=context:h bind:m (get-context context-option) -=+ !<(=outcome:h (slam p.compiled !>([event ctx]))) +=/ gate [p.p.compiled .*(q:subject:utils q.p.compiled)] +=+ !<(=outcome:h (slam gate !>([event ctx]))) ?: ?=(%.y -.outcome) - ~& "hook ran successfully" + %- (slog 'hook ran successfully' ~) (pure:m !>(p.outcome)) -~& "hook failed:" -%- (slog p.outcome) +%- (slog 'hook failed:' p.outcome) (pure:m !>(~)) +$ event-option $% [%ref path=@t] [%event event:h] == +$ context-option - $% [%origin =origin:h state=(unit vase)] + $% [%origin =origin:h state=(unit vase) config=(unit config:h)] [%context =context:h] == ++ get-context @@ -35,24 +33,27 @@ =/ m (strand ,context:h) ^- form:m ?: ?=(%context -.context-option) (pure:m context.context-option) - =/ [=origin:h state=(unit vase)] +.context-option + =/ [=origin:h state=(unit vase) config=(unit config:h)] +.context-option ;< =v-channels:c bind:m (scry:s v-channels:c /gx/channels/v3/v-channels/noun) - =/ channel=(unit [=nest:c vc=v-channel:c]) + =/ channel=(unit [=nest:c v-channel:c]) ?~ origin ~ `[origin (~(gut by v-channels) origin *v-channel:c)] ;< group=(unit group-ui:g) bind:m =/ n (strand (unit group-ui:g)) ?~ channel (pure:n ~) - =* flag group.perm.perm.vc.u.channel + =* flag group.perm.perm.u.channel ;< live=? bind:n (scry:s ? /gu/groups/$) ?. live (pure:n `*group-ui:g) ;< exists=? bind:n (scry:s ? /gx/groups/exists/(scot %p p.flag)/[q.flag]/noun) ?. exists (pure:n `*group-ui:g) ;< =group-ui:g bind:n - (scry:s group-ui:g /groups/groups/(scot %p p.flag)/[q.flag]/v1/noun) + (scry:s group-ui:g /gx/groups/groups/(scot %p p.flag)/[q.flag]/v1/noun) (pure:n (some group-ui)) + =/ cfg=config:h + ?~ config ~ + u.config ;< =bowl:spider bind:m get-bowl:s =/ hook *hook:h %- pure:m @@ -60,6 +61,7 @@ group v-channels hook(state ?~(state !>(~) u.state)) + ?~(origin ~ (~(gut by config.hook) origin ~)) [now our src eny]:bowl == -- diff --git a/desk/ted/hook/schedule.hoon b/desk/ted/hook/schedule.hoon index 30eafe4df9..be3a15225a 100644 --- a/desk/ted/hook/schedule.hoon +++ b/desk/ted/hook/schedule.hoon @@ -8,7 +8,7 @@ |^ =+ !<([~ =id:h =origin:h =action] arg) ;< our=@p bind:m get-our:s -;< ~ bind:m (watch:s /responses [our %channels-server] /hooks/v0) +;< ~ bind:m (watch:s /responses [our %channels-server] /v0/hooks) =/ =cage :- %hook-action-0 !> @@ -21,14 +21,16 @@ =+ !<(=response:h q.cage) ?> ?=(?(%wait %rest) -.response) ?: ?=(%rest -.response) - ~& "stopped scheduled hook {} running on {}" + %- (slog (crip "stopped scheduled hook {} running on {}") ~) (pure:m !>(~)) ;< now=time bind:m get-time:s -=/ fires-at (add now schedule.response) -~& "starting hook {}, scheduled to run on {} at {}" +=/ fires-at + ?@ schedule.response (add now schedule.response) + next.schedule.response +%- (slog (crip "starting hook {}, scheduled to run on {} at {}") ~) (pure:m !>(~)) +$ action $% [%stop ~] - [%start schedule=@dr =config:h] + [%start schedule=$@(@dr schedule:h) =config:h] == -- \ No newline at end of file From a099d1c98b921466bc0a24b21bb4399880565512 Mon Sep 17 00:00:00 2001 From: Patrick O'Sullivan Date: Tue, 26 Nov 2024 15:38:28 -0600 Subject: [PATCH 19/52] desktop: reduce padding on Group/ChannelListItem --- packages/ui/src/components/ListItem/ChannelListItem.tsx | 5 ++++- packages/ui/src/components/ListItem/GroupListItem.tsx | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/ListItem/ChannelListItem.tsx b/packages/ui/src/components/ListItem/ChannelListItem.tsx index 4d6aef6943..01b3f8362c 100644 --- a/packages/ui/src/components/ListItem/ChannelListItem.tsx +++ b/packages/ui/src/components/ListItem/ChannelListItem.tsx @@ -4,6 +4,7 @@ import { useMemo } from 'react'; import { View } from 'tamagui'; import { isWeb } from 'tamagui'; +import useIsWindowNarrow from '../../hooks/useIsWindowNarrow'; import * as utils from '../../utils'; import { capitalize } from '../../utils'; import { Badge } from '../Badge'; @@ -59,6 +60,8 @@ export function ChannelListItem({ } }, [model, firstMemberId, memberCount]); + const isWindowNarrow = useIsWindowNarrow(); + return ( - + - + {title} From f3b4410b2c8e86af7a27bc4631d6a58a6aa44741 Mon Sep 17 00:00:00 2001 From: Patrick O'Sullivan Date: Tue, 26 Nov 2024 15:41:20 -0600 Subject: [PATCH 20/52] add pressable to GalleryPost, fixes tlon-3279 --- .../components/GalleryPost/GalleryPost.tsx | 87 +++++++++---------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/packages/ui/src/components/GalleryPost/GalleryPost.tsx b/packages/ui/src/components/GalleryPost/GalleryPost.tsx index 5768046552..e6e952c639 100644 --- a/packages/ui/src/components/GalleryPost/GalleryPost.tsx +++ b/packages/ui/src/components/GalleryPost/GalleryPost.tsx @@ -17,6 +17,7 @@ import { PostContent, usePostContent, } from '../PostContent/contentUtils'; +import Pressable from '../Pressable'; import { SendPostRetrySheet } from '../SendPostRetrySheet'; import { Text } from '../TextV2'; @@ -76,50 +77,48 @@ export function GalleryPost({ } return ( - - - {showAuthor && !post.hidden && !post.isDeleted && ( - - - - {deliveryFailed && ( - - Tap to retry - - )} - - - )} - - + + + + {showAuthor && !post.hidden && !post.isDeleted && ( + + + + {deliveryFailed && ( + + Tap to retry + + )} + + + )} + + + ); } From 5b4498f0b3c3f5cb95c5db292dbc943392f43217 Mon Sep 17 00:00:00 2001 From: Patrick O'Sullivan Date: Tue, 26 Nov 2024 15:47:28 -0600 Subject: [PATCH 21/52] fixed padding, flipped m/l --- packages/ui/src/components/ListItem/ChannelListItem.tsx | 2 +- packages/ui/src/components/ListItem/GroupListItem.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/ListItem/ChannelListItem.tsx b/packages/ui/src/components/ListItem/ChannelListItem.tsx index 01b3f8362c..eda49e628c 100644 --- a/packages/ui/src/components/ListItem/ChannelListItem.tsx +++ b/packages/ui/src/components/ListItem/ChannelListItem.tsx @@ -69,7 +69,7 @@ export function ChannelListItem({ onPress={handlePress} onLongPress={handleLongPress} > - + From 0f5f622116d3d1b70b2b5a0126562e5a6a21214e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miko=C5=82aj=20Paraniak?= Date: Wed, 27 Nov 2024 11:12:52 +0800 Subject: [PATCH 22/52] channels: fix +ca-recheck crash due to an invalid scry path --- desk/app/channels.hoon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desk/app/channels.hoon b/desk/app/channels.hoon index 57e365c395..d2627c408e 100644 --- a/desk/app/channels.hoon +++ b/desk/app/channels.hoon @@ -2383,7 +2383,7 @@ |= sects=(set sect:g) =/ =flag:g group.perm.perm.channel =/ exists-path - (scry-path %groups /exists/(scot %p p.flag)/[q.flag]) + (scry-path %groups /exists/(scot %p p.flag)/[q.flag]/noun) =+ .^(exists=? %gx exists-path) ?. exists ca-core =/ =path From 69879c2aad9259a5bf1818b7c647ad1e526eddb2 Mon Sep 17 00:00:00 2001 From: Patrick O'Sullivan Date: Wed, 27 Nov 2024 06:13:35 -0600 Subject: [PATCH 23/52] Revert ListItem padding changes (it was worth a shot :)) --- packages/ui/src/components/ListItem/ChannelListItem.tsx | 4 +--- packages/ui/src/components/ListItem/GroupListItem.tsx | 9 +-------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/ui/src/components/ListItem/ChannelListItem.tsx b/packages/ui/src/components/ListItem/ChannelListItem.tsx index eda49e628c..04dc2d66de 100644 --- a/packages/ui/src/components/ListItem/ChannelListItem.tsx +++ b/packages/ui/src/components/ListItem/ChannelListItem.tsx @@ -60,8 +60,6 @@ export function ChannelListItem({ } }, [model, firstMemberId, memberCount]); - const isWindowNarrow = useIsWindowNarrow(); - return ( - + - + {title} From 3b379f2c04ffe1d0ae7304f37f54d9ac6aa48129 Mon Sep 17 00:00:00 2001 From: Patrick O'Sullivan Date: Wed, 27 Nov 2024 07:01:32 -0600 Subject: [PATCH 24/52] desktop: fix routing/crash issues in settings, add contacts, add themes --- .../app/features/settings/AppInfoScreen.tsx | 17 +++++++++++------ packages/app/features/settings/ThemeScreen.tsx | 4 ++-- .../features/settings/UserBugReportScreen.tsx | 2 +- packages/app/features/top/ContactsScreen.tsx | 2 +- packages/app/hooks/useTelemetry.ts | 2 +- .../app/navigation/desktop/HomeNavigator.tsx | 8 +++++--- .../desktop/ProfileScreenNavigator.tsx | 8 ++++++-- .../app/navigation/desktop/TopLevelDrawer.tsx | 3 +++ 8 files changed, 30 insertions(+), 16 deletions(-) diff --git a/packages/app/features/settings/AppInfoScreen.tsx b/packages/app/features/settings/AppInfoScreen.tsx index 584c120e2c..39c9dace94 100644 --- a/packages/app/features/settings/AppInfoScreen.tsx +++ b/packages/app/features/settings/AppInfoScreen.tsx @@ -1,6 +1,5 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { useDebugStore } from '@tloncorp/shared'; -import { getCurrentUserId } from '@tloncorp/shared/api'; import * as store from '@tloncorp/shared/store'; import { AppSetting, @@ -23,6 +22,7 @@ import { getEmailClients, openComposer } from 'react-native-email-link'; import { ScrollView } from 'react-native-gesture-handler'; import { NOTIFY_PROVIDER, NOTIFY_SERVICE } from '../../constants'; +import { useCurrentUserId } from '../../hooks/useCurrentUser'; import { useTelemetry } from '../../hooks/useTelemetry'; import { setDebug } from '../../lib/debug'; import { getEasUpdateDisplay } from '../../lib/platformHelpers'; @@ -32,13 +32,17 @@ const BUILD_VERSION = `${Platform.OS === 'ios' ? 'iOS' : 'Android'} ${Applicatio type Props = NativeStackScreenProps; -function makeDebugEmail(appInfo: any, platformInfo: any) { +function makeDebugEmail( + appInfo: any, + platformInfo: any, + currentUserId: string +) { return ` ---------------------------------------------- Insert description of problem here. ---------------------------------------------- -Tlon ID: ${getCurrentUserId()} +Tlon ID: ${currentUserId} Platform Information: ${JSON.stringify(platformInfo)} @@ -54,6 +58,7 @@ export function AppInfoScreen(props: Props) { const easUpdateDisplay = useMemo(() => getEasUpdateDisplay(Updates), []); const [hasClients, setHasClients] = useState(true); const telemetry = useTelemetry(); + const currentUserId = useCurrentUserId(); const [telemetryDisabled, setTelemetryDisabled] = useState( telemetry.optedOut ); @@ -105,10 +110,10 @@ export function AppInfoScreen(props: Props) { openComposer({ to: 'support@tlon.io', - subject: `${getCurrentUserId()} uploaded logs ${id}`, - body: makeDebugEmail(appInfo, platformInfo), + subject: `${currentUserId} uploaded logs ${id}`, + body: makeDebugEmail(appInfo, platformInfo, currentUserId), }); - }, [hasClients]); + }, [hasClients, currentUserId]); return ( diff --git a/packages/app/features/settings/ThemeScreen.tsx b/packages/app/features/settings/ThemeScreen.tsx index 363681a29b..824948c006 100644 --- a/packages/app/features/settings/ThemeScreen.tsx +++ b/packages/app/features/settings/ThemeScreen.tsx @@ -71,7 +71,7 @@ export function ThemeScreen(props: Props) { }, []); return ( - <> + props.navigation.goBack()} @@ -106,6 +106,6 @@ export function ThemeScreen(props: Props) { ))} - + ); } diff --git a/packages/app/features/settings/UserBugReportScreen.tsx b/packages/app/features/settings/UserBugReportScreen.tsx index f303819521..c6cb3ced34 100644 --- a/packages/app/features/settings/UserBugReportScreen.tsx +++ b/packages/app/features/settings/UserBugReportScreen.tsx @@ -43,7 +43,7 @@ export function UserBugReportScreen({ navigation }: Props) { ); return ( - + navigation.goBack()} diff --git a/packages/app/features/top/ContactsScreen.tsx b/packages/app/features/top/ContactsScreen.tsx index b9b570b101..9368e8b51c 100644 --- a/packages/app/features/top/ContactsScreen.tsx +++ b/packages/app/features/top/ContactsScreen.tsx @@ -63,7 +63,7 @@ export default function ContactsScreen(props: Props) { return ( - + { headerShown: false, drawerStyle: { width: 340, + backgroundColor: getVariableValue(useTheme().background), }, }} > @@ -68,9 +70,9 @@ function MainStack() { screenOptions={{ headerShown: false, }} - initialRouteName="ChatList" + initialRouteName="Home" > - + ; } diff --git a/packages/app/navigation/desktop/ProfileScreenNavigator.tsx b/packages/app/navigation/desktop/ProfileScreenNavigator.tsx index 99476e3a8b..0c813d1d55 100644 --- a/packages/app/navigation/desktop/ProfileScreenNavigator.tsx +++ b/packages/app/navigation/desktop/ProfileScreenNavigator.tsx @@ -7,7 +7,9 @@ import { FeatureFlagScreen } from '../../features/settings/FeatureFlagScreen'; import { ManageAccountScreen } from '../../features/settings/ManageAccountScreen'; import ProfileScreen from '../../features/settings/ProfileScreen'; import { PushNotificationSettingsScreen } from '../../features/settings/PushNotificationSettingsScreen'; +import { ThemeScreen } from '../../features/settings/ThemeScreen'; import { UserBugReportScreen } from '../../features/settings/UserBugReportScreen'; +import ContactsScreen from '../../features/top/ContactsScreen'; import { UserProfileScreen } from '../../features/top/UserProfileScreen'; const ProfileScreenStack = createNativeStackNavigator(); @@ -15,13 +17,14 @@ const ProfileScreenStack = createNativeStackNavigator(); export const ProfileScreenNavigator = () => { return ( + @@ -53,6 +56,7 @@ export const ProfileScreenNavigator = () => { name="EditProfile" component={EditProfileScreen} /> + ); }; diff --git a/packages/app/navigation/desktop/TopLevelDrawer.tsx b/packages/app/navigation/desktop/TopLevelDrawer.tsx index 6ff8c53641..2e8e3b414a 100644 --- a/packages/app/navigation/desktop/TopLevelDrawer.tsx +++ b/packages/app/navigation/desktop/TopLevelDrawer.tsx @@ -4,6 +4,7 @@ import { } from '@react-navigation/drawer'; import * as store from '@tloncorp/shared/store'; import { AvatarNavIcon, NavIcon, YStack, useWebAppUpdate } from '@tloncorp/ui'; +import { getVariableValue, useTheme } from 'tamagui'; import { ActivityScreen } from '../../features/top/ActivityScreen'; import { useCurrentUserId } from '../../hooks/useCurrentUser'; @@ -72,6 +73,8 @@ export const TopLevelDrawer = () => { headerShown: false, drawerStyle: { width: 48, + backgroundColor: getVariableValue(useTheme().background), + borderRightColor: getVariableValue(useTheme().border), }, }} > From 45e9266de9d1fc826bd205b3af9cb434921abc76 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 27 Nov 2024 15:00:52 +0000 Subject: [PATCH 25/52] update new frontend glob: [skip actions] --- tm-alpha-desk/desk.docket-0 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tm-alpha-desk/desk.docket-0 b/tm-alpha-desk/desk.docket-0 index e92b9d7141..2222b2ddde 100644 --- a/tm-alpha-desk/desk.docket-0 +++ b/tm-alpha-desk/desk.docket-0 @@ -2,7 +2,7 @@ info+'Start, host, and cultivate communities. Own your communications, organize your resources, and share documents. Tlon is a decentralized platform that offers a full, communal suite of tools for messaging, writing and sharing media with others.' color+0xde.dede image+'https://bootstrap.urbit.org/tlon.svg?v=1' - glob-http+['https://bootstrap.urbit.org/glob-0v6.e7n2f.8e8kv.4a2da.o8jm3.2f5q6.glob' 0v6.e7n2f.8e8kv.4a2da.o8jm3.2f5q6] + glob-http+['https://bootstrap.urbit.org/glob-0v6.i9laj.n1cm9.1vmbd.j8m35.34am1.glob' 0v6.i9laj.n1cm9.1vmbd.j8m35.34am1] base+'tm-alpha' version+[1 0 0] website+'https://tlon.io' From 5087a29e79d12a616cb39595ea21086e9d5e7622 Mon Sep 17 00:00:00 2001 From: Dan Brewster Date: Wed, 27 Nov 2024 10:07:14 -0500 Subject: [PATCH 26/52] refactor chat query (#4225) * refactor chat query * cleanup --- packages/app/features/top/ChatListScreen.tsx | 76 +++--- packages/app/fixtures/GroupList.fixture.tsx | 94 +++++--- packages/app/fixtures/fakeData.ts | 9 - packages/app/hooks/useBootSequence.ts | 2 +- packages/app/hooks/useGroupContext.ts | 2 +- packages/shared/src/api/activityApi.ts | 14 +- packages/shared/src/db/queries.test.ts | 10 +- packages/shared/src/db/queries.ts | 226 +++++++----------- packages/shared/src/db/types.ts | 23 +- packages/shared/src/logic/utils.ts | 19 -- packages/shared/src/store/activityActions.ts | 36 ++- packages/shared/src/store/channelActions.ts | 27 ++- packages/shared/src/store/dbHooks.ts | 45 +--- packages/shared/src/store/sync.test.ts | 6 +- packages/ui/src/components/ChatList.tsx | 114 +++++---- .../ui/src/components/ChatOptionsSheet.tsx | 2 +- .../components/ListItem/ChannelListItem.tsx | 9 +- .../src/components/ListItem/ChatListItem.tsx | 78 +----- .../src/components/ListItem/GroupListItem.tsx | 4 +- .../ListItem/InteractableChatListItem.tsx | 22 +- packages/ui/src/contexts/chatOptions.tsx | 2 +- 21 files changed, 356 insertions(+), 464 deletions(-) diff --git a/packages/app/features/top/ChatListScreen.tsx b/packages/app/features/top/ChatListScreen.tsx index 949c6f4af9..de48a4cca7 100644 --- a/packages/app/features/top/ChatListScreen.tsx +++ b/packages/app/features/top/ChatListScreen.tsx @@ -54,23 +54,17 @@ export function ChatListScreenView({ const [screenTitle, setScreenTitle] = useState('Home'); const [inviteSheetGroup, setInviteSheetGroup] = useState(); const chatOptionsSheetRef = useRef(null); - const [longPressedChat, setLongPressedChat] = useState< - db.Channel | db.Group | null - >(null); + const [longPressedChat, setLongPressedChat] = useState(null); const chatOptionsGroupId = useMemo(() => { - if (!longPressedChat) { - return; - } - return logic.isGroup(longPressedChat) - ? longPressedChat.id - : longPressedChat.group?.id; + return longPressedChat?.type === 'group' + ? longPressedChat.group.id + : undefined; }, [longPressedChat]); const chatOptionsChannelId = useMemo(() => { - if (!longPressedChat || logic.isGroup(longPressedChat)) { - return; - } - return longPressedChat.id; + return longPressedChat?.type === 'channel' + ? longPressedChat.channel.id + : undefined; }, [longPressedChat]); const [activeTab, setActiveTab] = useState<'all' | 'groups' | 'messages'>( @@ -87,9 +81,7 @@ export function ChatListScreenView({ enabled: isFocused, }); const pinned = useMemo(() => pins ?? [], [pins]); - const { data: pendingChats } = store.usePendingChats({ - enabled: isFocused, - }); + const { data: chats } = store.useCurrentChats({ enabled: isFocused, }); @@ -149,9 +141,9 @@ export function ChatListScreenView({ return { pinned: chats?.pinned ?? [], unpinned: chats?.unpinned ?? [], - pendingChats: pendingChats ?? [], + pending: chats?.pending ?? [], }; - }, [chats, pendingChats]); + }, [chats]); const handleNavigateToFindGroups = useCallback(() => { setAddGroupOpen(false); @@ -183,41 +175,39 @@ export function ChatListScreenView({ const [isChannelSwitcherEnabled] = useFeatureFlag('channelSwitcher'); const onPressChat = useCallback( - (item: db.Channel | db.Group) => { - if (logic.isGroup(item)) { + (item: db.Chat) => { + if (item.type === 'group' && item.isPending) { setSelectedGroupId(item.id); - } else if ( - item.group && - !isChannelSwitcherEnabled && - // Should navigate to channel if it's pinned as a channel - (!item.pin || item.pin.type === 'group') - ) { + } else if (item.type === 'group' && !isChannelSwitcherEnabled) { navigation.navigate('GroupChannels', { groupId: item.group.id }); + } else if (item.type === 'group') { + if (!item.group.channels?.length) { + throw new Error('cant open group with no channels'); + } + navigation.navigate('Channel', { + channelId: item.group.channels[0].id, + groupId: item.group.id, + }); } else { const screenName = screenNameFromChannelId(item.id); - navigation.navigate(screenName, { channelId: item.id, - selectedPostId: item.firstUnreadPostId, }); } }, [isChannelSwitcherEnabled, navigation] ); - const onLongPressChat = useCallback((item: db.Channel | db.Group) => { - if (logic.isChannel(item) && !item.isDmInvite) { - setLongPressedChat(item); - if (item.pin?.type === 'channel' || !item.group) { - chatOptionsSheetRef.current?.open(item.id, item.type); - } else { - chatOptionsSheetRef.current?.open( - item.group.id, - 'group', - item.group.unread?.count ?? undefined - ); - } + const onLongPressChat = useCallback((item: db.Chat) => { + if (item.isPending) { + return; } + setLongPressedChat(item); + chatOptionsSheetRef.current?.open( + item.id, + item.type === 'channel' ? item.channel.type : 'group', + item.unreadCount + ); }, []); const handleGroupPreviewSheetOpenChange = useCallback((open: boolean) => { @@ -234,7 +224,9 @@ export function ChatListScreenView({ const isTlonEmployee = useMemo(() => { const allChats = [...resolvedChats.pinned, ...resolvedChats.unpinned]; - return !!allChats.find((obj) => obj.groupId === TLON_EMPLOYEE_GROUP); + return !!allChats.find( + (chat) => chat.type === 'group' && chat.group.id === TLON_EMPLOYEE_GROUP + ); }, [resolvedChats]); useEffect(() => { @@ -336,7 +328,7 @@ export function ChatListScreenView({ setActiveTab={setActiveTab} pinned={resolvedChats.pinned} unpinned={resolvedChats.unpinned} - pendingChats={resolvedChats.pendingChats} + pending={resolvedChats.pending} onLongPressItem={onLongPressChat} onPressItem={onPressChat} onSectionChange={handleSectionChange} diff --git a/packages/app/fixtures/GroupList.fixture.tsx b/packages/app/fixtures/GroupList.fixture.tsx index 215eef729f..dfaa964810 100644 --- a/packages/app/fixtures/GroupList.fixture.tsx +++ b/packages/app/fixtures/GroupList.fixture.tsx @@ -13,7 +13,7 @@ import { let id = 0; -function makeChannelSummary({ +function makeChat({ group, channel, lastPost, @@ -23,37 +23,56 @@ function makeChannelSummary({ group?: db.Group; lastPost?: db.Post; members?: (db.ChatMember & { contact: db.Contact | null })[]; -}): db.Channel { - return { - id: 'channel-' + id++, - type: 'chat', - title: '', - description: '', - groupId: group?.id ?? null, - iconImage: null, - iconImageColor: null, - coverImage: null, - coverImageColor: null, - addedToGroupAt: null, - currentUserIsMember: null, - postCount: null, - unreadCount: group?.unreadCount ?? null, - firstUnreadPostId: null, - lastPostId: group?.lastPostId ?? null, - lastPostAt: group?.lastPostAt ?? null, - syncedAt: null, - remoteUpdatedAt: null, - ...channel, +}): db.Chat { + if (group) { + const groupId = 'group-' + id++; + return { + type: 'group', + id: groupId, + group: { ...group, id: groupId }, + isPending: false, + pin: group.pin ?? null, + timestamp: group.unread?.updatedAt ?? group.lastPostAt ?? 0, + volumeSettings: group.volumeSettings ?? null, + unreadCount: 0, + }; + } - group: group ?? null, - unread: null, - lastPost: lastPost ?? group?.lastPost ?? null, + const channelId = 'channel-' + id++; + return { + type: 'channel', + id: channelId, + timestamp: lastPost?.sentAt ?? 0, + isPending: !!channel?.isDmInvite, + volumeSettings: channel?.volumeSettings ?? null, pin: null, - members: members ?? null, + unreadCount: 0, + channel: { + id: channelId, + type: 'chat', + title: '', + description: '', + iconImage: null, + iconImageColor: null, + coverImage: null, + coverImageColor: null, + addedToGroupAt: null, + currentUserIsMember: null, + postCount: null, + firstUnreadPostId: null, + syncedAt: null, + remoteUpdatedAt: null, + ...channel, + group: group ?? null, + unread: null, + lastPost: lastPost ?? null, + pin: null, + members: members ?? null, + }, }; } -const dmSummary = makeChannelSummary({ +const dmSummary = makeChat({ channel: { type: 'dm' }, lastPost: createFakePost(), members: [ @@ -67,7 +86,7 @@ const dmSummary = makeChannelSummary({ ], }); -const groupDmSummary = makeChannelSummary({ +const groupDmSummary = makeChat({ channel: { type: 'groupDm' }, lastPost: createFakePost(), group: groupWithLongTitle, @@ -98,7 +117,7 @@ const groupDmSummary = makeChannelSummary({ export default { basic: ( - + {}} @@ -106,21 +125,20 @@ export default { onSearchQueryChange={() => {}} showSearchInput={false} pinned={[groupWithLongTitle, groupWithImage].map((g) => - makeChannelSummary({ group: g }) + makeChat({ group: g }) )} unpinned={[ groupWithColorAndNoImage, groupWithImage, - groupWithSvgImage, groupWithNoColorOrImage, - ].map((g) => makeChannelSummary({ group: g }))} - pendingChats={[]} + ].map((g) => makeChat({ group: g }))} + pending={[]} onSearchToggle={() => {}} /> ), emptyPinned: ( - + {}} @@ -131,8 +149,8 @@ export default { groupWithImage, groupWithSvgImage, groupWithNoColorOrImage, - ].map((g) => makeChannelSummary({ group: g }))} - pendingChats={[]} + ].map((g) => makeChat({ group: g }))} + pending={[]} searchQuery="" onSearchQueryChange={() => {}} onSearchToggle={() => {}} @@ -140,14 +158,14 @@ export default { ), loading: ( - + {}} showSearchInput={false} pinned={[]} unpinned={[]} - pendingChats={[]} + pending={[]} searchQuery="" onSearchQueryChange={() => {}} onSearchToggle={() => {}} diff --git a/packages/app/fixtures/fakeData.ts b/packages/app/fixtures/fakeData.ts index 5ff3e8224b..aaaf7b413a 100644 --- a/packages/app/fixtures/fakeData.ts +++ b/packages/app/fixtures/fakeData.ts @@ -804,7 +804,6 @@ export const groupWithColorAndNoImage: db.Group = { currentUserIsHost: false, title: 'Test Group', privacy: 'private', - unreadCount: 1, iconImage: null, iconImageColor: '#FF00FF', coverImage: null, @@ -813,7 +812,6 @@ export const groupWithColorAndNoImage: db.Group = { currentUserIsMember: true, lastPostId: 'test-post', lastPostAt: dates.now, - lastChannel: tlonLocalSupport.title, lastPost: { ...createFakePost() }, }; @@ -822,7 +820,6 @@ export const groupWithLongTitle: db.Group = { id: '~nibset-napwyn/tlon/long-title', title: 'And here, a reallly long title, wazzup, ok', lastPostAt: dates.earlierToday, - lastChannel: tlonLocalSupport.title, lastPost: { ...createFakePost(), textContent: @@ -836,8 +833,6 @@ export const groupWithNoColorOrImage: db.Group = { iconImageColor: null, lastPost: createFakePost(), lastPostAt: dates.yesterday, - lastChannel: tlonLocalSupport.title, - unreadCount: Math.floor(random() * 20), }; export const groupWithImage: db.Group = { @@ -847,8 +842,6 @@ export const groupWithImage: db.Group = { 'https://dans-gifts.s3.amazonaws.com/dans-gifts/solfer-magfed/2024.4.6..15.49.54..4a7e.f9db.22d0.e560-IMG_4770.jpg', lastPost: createFakePost(), lastPostAt: dates.lastWeek, - lastChannel: tlonLocalSupport.title, - unreadCount: Math.floor(random() * 20), }; export const groupWithSvgImage: db.Group = { @@ -857,8 +850,6 @@ export const groupWithSvgImage: db.Group = { iconImage: 'https://tlon.io/local-icon.svg', lastPost: createFakePost(), lastPostAt: dates.lastMonth, - lastChannel: tlonLocalSupport.title, - unreadCount: Math.floor(random() * 20), }; function randInt(min: number, max: number) { diff --git a/packages/app/hooks/useBootSequence.ts b/packages/app/hooks/useBootSequence.ts index 64d00576d8..747901dac4 100644 --- a/packages/app/hooks/useBootSequence.ts +++ b/packages/app/hooks/useBootSequence.ts @@ -221,7 +221,7 @@ export function useBootSequence({ if (dmIsGood && groupIsGood) { logger.crumb('successfully accepted invites'); if (updatedTlonTeamDm) { - store.pinItem(updatedTlonTeamDm); + store.pinChannel(updatedTlonTeamDm); } return NodeBootPhase.READY; } diff --git a/packages/app/hooks/useGroupContext.ts b/packages/app/hooks/useGroupContext.ts index 0ffae7b75b..8ea6dc9cb4 100644 --- a/packages/app/hooks/useGroupContext.ts +++ b/packages/app/hooks/useGroupContext.ts @@ -231,7 +231,7 @@ export const useGroupContext = ({ const togglePinned = useCallback(async () => { if (group && group.channels[0]) { - group.pin ? store.unpinItem(group.pin) : store.pinItem(group.channels[0]); + group.pin ? store.unpinItem(group.pin) : store.pinGroup(group); } }, [group]); diff --git a/packages/shared/src/api/activityApi.ts b/packages/shared/src/api/activityApi.ts index 80c0dbe44f..4a87b0597f 100644 --- a/packages/shared/src/api/activityApi.ts +++ b/packages/shared/src/api/activityApi.ts @@ -847,17 +847,19 @@ export function getThreadSource({ return { source, sourceId }; } -export function getRootSourceFromChannel(channel: db.Channel): { +export function getRootSourceFromChat(chat: db.Chat): { source: ub.Source; sourceId: string; } { let source: ub.Source; - if (channel.type === 'dm') { - source = { dm: { ship: channel.id } }; - } else if (channel.type === 'groupDm') { - source = { dm: { club: channel.id } }; + if (chat.type === 'group') { + source = { group: chat.id }; + } else if (chat.channel.type === 'dm') { + source = { dm: { ship: chat.channel.id } }; + } else if (chat.channel.type === 'groupDm') { + source = { dm: { club: chat.channel.id } }; } else { - source = { group: channel.groupId! }; + throw new Error('Cannot get source for non-dm channel chat'); } const sourceId = ub.sourceToString(source); diff --git a/packages/shared/src/db/queries.test.ts b/packages/shared/src/db/queries.test.ts index a45fda48ac..a58e31ea71 100644 --- a/packages/shared/src/db/queries.test.ts +++ b/packages/shared/src/db/queries.test.ts @@ -42,11 +42,15 @@ test('uses init data to get chat list', async () => { await syncInitData(); const result = await queries.getChats(); - expect(result.map((r) => r.id).slice(0, 8)).toEqual([ + + expect(result.pinned.map((r) => r.id)).toEqual([ '0v4.00000.qd6oi.a3f6t.5sd9v.fjmp2', - 'chat/~nibset-napwyn/commons', + ]); + + expect(result.unpinned.map((r) => r.id).slice(0, 7)).toEqual([ + '~nibset-napwyn/tlon', '~nocsyx-lassul', - 'chat/~pondus-watbel/new-channel', + '~pondus-watbel/testing-facility', '~ravseg-nosduc', '~solfer-magfed', '~hansel-ribbur', diff --git a/packages/shared/src/db/queries.ts b/packages/shared/src/db/queries.ts index 25b4974385..b7732993a1 100644 --- a/packages/shared/src/db/queries.ts +++ b/packages/shared/src/db/queries.ts @@ -79,7 +79,7 @@ import { ActivityEvent, Channel, ChannelUnread, - ChatMember, + Chat, ClientMeta, Contact, Group, @@ -196,33 +196,6 @@ export const getGroups = createReadQuery( ] ); -export const getPendingChats = createReadQuery( - 'getPendingChats', - async (ctx: QueryCtx) => { - const pendingGroups = await ctx.db.query.groups.findMany({ - where: or( - eq($groups.haveInvite, true), - isNotNull($groups.joinStatus), - eq($groups.haveRequestedInvite, true) - ), - }); - - const pendingChannels = await ctx.db.query.channels.findMany({ - where: eq($channels.isDmInvite, true), - with: { - members: { - with: { - contact: true, - }, - }, - }, - }); - - return [...pendingChannels, ...pendingGroups]; - }, - ['groups', 'channels'] -); - export const getUnjoinedGroupChannels = createReadQuery( 'getUnjoinedGroupChannels', async (groupId: string, ctx: QueryCtx) => { @@ -315,119 +288,106 @@ export const getAllChannels = createReadQuery( export const getChats = createReadQuery( 'getChats', - async (ctx: QueryCtx): Promise => { - const partitionedGroupsQuery = ctx.db - .select({ - ...getTableColumns($channels), - rowNumber: - sql`ROW_NUMBER() OVER(PARTITION BY ${$channels.groupId} ORDER BY COALESCE(${$channels.lastPostAt}, ${$channelUnreads.updatedAt}) DESC)`.as( - 'row_number' - ), - }) - .from($channels) - .where( - and(isNotNull($channels.groupId), eq($groups.currentUserIsMember, true)) - ) - .leftJoin($channelUnreads, eq($channelUnreads.channelId, $channels.id)) - .leftJoin($groups, eq($groups.id, $channels.groupId)) - .as('q'); - - const groupChannels = ctx.db - .select() - .from(partitionedGroupsQuery) - .where(eq(partitionedGroupsQuery.rowNumber, 1)); + async ( + ctx: QueryCtx + ): Promise<{ pinned: Chat[]; pending: Chat[]; unpinned: Chat[] }> => { + const groups = await ctx.db.query.groups.findMany({ + where: or( + eq($groups.currentUserIsMember, true), + eq($groups.isNew, true), + isNotNull($groups.joinStatus) + ), + with: { + volumeSettings: true, + unread: true, + channels: { + orderBy: [desc($channels.lastPostAt)], + with: { + lastPost: true, + }, + }, + // Just need the first 3 members for possible title generation purposes + members: { + limit: 3, + orderBy: [asc($chatMembers.joinedAt)], + with: { + contact: true, + }, + }, + pin: true, + lastPost: true, + }, + }); - const allChannels = ctx.db - .select({ - ...getTableColumns($channels), - rowNumber: sql`0`.as('row_number'), - }) - .from($channels) - .where(and(isNull($channels.groupId), eq($channels.isDmInvite, false))) - .union(groupChannels) - .as('ac'); + const channels = await ctx.db.query.channels.findMany({ + where: isNull($channels.groupId), + with: { + volumeSettings: true, + unread: true, + members: { + with: { + contact: true, + }, + }, + pin: true, + lastPost: true, + }, + }); - const $groupVolumeSettings = ctx.db - .select() - .from($volumeSettings) - .where(eq($volumeSettings.itemType, 'group')) - .as('gvs'); + const groupChats: Chat[] = groups.map((g) => ({ + id: g.id, + type: 'group', + pin: g.pin, + timestamp: g.unread?.updatedAt ?? g.lastPostAt ?? 0, + volumeSettings: g.volumeSettings, + unreadCount: g.unread?.count ?? 0, + group: g, + isPending: + g.haveInvite === true || + !!g.joinStatus || + g.haveRequestedInvite || + false, + })); - const result = await ctx.db - .select({ - ...allQueryColumns(allChannels), - group: getTableColumns($groups), - groupUnread: getTableColumns($groupUnreads), - groupVolumeSettings: allQueryColumns($groupVolumeSettings), - volumeSettings: getTableColumns($volumeSettings), - unread: getTableColumns($channelUnreads), - pin: getTableColumns($pins), - lastPost: getTableColumns($posts), - member: { - ...getTableColumns($chatMembers), - }, - contact: getTableColumns($contacts), - }) - .from(allChannels) - .leftJoin( - $groupVolumeSettings, - eq($groupVolumeSettings.itemId, allChannels.groupId) - ) - .leftJoin($volumeSettings, eq($volumeSettings.itemId, allChannels.id)) - .leftJoin($groups, eq($groups.id, allChannels.groupId)) - .leftJoin($groupUnreads, eq($groupUnreads.groupId, allChannels.groupId)) - .leftJoin($channelUnreads, eq($channelUnreads.channelId, allChannels.id)) - .leftJoin( - $pins, - or( - eq(allChannels.groupId, $pins.itemId), - eq(allChannels.id, $pins.itemId) - ) - ) - .leftJoin($posts, eq($posts.id, allChannels.lastPostId)) - .leftJoin($chatMembers, eq($chatMembers.chatId, allChannels.id)) - .leftJoin($contacts, eq($contacts.id, $chatMembers.contactId)) - .orderBy( - ascNullsLast($pins.index), - sql`(CASE WHEN ${$groups.isNew} = 1 THEN 1 ELSE 0 END) DESC`, - sql`COALESCE(${$channelUnreads.updatedAt}, ${allChannels.lastPostAt}) DESC` - ); + const channelChats: Chat[] = channels.map((c) => ({ + id: c.id, + type: 'channel', + channel: c, + pin: c.pin, + volumeSettings: c.volumeSettings, + unreadCount: c.unread?.count ?? 0, + timestamp: c.unread?.updatedAt ?? c.lastPostAt ?? 0, + isPending: !!c.isDmInvite, + })); - const [chatMembers, filteredChannels] = result.reduce< - [ - Record, - typeof result, - ] - >( - ([members, filteredChannels], channel) => { - if (!channel.member || !members[channel.id]) { - filteredChannels.push(channel); - } - if (channel.member) { - members[channel.id] ||= []; - members[channel.id].push({ - ...channel.member, - contact: channel.contact ?? null, - }); + const { pinnedChats, pendingChats, otherChats } = [ + ...channelChats, + ...groupChats, + ].reduce( + (acc, chat) => { + if (chat.pin) { + acc.pinnedChats.push(chat); + } else if (chat.isPending) { + acc.pendingChats.push(chat); + } else { + acc.otherChats.push(chat); } - return [members, filteredChannels]; + return acc; }, - [{}, [] as typeof result] + { + pinnedChats: [], + pendingChats: [], + otherChats: [], + } as Record ); - return filteredChannels.map((c) => { - return { - ...c, - members: chatMembers[c.id] ?? null, - group: !c.group - ? null - : { - ...c.group, - unread: c.groupUnread, - volumeSettings: c.groupVolumeSettings, - }, - }; - }); + return { + pinned: pinnedChats.sort( + (a, b) => (a.pin?.index ?? 0) - (b.pin?.index ?? 0) + ), + pending: pendingChats, + unpinned: otherChats.sort((a, b) => b.timestamp - a.timestamp), + }; }, [ 'groups', diff --git a/packages/shared/src/db/types.ts b/packages/shared/src/db/types.ts index 5a7933f91a..21a6cb194f 100644 --- a/packages/shared/src/db/types.ts +++ b/packages/shared/src/db/types.ts @@ -44,12 +44,8 @@ export type ActivityEvent = BaseModel<'activityEvents'>; export type ActivityEventContactUpdateGroups = ActivityEvent['contactUpdateGroups']; export type ActivityBucket = schema.ActivityBucket; -// TODO: We need to include unread count here because it's returned by the chat -// list query, but doesn't feel great. -export type Group = BaseModel<'groups'> & { - unreadCount?: number | null; - lastChannel?: string | null; -}; +export type Group = BaseModel<'groups'>; + export type ClientMeta = Pick< Group, | 'title' @@ -100,6 +96,21 @@ export type Settings = BaseModel<'settings'>; export type PostWindow = BaseModel<'postWindows'>; export type VolumeSettings = BaseModel<'volumeSettings'>; +export type Chat = { + id: string; + pin: Pin | null; + volumeSettings: VolumeSettings | null; + timestamp: number; + isPending: boolean; + unreadCount: number; +} & ({ type: 'group'; group: Group } | { type: 'channel'; channel: Channel }); + +export interface GroupedChats { + pinned: Chat[]; + unpinned: Chat[]; + pending: Chat[]; +} + export interface GroupEvent extends ActivityEvent { type: 'group-ask'; group: Group; diff --git a/packages/shared/src/logic/utils.ts b/packages/shared/src/logic/utils.ts index d997b58ed7..be198ef6fe 100644 --- a/packages/shared/src/logic/utils.ts +++ b/packages/shared/src/logic/utils.ts @@ -212,25 +212,6 @@ export function normalizeUrbitColor(color: string): string { return `#${lengthAdjustedColor}`; } -export function getPinPartial(channel: db.Channel): { - type: db.PinType; - itemId: string; -} { - if (channel.groupId) { - return { type: 'group', itemId: channel.groupId }; - } - - if (channel.type === 'dm') { - return { type: 'dm', itemId: channel.id }; - } - - if (channel.type === 'groupDm') { - return { type: 'groupDm', itemId: channel.id }; - } - - return { type: 'channel', itemId: channel.id }; -} - const MS_PER_DAY = 24 * 60 * 60 * 1000; const timezoneOffset = new Date().getTimezoneOffset() * 60 * 1000; diff --git a/packages/shared/src/store/activityActions.ts b/packages/shared/src/store/activityActions.ts index 3b9fdb233f..5d427e97fb 100644 --- a/packages/shared/src/store/activityActions.ts +++ b/packages/shared/src/store/activityActions.ts @@ -7,26 +7,26 @@ import { whomIsMultiDm } from '../urbit'; const logger = createDevLogger('activityActions', false); -export async function muteChat(channel: db.Channel) { - const initialSettings = await getChatVolumeSettings(channel); - const muteLevel = channel.groupId ? 'soft' : 'hush'; +export async function muteChat(chat: db.Chat) { + const initialSettings = await getChatVolumeSettings(chat); + const muteLevel = chat.type === 'group' ? 'soft' : 'hush'; db.setVolumes({ volumes: [ { - itemId: channel.groupId ?? channel.id, - itemType: channel.groupId ? 'group' : 'channel', + itemId: chat.id, + itemType: chat.type, level: muteLevel, }, ], }); try { - const { source } = api.getRootSourceFromChannel(channel); + const { source } = api.getRootSourceFromChat(chat); const volume = ub.getVolumeMap(muteLevel, true); await api.adjustVolumeSetting(source, volume); } catch (e) { - logger.log(`failed to mute group ${channel.id}`, e); + logger.log(`failed to mute chat ${chat.id}`, e); // revert the optimistic update if (initialSettings) { await db.setVolumes({ volumes: [initialSettings] }); @@ -34,24 +34,24 @@ export async function muteChat(channel: db.Channel) { } } -export async function unmuteChat(channel: db.Channel) { - const initialSettings = await getChatVolumeSettings(channel); +export async function unmuteChat(chat: db.Chat) { + const initialSettings = await getChatVolumeSettings(chat); db.setVolumes({ volumes: [ { - itemId: channel.groupId ?? channel.id, - itemType: channel.groupId ? 'group' : 'channel', + itemId: chat.id, + itemType: chat.type, level: 'default', }, ], }); try { - const { source } = api.getRootSourceFromChannel(channel); + const { source } = api.getRootSourceFromChat(chat); await api.adjustVolumeSetting(source, null); } catch (e) { - logger.log(`failed to unmute chat ${channel.id}`, e); + logger.log(`failed to unmute chat ${chat.id}`, e); // revert the optimistic update if (initialSettings) { await db.setVolumes({ volumes: [initialSettings] }); @@ -59,14 +59,8 @@ export async function unmuteChat(channel: db.Channel) { } } -async function getChatVolumeSettings(chat: db.Channel) { - if (chat.groupId) { - return ( - chat.group?.volumeSettings ?? (await db.getVolumeSetting(chat.groupId)) - ); - } else { - return chat.volumeSettings ?? (await db.getVolumeSetting(chat.id)); - } +async function getChatVolumeSettings(chat: db.Chat) { + return chat.volumeSettings ?? (await db.getVolumeSetting(chat.id)); } export async function muteThread({ diff --git a/packages/shared/src/store/channelActions.ts b/packages/shared/src/store/channelActions.ts index 6201e6ef35..58d2cfcd08 100644 --- a/packages/shared/src/store/channelActions.ts +++ b/packages/shared/src/store/channelActions.ts @@ -149,17 +149,32 @@ export async function updateChannel({ } } -export async function pinItem(channel: db.Channel) { - // optimistic update - const partialPin = logic.getPinPartial(channel); - db.insertPinnedItem(partialPin); +export async function pinChat(chat: db.Chat) { + return chat.type === 'group' + ? pinGroup(chat.group) + : pinChannel(chat.channel); +} + +export async function pinGroup(group: db.Group) { + return savePin({ type: 'group', itemId: group.id }); +} + +export async function pinChannel(channel: db.Channel) { + const type = + channel.type === 'dm' || channel.type === 'groupDm' + ? channel.type + : 'channel'; + return savePin({ type, itemId: channel.id }); +} +async function savePin(pin: { type: db.PinType; itemId: string }) { + db.insertPinnedItem(pin); try { - await api.pinItem(partialPin.itemId); + await api.pinItem(pin.itemId); } catch (e) { console.error('Failed to pin item', e); // rollback optimistic update - db.deletePinnedItem(partialPin); + db.deletePinnedItem(pin); } } diff --git a/packages/shared/src/store/dbHooks.ts b/packages/shared/src/store/dbHooks.ts index af7f356359..e357e5f33b 100644 --- a/packages/shared/src/store/dbHooks.ts +++ b/packages/shared/src/store/dbHooks.ts @@ -7,6 +7,7 @@ import { useMemo } from 'react'; import * as api from '../api'; import * as db from '../db'; +import { GroupedChats } from '../db/types'; import * as ub from '../urbit'; import { hasCustomS3Creds, hasHostingUploadCreds } from './storage'; import { @@ -18,14 +19,6 @@ import { keyFromQueryDeps, useKeyFromQueryDeps } from './useKeyFromQueryDeps'; export * from './useChannelSearch'; -// Assorted small hooks for fetching data from the database. -// Can break em out as they get bigger. - -export interface CurrentChats { - pinned: db.Channel[]; - unpinned: db.Channel[]; -} - export type CustomQueryConfig = Pick< UseQueryOptions, 'enabled' @@ -41,43 +34,13 @@ export const useAllChannels = ({ enabled }: { enabled?: boolean }) => { }; export const useCurrentChats = ( - queryConfig?: CustomQueryConfig -): UseQueryResult => { + queryConfig?: CustomQueryConfig +): UseQueryResult => { return useQuery({ queryFn: async () => { - const channels = await db.getChats(); - return { channels }; + return db.getChats(); }, queryKey: ['currentChats', useKeyFromQueryDeps(db.getChats)], - select({ channels }) { - for (let i = 0; i < channels.length; ++i) { - if (!channels[i].pin) { - return { - pinned: channels.slice(0, i), - unpinned: channels.slice(i), - }; - } - } - return { - pinned: channels, - unpinned: [], - }; - }, - ...queryConfig, - }); -}; - -export type PendingChats = (db.Group | db.Channel)[]; - -export const usePendingChats = ( - queryConfig?: CustomQueryConfig -): UseQueryResult => { - return useQuery({ - queryFn: async () => { - const pendingChats = await db.getPendingChats(); - return pendingChats; - }, - queryKey: ['pendingChats', useKeyFromQueryDeps(db.getPendingChats)], ...queryConfig, }); }; diff --git a/packages/shared/src/store/sync.test.ts b/packages/shared/src/store/sync.test.ts index 44f3479a5d..6dd2cbe530 100644 --- a/packages/shared/src/store/sync.test.ts +++ b/packages/shared/src/store/sync.test.ts @@ -296,9 +296,11 @@ test('syncs last posts', async () => { await syncLatestPosts(); const chats = await db.getChats(); const NUM_EMPTY_TEST_GROUPS = 6; - const chatsWithLatestPosts = chats.filter((c) => c.lastPost); + const chatsWithLatestPosts = chats.unpinned.filter((c) => + c.type === 'channel' ? c.channel.lastPost : c.group.lastPost + ); expect(chatsWithLatestPosts.length).toEqual( - chats.length - NUM_EMPTY_TEST_GROUPS + chats.unpinned.length - NUM_EMPTY_TEST_GROUPS ); }); diff --git a/packages/ui/src/components/ChatList.tsx b/packages/ui/src/components/ChatList.tsx index 06c64cced0..7542228ad2 100644 --- a/packages/ui/src/components/ChatList.tsx +++ b/packages/ui/src/components/ChatList.tsx @@ -1,7 +1,5 @@ import { FlashList, ListRenderItem } from '@shopify/flash-list'; import * as db from '@tloncorp/shared/db'; -import * as logic from '@tloncorp/shared/logic'; -import * as store from '@tloncorp/shared/store'; import Fuse from 'fuse.js'; import { debounce } from 'lodash'; import React, { @@ -19,6 +17,7 @@ import Animated, { } from 'react-native-reanimated'; import { Text, View, YStack, getTokenValue } from 'tamagui'; +import { useCalm } from '../contexts'; import { interactionWithTiming } from '../utils/animation'; import { TextInputWithIconAndButton } from './Form'; import { ChatListItem, InteractableChatListItem } from './ListItem'; @@ -26,17 +25,15 @@ import Pressable from './Pressable'; import { SectionListHeader } from './SectionList'; import { Tabs } from './Tabs'; -export type Chat = db.Channel | db.Group; - export type TabName = 'all' | 'groups' | 'messages'; type SectionHeaderData = { type: 'sectionHeader'; title: string }; -type ChatListItemData = Chat | SectionHeaderData; +type ChatListItemData = db.Chat | SectionHeaderData; export const ChatList = React.memo(function ChatListComponent({ pinned, unpinned, - pendingChats, + pending, onLongPressItem, onPressItem, activeTab, @@ -45,10 +42,9 @@ export const ChatList = React.memo(function ChatListComponent({ searchQuery, onSearchQueryChange, onSearchToggle, -}: store.CurrentChats & { - pendingChats: store.PendingChats; - onPressItem?: (chat: Chat) => void; - onLongPressItem?: (chat: Chat) => void; +}: db.GroupedChats & { + onPressItem?: (chat: db.Chat) => void; + onLongPressItem?: (chat: db.Chat) => void; onSectionChange?: (title: string) => void; activeTab: TabName; setActiveTab: (tab: TabName) => void; @@ -60,7 +56,7 @@ export const ChatList = React.memo(function ChatListComponent({ const displayData = useFilteredChats({ pinned, unpinned, - pending: pendingChats, + pending, searchQuery, activeTab, }); @@ -92,7 +88,7 @@ export const ChatList = React.memo(function ChatListComponent({ {item.title} ); - } else if (logic.isChannel(item)) { + } else if (item.type === 'channel' || !item.isPending) { return ( performSearch(debouncedQuery), @@ -331,42 +314,53 @@ function useFilteredChats({ }, [activeTab, pending, searchQuery, searchResults, unpinned, pinned]); } -function filterPendingChats(pending: Chat[], activeTab: TabName) { +function filterPendingChats(pending: db.Chat[], activeTab: TabName) { if (activeTab === 'all') return pending; return pending.filter((chat) => { - const isGroupChannel = logic.isGroup(chat); - return activeTab === 'groups' ? isGroupChannel : !isGroupChannel; + const isGroup = chat.type === 'group'; + return activeTab === 'groups' ? isGroup : !isGroup; }); } -function filterChats(chats: Chat[], activeTab: TabName) { +function filterChats(chats: db.Chat[], activeTab: TabName) { if (activeTab === 'all') return chats; return chats.filter((chat) => { - const isGroupChannel = logic.isGroupChannelId(chat.id); - return activeTab === 'groups' ? isGroupChannel : !isGroupChannel; + const isGroup = chat.type === 'group'; + return activeTab === 'groups' ? isGroup : !isGroup; }); } function useChatSearch({ pinned, unpinned, + pending, }: { - pinned: Chat[]; - unpinned: Chat[]; + pinned: db.Chat[]; + unpinned: db.Chat[]; + pending: db.Chat[]; }) { + const { disableNicknames } = useCalm(); + const fuse = useMemo(() => { - const allData = [...pinned, ...unpinned]; + const allData = [...pinned, ...unpinned, ...pending]; return new Fuse(allData, { keys: [ - 'id', - 'group.title', - 'contact.nickname', - 'members.contact.nickname', - 'members.contact.id', + { + name: 'title', + getFn: (chat: db.Chat) => { + const title = getChatTitle(chat, disableNicknames); + return Array.isArray(title) + ? title.map(normalizeString) + : normalizeString(title); + }, + }, ], - threshold: 0.3, }); - }, [pinned, unpinned]); + }, [pinned, unpinned, pending, disableNicknames]); + + function normalizeString(str: string) { + return str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); + } const performSearch = useCallback( (query: string) => { @@ -381,6 +375,34 @@ function useChatSearch({ return performSearch; } +function getChatTitle( + chat: db.Chat, + disableNicknames: boolean +): string | string[] { + if (chat.type === 'channel') { + if (chat.channel.title) { + return chat.channel.title; + } else if (chat.channel.members) { + return chat.channel.members + .map((member) => { + const nickname = member.contact + ? (member.contact as db.Contact).nickname + : null; + return nickname && !disableNicknames ? nickname : member.contactId; + }) + .join(', '); + } else { + return []; + } + } else { + if (chat.group.title) { + return chat.group.title; + } else { + return []; + } + } +} + function useDebouncedValue(input: T, delay: number) { const [value, setValue] = useState(input); const debouncedSetValue = useMemo( diff --git a/packages/ui/src/components/ChatOptionsSheet.tsx b/packages/ui/src/components/ChatOptionsSheet.tsx index 8de60c3b64..caf158f489 100644 --- a/packages/ui/src/components/ChatOptionsSheet.tsx +++ b/packages/ui/src/components/ChatOptionsSheet.tsx @@ -665,7 +665,7 @@ export function ChannelOptions({ } channel.pin ? store.unpinItem(channel.pin) - : store.pinItem(channel); + : store.pinChannel(channel); }, }, ], diff --git a/packages/ui/src/components/ListItem/ChannelListItem.tsx b/packages/ui/src/components/ListItem/ChannelListItem.tsx index 04dc2d66de..497a937a05 100644 --- a/packages/ui/src/components/ListItem/ChannelListItem.tsx +++ b/packages/ui/src/components/ListItem/ChannelListItem.tsx @@ -1,15 +1,12 @@ import type * as db from '@tloncorp/shared/db'; import * as logic from '@tloncorp/shared/logic'; import { useMemo } from 'react'; -import { View } from 'tamagui'; -import { isWeb } from 'tamagui'; +import { View, isWeb } from 'tamagui'; -import useIsWindowNarrow from '../../hooks/useIsWindowNarrow'; import * as utils from '../../utils'; import { capitalize } from '../../utils'; import { Badge } from '../Badge'; import { Button } from '../Button'; -import { Chat } from '../ChatList'; import { Icon } from '../Icon'; import Pressable from '../Pressable'; import { ListItem, type ListItemProps } from './ListItem'; @@ -27,7 +24,7 @@ export function ChannelListItem({ useTypeIcon?: boolean; customSubtitle?: string; dimmed?: boolean; -} & ListItemProps & { model: db.Channel }) { +} & ListItemProps) { const unreadCount = model.unread?.count ?? 0; const title = utils.useChannelTitle(model); const firstMemberId = model.members?.[0]?.contactId ?? ''; @@ -82,7 +79,7 @@ export function ChannelListItem({ {subtitle} )} - {model.lastPost && ( + {model.lastPost && !model.isDmInvite && ( ) { +}: ListItemProps) { const handlePress = logic.useMutableCallback(() => { onPress?.(model); }); @@ -21,74 +20,23 @@ export const ChatListItem = React.memo(function ChatListItemComponent({ onLongPress?.(model); }); - // if the chat list item is a group, it's pending - if (logic.isGroup(model)) { + if (model.type === 'group') { return ( + ); + } else { + return ( + ); } - - if (logic.isChannel(model)) { - if ( - model.type === 'dm' || - model.type === 'groupDm' || - model.pin?.type === 'channel' - ) { - return ( - - ); - } else if (model.group) { - return ( - - ); - } - } - - console.warn('unable to render chat list item', model.id, model); - return null; }); - -function GroupListItemAdapter({ - model, - groupModel, - onPress, - onLongPress, - ...props -}: ListItemProps & { - groupModel: db.Group; - onPress: () => void; - onLongPress: () => void; -}) { - const resolvedModel = useMemo(() => { - return { - ...groupModel, - unreadCount: model.unread?.count, - lastPost: model.lastPost, - lastChannel: model.title, - }; - }, [model, groupModel]); - return ( - - ); -} diff --git a/packages/ui/src/components/ListItem/GroupListItem.tsx b/packages/ui/src/components/ListItem/GroupListItem.tsx index 5a5a550dc8..1258e56fad 100644 --- a/packages/ui/src/components/ListItem/GroupListItem.tsx +++ b/packages/ui/src/components/ListItem/GroupListItem.tsx @@ -43,11 +43,11 @@ export const GroupListItem = ({ {customSubtitle && ( {customSubtitle} )} - {model.lastPost && !customSubtitle && ( + {model.lastPost && model.channels?.length && !customSubtitle && ( - {model.lastChannel} + {model.channels[0].title} )} {!isPending && model.lastPost ? ( diff --git a/packages/ui/src/components/ListItem/InteractableChatListItem.tsx b/packages/ui/src/components/ListItem/InteractableChatListItem.tsx index 838e768cd4..b02ab42a82 100644 --- a/packages/ui/src/components/ListItem/InteractableChatListItem.tsx +++ b/packages/ui/src/components/ListItem/InteractableChatListItem.tsx @@ -20,7 +20,6 @@ import Animated, { import { ColorTokens, Stack, View, getTokenValue, isWeb } from 'tamagui'; import * as utils from '../../utils'; -import { Chat } from '../ChatList'; import { Icon, IconType } from '../Icon'; import { ChatListItem } from './ChatListItem'; import { ListItemProps } from './ListItem'; @@ -30,19 +29,14 @@ function BaseInteractableChatRow({ model, onPress, onLongPress, -}: ListItemProps & { model: db.Channel }) { +}: ListItemProps) { const swipeableRef = useRef(null); const [currentSwipeDirection, setCurrentSwipeDirection] = useState< 'left' | 'right' | null >(null); const isMuted = useMemo(() => { - if (model.group) { - return logic.isMuted(model.group.volumeSettings?.level, 'group'); - } else if (model.type === 'dm' || model.type === 'groupDm') { - return logic.isMuted(model.volumeSettings?.level, 'channel'); - } - return false; + return logic.isMuted(model.volumeSettings?.level, model.type); }, [model]); // prevent color flicker when unmuting @@ -60,16 +54,16 @@ function BaseInteractableChatRow({ utils.triggerHaptic('swipeAction'); switch (actionId) { case 'pin': - model.pin ? store.unpinItem(model.pin) : store.pinItem(model); + model.pin ? store.unpinItem(model.pin) : store.pinChat(model); break; case 'mute': isMuted ? store.unmuteChat(model) : store.muteChat(model); break; case 'markRead': - if (model.group) { + if (model.type === 'group') { store.markGroupRead(model.group, true); } else { - store.markChannelRead(model); + store.markChannelRead(model.channel); } break; default: @@ -89,9 +83,7 @@ function BaseInteractableChatRow({ const renderLeftActions = useCallback( (progress: SharedValue, drag: SharedValue) => { - const hasUnread = model.group - ? (model.group.unread?.count ?? 0) > 0 - : (model.unread?.count ?? 0) > 0; + const hasUnread = model.unreadCount > 0; if (currentSwipeDirection === 'right' || !hasUnread) { return ; @@ -105,7 +97,7 @@ function BaseInteractableChatRow({ /> ); }, - [handleAction, model.group, model.unread?.count, currentSwipeDirection] + [model.unreadCount, currentSwipeDirection, handleAction] ); const renderRightActions = useCallback( diff --git a/packages/ui/src/contexts/chatOptions.tsx b/packages/ui/src/contexts/chatOptions.tsx index fbed826993..f69af88afb 100644 --- a/packages/ui/src/contexts/chatOptions.tsx +++ b/packages/ui/src/contexts/chatOptions.tsx @@ -74,7 +74,7 @@ export const ChatOptionsProvider = ({ const onTogglePinned = useCallback(() => { if (group && group.channels[0]) { - group.pin ? store.unpinItem(group.pin) : store.pinItem(group.channels[0]); + group.pin ? store.unpinItem(group.pin) : store.pinGroup(group); } }, [group]); From a1b9bee1a355acecbf8badd811e438ca22bdad84 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 27 Nov 2024 15:13:49 +0000 Subject: [PATCH 27/52] update glob: [skip actions] --- desk/desk.docket-0 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desk/desk.docket-0 b/desk/desk.docket-0 index c574fb2399..410a7903e4 100644 --- a/desk/desk.docket-0 +++ b/desk/desk.docket-0 @@ -2,7 +2,7 @@ info+'Start, host, and cultivate communities. Own your communications, organize your resources, and share documents. Tlon is a decentralized platform that offers a full, communal suite of tools for messaging, writing and sharing media with others.' color+0xde.dede image+'https://bootstrap.urbit.org/tlon.svg?v=1' - glob-http+['https://bootstrap.urbit.org/glob-0v6.fu63g.0sq11.err1r.f2b33.8neao.glob' 0v6.fu63g.0sq11.err1r.f2b33.8neao] + glob-http+['https://bootstrap.urbit.org/glob-0v5.sk7ei.f8cfj.0osok.hapmm.rb9ng.glob' 0v5.sk7ei.f8cfj.0osok.hapmm.rb9ng] base+'groups' version+[6 4 2] website+'https://tlon.io' From d05326340c024fe7b2bfa557e5c037a45cd51e12 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 27 Nov 2024 15:14:06 +0000 Subject: [PATCH 28/52] update new frontend glob: [skip actions] --- tm-alpha-desk/desk.docket-0 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tm-alpha-desk/desk.docket-0 b/tm-alpha-desk/desk.docket-0 index 2222b2ddde..b6082fc01f 100644 --- a/tm-alpha-desk/desk.docket-0 +++ b/tm-alpha-desk/desk.docket-0 @@ -2,7 +2,7 @@ info+'Start, host, and cultivate communities. Own your communications, organize your resources, and share documents. Tlon is a decentralized platform that offers a full, communal suite of tools for messaging, writing and sharing media with others.' color+0xde.dede image+'https://bootstrap.urbit.org/tlon.svg?v=1' - glob-http+['https://bootstrap.urbit.org/glob-0v6.i9laj.n1cm9.1vmbd.j8m35.34am1.glob' 0v6.i9laj.n1cm9.1vmbd.j8m35.34am1] + glob-http+['https://bootstrap.urbit.org/glob-0v3.o6vqk.mkt3j.bgf7f.n04r1.97gam.glob' 0v3.o6vqk.mkt3j.bgf7f.n04r1.97gam] base+'tm-alpha' version+[1 0 0] website+'https://tlon.io' From 34f183a24032faf48bd2e90dd68b7b87e790d204 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 27 Nov 2024 15:15:29 +0000 Subject: [PATCH 29/52] update new frontend glob: [skip actions] --- tm-alpha-desk/desk.docket-0 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tm-alpha-desk/desk.docket-0 b/tm-alpha-desk/desk.docket-0 index b6082fc01f..9e22d51ace 100644 --- a/tm-alpha-desk/desk.docket-0 +++ b/tm-alpha-desk/desk.docket-0 @@ -2,7 +2,7 @@ info+'Start, host, and cultivate communities. Own your communications, organize your resources, and share documents. Tlon is a decentralized platform that offers a full, communal suite of tools for messaging, writing and sharing media with others.' color+0xde.dede image+'https://bootstrap.urbit.org/tlon.svg?v=1' - glob-http+['https://bootstrap.urbit.org/glob-0v3.o6vqk.mkt3j.bgf7f.n04r1.97gam.glob' 0v3.o6vqk.mkt3j.bgf7f.n04r1.97gam] + glob-http+['https://bootstrap.urbit.org/glob-0v6.sf1ku.j727t.hkgu4.3mt9a.rbvm5.glob' 0v6.sf1ku.j727t.hkgu4.3mt9a.rbvm5] base+'tm-alpha' version+[1 0 0] website+'https://tlon.io' From 8c1b7964ba67e3e623c191c89160c017086ccd14 Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Wed, 27 Nov 2024 11:39:31 -0600 Subject: [PATCH 30/52] hooks: only host can modify --- desk/app/channels-server.hoon | 1 + 1 file changed, 1 insertion(+) diff --git a/desk/app/channels-server.hoon b/desk/app/channels-server.hoon index edf10cdfec..ddbfe13fa5 100644 --- a/desk/app/channels-server.hoon +++ b/desk/app/channels-server.hoon @@ -346,6 +346,7 @@ :: %hook-action-0 =+ !<(=action:h vase) + ?> =(our src):bowl ?- -.action %add ho-abet:(ho-add:ho-core [name src]:action) From 60f8d57ef43922c326c59001de7d71e61c0a3e82 Mon Sep 17 00:00:00 2001 From: Patrick O'Sullivan Date: Wed, 27 Nov 2024 12:03:23 -0600 Subject: [PATCH 31/52] desktop: fix issue with stale data breaking the app on first load --- apps/tlon-web-new/src/logic/useAppUpdates.ts | 2 ++ apps/tlon-web-new/src/main.tsx | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/tlon-web-new/src/logic/useAppUpdates.ts b/apps/tlon-web-new/src/logic/useAppUpdates.ts index 5487edea9c..8324e81c3d 100644 --- a/apps/tlon-web-new/src/logic/useAppUpdates.ts +++ b/apps/tlon-web-new/src/logic/useAppUpdates.ts @@ -1,3 +1,4 @@ +import { queryClient } from '@tloncorp/shared'; import { createContext, useCallback, useEffect, useState } from 'react'; import { useRegisterSW } from 'virtual:pwa-register/react'; @@ -78,6 +79,7 @@ export default function useAppUpdates() { : `${window.location.href}?updatedAt=${Date.now()}`; if (needRefresh) { + queryClient.clear(); try { await updateServiceWorker(false); } catch (e) { diff --git a/apps/tlon-web-new/src/main.tsx b/apps/tlon-web-new/src/main.tsx index 7d94a02cb2..278636433d 100644 --- a/apps/tlon-web-new/src/main.tsx +++ b/apps/tlon-web-new/src/main.tsx @@ -57,8 +57,8 @@ setupDb().then(() => { From db6f11bc9d4f1e7174f6f0f603f39c68a9bad9f2 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 27 Nov 2024 18:28:18 +0000 Subject: [PATCH 32/52] update new frontend glob: [skip actions] --- tm-alpha-desk/desk.docket-0 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tm-alpha-desk/desk.docket-0 b/tm-alpha-desk/desk.docket-0 index 9e22d51ace..56c68777e1 100644 --- a/tm-alpha-desk/desk.docket-0 +++ b/tm-alpha-desk/desk.docket-0 @@ -2,7 +2,7 @@ info+'Start, host, and cultivate communities. Own your communications, organize your resources, and share documents. Tlon is a decentralized platform that offers a full, communal suite of tools for messaging, writing and sharing media with others.' color+0xde.dede image+'https://bootstrap.urbit.org/tlon.svg?v=1' - glob-http+['https://bootstrap.urbit.org/glob-0v6.sf1ku.j727t.hkgu4.3mt9a.rbvm5.glob' 0v6.sf1ku.j727t.hkgu4.3mt9a.rbvm5] + glob-http+['https://bootstrap.urbit.org/glob-0vjkg1r.fhh7a.4um0o.v4oa9.8bpuq.glob' 0vjkg1r.fhh7a.4um0o.v4oa9.8bpuq] base+'tm-alpha' version+[1 0 0] website+'https://tlon.io' From 1ac90e0349b402404bec94a3ec1531bc2386d897 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Wed, 27 Nov 2024 14:30:00 -0500 Subject: [PATCH 33/52] queries: omit you from suggestions --- packages/shared/src/db/queries.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/db/queries.ts b/packages/shared/src/db/queries.ts index b7732993a1..cce64ed748 100644 --- a/packages/shared/src/db/queries.ts +++ b/packages/shared/src/db/queries.ts @@ -2811,10 +2811,12 @@ export const getUserContacts = createReadQuery( export const getSuggestedContacts = createReadQuery( 'getSuggestedContacts', async (ctx: QueryCtx) => { + const currentUserId = getCurrentUserId(); return ctx.db.query.contacts.findMany({ where: and( eq($contacts.isContact, false), - eq($contacts.isContactSuggestion, true) + eq($contacts.isContactSuggestion, true), + not(eq($contacts.id, currentUserId)) ), with: { pinnedGroups: { From b1ca91a26e962f77a3fa04d6b3184c840cef1da3 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Wed, 27 Nov 2024 14:30:16 -0500 Subject: [PATCH 34/52] ContactsScreenView: better "you" logic --- packages/ui/src/components/ContactsScreenView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/ContactsScreenView.tsx b/packages/ui/src/components/ContactsScreenView.tsx index d5ebce9c04..a93ff9aa30 100644 --- a/packages/ui/src/components/ContactsScreenView.tsx +++ b/packages/ui/src/components/ContactsScreenView.tsx @@ -54,7 +54,7 @@ export function ContactsScreenView(props: Props) { if (trimmedSuggested.length > 0) { result.push({ - title: 'Suggested by Pals and DMs', + title: 'Suggested from %pals and DMs', data: trimmedSuggested, }); } @@ -72,7 +72,7 @@ export function ContactsScreenView(props: Props) { showNickname showEndContent endContent={ - item.isContactSuggestion ? ( + item.isContactSuggestion && !isSelf ? ( ) : isSelf ? ( From 47c55d01486e1f70c9d1b3c38495f38d99fd1a9e Mon Sep 17 00:00:00 2001 From: ~latter-bolden Date: Wed, 27 Nov 2024 13:31:59 -0600 Subject: [PATCH 35/52] add + icon to contacts screen for quicker adding --- .../features/contacts/AddContactsScreen.tsx | 34 +++++++++++ packages/app/features/top/ContactsScreen.tsx | 6 ++ packages/app/navigation/RootStack.tsx | 2 + packages/app/navigation/types.ts | 1 + packages/shared/src/api/contactsApi.ts | 8 +++ packages/shared/src/store/contactActions.ts | 24 ++++++++ .../ui/src/components/AddContactsView.tsx | 59 +++++++++++++++++++ packages/ui/src/components/ContactBook.tsx | 12 +++- packages/ui/src/components/ContactRow.tsx | 10 +++- packages/ui/src/index.tsx | 1 + 10 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 packages/app/features/contacts/AddContactsScreen.tsx create mode 100644 packages/ui/src/components/AddContactsView.tsx diff --git a/packages/app/features/contacts/AddContactsScreen.tsx b/packages/app/features/contacts/AddContactsScreen.tsx new file mode 100644 index 0000000000..7fea97b321 --- /dev/null +++ b/packages/app/features/contacts/AddContactsScreen.tsx @@ -0,0 +1,34 @@ +import type { NativeStackScreenProps } from '@react-navigation/native-stack'; +import * as store from '@tloncorp/shared/store'; +import { AddContactsView } from '@tloncorp/ui'; +import { useCallback } from 'react'; + +import type { RootStackParamList } from '../../navigation/types'; + +type Props = NativeStackScreenProps; + +export function AddContactsScreen(props: Props) { + // const handleGoToChannel = useCallback( + // (channel: db.Channel) => { + // props.navigation.reset({ + // index: 1, + // routes: [ + // { name: 'ChatList' }, + // { name: 'Channel', params: { channelId: channel.id } }, + // ], + // }); + // }, + // [props.navigation] + // ); + + const handleAddContacts = useCallback((addIds: string[]) => { + store.addContacts(addIds); + }, []); + + return ( + props.navigation.goBack()} + addContacts={handleAddContacts} + /> + ); +} diff --git a/packages/app/features/top/ContactsScreen.tsx b/packages/app/features/top/ContactsScreen.tsx index 9368e8b51c..67a8ebd6a4 100644 --- a/packages/app/features/top/ContactsScreen.tsx +++ b/packages/app/features/top/ContactsScreen.tsx @@ -66,6 +66,12 @@ export default function ContactsScreen(props: Props) { navigate('AddContacts')} + /> + } rightControls={ {/* individual screens */} + { }); }; +// TODO: once we can add in bulk from the backend, do so +export const addUserContacts = async (contactIds: string[]) => { + const promises = contactIds.map((contactId) => { + return addContact(contactId); + }); + return Promise.all(promises); +}; + export const removeContact = async (contactId: string) => { return poke({ app: 'contacts', diff --git a/packages/shared/src/store/contactActions.ts b/packages/shared/src/store/contactActions.ts index 8216b20382..edbd8658eb 100644 --- a/packages/shared/src/store/contactActions.ts +++ b/packages/shared/src/store/contactActions.ts @@ -21,6 +21,30 @@ export async function addContact(contactId: string) { } } +export async function addContacts(contacts: string[]) { + const optimisticUpdates = contacts.map((contactId) => + db.updateContact({ + id: contactId, + isContact: true, + isContactSuggestion: false, + }) + ); + await Promise.all(optimisticUpdates); + + try { + await api.addUserContacts(contacts); + } catch (e) { + // Rollback the update + const rolbacks = contacts.map((contactId) => + db.updateContact({ + id: contactId, + isContact: false, + }) + ); + await Promise.all(rolbacks); + } +} + export async function removeContact(contactId: string) { // Optimistic update await db.updateContact({ id: contactId, isContact: false }); diff --git a/packages/ui/src/components/AddContactsView.tsx b/packages/ui/src/components/AddContactsView.tsx new file mode 100644 index 0000000000..2109933c60 --- /dev/null +++ b/packages/ui/src/components/AddContactsView.tsx @@ -0,0 +1,59 @@ +import * as store from '@tloncorp/shared/store'; +import { useCallback, useMemo, useState } from 'react'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { View, YStack } from 'tamagui'; + +import { Button } from './Button'; +import { ContactBook } from './ContactBook'; +import { ScreenHeader } from './ScreenHeader'; + +export function AddContactsView(props: { + goBack: () => void; + addContacts: (ids: string[]) => void; +}) { + const { bottom } = useSafeAreaInsets(); + const [newContacts, setNewContacts] = useState([]); + const handleAddContacts = useCallback(() => { + props.addContacts(newContacts); + props.goBack(); + }, [newContacts, props]); + + const { data: existingContacts } = store.useUserContacts(); + const existingIds = useMemo(() => { + return existingContacts?.map((c) => c.id) ?? []; + }, [existingContacts]); + + console.log(`existingIds`, existingIds); + + return ( + + + + + + + + + ); +} diff --git a/packages/ui/src/components/ContactBook.tsx b/packages/ui/src/components/ContactBook.tsx index bc1b89973a..fb97444b0d 100644 --- a/packages/ui/src/components/ContactBook.tsx +++ b/packages/ui/src/components/ContactBook.tsx @@ -27,6 +27,7 @@ export function ContactBook({ searchPlaceholder = '', onSelect, multiSelect = false, + immutableIds = [], onSelectedChange, onScrollChange, explanationComponent, @@ -34,6 +35,7 @@ export function ContactBook({ height, width, }: { + immutableIds?: string[]; searchPlaceholder?: string; searchable?: boolean; onSelect?: (contactId: string) => void; @@ -46,6 +48,7 @@ export function ContactBook({ width?: number; }) { const contacts = useContacts(); + const immutableSet = useMemo(() => new Set(immutableIds), [immutableIds]); const contactsIndex = useContactIndex(); const segmentedContacts = useAlphabeticallySegmentedContacts( contacts ?? [], @@ -71,6 +74,10 @@ export function ContactBook({ const [selected, setSelected] = useState([]); const handleSelect = useCallback( (contactId: string) => { + if (immutableSet.has(contactId)) { + return; + } + if (multiSelect) { if (selected.includes(contactId)) { const newSelected = selected.filter((id) => id !== contactId); @@ -85,7 +92,7 @@ export function ContactBook({ onSelect?.(contactId); } }, - [multiSelect, onSelect, onSelectedChange, selected] + [immutableSet, multiSelect, onSelect, onSelectedChange, selected] ); const renderItem = useCallback( @@ -96,6 +103,7 @@ export function ContactBook({ backgroundColor={'$secondaryBackground'} key={item.id} contact={item} + immutable={immutableSet.has(item.id)} selectable={multiSelect} selected={isSelected} onPress={handleSelect} @@ -103,7 +111,7 @@ export function ContactBook({ /> ); }, - [selected, multiSelect, handleSelect] + [selected, immutableSet, multiSelect, handleSelect] ); const scrollPosition = useRef(0); diff --git a/packages/ui/src/components/ContactRow.tsx b/packages/ui/src/components/ContactRow.tsx index 261b0a5378..75e7d064ec 100644 --- a/packages/ui/src/components/ContactRow.tsx +++ b/packages/ui/src/components/ContactRow.tsx @@ -12,6 +12,7 @@ function ContactRowItemRaw({ contact, selected = false, selectable = false, + immutable = false, onPress, pressStyle, backgroundColor, @@ -21,6 +22,7 @@ function ContactRowItemRaw({ onPress: (id: string) => void; selectable?: boolean; selected?: boolean; + immutable?: boolean; } & Omit) { const displayName = useMemo(() => getDisplayName(contact), [contact]); @@ -56,8 +58,12 @@ function ContactRowItemRaw({ height="$4xl" width="$4xl" > - {selected ? ( - + {selected || immutable ? ( + ) : ( Date: Wed, 27 Nov 2024 13:33:29 -0600 Subject: [PATCH 36/52] remove commented out method --- .../app/features/contacts/AddContactsScreen.tsx | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/app/features/contacts/AddContactsScreen.tsx b/packages/app/features/contacts/AddContactsScreen.tsx index 7fea97b321..c38d0de685 100644 --- a/packages/app/features/contacts/AddContactsScreen.tsx +++ b/packages/app/features/contacts/AddContactsScreen.tsx @@ -8,19 +8,6 @@ import type { RootStackParamList } from '../../navigation/types'; type Props = NativeStackScreenProps; export function AddContactsScreen(props: Props) { - // const handleGoToChannel = useCallback( - // (channel: db.Channel) => { - // props.navigation.reset({ - // index: 1, - // routes: [ - // { name: 'ChatList' }, - // { name: 'Channel', params: { channelId: channel.id } }, - // ], - // }); - // }, - // [props.navigation] - // ); - const handleAddContacts = useCallback((addIds: string[]) => { store.addContacts(addIds); }, []); From a65bd388bfeec4159a257b99e22a4c9ce12017de Mon Sep 17 00:00:00 2001 From: ~latter-bolden Date: Wed, 27 Nov 2024 13:40:14 -0600 Subject: [PATCH 37/52] prevent ever marking yourself a suggestion when deserializing --- packages/shared/src/api/contactsApi.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/api/contactsApi.ts b/packages/shared/src/api/contactsApi.ts index 5e837d46ba..283ea74340 100644 --- a/packages/shared/src/api/contactsApi.ts +++ b/packages/shared/src/api/contactsApi.ts @@ -247,6 +247,7 @@ export const v0PeerToClientProfile = ( isContactSuggestion?: boolean; } ): db.Contact => { + const currentUserId = getCurrentUserId(); return { id, peerNickname: contact?.nickname ?? null, @@ -262,7 +263,7 @@ export const v0PeerToClientProfile = ( })) ?? [], isContact: false, - isContactSuggestion: config?.isContactSuggestion, + isContactSuggestion: config?.isContactSuggestion && id !== currentUserId, }; }; @@ -287,6 +288,7 @@ export const v1PeerToClientProfile = ( isContactSuggestion?: boolean; } ): db.Contact => { + const currentUserId = getCurrentUserId(); return { id, peerNickname: contact.nickname?.value ?? null, @@ -301,7 +303,8 @@ export const v1PeerToClientProfile = ( contactId: id, })) ?? [], isContact: config?.isContact, - isContactSuggestion: config?.isContactSuggestion && !config?.isContact, + isContactSuggestion: + config?.isContactSuggestion && !config?.isContact && id !== currentUserId, }; }; From b85eeca589d14c8d21379d4c8a2c6e20d36d974c Mon Sep 17 00:00:00 2001 From: ~latter-bolden Date: Wed, 27 Nov 2024 13:44:59 -0600 Subject: [PATCH 38/52] add to profile screen navigator for new web --- packages/app/navigation/desktop/ProfileScreenNavigator.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/app/navigation/desktop/ProfileScreenNavigator.tsx b/packages/app/navigation/desktop/ProfileScreenNavigator.tsx index 0c813d1d55..9e46d44e71 100644 --- a/packages/app/navigation/desktop/ProfileScreenNavigator.tsx +++ b/packages/app/navigation/desktop/ProfileScreenNavigator.tsx @@ -1,5 +1,6 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { AddContactsScreen } from '../../features/contacts/AddContactsScreen'; import { AppInfoScreen } from '../../features/settings/AppInfoScreen'; import { BlockedUsersScreen } from '../../features/settings/BlockedUsersScreen'; import { EditProfileScreen } from '../../features/settings/EditProfileScreen'; @@ -24,9 +25,10 @@ export const ProfileScreenNavigator = () => { > + Date: Wed, 27 Nov 2024 13:48:42 -0600 Subject: [PATCH 39/52] ops: bump 4.2.4 --- apps/tlon-mobile/android/app/build.gradle | 2 +- apps/tlon-mobile/ios/Landscape.xcodeproj/project.pbxproj | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/tlon-mobile/android/app/build.gradle b/apps/tlon-mobile/android/app/build.gradle index 0268fbb891..af13d348a5 100644 --- a/apps/tlon-mobile/android/app/build.gradle +++ b/apps/tlon-mobile/android/app/build.gradle @@ -88,7 +88,7 @@ android { targetSdkVersion rootProject.ext.targetSdkVersion compileSdk rootProject.ext.compileSdkVersion versionCode 108 - versionName "4.2.3" + versionName "4.2.4" buildConfigField("boolean", "REACT_NATIVE_UNSTABLE_USE_RUNTIME_SCHEDULER_ALWAYS", (findProperty("reactNative.unstable_useRuntimeSchedulerAlways") ?: true).toString()) } diff --git a/apps/tlon-mobile/ios/Landscape.xcodeproj/project.pbxproj b/apps/tlon-mobile/ios/Landscape.xcodeproj/project.pbxproj index ec04793a11..5d30e4e75d 100644 --- a/apps/tlon-mobile/ios/Landscape.xcodeproj/project.pbxproj +++ b/apps/tlon-mobile/ios/Landscape.xcodeproj/project.pbxproj @@ -1427,7 +1427,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.2.3; + MARKETING_VERSION = 4.2.4; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1465,7 +1465,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.2.3; + MARKETING_VERSION = 4.2.4; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1689,7 +1689,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.2.3; + MARKETING_VERSION = 4.2.4; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1732,7 +1732,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 4.2.3; + MARKETING_VERSION = 4.2.4; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", From 39daf803e8013bf217523ed0d71bcf68d388e73f Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 27 Nov 2024 19:55:56 +0000 Subject: [PATCH 40/52] update glob: [skip actions] --- desk/desk.docket-0 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desk/desk.docket-0 b/desk/desk.docket-0 index 410a7903e4..dce125ff4d 100644 --- a/desk/desk.docket-0 +++ b/desk/desk.docket-0 @@ -2,7 +2,7 @@ info+'Start, host, and cultivate communities. Own your communications, organize your resources, and share documents. Tlon is a decentralized platform that offers a full, communal suite of tools for messaging, writing and sharing media with others.' color+0xde.dede image+'https://bootstrap.urbit.org/tlon.svg?v=1' - glob-http+['https://bootstrap.urbit.org/glob-0v5.sk7ei.f8cfj.0osok.hapmm.rb9ng.glob' 0v5.sk7ei.f8cfj.0osok.hapmm.rb9ng] + glob-http+['https://bootstrap.urbit.org/glob-0v1.q6jaf.bme3f.povqt.eo08j.d6qtq.glob' 0v1.q6jaf.bme3f.povqt.eo08j.d6qtq] base+'groups' version+[6 4 2] website+'https://tlon.io' From 843d131dc3937388df274d8e5f10988c6b0f1ae0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 27 Nov 2024 19:56:21 +0000 Subject: [PATCH 41/52] update new frontend glob: [skip actions] --- tm-alpha-desk/desk.docket-0 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tm-alpha-desk/desk.docket-0 b/tm-alpha-desk/desk.docket-0 index 56c68777e1..2a8788a36d 100644 --- a/tm-alpha-desk/desk.docket-0 +++ b/tm-alpha-desk/desk.docket-0 @@ -2,7 +2,7 @@ info+'Start, host, and cultivate communities. Own your communications, organize your resources, and share documents. Tlon is a decentralized platform that offers a full, communal suite of tools for messaging, writing and sharing media with others.' color+0xde.dede image+'https://bootstrap.urbit.org/tlon.svg?v=1' - glob-http+['https://bootstrap.urbit.org/glob-0vjkg1r.fhh7a.4um0o.v4oa9.8bpuq.glob' 0vjkg1r.fhh7a.4um0o.v4oa9.8bpuq] + glob-http+['https://bootstrap.urbit.org/glob-0v4.hjquo.to0hs.v3ebc.0hmmb.j64mf.glob' 0v4.hjquo.to0hs.v3ebc.0hmmb.j64mf] base+'tm-alpha' version+[1 0 0] website+'https://tlon.io' From f5cd0c4f09ae1fb81f96183b0f0a79534372d4a8 Mon Sep 17 00:00:00 2001 From: ~latter-bolden Date: Wed, 27 Nov 2024 14:14:49 -0600 Subject: [PATCH 42/52] fix color deserializer --- packages/shared/src/logic/utils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/logic/utils.ts b/packages/shared/src/logic/utils.ts index be198ef6fe..2d44a104f2 100644 --- a/packages/shared/src/logic/utils.ts +++ b/packages/shared/src/logic/utils.ts @@ -207,8 +207,9 @@ export function normalizeUrbitColor(color: string): string { return color; } - const colorString = color.slice(2).replace('.', '').toUpperCase(); - const lengthAdjustedColor = colorString.padStart(6, '0'); + const noDots = color.replace('.', ''); + const prefixStripped = color.startsWith('0x') ? noDots.slice(2) : noDots; + const lengthAdjustedColor = prefixStripped.toUpperCase().padStart(6, '0'); return `#${lengthAdjustedColor}`; } From 773bbecf08bb28e40dff0f3bc6acfd54f15d0382 Mon Sep 17 00:00:00 2001 From: ~latter-bolden Date: Wed, 27 Nov 2024 14:30:31 -0600 Subject: [PATCH 43/52] fix display name for empty string nicknames --- packages/ui/src/utils/user.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/utils/user.ts b/packages/ui/src/utils/user.ts index 3c7481fd19..3783b8cc35 100644 --- a/packages/ui/src/utils/user.ts +++ b/packages/ui/src/utils/user.ts @@ -23,5 +23,10 @@ export function formatUserId( } export function getDisplayName(contact: db.Contact) { - return contact.nickname ?? contact.id; + if (contact.nickname && contact.nickname.length) { + return contact.nickname; + } + + const formatted = formatUserId(contact.id); + return formatted?.display ?? contact.id; } From 48549cd3e38395bb89dee1a0ed6d313e0c4079bc Mon Sep 17 00:00:00 2001 From: github-actions Date: Wed, 27 Nov 2024 20:55:22 +0000 Subject: [PATCH 44/52] update new frontend glob: [skip actions] --- tm-alpha-desk/desk.docket-0 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tm-alpha-desk/desk.docket-0 b/tm-alpha-desk/desk.docket-0 index 2a8788a36d..77e9d58704 100644 --- a/tm-alpha-desk/desk.docket-0 +++ b/tm-alpha-desk/desk.docket-0 @@ -2,7 +2,7 @@ info+'Start, host, and cultivate communities. Own your communications, organize your resources, and share documents. Tlon is a decentralized platform that offers a full, communal suite of tools for messaging, writing and sharing media with others.' color+0xde.dede image+'https://bootstrap.urbit.org/tlon.svg?v=1' - glob-http+['https://bootstrap.urbit.org/glob-0v4.hjquo.to0hs.v3ebc.0hmmb.j64mf.glob' 0v4.hjquo.to0hs.v3ebc.0hmmb.j64mf] + glob-http+['https://bootstrap.urbit.org/glob-0v6.ar89l.ohn8c.72n4o.krb3l.hb512.glob' 0v6.ar89l.ohn8c.72n4o.krb3l.hb512] base+'tm-alpha' version+[1 0 0] website+'https://tlon.io' From 0e4d0fb6e400883174209537b13b1d7abbce1c7c Mon Sep 17 00:00:00 2001 From: Hunter Miller Date: Sat, 30 Nov 2024 09:26:11 -0600 Subject: [PATCH 45/52] hooks: allow no-op'ing if none --- desk/app/channels-server.hoon | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/desk/app/channels-server.hoon b/desk/app/channels-server.hoon index edf10cdfec..76f17703a5 100644 --- a/desk/app/channels-server.hoon +++ b/desk/app/channels-server.hoon @@ -686,7 +686,6 @@ |= =c-channel:c ^+ ca-core ?> am-host:ca-perms - ~& "received command {}" ?- -.c-channel %view ?> (is-admin:ca-perms src.bowl) @@ -725,7 +724,6 @@ %post =^ update=(unit u-channel:c) ca-core (ca-c-post c-post.c-channel) - ~& "received post update {}" ?~ update ca-core (ca-update u.update) == @@ -734,11 +732,9 @@ |= =c-post:c ^- [(unit u-channel:c) _ca-core] ?> (can-write:ca-perms src.bowl writers.perm.perm.channel) - ~& "running post command" =* no-op `ca-core ?- -.c-post %add - ~& "adding post" ?> |(=(src.bowl our.bowl) =(src.bowl author.essay.c-post)) ?> =(kind.nest -.kind-data.essay.c-post) =/ id=id-post:c @@ -749,7 +745,6 @@ =/ new=v-post:c [[id ~ ~] 0 essay.c-post] =^ result=(each event:h tang) cor =/ =event:h [%on-post %add new] - ~& "running post hooks" (run-hooks event nest 'post blocked') ?: ?=(%.n -.result) ((slog p.result) [~ ca-core]) @@ -1160,7 +1155,7 @@ [result (run-hook-effects effects nest)] =/ current-event event =| effects=(list effect:h) - =/ order (~(got by order.hooks) nest) + =/ order (~(gut by order.hooks) nest ~) =/ channel `[nest (~(got by v-channels) nest)] =/ =context:h (get-hook-context channel *config:h) |- From 6f8139724cad518cf870228ac1c12ea63f6f55b4 Mon Sep 17 00:00:00 2001 From: Patrick O'Sullivan Date: Mon, 2 Dec 2024 14:00:31 -0600 Subject: [PATCH 46/52] desktop: fix big input, and other desktop web issues --- packages/ui/src/components/AuthorRow.tsx | 2 +- packages/ui/src/components/BigInput.tsx | 41 ++---- packages/ui/src/components/Channel/index.tsx | 12 +- .../ChatMessageActions/MessageContainer.tsx | 2 +- packages/ui/src/components/Image.tsx | 5 +- .../MessageInput/MessageInputBase.tsx | 1 + .../ui/src/components/MessageInput/index.tsx | 92 ++++++++----- .../components/NotebookPost/NotebookPost.tsx | 126 ++++++++++-------- packages/ui/src/components/PostScreenView.tsx | 1 - 9 files changed, 159 insertions(+), 123 deletions(-) diff --git a/packages/ui/src/components/AuthorRow.tsx b/packages/ui/src/components/AuthorRow.tsx index 2f6836d14e..04944762d3 100644 --- a/packages/ui/src/components/AuthorRow.tsx +++ b/packages/ui/src/components/AuthorRow.tsx @@ -160,7 +160,7 @@ export function ChatAuthorRow({ ) : null} - {deliveryStatus && deliveryStatus !== 'failed' ? ( + {!!deliveryStatus && deliveryStatus !== 'failed' ? ( ) : null} diff --git a/packages/ui/src/components/BigInput.tsx b/packages/ui/src/components/BigInput.tsx index 3a3b58a498..7f736e9eb0 100644 --- a/packages/ui/src/components/BigInput.tsx +++ b/packages/ui/src/components/BigInput.tsx @@ -1,11 +1,9 @@ // import { EditorBridge } from '@10play/tentap-editor'; import * as db from '@tloncorp/shared/db'; -import { useMemo, useRef, useState } from 'react'; -import { Dimensions, KeyboardAvoidingView, Platform } from 'react-native'; +import { useMemo, useState } from 'react'; import { TouchableOpacity } from 'react-native-gesture-handler'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; // TODO: replace input with our own input component -import { Input, ScrollView, View, YStack, getTokenValue } from 'tamagui'; +import { Input, View, YStack, getTokenValue } from 'tamagui'; import { ImageAttachment, useAttachmentContext } from '../contexts/attachment'; import AttachmentSheet from './AttachmentSheet'; @@ -37,16 +35,8 @@ export function BigInput({ } & MessageInputProps) { const [title, setTitle] = useState(editingPost?.title ?? ''); const [showAttachmentSheet, setShowAttachmentSheet] = useState(false); - // const editorRef = useRef<{ - // editor: TlonEditorBridge | null; - // setEditor: (editor: any) => void; - // }>(null); - const { top } = useSafeAreaInsets(); - const { width } = Dimensions.get('screen'); const titleInputHeight = getTokenValue('$4xl', 'size'); const imageButtonHeight = getTokenValue('$4xl', 'size'); - const keyboardVerticalOffset = - Platform.OS === 'ios' ? top + titleInputHeight : top; const { attachments, attachAssets } = useAttachmentContext(); const imageAttachment = useMemo(() => { @@ -90,11 +80,13 @@ export function BigInput({ > {imageAttachment ? ( )} - - + {/* channelType === 'notebook' && editorRef.current && editorRef.current.editor && ( diff --git a/packages/ui/src/components/Channel/index.tsx b/packages/ui/src/components/Channel/index.tsx index 6eb23dd62d..ebc496fb47 100644 --- a/packages/ui/src/components/Channel/index.tsx +++ b/packages/ui/src/components/Channel/index.tsx @@ -316,10 +316,7 @@ export function Channel({ initialAttachments={initialAttachments} uploadAsset={uploadAsset} > - + - + ); } diff --git a/packages/ui/src/components/Image.tsx b/packages/ui/src/components/Image.tsx index 19f579229a..9b707de86c 100644 --- a/packages/ui/src/components/Image.tsx +++ b/packages/ui/src/components/Image.tsx @@ -16,6 +16,8 @@ const WebImage = ({ source, style, alt, onLoad, ...props }: any) => { } }; + const { contentFit } = props; + return ( { style={{ ...StyleSheet.flatten(style), maxWidth: '100%', - height: 'auto', + height: props.height ? props.height : 'auto', + objectFit: contentFit ? contentFit : 'contain', }} onLoad={handleLoad} {...props} diff --git a/packages/ui/src/components/MessageInput/MessageInputBase.tsx b/packages/ui/src/components/MessageInput/MessageInputBase.tsx index b636830e70..ba41c8ac81 100644 --- a/packages/ui/src/components/MessageInput/MessageInputBase.tsx +++ b/packages/ui/src/components/MessageInput/MessageInputBase.tsx @@ -132,6 +132,7 @@ export const MessageInputContainer = memo( gap="$l" alignItems="flex-end" justifyContent="space-between" + backgroundColor="$background" > {goBack ? ( diff --git a/packages/ui/src/components/MessageInput/index.tsx b/packages/ui/src/components/MessageInput/index.tsx index 523c41d1cf..e2cda27cfe 100644 --- a/packages/ui/src/components/MessageInput/index.tsx +++ b/packages/ui/src/components/MessageInput/index.tsx @@ -7,7 +7,6 @@ import HardBreak from '@tiptap/extension-hard-break'; import History from '@tiptap/extension-history'; import Italic from '@tiptap/extension-italic'; import Link from '@tiptap/extension-link'; -import Mention from '@tiptap/extension-mention'; import Paragraph from '@tiptap/extension-paragraph'; import Placeholder from '@tiptap/extension-placeholder'; import Strike from '@tiptap/extension-strike'; @@ -26,9 +25,7 @@ import { import * as db from '@tloncorp/shared/db'; import { Block, - Image, Inline, - JSONContent, Story, citeToPath, constructStory, @@ -37,14 +34,14 @@ import { } from '@tloncorp/shared/urbit'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { Keyboard } from 'react-native'; -import { View, YStack } from 'tamagui'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { View, YStack, getTokenValue, useWindowDimensions } from 'tamagui'; import { Attachment, UploadedImageAttachment, useAttachmentContext, } from '../../contexts/attachment'; -import { Input } from '../Input'; import { AttachmentPreviewList } from './AttachmentPreviewList'; import { MessageInputContainer, MessageInputProps } from './MessageInputBase'; @@ -88,15 +85,28 @@ export function MessageInput({ waitForAttachmentUploads, } = useAttachmentContext(); - const [containerHeight, setContainerHeight] = useState(initialHeight); const [isSending, setIsSending] = useState(false); const [sendError, setSendError] = useState(false); const [hasSetInitialContent, setHasSetInitialContent] = useState(false); - const [imageOnEditedPost, setImageOnEditedPost] = useState(); const [editorIsEmpty, setEditorIsEmpty] = useState(attachments.length === 0); const [showMentionPopup, setShowMentionPopup] = useState(false); const [mentionText, setMentionText] = useState(); + const { bottom, top } = useSafeAreaInsets(); + const { height } = useWindowDimensions(); + const headerHeight = 48; + const titleInputHeight = 48; + const inputBasePadding = getTokenValue('$s', 'space'); + const imageInputButtonHeight = 50; + const basicOffset = useMemo( + () => top + headerHeight + titleInputHeight + imageInputButtonHeight, + [top, headerHeight, titleInputHeight, imageInputButtonHeight] + ); + const bigInputHeightBasic = useMemo( + () => height - basicOffset - bottom - inputBasePadding * 2, + [height, basicOffset, bottom, inputBasePadding] + ); + const extensions = [ Blockquote, Bold, @@ -159,12 +169,7 @@ export function MessageInput({ blocks, } = extractContentTypesFromPost(editingPost); - if ( - !story || - story?.length === 0 || - !postReferences || - blocks.length === 0 - ) { + if (story === null && !postReferences && blocks.length === 0) { return; } @@ -195,12 +200,27 @@ export function MessageInput({ resetAttachments(attachments); + if (story === null) { + return; + } + const tiptapContent = tiptap.diaryMixedToJSON( story.filter( (c) => !('type' in c) && !('block' in c && 'image' in c.block) ) as Story ); editor.commands.setContent(tiptapContent); + + if (editingPost.image) { + addAttachment({ + type: 'image', + file: { + uri: editingPost.image, + height: 0, + width: 0, + }, + }); + } } }); } catch (e) { @@ -209,7 +229,14 @@ export function MessageInput({ setHasSetInitialContent(true); } } - }, [editor, getDraft, hasSetInitialContent, editingPost, resetAttachments]); + }, [ + editor, + getDraft, + hasSetInitialContent, + editingPost, + resetAttachments, + addAttachment, + ]); useEffect(() => { if (editor && shouldBlur && editor.isFocused) { @@ -451,17 +478,6 @@ export function MessageInput({ return []; }); - if (imageOnEditedPost) { - blocks.push({ - image: { - src: imageOnEditedPost.image.src, - height: imageOnEditedPost.image.height, - width: imageOnEditedPost.image.width, - alt: imageOnEditedPost.image.alt, - }, - }); - } - if (blocks && blocks.length > 0) { if (channelType === 'chat') { story.unshift(...blocks.map((block) => ({ block }))); @@ -470,11 +486,27 @@ export function MessageInput({ } } + const metadata: db.PostMetadata = {}; + if (title && title.length > 0) { + metadata['title'] = title; + } + + if (image) { + const attachment = finalAttachments.find( + (a): a is UploadedImageAttachment => + a.type === 'image' && a.file.uri === image.uri + ); + if (!attachment) { + throw new Error('unable to attach image'); + } + metadata['image'] = attachment.uploadState.remoteUri; + } + if (isEdit && editingPost) { if (editingPost.parentId) { - await editPost?.(editingPost, story, editingPost.parentId); + await editPost?.(editingPost, story, editingPost.parentId, metadata); } - await editPost?.(editingPost, story); + await editPost?.(editingPost, story, undefined, metadata); setEditingPost?.(undefined); } else { const metadata: db.PostMetadata = {}; @@ -503,14 +535,12 @@ export function MessageInput({ clearAttachments(); clearDraft(); setShowBigInput?.(false); - setImageOnEditedPost(null); }, [ json, onSend, editor, waitForAttachmentUploads, - imageOnEditedPost, editingPost, clearAttachments, clearDraft, @@ -560,7 +590,7 @@ export function MessageInput({ setShouldBlur={setShouldBlur} onPressSend={handleSend} onPressEdit={handleEdit} - containerHeight={containerHeight} + containerHeight={initialHeight} mentionText={mentionText} groupMembers={groupMembers} onSelectMention={onSelectMention} @@ -585,7 +615,7 @@ export function MessageInput({ borderRadius="$xl" > {showInlineAttachments && } - + void; @@ -48,6 +53,7 @@ export function NotebookPost({ viewMode?: 'activity'; isHighlighted?: boolean; size?: '$l' | '$s' | '$xs'; + hideOverflowMenu?: boolean; }) { const [showRetrySheet, setShowRetrySheet] = useState(false); const handleLongPress = useCallback(() => { @@ -89,69 +95,77 @@ export function NotebookPost({ const hasReplies = post.replyCount && post.replyTime && post.replyContactIds; return ( <> - - {post.hidden ? ( - - - You have hidden this post. - - - ) : ( - <> - - - {viewMode !== 'activity' && ( - - {post.textContent} + + {post.hidden ? ( + + + You have hidden this post. - )} - - {showReplies && hasReplies ? ( - + ) : ( + <> + - ) : null} - - )} - {post.deliveryStatus === 'failed' ? ( - - - Message failed to send - - - ) : null} - - + {viewMode !== 'activity' && ( + + {post.textContent} + + )} + + {showReplies && hasReplies ? ( + + ) : null} + + )} + + {post.deliveryStatus === 'failed' ? ( + + + Message failed to send + + + ) : null} + + + + {isWeb && !hideOverflowMenu && ( + + + + )} ); } @@ -174,7 +188,7 @@ function NotebookPostHeader({ return ( - {post.image && size !== '$xs' && ( + {!!post.image && size !== '$xs' && ( { if (isEditingParent) { - console.log('setEditingPost', undefined); setEditingPost?.(undefined); if (channel.type !== 'notebook') { goBack?.(); From ac1f90aeec606749b80772677b1e45bf7702c8a5 Mon Sep 17 00:00:00 2001 From: James Acklin Date: Mon, 2 Dec 2024 15:07:55 -0500 Subject: [PATCH 47/52] ProfileStatusSheet: add send button --- .../ui/src/components/ProfileStatusSheet.tsx | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/packages/ui/src/components/ProfileStatusSheet.tsx b/packages/ui/src/components/ProfileStatusSheet.tsx index c7983b6c80..3c3ea79c03 100644 --- a/packages/ui/src/components/ProfileStatusSheet.tsx +++ b/packages/ui/src/components/ProfileStatusSheet.tsx @@ -1,11 +1,13 @@ import { useCallback, useRef } from 'react'; import { useForm } from 'react-hook-form'; import { Keyboard } from 'react-native'; -import { YStack } from 'tamagui'; +import { XStack, YStack } from 'tamagui'; import { useContact, useCurrentUserId } from '../contexts'; import { ActionSheet } from './ActionSheet'; +import { Button } from './Button'; import { ControlledTextField } from './Form'; +import { Icon } from './Icon'; export default function ProfileStatusSheet({ open, @@ -57,27 +59,38 @@ export default function ProfileStatusSheet({ snapPoints={[60]} > - - + + + + + From 9fd7c11d7478a597ad264bbe1ff7d39ab62ad965 Mon Sep 17 00:00:00 2001 From: Patrick O'Sullivan Date: Mon, 2 Dec 2024 15:34:52 -0600 Subject: [PATCH 48/52] desktop: add overflow menu button to gallery posts, default web image height to 100% rather than auto --- .../components/GalleryPost/GalleryPost.tsx | 103 ++++++++++-------- packages/ui/src/components/Image.tsx | 4 +- .../components/NotebookPost/NotebookPost.tsx | 2 +- 3 files changed, 63 insertions(+), 46 deletions(-) diff --git a/packages/ui/src/components/GalleryPost/GalleryPost.tsx b/packages/ui/src/components/GalleryPost/GalleryPost.tsx index e6e952c639..d96fa59c2c 100644 --- a/packages/ui/src/components/GalleryPost/GalleryPost.tsx +++ b/packages/ui/src/components/GalleryPost/GalleryPost.tsx @@ -3,10 +3,11 @@ import * as db from '@tloncorp/shared/db'; import { truncate } from 'lodash'; import { ComponentProps, useCallback, useMemo, useState } from 'react'; import { PropsWithChildren } from 'react'; -import { View, XStack, styled } from 'tamagui'; +import { View, XStack, isWeb, styled } from 'tamagui'; import { DetailViewAuthorRow } from '../AuthorRow'; import { ContactAvatar } from '../Avatar'; +import { Button } from '../Button'; import { Icon } from '../Icon'; import { useBoundHandler } from '../ListItem/listItemUtils'; import { createContentRenderer } from '../PostContent/ContentRenderer'; @@ -35,6 +36,7 @@ export function GalleryPost({ onPressRetry, onPressDelete, showAuthor = true, + hideOverflowMenu, ...props }: { post: db.Post; @@ -44,6 +46,7 @@ export function GalleryPost({ onPressDelete?: (post: db.Post) => void; showAuthor?: boolean; isHighlighted?: boolean; + hideOverflowMenu?: boolean; } & Omit, 'onPress' | 'onLongPress'>) { const [showRetrySheet, setShowRetrySheet] = useState(false); @@ -77,48 +80,62 @@ export function GalleryPost({ } return ( - - - - {showAuthor && !post.hidden && !post.isDeleted && ( - + + + + {showAuthor && !post.hidden && !post.isDeleted && ( + + + + {deliveryFailed && ( + + Tap to retry + + )} + + + )} + + + + {isWeb && !hideOverflowMenu && ( + + + + )} + ); } @@ -338,7 +355,7 @@ const SmallContentRenderer = createContentRenderer({ }, image: { height: '100%', - imageProps: { aspectRatio: 'unset', height: '100%' }, + imageProps: { aspectRatio: 'unset', height: '100%', contentFit: 'cover' }, ...noWrapperPadding, }, video: { diff --git a/packages/ui/src/components/Image.tsx b/packages/ui/src/components/Image.tsx index 9b707de86c..bd72d302e3 100644 --- a/packages/ui/src/components/Image.tsx +++ b/packages/ui/src/components/Image.tsx @@ -25,8 +25,8 @@ const WebImage = ({ source, style, alt, onLoad, ...props }: any) => { style={{ ...StyleSheet.flatten(style), maxWidth: '100%', - height: props.height ? props.height : 'auto', - objectFit: contentFit ? contentFit : 'contain', + height: props.height ? props.height : '100%', + objectFit: contentFit ? contentFit : undefined, }} onLoad={handleLoad} {...props} diff --git a/packages/ui/src/components/NotebookPost/NotebookPost.tsx b/packages/ui/src/components/NotebookPost/NotebookPost.tsx index eeafdb998b..3235adc90a 100644 --- a/packages/ui/src/components/NotebookPost/NotebookPost.tsx +++ b/packages/ui/src/components/NotebookPost/NotebookPost.tsx @@ -8,8 +8,8 @@ import { YStack, createStyledContext, styled, + isWeb, } from 'tamagui'; -import { isWeb } from 'tamagui'; import { DetailViewAuthorRow } from '../AuthorRow'; import { Button } from '../Button'; From de72f3217b2e75ceb6bc904e00d608f8b3d9b606 Mon Sep 17 00:00:00 2001 From: Patrick O'Sullivan Date: Tue, 3 Dec 2024 07:00:46 -0600 Subject: [PATCH 49/52] make new OverflowMenuButton component, move it inside the frames, disable Pressable while we're hovering over the overflow button --- .../components/ChatMessage/ChatMessage.tsx | 15 +- .../components/GalleryPost/GalleryPost.tsx | 124 +++++++------- .../components/NotebookPost/NotebookPost.tsx | 154 ++++++++++-------- .../ui/src/components/OverflowMenuButton.tsx | 38 +++++ 4 files changed, 195 insertions(+), 136 deletions(-) create mode 100644 packages/ui/src/components/OverflowMenuButton.tsx diff --git a/packages/ui/src/components/ChatMessage/ChatMessage.tsx b/packages/ui/src/components/ChatMessage/ChatMessage.tsx index ec606de16c..4b9133b167 100644 --- a/packages/ui/src/components/ChatMessage/ChatMessage.tsx +++ b/packages/ui/src/components/ChatMessage/ChatMessage.tsx @@ -4,8 +4,8 @@ import { ComponentProps, memo, useCallback, useMemo, useState } from 'react'; import { View, XStack, YStack, isWeb } from 'tamagui'; import AuthorRow from '../AuthorRow'; -import { Button } from '../Button'; import { Icon } from '../Icon'; +import { OverflowMenuButton } from '../OverflowMenuButton'; import { createContentRenderer } from '../PostContent/ContentRenderer'; import { usePostContent, @@ -196,12 +196,13 @@ const ChatMessage = ({ onPressDelete={handleDeletePressed} /> - {isWeb && !hideOverflowMenu && showOverflowOnHover && ( - - - + {!hideOverflowMenu && showOverflowOnHover && ( + )} ); diff --git a/packages/ui/src/components/GalleryPost/GalleryPost.tsx b/packages/ui/src/components/GalleryPost/GalleryPost.tsx index d96fa59c2c..d77d0012b6 100644 --- a/packages/ui/src/components/GalleryPost/GalleryPost.tsx +++ b/packages/ui/src/components/GalleryPost/GalleryPost.tsx @@ -3,13 +3,13 @@ import * as db from '@tloncorp/shared/db'; import { truncate } from 'lodash'; import { ComponentProps, useCallback, useMemo, useState } from 'react'; import { PropsWithChildren } from 'react'; -import { View, XStack, isWeb, styled } from 'tamagui'; +import { View, XStack, styled } from 'tamagui'; import { DetailViewAuthorRow } from '../AuthorRow'; import { ContactAvatar } from '../Avatar'; -import { Button } from '../Button'; import { Icon } from '../Icon'; import { useBoundHandler } from '../ListItem/listItemUtils'; +import { OverflowMenuButton } from '../OverflowMenuButton'; import { createContentRenderer } from '../PostContent/ContentRenderer'; import { BlockData, @@ -49,6 +49,7 @@ export function GalleryPost({ hideOverflowMenu?: boolean; } & Omit, 'onPress' | 'onLongPress'>) { const [showRetrySheet, setShowRetrySheet] = useState(false); + const [disableHandlePress, setDisableHandlePress] = useState(false); const handleRetryPressed = useCallback(() => { onPressRetry?.(post); @@ -75,67 +76,76 @@ export function GalleryPost({ const handleLongPress = useBoundHandler(post, onLongPress); + const onPressOverflow = useCallback(() => { + handleLongPress(); + }, [handleLongPress]); + + const onHoverIntoOverflow = useCallback(() => { + setDisableHandlePress(true); + }, []); + + const onHoverOutOfOverflow = useCallback(() => { + setDisableHandlePress(false); + }, []); + if (post.isDeleted) { return null; } return ( - <> - - - - {showAuthor && !post.hidden && !post.isDeleted && ( - - - - {deliveryFailed && ( - - Tap to retry - - )} - - - )} - - - - {isWeb && !hideOverflowMenu && ( - - - - )} - + + + {deliveryFailed && ( + + Tap to retry + + )} + + + )} + + {!hideOverflowMenu && ( + + )} + + ); } diff --git a/packages/ui/src/components/NotebookPost/NotebookPost.tsx b/packages/ui/src/components/NotebookPost/NotebookPost.tsx index 3235adc90a..c2f1c948f9 100644 --- a/packages/ui/src/components/NotebookPost/NotebookPost.tsx +++ b/packages/ui/src/components/NotebookPost/NotebookPost.tsx @@ -8,14 +8,12 @@ import { YStack, createStyledContext, styled, - isWeb, } from 'tamagui'; import { DetailViewAuthorRow } from '../AuthorRow'; -import { Button } from '../Button'; import { ChatMessageReplySummary } from '../ChatMessage/ChatMessageReplySummary'; -import { Icon } from '../Icon'; import { Image } from '../Image'; +import { OverflowMenuButton } from '../OverflowMenuButton'; import { createContentRenderer } from '../PostContent/ContentRenderer'; import { usePostContent, @@ -56,6 +54,8 @@ export function NotebookPost({ hideOverflowMenu?: boolean; }) { const [showRetrySheet, setShowRetrySheet] = useState(false); + const [disableHandlePress, setDisableHandlePress] = useState(false); + const handleLongPress = useCallback(() => { onLongPress?.(post); }, [post, onLongPress]); @@ -88,85 +88,95 @@ export function NotebookPost({ onPress?.(post); }, [post, onPress, deliveryFailed]); + const onPressOverflow = useCallback(() => { + handleLongPress(); + }, [handleLongPress]); + + const onHoverIntoOverflow = useCallback(() => { + setDisableHandlePress(true); + }, []); + + const onHoverOutOfOverflow = useCallback(() => { + setDisableHandlePress(false); + }, []); + if (!post || post.isDeleted) { return null; } const hasReplies = post.replyCount && post.replyTime && post.replyContactIds; return ( - <> - - - {post.hidden ? ( - - - You have hidden this post. + + + {post.hidden ? ( + + + You have hidden this post. + + + ) : ( + <> + + + {viewMode !== 'activity' && ( + + {post.textContent} - - ) : ( - <> - + ) : null} + + )} - {viewMode !== 'activity' && ( - - {post.textContent} - - )} - - {showReplies && hasReplies ? ( - - ) : null} - - )} - - {post.deliveryStatus === 'failed' ? ( - - - Message failed to send - - - ) : null} - - - - {isWeb && !hideOverflowMenu && ( - - - - )} - + {post.deliveryStatus === 'failed' ? ( + + + Message failed to send + + + ) : null} + {!hideOverflowMenu && ( + + )} + + + ); } diff --git a/packages/ui/src/components/OverflowMenuButton.tsx b/packages/ui/src/components/OverflowMenuButton.tsx new file mode 100644 index 0000000000..34910c171c --- /dev/null +++ b/packages/ui/src/components/OverflowMenuButton.tsx @@ -0,0 +1,38 @@ +import { ComponentProps } from 'react'; +import { View, isWeb } from 'tamagui'; + +import { Button } from './Button'; +import { Icon } from './Icon'; + +export function OverflowMenuButton({ + onPress, + backgroundColor, + ...viewProps +}: { + onPress: () => void; + backgroundColor?: string; +} & ComponentProps) { + if (!isWeb) { + return null; + } + + return ( + + + + ); +} From 1e652557cd358eb7a9bd6c988e850c21b0f6f915 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 3 Dec 2024 13:50:07 +0000 Subject: [PATCH 50/52] update new frontend glob: [skip actions] --- tm-alpha-desk/desk.docket-0 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tm-alpha-desk/desk.docket-0 b/tm-alpha-desk/desk.docket-0 index 77e9d58704..60af1a7c96 100644 --- a/tm-alpha-desk/desk.docket-0 +++ b/tm-alpha-desk/desk.docket-0 @@ -2,7 +2,7 @@ info+'Start, host, and cultivate communities. Own your communications, organize your resources, and share documents. Tlon is a decentralized platform that offers a full, communal suite of tools for messaging, writing and sharing media with others.' color+0xde.dede image+'https://bootstrap.urbit.org/tlon.svg?v=1' - glob-http+['https://bootstrap.urbit.org/glob-0v6.ar89l.ohn8c.72n4o.krb3l.hb512.glob' 0v6.ar89l.ohn8c.72n4o.krb3l.hb512] + glob-http+['https://bootstrap.urbit.org/glob-0v1.267eb.hn3gf.ludgo.1kct0.eas77.glob' 0v1.267eb.hn3gf.ludgo.1kct0.eas77] base+'tm-alpha' version+[1 0 0] website+'https://tlon.io' From 2ca7a124f6b038ba0e239639e9b76579f797b308 Mon Sep 17 00:00:00 2001 From: ~latter-bolden Date: Tue, 3 Dec 2024 08:43:03 -0600 Subject: [PATCH 51/52] use token padding --- packages/ui/src/components/ProfileStatusSheet.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/components/ProfileStatusSheet.tsx b/packages/ui/src/components/ProfileStatusSheet.tsx index 3c3ea79c03..1b0198c2bb 100644 --- a/packages/ui/src/components/ProfileStatusSheet.tsx +++ b/packages/ui/src/components/ProfileStatusSheet.tsx @@ -86,7 +86,7 @@ export default function ProfileStatusSheet({ hero onPress={handleSave} disabled={!isValid} - padding={11.5} + paddingHorizontal="$l" > From 07e67772746daa4441e0befa95c19641d624ee75 Mon Sep 17 00:00:00 2001 From: ~latter-bolden Date: Tue, 3 Dec 2024 15:53:30 -0600 Subject: [PATCH 52/52] include non notifying contact updates in init feed, bundle them separately --- desk/app/activity.hoon | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/desk/app/activity.hoon b/desk/app/activity.hoon index a89fc84acf..47418eeebf 100644 --- a/desk/app/activity.hoon +++ b/desk/app/activity.hoon @@ -590,7 +590,7 @@ :: we only care about posts/replies events that are notified, and we :: don't want to include events from sources whose latest event is :: after the start so we always get "new" sources when paging - ?. ?& notified.event + ?. ?& ?|(notified.event ?=(%contact -<.event)) (lth latest.src-info start) ?= $? %post %reply %dm-post %dm-reply %flag-post %flag-reply %group-ask @@ -613,6 +613,14 @@ ?^ mention :- sources.acc [(sub limit.acc 1) (snoc happenings.acc u.mention) collapsed.acc] + =/ contact-bundle=(unit activity-bundle:a) + ?. ?=(%all type) ~ + =/ is-contact-event ?=(%contact -<.event) + ?. is-contact-event ~ + `[source time ~[[time event]]] + ?^ contact-bundle + :- sources.acc + [(sub limit.acc 1) (snoc happenings.acc u.contact-bundle) collapsed.acc] =/ care ?| ?=(%all type) &(?=(%replies type) ?=(?(%reply %dm-reply) -<.event))